diff --git a/compiler/can/src/expr.rs b/compiler/can/src/expr.rs index 7b76de6a9c..d97e814495 100644 --- a/compiler/can/src/expr.rs +++ b/compiler/can/src/expr.rs @@ -9,12 +9,13 @@ use crate::num::{ use crate::pattern::{canonicalize_pattern, Pattern}; use crate::procedure::References; use crate::scope::Scope; +use inlinable_string::InlinableString; use roc_collections::all::{ImSet, MutMap, MutSet, SendMap}; use roc_module::ident::{Lowercase, TagName}; use roc_module::low_level::LowLevel; use roc_module::operator::CalledVia; use roc_module::symbol::Symbol; -use roc_parse::ast::{self, StrLiteral, StrSegment}; +use roc_parse::ast::{self, StrLiteral}; use roc_parse::pattern::PatternType::*; use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError}; use roc_region::all::{Located, Region}; @@ -44,6 +45,12 @@ impl Output { } } +#[derive(Clone, Debug, PartialEq)] +pub enum StrSegment { + Interpolation(Located), + Plaintext(InlinableString), +} + #[derive(Clone, Debug, PartialEq)] pub enum Expr { // Literals @@ -55,10 +62,7 @@ pub enum Expr { // Int and Float store a variable to generate better error messages Int(Variable, i64), Float(Variable, f64), - Str { - interpolations: Vec<(Box, Symbol)>, - suffix: Box, - }, + Str(Vec), List { list_var: Variable, // required for uniqueness of the list elem_var: Variable, @@ -249,7 +253,7 @@ pub fn canonicalize_expr<'a>( ) } } - ast::Expr::Str(literal) => flatten_str_literal(env, scope, literal), + ast::Expr::Str(literal) => flatten_str_literal(env, var_store, scope, literal), ast::Expr::List(loc_elems) => { if loc_elems.is_empty() { ( @@ -1320,32 +1324,39 @@ pub fn inline_calls(var_store: &mut VarStore, scope: &mut Scope, expr: Expr) -> } } -fn flatten_str_literal( - env: &mut Env<'_>, +fn flatten_str_literal<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, scope: &mut Scope, - literal: &StrLiteral<'_>, + literal: &StrLiteral<'a>, ) -> (Expr, Output) { use ast::StrLiteral::*; match literal { PlainLine(str_slice) => ( - Expr::Str { - interpolations: Vec::new(), - suffix: (*str_slice).into(), - }, + Expr::Str(vec![StrSegment::Plaintext((*str_slice).into())]), Output::default(), ), - LineWithEscapes(segments) => flatten_str_lines(env, scope, &[segments]), - Block(lines) => flatten_str_lines(env, scope, lines), + Line(segments) => flatten_str_lines(env, var_store, scope, &[segments]), + Block(lines) => flatten_str_lines(env, var_store, scope, lines), } } -fn flatten_str_lines( - env: &mut Env<'_>, +fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool { + match expr { + ast::Expr::Var { .. } => true, + ast::Expr::Access(sub_expr, _) => is_valid_interpolation(sub_expr), + _ => false, + } +} + +fn flatten_str_lines<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, scope: &mut Scope, - lines: &[&[StrSegment<'_>]], + lines: &[&[ast::StrSegment<'a>]], ) -> (Expr, Output) { - use StrSegment::*; + use ast::StrSegment::*; let mut buf = String::new(); let mut interpolations = Vec::new(); @@ -1360,37 +1371,32 @@ fn flatten_str_lines( Unicode(loc_digits) => { todo!("parse unicode digits {:?}", loc_digits); } - Interpolated { - module_name, - ident, - region, - } => { - let (expr, new_output) = - canonicalize_lookup(env, scope, module_name, ident, region.clone()); + Interpolated(loc_expr) => { + if is_valid_interpolation(loc_expr.value) { + let (loc_expr, new_output) = canonicalize_expr( + env, + var_store, + scope, + loc_expr.region, + loc_expr.value, + ); - output.union(new_output); + output.union(new_output); - match expr { - Expr::Var(symbol) => { - interpolations.push((buf.into(), symbol)); - } - _ => { - todo!("TODO gracefully handle non-ident in string interpolation."); - } + interpolations.push(StrSegment::Interpolation(loc_expr)); + } else { + env.problem(Problem::InvalidInterpolation(loc_expr.region)); + + return ( + Expr::RuntimeError(RuntimeError::InvalidInterpolation(loc_expr.region)), + output, + ); } - - buf = String::new(); } EscapedChar(ch) => buf.push(*ch), } } } - ( - Expr::Str { - interpolations, - suffix: buf.into(), - }, - output, - ) + (Expr::Str(interpolations), output) } diff --git a/compiler/can/src/pattern.rs b/compiler/can/src/pattern.rs index 9bebbeed38..07b66577b1 100644 --- a/compiler/can/src/pattern.rs +++ b/compiler/can/src/pattern.rs @@ -471,7 +471,7 @@ fn flatten_str_literal(literal: &StrLiteral<'_>) -> Pattern { match literal { PlainLine(str_slice) => Pattern::StrLiteral((*str_slice).into()), - LineWithEscapes(segments) => flatten_str_lines(&[segments]), + Line(segments) => flatten_str_lines(&[segments]), Block(lines) => flatten_str_lines(lines), } } @@ -490,8 +490,8 @@ fn flatten_str_lines(lines: &[&[StrSegment<'_>]]) -> Pattern { Unicode(loc_digits) => { todo!("parse unicode digits {:?}", loc_digits); } - Interpolated { region, .. } => { - return Pattern::UnsupportedPattern(region.clone()); + Interpolated(loc_expr) => { + return Pattern::UnsupportedPattern(loc_expr.region); } EscapedChar(ch) => buf.push(*ch), } diff --git a/compiler/can/tests/test_can.rs b/compiler/can/tests/test_can.rs index 4dec7f0f77..9bb8343f47 100644 --- a/compiler/can/tests/test_can.rs +++ b/compiler/can/tests/test_can.rs @@ -1236,113 +1236,6 @@ mod test_can { // ); // } - #[test] - fn string_with_interpolation_at_start() { - let src = indoc!( - r#" - "\(abc)defg" - "# - ); - let arena = Bump::new(); - let CanExprOut { - loc_expr, problems, .. - } = can_expr_with(&arena, test_home(), src); - assert_eq!(problems, Vec::new()); - // let (args, ret) = (vec![("", Located::new(0, 2, 0, 4, Var("abc")))], "defg"); - // let arena = Bump::new(); - // let actual = parse_with(&arena, input); - - // assert_eq!( - // Ok(Expr::InterpolatedStr(&( - // arena.alloc_slice_clone(&args), - // ret - // ))), - // actual - // ); - } - - #[test] - fn string_with_interpolation_at_end() { - let src = indoc!( - r#" - "abcd\(efg)" - "# - ); - // let (args, ret) = (vec![("abcd", Located::new(0, 6, 0, 8, Var("efg")))], ""); - // let arena = Bump::new(); - // let actual = parse_with(&arena, input); - - // assert_eq!( - // Ok(InterpolatedStr(&(arena.alloc_slice_clone(&args), ret))), - // actual - // ); - } - - #[test] - fn string_with_interpolation_in_middle() { - let src = indoc!( - r#" - "abc\(defg)hij" - "# - ); - // let (args, ret) = (vec![("abc", Located::new(0, 5, 0, 8, Var("defg")))], "hij"); - // let arena = Bump::new(); - // let actual = parse_with(&arena, input); - - // assert_eq!( - // Ok(InterpolatedStr(&(arena.alloc_slice_clone(&args), ret))), - // actual - // ); - } - - #[test] - fn string_with_two_interpolations_in_middle() { - let src = indoc!( - r#" - "abc\(defg)hi\(jkl)mn" - "# - ); - // let (args, ret) = ( - // vec![ - // ("abc", Located::new(0, 5, 0, 8, Var("defg"))), - // ("hi", Located::new(0, 14, 0, 16, Var("jkl"))), - // ], - // "mn", - // ); - // let arena = Bump::new(); - // let actual = parse_with(&arena, input); - - // assert_eq!( - // Ok(InterpolatedStr(&(arena.alloc_slice_clone(&args), ret))), - // actual - // ); - } - - #[test] - fn string_with_four_interpolations() { - let src = indoc!( - r#" - "\(abc)def\(ghi)jkl\(mno)pqrs\(tuv)" - "# - ); - // let (args, ret) = ( - // vec![ - // ("", Located::new(0, 2, 0, 4, Var("abc"))), - // ("def", Located::new(0, 11, 0, 13, Var("ghi"))), - // ("jkl", Located::new(0, 20, 0, 22, Var("mno"))), - // ("pqrs", Located::new(0, 30, 0, 32, Var("tuv"))), - // ], - // "", - // ); - // let arena = Bump::new(); - // let actual = parse_with(&arena, input); - - // assert_eq!( - // Ok(InterpolatedStr(&(arena.alloc_slice_clone(&args), ret))), - // actual - // ); - } - // #[test] // fn string_with_escaped_interpolation() { // assert_parses_to( diff --git a/compiler/constrain/src/expr.rs b/compiler/constrain/src/expr.rs index dc4acbdc05..4e91a9ccf0 100644 --- a/compiler/constrain/src/expr.rs +++ b/compiler/constrain/src/expr.rs @@ -199,14 +199,34 @@ pub fn constrain_expr( exists(vars, And(cons)) } - Str { interpolations, .. } => { - todo!( - "constrain interpolations in a string literal {:?}", - interpolations - ); + Str(segments) => { + use crate::builtins::str_type; + use roc_can::expr::StrSegment::*; - // use crate::builtins::{empty_list_type, float_literal, int_literal, list_type, str_type}; - // Eq(str_type(), expected, Category::Str, region) + let mut cons = Vec::with_capacity(segments.len() + 1); + let expect_interpolated = + |region| Expected::ForReason(Reason::StrInterpolation, str_type(), region); + + for segment in segments { + match segment { + Plaintext(_) => { + // Plaintext strings add no constraints + } + Interpolation(loc_expr) => { + cons.push(constrain_expr( + env, + loc_expr.region, + &loc_expr.value, + expect_interpolated(loc_expr.region), + )); + } + } + } + + // The expression as a whole should have the type Str. + cons.push(Eq(str_type(), expected, Category::Str, region)); + + And(cons) } List { elem_var, diff --git a/compiler/constrain/src/uniq.rs b/compiler/constrain/src/uniq.rs index ec2ca083b3..c6906f3633 100644 --- a/compiler/constrain/src/uniq.rs +++ b/compiler/constrain/src/uniq.rs @@ -503,8 +503,8 @@ pub fn constrain_expr( ]), ) } - Str { interpolations, .. } => { - todo!("uniq constrain interpolations {:?}", interpolations); + Str(segments) => { + todo!("uniq constrain interpolations {:?}", segments); // let uniq_type = var_store.fresh(); // let inferred = str_type(Bool::variable(uniq_type)); diff --git a/compiler/problem/src/can.rs b/compiler/problem/src/can.rs index 523ccd965c..d425faa12e 100644 --- a/compiler/problem/src/can.rs +++ b/compiler/problem/src/can.rs @@ -55,6 +55,7 @@ pub enum Problem { alias_name: Symbol, region: Region, }, + InvalidInterpolation(Region), } #[derive(Clone, Debug, PartialEq)] @@ -125,6 +126,8 @@ pub enum RuntimeError { NonExhaustivePattern, + InvalidInterpolation(Region), + /// When the author specifies a type annotation but no implementation NoImplementation, } diff --git a/compiler/reporting/src/error/canonicalize.rs b/compiler/reporting/src/error/canonicalize.rs index 79bf9db1d0..84496c367a 100644 --- a/compiler/reporting/src/error/canonicalize.rs +++ b/compiler/reporting/src/error/canonicalize.rs @@ -262,6 +262,12 @@ pub fn can_problem<'b>( alloc.reflow(" can occur in this position."), ]), ]), + Problem::InvalidInterpolation(region) => { + todo!( + "TODO report an invalid string interpolation at region {:?}", + region + ); + } Problem::RuntimeError(runtime_error) => pretty_runtime_error(alloc, runtime_error), }; @@ -524,6 +530,12 @@ fn pretty_runtime_error<'b>( alloc.region(region), alloc.reflow("Only variables can be updated with record update syntax."), ]), + RuntimeError::InvalidInterpolation(region) => { + todo!( + "TODO runtime error for an invalid string interpolation at region {:?}", + region + ); + } RuntimeError::NoImplementation => todo!("no implementation, unreachable"), RuntimeError::NonExhaustivePattern => { unreachable!("not currently reported (but can blow up at runtime)") diff --git a/compiler/reporting/src/error/type.rs b/compiler/reporting/src/error/type.rs index 896d5842bc..45fd11347d 100644 --- a/compiler/reporting/src/error/type.rs +++ b/compiler/reporting/src/error/type.rs @@ -781,7 +781,7 @@ fn to_expr_report<'b>( unreachable!("I don't think these can be reached") } - Reason::InterpolatedStringVar => { + Reason::StrInterpolation => { unimplemented!("string interpolation is not implemented yet") } @@ -875,6 +875,10 @@ fn add_category<'b>( Int => alloc.concat(vec![this_is, alloc.text(" an integer of type:")]), Float => alloc.concat(vec![this_is, alloc.text(" a float of type:")]), Str => alloc.concat(vec![this_is, alloc.text(" a string of type:")]), + StrInterpolation => alloc.concat(vec![ + this_is, + alloc.text(" a value in a string interpolation, which was of type:"), + ]), Lambda => alloc.concat(vec![this_is, alloc.text(" an anonymous function of type:")]), diff --git a/compiler/solve/tests/solve_expr.rs b/compiler/solve/tests/solve_expr.rs index c2864e99ec..4a94d6e27f 100644 --- a/compiler/solve/tests/solve_expr.rs +++ b/compiler/solve/tests/solve_expr.rs @@ -277,21 +277,53 @@ mod solve_expr { ); } - // // INTERPOLATED STRING + // INTERPOLATED STRING - // #[test] - // fn infer_interpolated_string() { - // infer_eq( - // indoc!( - // r#" - // whatItIs = "great" + #[test] + fn infer_interpolated_string() { + infer_eq( + indoc!( + r#" + whatItIs = "great" - // "type inference is \(whatItIs)!" - // "# - // ), - // "Str", - // ); - // } + "type inference is \(whatItIs)!" + "# + ), + "Str", + ); + } + + #[test] + fn infer_interpolated_var() { + infer_eq( + indoc!( + r#" + whatItIs = "great" + + str = "type inference is \(whatItIs)!" + + whatItIs + "# + ), + "Str", + ); + } + + #[test] + fn infer_interpolated_field() { + infer_eq( + indoc!( + r#" + rec = { whatItIs: "great" } + + str = "type inference is \(rec.whatItIs)!" + + rec + "# + ), + "{ whatItIs : Str }", + ); + } // LIST MISMATCH diff --git a/compiler/types/src/subs.rs b/compiler/types/src/subs.rs index 19bab67ca4..135de82b09 100644 --- a/compiler/types/src/subs.rs +++ b/compiler/types/src/subs.rs @@ -151,10 +151,9 @@ impl Variable { pub const EMPTY_TAG_UNION: Variable = Variable(2); // Builtins const BOOL_ENUM: Variable = Variable(3); - pub const BOOL: Variable = Variable(4); - pub const LIST_GET: Variable = Variable(5); + pub const BOOL: Variable = Variable(4); // Used in `if` conditions - pub const NUM_RESERVED_VARS: usize = 6; + pub const NUM_RESERVED_VARS: usize = 5; const FIRST_USER_SPACE_VAR: Variable = Variable(Self::NUM_RESERVED_VARS as u32); diff --git a/compiler/types/src/types.rs b/compiler/types/src/types.rs index 42b1dd74b1..bf65d8a6e6 100644 --- a/compiler/types/src/types.rs +++ b/compiler/types/src/types.rs @@ -904,7 +904,7 @@ pub enum Reason { FloatLiteral, IntLiteral, NumLiteral, - InterpolatedStringVar, + StrInterpolation, WhenBranch { index: Index, }, @@ -930,6 +930,7 @@ pub enum Category { TagApply(TagName), Lambda, Uniqueness, + StrInterpolation, // storing variables in the ast Storage,