Constrain string interpolation

This commit is contained in:
Richard Feldman 2020-08-29 21:37:46 -04:00
parent 5080a7e24b
commit 274e7e786d
11 changed files with 150 additions and 180 deletions

View file

@ -9,12 +9,13 @@ use crate::num::{
use crate::pattern::{canonicalize_pattern, Pattern}; use crate::pattern::{canonicalize_pattern, Pattern};
use crate::procedure::References; use crate::procedure::References;
use crate::scope::Scope; use crate::scope::Scope;
use inlinable_string::InlinableString;
use roc_collections::all::{ImSet, MutMap, MutSet, SendMap}; use roc_collections::all::{ImSet, MutMap, MutSet, SendMap};
use roc_module::ident::{Lowercase, TagName}; use roc_module::ident::{Lowercase, TagName};
use roc_module::low_level::LowLevel; use roc_module::low_level::LowLevel;
use roc_module::operator::CalledVia; use roc_module::operator::CalledVia;
use roc_module::symbol::Symbol; 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_parse::pattern::PatternType::*;
use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError}; use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError};
use roc_region::all::{Located, Region}; use roc_region::all::{Located, Region};
@ -44,6 +45,12 @@ impl Output {
} }
} }
#[derive(Clone, Debug, PartialEq)]
pub enum StrSegment {
Interpolation(Located<Expr>),
Plaintext(InlinableString),
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Expr { pub enum Expr {
// Literals // Literals
@ -55,10 +62,7 @@ pub enum Expr {
// Int and Float store a variable to generate better error messages // Int and Float store a variable to generate better error messages
Int(Variable, i64), Int(Variable, i64),
Float(Variable, f64), Float(Variable, f64),
Str { Str(Vec<StrSegment>),
interpolations: Vec<(Box<str>, Symbol)>,
suffix: Box<str>,
},
List { List {
list_var: Variable, // required for uniqueness of the list list_var: Variable, // required for uniqueness of the list
elem_var: Variable, 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) => { ast::Expr::List(loc_elems) => {
if loc_elems.is_empty() { 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( fn flatten_str_literal<'a>(
env: &mut Env<'_>, env: &mut Env<'a>,
var_store: &mut VarStore,
scope: &mut Scope, scope: &mut Scope,
literal: &StrLiteral<'_>, literal: &StrLiteral<'a>,
) -> (Expr, Output) { ) -> (Expr, Output) {
use ast::StrLiteral::*; use ast::StrLiteral::*;
match literal { match literal {
PlainLine(str_slice) => ( PlainLine(str_slice) => (
Expr::Str { Expr::Str(vec![StrSegment::Plaintext((*str_slice).into())]),
interpolations: Vec::new(),
suffix: (*str_slice).into(),
},
Output::default(), Output::default(),
), ),
LineWithEscapes(segments) => flatten_str_lines(env, scope, &[segments]), Line(segments) => flatten_str_lines(env, var_store, scope, &[segments]),
Block(lines) => flatten_str_lines(env, scope, lines), Block(lines) => flatten_str_lines(env, var_store, scope, lines),
} }
} }
fn flatten_str_lines( fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool {
env: &mut Env<'_>, 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, scope: &mut Scope,
lines: &[&[StrSegment<'_>]], lines: &[&[ast::StrSegment<'a>]],
) -> (Expr, Output) { ) -> (Expr, Output) {
use StrSegment::*; use ast::StrSegment::*;
let mut buf = String::new(); let mut buf = String::new();
let mut interpolations = Vec::new(); let mut interpolations = Vec::new();
@ -1360,37 +1371,32 @@ fn flatten_str_lines(
Unicode(loc_digits) => { Unicode(loc_digits) => {
todo!("parse unicode digits {:?}", loc_digits); todo!("parse unicode digits {:?}", loc_digits);
} }
Interpolated { Interpolated(loc_expr) => {
module_name, if is_valid_interpolation(loc_expr.value) {
ident, let (loc_expr, new_output) = canonicalize_expr(
region, env,
} => { var_store,
let (expr, new_output) = scope,
canonicalize_lookup(env, scope, module_name, ident, region.clone()); loc_expr.region,
loc_expr.value,
);
output.union(new_output); output.union(new_output);
match expr { interpolations.push(StrSegment::Interpolation(loc_expr));
Expr::Var(symbol) => { } else {
interpolations.push((buf.into(), symbol)); env.problem(Problem::InvalidInterpolation(loc_expr.region));
}
_ => { return (
todo!("TODO gracefully handle non-ident in string interpolation."); Expr::RuntimeError(RuntimeError::InvalidInterpolation(loc_expr.region)),
} output,
);
} }
buf = String::new();
} }
EscapedChar(ch) => buf.push(*ch), EscapedChar(ch) => buf.push(*ch),
} }
} }
} }
( (Expr::Str(interpolations), output)
Expr::Str {
interpolations,
suffix: buf.into(),
},
output,
)
} }

View file

@ -471,7 +471,7 @@ fn flatten_str_literal(literal: &StrLiteral<'_>) -> Pattern {
match literal { match literal {
PlainLine(str_slice) => Pattern::StrLiteral((*str_slice).into()), 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), Block(lines) => flatten_str_lines(lines),
} }
} }
@ -490,8 +490,8 @@ fn flatten_str_lines(lines: &[&[StrSegment<'_>]]) -> Pattern {
Unicode(loc_digits) => { Unicode(loc_digits) => {
todo!("parse unicode digits {:?}", loc_digits); todo!("parse unicode digits {:?}", loc_digits);
} }
Interpolated { region, .. } => { Interpolated(loc_expr) => {
return Pattern::UnsupportedPattern(region.clone()); return Pattern::UnsupportedPattern(loc_expr.region);
} }
EscapedChar(ch) => buf.push(*ch), EscapedChar(ch) => buf.push(*ch),
} }

View file

@ -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] // #[test]
// fn string_with_escaped_interpolation() { // fn string_with_escaped_interpolation() {
// assert_parses_to( // assert_parses_to(

View file

@ -199,14 +199,34 @@ pub fn constrain_expr(
exists(vars, And(cons)) exists(vars, And(cons))
} }
Str { interpolations, .. } => { Str(segments) => {
todo!( use crate::builtins::str_type;
"constrain interpolations in a string literal {:?}", use roc_can::expr::StrSegment::*;
interpolations
);
// use crate::builtins::{empty_list_type, float_literal, int_literal, list_type, str_type}; let mut cons = Vec::with_capacity(segments.len() + 1);
// Eq(str_type(), expected, Category::Str, region) 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 { List {
elem_var, elem_var,

View file

@ -503,8 +503,8 @@ pub fn constrain_expr(
]), ]),
) )
} }
Str { interpolations, .. } => { Str(segments) => {
todo!("uniq constrain interpolations {:?}", interpolations); todo!("uniq constrain interpolations {:?}", segments);
// let uniq_type = var_store.fresh(); // let uniq_type = var_store.fresh();
// let inferred = str_type(Bool::variable(uniq_type)); // let inferred = str_type(Bool::variable(uniq_type));

View file

@ -55,6 +55,7 @@ pub enum Problem {
alias_name: Symbol, alias_name: Symbol,
region: Region, region: Region,
}, },
InvalidInterpolation(Region),
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -125,6 +126,8 @@ pub enum RuntimeError {
NonExhaustivePattern, NonExhaustivePattern,
InvalidInterpolation(Region),
/// When the author specifies a type annotation but no implementation /// When the author specifies a type annotation but no implementation
NoImplementation, NoImplementation,
} }

View file

@ -262,6 +262,12 @@ pub fn can_problem<'b>(
alloc.reflow(" can occur in this position."), 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), Problem::RuntimeError(runtime_error) => pretty_runtime_error(alloc, runtime_error),
}; };
@ -524,6 +530,12 @@ fn pretty_runtime_error<'b>(
alloc.region(region), alloc.region(region),
alloc.reflow("Only variables can be updated with record update syntax."), 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::NoImplementation => todo!("no implementation, unreachable"),
RuntimeError::NonExhaustivePattern => { RuntimeError::NonExhaustivePattern => {
unreachable!("not currently reported (but can blow up at runtime)") unreachable!("not currently reported (but can blow up at runtime)")

View file

@ -781,7 +781,7 @@ fn to_expr_report<'b>(
unreachable!("I don't think these can be reached") unreachable!("I don't think these can be reached")
} }
Reason::InterpolatedStringVar => { Reason::StrInterpolation => {
unimplemented!("string interpolation is not implemented yet") 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:")]), Int => alloc.concat(vec![this_is, alloc.text(" an integer of type:")]),
Float => alloc.concat(vec![this_is, alloc.text(" a float 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:")]), 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:")]), Lambda => alloc.concat(vec![this_is, alloc.text(" an anonymous function of type:")]),

View file

@ -277,21 +277,53 @@ mod solve_expr {
); );
} }
// // INTERPOLATED STRING // INTERPOLATED STRING
// #[test] #[test]
// fn infer_interpolated_string() { fn infer_interpolated_string() {
// infer_eq( infer_eq(
// indoc!( indoc!(
// r#" r#"
// whatItIs = "great" whatItIs = "great"
// "type inference is \(whatItIs)!" "type inference is \(whatItIs)!"
// "# "#
// ), ),
// "Str", "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 // LIST MISMATCH

View file

@ -151,10 +151,9 @@ impl Variable {
pub const EMPTY_TAG_UNION: Variable = Variable(2); pub const EMPTY_TAG_UNION: Variable = Variable(2);
// Builtins // Builtins
const BOOL_ENUM: Variable = Variable(3); const BOOL_ENUM: Variable = Variable(3);
pub const BOOL: Variable = Variable(4); pub const BOOL: Variable = Variable(4); // Used in `if` conditions
pub const LIST_GET: Variable = Variable(5);
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); const FIRST_USER_SPACE_VAR: Variable = Variable(Self::NUM_RESERVED_VARS as u32);

View file

@ -904,7 +904,7 @@ pub enum Reason {
FloatLiteral, FloatLiteral,
IntLiteral, IntLiteral,
NumLiteral, NumLiteral,
InterpolatedStringVar, StrInterpolation,
WhenBranch { WhenBranch {
index: Index, index: Index,
}, },
@ -930,6 +930,7 @@ pub enum Category {
TagApply(TagName), TagApply(TagName),
Lambda, Lambda,
Uniqueness, Uniqueness,
StrInterpolation,
// storing variables in the ast // storing variables in the ast
Storage, Storage,