#[macro_use] extern crate pretty_assertions; #[macro_use] extern crate indoc; extern crate bumpalo; extern crate roc_can; extern crate roc_parse; extern crate roc_region; mod helpers; #[cfg(test)] mod test_can { use crate::helpers::{can_expr_with, test_home, CanExprOut}; use bumpalo::Bump; use roc_can::expr::Expr::{self, *}; use roc_can::expr::Recursive; use roc_problem::can::{Problem, RuntimeError}; use roc_region::all::{Located, Region}; use std::{f64, i64}; fn assert_can(input: &str, expected: Expr) { let arena = Bump::new(); let actual_out = can_expr_with(&arena, test_home(), input); assert_eq!(actual_out.loc_expr.value, expected); } fn assert_can_float(input: &str, expected: f64) { let arena = Bump::new(); let actual_out = can_expr_with(&arena, test_home(), input); match actual_out.loc_expr.value { Expr::Float(_, actual) => { assert_eq!(expected, actual); } actual => { panic!("Expected a Float, but got: {:?}", actual); } } } fn assert_can_int(input: &str, expected: i64) { let arena = Bump::new(); let actual_out = can_expr_with(&arena, test_home(), input); match actual_out.loc_expr.value { Expr::Int(_, actual) => { assert_eq!(expected, actual); } actual => { panic!("Expected an Int, but got: {:?}", actual); } } } fn assert_can_num(input: &str, expected: i64) { let arena = Bump::new(); let actual_out = can_expr_with(&arena, test_home(), input); match actual_out.loc_expr.value { Expr::Num(_, actual) => { assert_eq!(expected, actual); } actual => { panic!("Expected a Num, but got: {:?}", actual); } } } // NUMBER LITERALS #[test] fn int_too_large() { let string = (i64::MAX as i128 + 1).to_string(); assert_can( &string.clone(), RuntimeError(RuntimeError::IntOutsideRange(string.into())), ); } #[test] fn int_too_small() { let string = (i64::MIN as i128 - 1).to_string(); assert_can( &string.clone(), RuntimeError(RuntimeError::IntOutsideRange(string.into())), ); } #[test] fn float_too_large() { let string = format!("{}1.0", f64::MAX); assert_can( &string.clone(), RuntimeError(RuntimeError::FloatOutsideRange(string.into())), ); } #[test] fn float_too_small() { let string = format!("{}1.0", f64::MIN); assert_can( &string.clone(), RuntimeError(RuntimeError::FloatOutsideRange(string.into())), ); } #[test] fn zero() { assert_can_num("0", 0); } #[test] fn minus_zero() { assert_can_num("-0", 0); } #[test] fn zero_point_zero() { assert_can_float("0.0", 0.0); } #[test] fn minus_zero_point_zero() { assert_can_float("-0.0", -0.0); } #[test] fn hex_zero() { assert_can_int("0x0", 0x0); } #[test] fn hex_one_b() { assert_can_int("0x1b", 0x1b); } #[test] fn minus_hex_one_b() { assert_can_int("-0x1b", -0x1b); } #[test] fn octal_zero() { assert_can_int("0o0", 0o0); } #[test] fn octal_one_two() { assert_can_int("0o12", 0o12); } #[test] fn minus_octal_one_two() { assert_can_int("-0o12", -0o12); } #[test] fn binary_zero() { assert_can_int("0b0", 0b0); } #[test] fn binary_one_one() { assert_can_int("0b11", 0b11); } #[test] fn minus_binary_one_one() { assert_can_int("-0b11", -0b11); } // LOCALS // TODO rewrite this test to check only for UnusedDef reports // #[test] // fn closure_args_are_not_locals() { // // "arg" shouldn't make it into output.locals, because // // it only exists in the closure's arguments. // let arena = Bump::new(); // let src = indoc!( // r#" // func = \arg -> arg // func 2 // "# // ); // let (_actual, output, problems, _var_store, _vars, _constraint) = // can_expr_with(&arena, test_home(), src); // assert_eq!(problems, vec![]); // assert_eq!( // output, // Out { // lookups: vec!["func"], // calls: vec!["func"], // tail_call: None // } // .into_output(scope) // ); // } // TODO rewrite this test to check only for UnusedDef reports // #[test] // fn call_by_pointer_for_fn_args() { // // This function will get passed in as a pointer. // let src = indoc!( // r#" // apply = \f, x -> f x // identity = \a -> a // apply identity 5 // "# // ); // let arena = Bump::new(); // let (_actual, output, problems, _var_store, _vars, _constraint) = // can_expr_with(&arena, test_home(), src); // assert_eq!(problems, vec![]); // assert_eq!( // output, // Out { // lookups: vec!["identity", "apply"], // calls: vec!["f", "apply"], // tail_call: None // } // .into() // ); // } fn get_closure(expr: &Expr, i: usize) -> roc_can::expr::Recursive { match expr { LetRec(assignments, body, _, _) => { match &assignments.get(i).map(|def| &def.loc_expr.value) { Some(Closure(_, _, recursion, _, _)) => recursion.clone(), Some(other @ _) => { panic!("assignment at {} is not a closure, but a {:?}", i, other) } None => { if i > 0 { get_closure(&body.value, i - 1) } else { panic!("Looking for assignment at {} but the list is too short", i) } } } } LetNonRec(def, body, _, _) => { if i > 0 { // recurse in the body (not the def!) get_closure(&body.value, i - 1) } else { match &def.loc_expr.value { Closure(_, _, recursion, _, _) => recursion.clone(), other @ _ => { panic!("assignment at {} is not a closure, but a {:?}", i, other) } } } } // Closure(_, recursion, _, _) if i == 0 => recursion.clone(), _ => panic!( "expression is not a LetRec or a LetNonRec, but rather {:?}", expr ), } } #[test] fn recognize_tail_calls() { let src = indoc!( r#" g = \x -> when x is 0 -> 0 _ -> g (x - 1) # use parens to force the ordering! (h = \x -> when x is 0 -> 0 _ -> g (x - 1) (p = \x -> when x is 0 -> 0 1 -> g (x - 1) _ -> p (x - 1) # variables must be (indirectly) referenced in the body for analysis to work { x: p, y: h } )) "# ); let arena = Bump::new(); let CanExprOut { loc_expr, problems, .. } = can_expr_with(&arena, test_home(), src); assert_eq!(problems, Vec::new()); assert!(problems.iter().all(|problem| match problem { Problem::UnusedDef(_, _) => true, _ => false, })); let actual = loc_expr.value; let g_detected = get_closure(&actual, 0); let h_detected = get_closure(&actual, 1); let p_detected = get_closure(&actual, 2); assert_eq!(g_detected, Recursive::TailRecursive); assert_eq!(h_detected, Recursive::NotRecursive); assert_eq!(p_detected, Recursive::TailRecursive); } // TODO restore this test! It should report two unused defs (h and p), but only reports 1. // #[test] // fn reproduce_incorrect_unused_defs() { // let src = indoc!( // r#" // g = \x -> // when x is // 0 -> 0 // _ -> g (x - 1) // h = \x -> // when x is // 0 -> 0 // _ -> g (x - 1) // p = \x -> // when x is // 0 -> 0 // 1 -> g (x - 1) // _ -> p (x - 1) // # variables must be (indirectly) referenced in the body for analysis to work // # { x: p, y: h } // g // "# // ); // let arena = Bump::new(); // let CanExprOut { // loc_expr, problems, .. // } = can_expr_with(&arena, test_home(), src); // // There should be two UnusedDef problems: one for h, and one for p // assert_eq!(problems.len(), 2); // assert!(problems.iter().all(|problem| match problem { // Problem::UnusedDef(_, _) => true, // _ => false, // })); // let actual = loc_expr.value; // // NOTE: the indices associated with each of these can change! // // They come out of a hashmap, and are not sorted. // let g_detected = get_closure(&actual, 0); // let h_detected = get_closure(&actual, 2); // let p_detected = get_closure(&actual, 1); // assert_eq!(g_detected, Recursive::TailRecursive); // assert_eq!(h_detected, Recursive::NotRecursive); // assert_eq!(p_detected, Recursive::TailRecursive); // } #[test] fn when_tail_call() { let src = indoc!( r#" g = \x -> when x is 0 -> 0 _ -> g (x + 1) g 0 "# ); let arena = Bump::new(); let CanExprOut { loc_expr, problems, .. } = can_expr_with(&arena, test_home(), src); assert_eq!(problems, Vec::new()); let detected = get_closure(&loc_expr.value, 0); assert_eq!(detected, Recursive::TailRecursive); } #[test] fn immediate_tail_call() { let src = indoc!( r#" f = \x -> f x f 0 "# ); let arena = Bump::new(); let CanExprOut { loc_expr, problems, .. } = can_expr_with(&arena, test_home(), src); assert_eq!(problems, Vec::new()); let detected = get_closure(&loc_expr.value, 0); assert_eq!(detected, Recursive::TailRecursive); } #[test] fn when_condition_is_no_tail_call() { let src = indoc!( r#" q = \x -> when q x is _ -> 0 q 0 "# ); let arena = Bump::new(); let CanExprOut { loc_expr, problems, .. } = can_expr_with(&arena, test_home(), src); assert_eq!(problems, Vec::new()); let detected = get_closure(&loc_expr.value, 0); assert_eq!(detected, Recursive::Recursive); } #[test] fn good_mutual_recursion() { let src = indoc!( r#" q = \x -> when x is 0 -> 0 _ -> p (x - 1) p = \x -> when x is 0 -> 0 _ -> q (x - 1) q p "# ); let arena = Bump::new(); let CanExprOut { loc_expr, problems, .. } = can_expr_with(&arena, test_home(), src); assert_eq!(problems, Vec::new()); let actual = loc_expr.value; let detected = get_closure(&actual, 0); assert_eq!(detected, Recursive::Recursive); let detected = get_closure(&actual, 1); assert_eq!(detected, Recursive::Recursive); } #[test] fn valid_self_recursion() { let src = indoc!( r#" boom = \_ -> boom {} boom "# ); let arena = Bump::new(); let CanExprOut { loc_expr, problems, .. } = can_expr_with(&arena, test_home(), src); assert_eq!(problems, Vec::new()); let is_circular_def = if let RuntimeError(RuntimeError::CircularDef(_, _)) = loc_expr.value { true } else { false }; assert_eq!(is_circular_def, false); } #[test] fn invalid_self_recursion() { let src = indoc!( r#" x = x x "# ); let arena = Bump::new(); let CanExprOut { loc_expr, problems, .. } = can_expr_with(&arena, test_home(), src); let is_circular_def = if let RuntimeError(RuntimeError::CircularDef(_, _)) = loc_expr.value { true } else { false }; let problem = Problem::RuntimeError(RuntimeError::CircularDef( vec![Located::at(Region::new(0, 0, 0, 1), "x".into())], vec![(Region::new(0, 0, 0, 1), Region::new(0, 0, 4, 5))], )); assert_eq!(is_circular_def, true); assert_eq!(problems, vec![problem]); } #[test] fn invalid_mutual_recursion() { let src = indoc!( r#" x = y y = z z = x x "# ); let arena = Bump::new(); let CanExprOut { loc_expr, problems, .. } = can_expr_with(&arena, test_home(), src); let problem = Problem::RuntimeError(RuntimeError::CircularDef( vec![ Located::at(Region::new(0, 0, 0, 1), "x".into()), Located::at(Region::new(1, 1, 0, 1), "y".into()), Located::at(Region::new(2, 2, 0, 1), "z".into()), ], vec![ (Region::new(0, 0, 0, 1), Region::new(0, 0, 4, 5)), (Region::new(1, 1, 0, 1), Region::new(1, 1, 4, 5)), (Region::new(2, 2, 0, 1), Region::new(2, 2, 4, 5)), ], )); assert_eq!(problems, vec![problem]); match loc_expr.value { RuntimeError(RuntimeError::CircularDef(_, _)) => (), actual => { panic!("Expected a CircularDef runtime error, but got {:?}", actual); } } } #[test] fn unused_def_regression() { let src = indoc!( r#" Booly : [ Yes, No, Maybe ] y : Booly y = No # There was a bug where annotating a def meant that its # references no longer got reported. # # https://github.com/rtfeldman/roc/issues/298 x : List Booly x = [ y ] x "# ); let arena = Bump::new(); let CanExprOut { problems, .. } = can_expr_with(&arena, test_home(), src); assert_eq!(problems, Vec::new()); } //#[test] //fn closing_over_locals() { // // "local" should be used, because the closure used it. // // However, "unused" should be unused. // let (_, output, problems, _) = can_expr(indoc!( // r#" // local = 5 // unused = 6 // func = \arg -> arg + local // 3 + func 2 // "# // )); // assert_eq!( // problems, // vec![Problem::UnusedAssignment(loc(( // "unused".to_string() // )))] // ); // assert_eq!( // output, // Out { // lookups: vec!["func", "local"], // calls: vec!["func"], // tail_call: None // } // .into() // ); //} //#[test] //fn unused_closure() { // // "unused" should be unused because it's in func, which is unused. // let (_, output, problems, _) = can_expr(indoc!( // r#" // local = 5 // unused = 6 // func = \arg -> arg + unused // local // "# // )); // assert_eq!( // problems, // vec![ // Problem::UnusedAssignment(loc(("unused".to_string()))), // Problem::UnusedAssignment(loc(("func".to_string()))), // ] // ); // assert_eq!( // output, // Out { // lookups: vec!["local"], // calls: vec![], // tail_call: None // } // .into() // ); //} // // UNRECOGNIZED // #[test] // fn basic_unrecognized_constant() { // let (expr, output, problems, _) = can_expr(indoc!( // r#" // x // "# // )); // assert_eq!( // problems, // vec![Problem::LookupNotInScope(loc(("x".to_string())))] // ); // assert_eq!(expr, LookupNotInScope(loc(("x".to_string())))); // assert_eq!( // output, // Out { // lookups: vec![], // calls: vec![], // tail_call: None // } // .into() // ); // } //#[test] //fn complex_unrecognized_constant() { // let (_, output, problems, _) = can_expr(indoc!( // r#" // a = 5 // b = 6 // a + b * z // "# // )); // assert_eq!( // problems, // vec![Problem::LookupNotInScope(loc(( // "z".to_string() // )))] // ); // assert_eq!( // output, // Out { // lookups: vec!["a", "b"], // calls: vec![], // tail_call: None // } // .into() // ); //} //// UNUSED //#[test] //fn mutual_unused_circular_vars() { // // This should report that both a and b are unused, since the return expr never references them. // // It should not report them as circular, since we haven't solved the halting problem here. // let (_, output, problems, _) = can_expr(indoc!( // r#" // a = \arg -> if arg > 0 then b 7 else 0 // b = \arg -> if arg > 0 then a (arg - 1) else 0 // c = 5 // c // "# // )); // assert_eq!(problems, vec![unused("a"), unused("b")]); // assert_eq!( // output, // Out { // lookups: vec!["c"], // calls: vec![], // tail_call: None // } // .into() // ); //} //#[test] //fn can_fibonacci() { // let (_, output, problems, _) = can_expr(indoc!( // r#" // fibonacci = \num -> // if num < 2 then // num // else // fibonacci (num - 1) + fibonacci (num - 2) // fibonacci 9 // "# // )); // assert_eq!(problems, vec![]); // assert_eq!( // output, // Out { // lookups: vec!["fibonacci"], // calls: vec!["fibonacci"], // tail_call: None // } // .into() // ); //} //#[test] //fn can_tail_call() { // // TODO check the global params - make sure this // // is considered a tail call, even though it only // // calls itself from one branch! // let (_, output, problems, _) = can_expr(indoc!( // r#" // factorial = \num -> // factorialHelp num 0 // factorialHelp = \num total -> // if num == 0 then // total // else // factorialHelp (num - 1) (total * num) // factorial 9 // "# // )); // assert_eq!(problems, vec![]); // assert_eq!( // output, // Out { // lookups: vec!["factorial", "factorialHelp"], // calls: vec!["factorial", "factorialHelp"], // tail_call: None // } // .into() // ); //} //#[test] //fn transitively_used_function() { // // This should report that neither a nor b are unused, // // since if you never call a function but do return it, that's okay! // let (_, output, problems, _) = can_expr(indoc!( // r#" // a = \_ -> 42 // b = a // b // "# // )); // assert_eq!(problems, Vec::new()); // assert_eq!( // output, // Out { // lookups: vec!["a", "b"], // calls: vec![], // tail_call: None // } // .into() // ); //} //// ASSIGNMENT REORDERING //#[test] //fn reorder_assignments() { // let (expr, output, problems, _) = can_expr(indoc!( // r#" // increment = \arg -> arg + 1 // z = (increment 2) + y // y = x + 1 // x = 9 // z * 3 // "# // )); // assert_eq!(problems, vec![]); // assert_eq!( // output, // Out { // lookups: vec!["increment", "x", "y", "z"], // calls: vec!["increment"], // tail_call: None // } // .into() // ); // let symbols = assigned_symbols(expr); // // In code gen, for everything to have been set before it gets read, // // the following must be true about when things are assigned: // // // // x must be assigned before y // // y must be assigned before z // // // // The order of the increment function doesn't matter. // assert_before("x", "y", &symbols); // assert_before("y", "z", &symbols); //} //#[test] //fn reorder_closed_over_assignments() { // let (expr, output, problems, _) = can_expr(indoc!( // r#" // z = func1 x // x = 9 // y = func2 3 // func1 = \arg -> func2 arg + y // func2 = \arg -> arg + x // z // "# // )); // assert_eq!(problems, vec![]); // assert_eq!( // output, // Out { // lookups: vec!["func1", "func2", "x", "y", "z"], // calls: vec!["func1", "func2"], // tail_call: None // } // .into() // ); // let symbols = assigned_symbols(expr); // // In code gen, for everything to have been set before it gets read, // // the following must be true about when things are assigned: // // // // x and func2 must be assigned (in either order) before y // // y and func1 must be assigned (in either order) before z // assert_before("x", "y", &symbols); // assert_before("func2", "y", &symbols); // assert_before("func1", "z", &symbols); // assert_before("y", "z", &symbols); //} //fn assert_before(before: &str, after: &str, symbols: &Vec) { // assert_ne!(before, after); // let before_symbol = sym(before); // let after_symbol = sym(after); // let before_index = symbols // .iter() // .position(|symbol| symbol == &before_symbol) // .unwrap_or_else(|| { // panic!( // "error in assert_before({:?}, {:?}): {:?} could not be found in {:?}", // before, // after, // sym(before), // symbols // ) // }); // let after_index = symbols // .iter() // .position(|symbol| symbol == &after_symbol) // .unwrap_or_else(|| { // panic!( // "error in assert_before({:?}, {:?}): {:?} could not be found in {:?}", // before, // after, // sym(after), // symbols // ) // }); // if before_index == after_index { // panic!( // "error in assert_before({:?}, {:?}): both were at index {} in {:?}", // before, after, after_index, symbols // ); // } else if before_index > after_index { // panic!("error in assert_before: {:?} appeared *after* {:?} (not before, as expected) in {:?}", before, after, symbols); // } //} //fn assigned_symbols(expr: Expr) -> Vec { // match expr { // Assign(assignments, _) => { // assignments.into_iter().map(|(pattern, _)| { // match pattern.value { // Identifier(symbol) => { // symbol // }, // _ => { // panic!("Called assigned_symbols passing an Assign expr with non-Identifier patterns!"); // } // } // }).collect() // }, // _ => { // panic!("Called assigned_symbols passing a non-Assign expr!"); // } // } //} //// CIRCULAR ASSIGNMENT //#[test] //fn circular_assignment() { // let (_, _, problems, _) = can_expr(indoc!( // r#" // c = d + 3 // b = 2 + c // d = a + 7 // a = b + 1 // 2 + d // "# // )); // assert_eq!( // problems, // vec![Problem::CircularAssignment(vec![ // // c should appear first because it's assigned first in the original expression. // loc(unqualified("c")), // loc(unqualified("d")), // loc(unqualified("a")), // loc(unqualified("b")), // ])] // ); //} //#[test] //fn always_function() { // // There was a bug where this reported UnusedArgument("val") // // since it was used only in the returned function only. // let (_, _, problems, _) = can_expr(indoc!( // r#" // \val -> \_ -> val // "# // )); // assert_eq!(problems, vec![]); //} //// TODO verify that Apply handles output.references.calls correctly //// UNSUPPORTED PATTERNS //// TODO verify that in closures and assignments, you can't assign to int/string/underscore/etc //// OPERATOR PRECEDENCE //// fn parse_with_precedence(input: &str) -> Result<(Expr, &str), easy::Errors> { //// parse_without_loc(input) //// .map(|(expr, remaining)| (expr::apply_precedence_and_associativity(loc(expr)).unwrap().value, remaining)) //// } //// #[test] //// fn two_operator_precedence() { //// assert_eq!( //// parse_with_precedence("x + y * 5"), //// Ok((BinOp( //// loc_box(var("x")), //// loc(Plus), //// loc_box( //// BinOp( //// loc_box(var("y")), //// loc(Star), //// loc_box(Int(5)) //// ) //// ), //// ), //// "")) //// ); //// assert_eq!( //// parse_with_precedence("x * y + 5"), //// Ok((BinOp( //// loc_box( //// BinOp( //// loc_box(var("x")), //// loc(Star), //// loc_box(var("y")), //// ) //// ), //// loc(Plus), //// loc_box(Int(5)) //// ), //// "")) //// ); //// } //// #[test] //// fn compare_and() { //// assert_eq!( //// parse_with_precedence("x > 1 || True"), //// Ok((BinOp( //// loc_box( //// BinOp( //// loc_box(var("x")), //// loc(GreaterThan), //// loc_box(Int(1)) //// ) //// ), //// loc(Or), //// loc_box(ApplyVariant(vname("True"), None)) //// ), //// "")) //// ); //// } //// HELPERS //#[test] //fn sort_cyclic_idents() { // let assigned_idents = unqualifieds(vec!["blah", "c", "b", "d", "a"]); // assert_eq!( // can::sort_cyclic_idents( // loc_unqualifieds(vec!["a", "b", "c", "d"]), // &mut assigned_idents.iter() // ), // loc_unqualifieds(vec!["c", "d", "a", "b"]) // ); //} // // //// STRING LITERALS // // #[test] // fn string_with_valid_unicode_escapes() { // expect_parsed_str("x\u{00A0}x", r#""x\u{00A0}x""#); // expect_parsed_str("x\u{101010}x", r#""x\u{101010}x""#); // } // #[test] // fn string_with_too_large_unicode_escape() { // // Should be too big - max size should be 10FFFF. // // (Rust has this restriction. I assume it's a good idea.) // assert_malformed_str( // r#""abc\u{110000}def""#, // vec![Located::new(0, 7, 0, 12, Problem::UnicodeCodePointTooLarge)], // ); // } // #[test] // fn string_with_no_unicode_digits() { // // No digits specified // assert_malformed_str( // r#""blah\u{}foo""#, // vec![Located::new(0, 5, 0, 8, Problem::NoUnicodeDigits)], // ); // } // #[test] // fn string_with_no_unicode_opening_brace() { // // No opening curly brace. It can't be sure if the closing brace // // was intended to be a closing brace for the unicode escape, so // // report that there were no digits specified. // assert_malformed_str( // r#""abc\u00A0}def""#, // vec![Located::new(0, 4, 0, 5, Problem::NoUnicodeDigits)], // ); // } // #[test] // fn string_with_no_unicode_closing_brace() { // // No closing curly brace // assert_malformed_str( // r#""blah\u{stuff""#, // vec![Located::new(0, 5, 0, 12, Problem::MalformedEscapedUnicode)], // ); // } // #[test] // fn string_with_no_unicode_braces() { // // No curly braces // assert_malformed_str( // r#""zzzz\uzzzzz""#, // vec![Located::new(0, 5, 0, 6, Problem::NoUnicodeDigits)], // ); // } // #[test] // fn string_with_interpolation_at_start() { // let input = indoc!( // r#" // "\(abc)defg" // "# // ); // 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(InterpolatedStr(&(arena.alloc_slice_clone(&args), ret))), // actual // ); // } // #[test] // fn string_with_interpolation_at_end() { // let input = 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 input = 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 input = 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 input = 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( // // This should NOT be string interpolation, because of the \\ // indoc!( // r#" // "abcd\\(efg)hij" // "# // ), // Str(r#"abcd\(efg)hij"#.into()), // ); // } // // #[test] // fn string_without_escape() { // expect_parsed_str("a", r#""a""#); // expect_parsed_str("ab", r#""ab""#); // expect_parsed_str("abc", r#""abc""#); // expect_parsed_str("123", r#""123""#); // expect_parsed_str("abc123", r#""abc123""#); // expect_parsed_str("123abc", r#""123abc""#); // expect_parsed_str("123 abc 456 def", r#""123 abc 456 def""#); // } // #[test] // fn string_with_special_escapes() { // expect_parsed_str(r#"x\x"#, r#""x\\x""#); // expect_parsed_str(r#"x"x"#, r#""x\"x""#); // expect_parsed_str("x\tx", r#""x\tx""#); // expect_parsed_str("x\rx", r#""x\rx""#); // expect_parsed_str("x\nx", r#""x\nx""#); // } // fn assert_malformed_str<'a>(input: &'a str, expected_probs: Vec>) { // let arena = Bump::new(); // let actual = parse_with(&arena, input); // assert_eq!( // Ok(Expr::MalformedStr(expected_probs.into_boxed_slice())), // actual // ); // } // // TODO test what happens when interpolated strings contain 1+ malformed idents // // TODO test hex/oct/binary conversion to numbers // // TODO test for \t \r and \n in string literals *outside* unicode escape sequence! }