diff --git a/src/domain/engine.rs b/src/domain/engine.rs index 06a292f..6632252 100644 --- a/src/domain/engine.rs +++ b/src/domain/engine.rs @@ -369,6 +369,13 @@ fn render_expression(expr: &Expression) -> String { .collect(); format!("[{}]", items.join(", ")) } + Expression::Tuple(elements, _) => { + let items: Vec<_> = elements + .iter() + .map(render_expression) + .collect(); + format!("({})", items.join(", ")) + } Expression::Hole(_) => "?".to_string(), Expression::Unit(_) => "()".to_string(), Expression::Separator => String::new(), diff --git a/src/editor/server.rs b/src/editor/server.rs index 5eec653..9c38740 100644 --- a/src/editor/server.rs +++ b/src/editor/server.rs @@ -774,8 +774,8 @@ impl TechniqueLanguageServer { "Invalid function call".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidLiteral(_) => { - ("Invalid literal".to_string(), DiagnosticSeverity::ERROR) + ParsingError::InvalidTuple(_) => { + ("Invalid tuple".to_string(), DiagnosticSeverity::ERROR) } ParsingError::InvalidCodeBlock(_) => { ("Invalid code block".to_string(), DiagnosticSeverity::ERROR) diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 3f564e8..25fc76c 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -1328,6 +1328,7 @@ impl<'i> Formatter<'i> { } Expression::Pair(pair, _) => self.append_pair(pair), Expression::List(elements, _) => self.append_list(elements), + Expression::Tuple(elements, _) => self.append_tuple(elements), Expression::Hole(_) => { self.add_fragment_reference(Syntax::Hole, "?"); } @@ -1505,13 +1506,27 @@ impl<'i> Formatter<'i> { { if i > 0 { self.add_fragment_reference(Syntax::Structure, ","); + self.add_fragment_reference(Syntax::Neutral, " "); } - self.add_fragment_reference(Syntax::Neutral, " "); self.append_expression(element); } - self.add_fragment_reference(Syntax::Neutral, " "); self.add_fragment_reference(Syntax::Structure, "]"); } + + fn append_tuple(&mut self, elements: &'i Vec) { + self.add_fragment_reference(Syntax::Structure, "("); + for (i, element) in elements + .iter() + .enumerate() + { + if i > 0 { + self.add_fragment_reference(Syntax::Structure, ","); + self.add_fragment_reference(Syntax::Neutral, " "); + } + self.append_expression(element); + } + self.add_fragment_reference(Syntax::Structure, ")"); + } } impl<'i> ToString for Formatter<'i> { diff --git a/src/language/types.rs b/src/language/types.rs index b262e79..d3fdbe4 100644 --- a/src/language/types.rs +++ b/src/language/types.rs @@ -447,6 +447,7 @@ pub enum Expression<'i> { Binding(Box>, Vec>, Span), Pair(Box>, Span), List(Vec>, Span), + Tuple(Vec>, Span), Hole(Span), Unit(Span), Separator, @@ -473,6 +474,7 @@ impl PartialEq for Expression<'_> { } (Expression::Pair(a, _), Expression::Pair(b, _)) => a == b, (Expression::List(a, _), Expression::List(b, _)) => a == b, + (Expression::Tuple(a, _), Expression::Tuple(b, _)) => a == b, (Expression::Hole(_), Expression::Hole(_)) => true, (Expression::Unit(_), Expression::Unit(_)) => true, (Expression::Separator, Expression::Separator) => true, diff --git a/src/linking/linker.rs b/src/linking/linker.rs index 8946810..7494c28 100644 --- a/src/linking/linker.rs +++ b/src/linking/linker.rs @@ -106,7 +106,7 @@ fn link_operation<'i>( link_operation(&mut entry.value, library, problems); } } - Operation::List(items) => { + Operation::List(items) | Operation::Tuple(items) => { for item in items { link_operation(item, library, problems); } diff --git a/src/parsing/checks/errors.rs b/src/parsing/checks/errors.rs index 1bf51f6..b1b49b5 100644 --- a/src/parsing/checks/errors.rs +++ b/src/parsing/checks/errors.rs @@ -264,7 +264,7 @@ making_coffee : } #[test] -fn invalid_literal_whitespace_parens() { +fn invalid_tuple_whitespace_parens() { expect_error( r#" making_coffee : @@ -272,12 +272,12 @@ making_coffee : 1. Do something { ( ) } "# .trim_ascii(), - ParsingError::InvalidLiteral(Span::new(39, 3)), + ParsingError::InvalidTuple(Span::new(39, 3)), ); } #[test] -fn invalid_literal_parenthesised_expression() { +fn invalid_tuple_parenthesised_expression() { expect_error( r#" making_coffee : @@ -285,7 +285,7 @@ making_coffee : 1. Do something { (x) } "# .trim_ascii(), - ParsingError::InvalidLiteral(Span::new(39, 3)), + ParsingError::InvalidTuple(Span::new(39, 3)), ); } diff --git a/src/parsing/checks/parser.rs b/src/parsing/checks/parser.rs index fa07ef4..2ab3b26 100644 --- a/src/parsing/checks/parser.rs +++ b/src/parsing/checks/parser.rs @@ -1406,6 +1406,144 @@ echo "Done"```) }"#, ); } +#[test] +fn function_arguments_accept_compound_expressions() { + // A nested tuple, list, or call each count as one argument. + let mut input = Parser::new(); + + input.initialize("{ thing((a, b, c)) }"); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("thing"), + parameters: vec![Expression::Tuple( + vec![ + Expression::Variable(Identifier::new("a"), Span::default()), + Expression::Variable(Identifier::new("b"), Span::default()), + Expression::Variable(Identifier::new("c"), Span::default()) + ], + Span::default() + )] + }, + Span::default() + )]) + ); + + // Same names as three loose arguments: arity 3, not arity 1. + input.initialize("{ thing(a, b, c) }"); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("thing"), + parameters: vec![ + Expression::Variable(Identifier::new("a"), Span::default()), + Expression::Variable(Identifier::new("b"), Span::default()), + Expression::Variable(Identifier::new("c"), Span::default()) + ] + }, + Span::default() + )]) + ); + + input.initialize("{ thing([a, b]) }"); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("thing"), + parameters: vec![Expression::List( + vec![ + Expression::Variable(Identifier::new("a"), Span::default()), + Expression::Variable(Identifier::new("b"), Span::default()) + ], + Span::default() + )] + }, + Span::default() + )]) + ); + + input.initialize("{ thing(other(x)) }"); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("thing"), + parameters: vec![Expression::Execution( + Function { + target: Identifier::new("other"), + parameters: vec![Expression::Variable( + Identifier::new("x"), + Span::default() + )] + }, + Span::default() + )] + }, + Span::default() + )]) + ); +} + +#[test] +fn multiline_argument_content_not_split_by_internal_separators() { + // A comma inside a ``` block is content, not an argument separator. + let mut input = Parser::new(); + + input.initialize( + r#"{ exec(```bash +ls -la, please +```) }"#, + ); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + Some("bash"), + vec!["ls -la, please"], + Span::default() + )] + }, + Span::default() + )]) + ); + + // A multiline argument followed by a genuine second argument: two + // parameters, not scrambled. + input.initialize( + r#"{ combine(```bash +echo "hello, world" +```, "second, arg") }"#, + ); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("combine"), + parameters: vec![ + Expression::Multiline( + Some("bash"), + vec!["echo \"hello, world\""], + Span::default() + ), + Expression::String(vec![Piece::Text("second, arg")], Span::default()) + ] + }, + Span::default() + )]) + ); +} + #[test] fn multiline() { let mut input = Parser::new(); @@ -1783,6 +1921,92 @@ fn lists() { Span::default() )]) ); + + // A comma inside a ``` multiline element does not split the list. + input.initialize( + r#"{ [ ```bash +ls -la, please +``` ] }"#, + ); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::List( + vec![Expression::Multiline( + Some("bash"), + vec!["ls -la, please"], + Span::default() + )], + Span::default() + )]) + ); +} + +#[test] +fn tuples() { + // Unlike lists, tuples are comma-only — no newline form. + let mut input = Parser::new(); + + input.initialize(r#"{ ( 2, "mice" ) }"#); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::Tuple( + vec![ + Expression::Number(Numeric::Integral(2), Span::default()), + Expression::String(vec![Piece::Text("mice")], Span::default()) + ], + Span::default() + )]) + ); + + // Three or more elements + input.initialize("{ (1, 2, 3) }"); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::Tuple( + vec![ + Expression::Number(Numeric::Integral(1), Span::default()), + Expression::Number(Numeric::Integral(2), Span::default()), + Expression::Number(Numeric::Integral(3), Span::default()) + ], + Span::default() + )]) + ); + + // A nested tuple's comma doesn't split the outer list. + input.initialize(r#"{ [ (1, "a"), (2, "b") ] }"#); + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![Expression::List( + vec![ + Expression::Tuple( + vec![ + Expression::Number(Numeric::Integral(1), Span::default()), + Expression::String(vec![Piece::Text("a")], Span::default()) + ], + Span::default() + ), + Expression::Tuple( + vec![ + Expression::Number(Numeric::Integral(2), Span::default()), + Expression::String(vec![Piece::Text("b")], Span::default()) + ], + Span::default() + ) + ], + Span::default() + )]) + ); + + // Exactly `()` remains the unit literal, not a zero-element tuple. + input.initialize("()"); + assert_eq!( + input.read_expression(), + Ok(Expression::Unit(Span::default())) + ); } #[test] diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index 75ab537..ecbe149 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -46,7 +46,7 @@ pub enum ParsingError { MixedSectionContent(Span), InvalidInvocation(Span), InvalidFunction(Span), - InvalidLiteral(Span), + InvalidTuple(Span), InvalidCodeBlock(Span), InvalidStep(Span), InvalidSubstep(Span), @@ -82,7 +82,7 @@ impl ParsingError { | ParsingError::MixedSectionContent(span) | ParsingError::InvalidInvocation(span) | ParsingError::InvalidFunction(span) - | ParsingError::InvalidLiteral(span) + | ParsingError::InvalidTuple(span) | ParsingError::InvalidCodeBlock(span) | ParsingError::InvalidStep(span) | ParsingError::InvalidSubstep(span) @@ -622,12 +622,21 @@ impl<'i> Parser<'i> { Ok(results) } - /// Split the current content (assumed to be inside bracket delimiters - /// representing a list) interior into elements, breaking on each - /// top-level comma or newline. Separators nested within parentheses, - /// brackets, or string literals do not split. Empty chunks (a trailing - /// comma, a blank line) are skipped. - fn take_elements(&mut self, function: F) -> Result, ParsingError> + /// Split content into elements at each top-level comma (and, if + /// `allow_newline`, each top-level newline too — lists/tablets allow + /// either, tuples/arguments are comma-only). Separators inside parens, + /// brackets, strings, or a ``` multiline block don't split. + /// + /// Slices each element out before parsing it, rather than parsing + /// incrementally and checking what follows, because some readers + /// reached from `read_expression` (e.g. `read_numeric_integral`, and + /// the `is_binding`/`is_numeric_integral` predicates) assume they're + /// handed exactly one isolated token and anchor to its end. + fn take_elements( + &mut self, + allow_newline: bool, + function: F, + ) -> Result, ParsingError> where F: Fn(&mut Parser<'i>) -> Result, { @@ -638,6 +647,8 @@ impl<'i> Parser<'i> { let mut start = 0; let mut depth = 0i32; let mut in_string = false; + let mut in_multiline = false; + let mut backticks = 0u8; let mut cut = |outer: &mut Parser<'i>, chunk: &'i str| -> Result<(), ParsingError> { let trimmed = chunk.trim_ascii(); @@ -654,12 +665,27 @@ impl<'i> Parser<'i> { }; for (i, c) in content.char_indices() { + if c == '`' { + backticks += 1; + if backticks == 3 { + in_multiline = !in_multiline; + backticks = 0; + } + continue; + } + backticks = 0; + match c { + _ if in_multiline => {} '"' => in_string = !in_string, _ if in_string => {} '(' | '[' => depth += 1, ')' | ']' => depth -= 1, - ',' | '\n' if depth == 0 => { + ',' if depth == 0 => { + cut(self, &content[start..i])?; + start = i + c.len_utf8(); + } + '\n' if depth == 0 && allow_newline => { cut(self, &content[start..i])?; start = i + c.len_utf8(); } @@ -1517,6 +1543,19 @@ impl<'i> Parser<'i> { self.advance(2); let span = self.span_since(start); Ok(Expression::Unit(span)) + } else if content.starts_with('(') { + self.read_tuple_literal() + } else if content.starts_with("```") { + let (lang, lines) = self + .take_block_delimited("```", |inner| inner.parse_multiline_content()) + .map_err(|err| match err { + ParsingError::Expected(span, "the corresponding end delimiter") => { + ParsingError::InvalidMultiline(Span::new(span.offset, 0)) + } + _ => err, + })?; + let span = self.span_since(start); + Ok(Expression::Multiline(lang, lines, span)) } else if is_numeric(content) { let numeric = self.read_numeric()?; let span = self.span_since(start); @@ -1560,18 +1599,6 @@ impl<'i> Parser<'i> { self.offset, width, ))); - } else if text - .trim() - .is_empty() - { - // Parentheses with nothing (`( )`) or a bare expression - // inside (`(x)`) are not the unit literal `()` and there - // is no grouping form, so this is a malformed literal. - let width = content - .find(')') - .map(|close| close + 1) - .unwrap_or(paren + 1); - return Err(ParsingError::InvalidLiteral(Span::new(self.offset, width))); } else { return Err(ParsingError::InvalidFunction(Span::new( self.offset, @@ -1760,7 +1787,7 @@ impl<'i> Parser<'i> { fn read_bracket_expression(&mut self) -> Result, ParsingError> { let start = self.offset; let elements = self.take_block_chars("a list", '[', ']', true, |outer| { - outer.take_elements(|inner| { + outer.take_elements(true, |inner| { if is_pair(inner.source) { let pair_start = inner.offset; let label = inner @@ -1780,6 +1807,21 @@ impl<'i> Parser<'i> { Ok(Expression::List(elements, span)) } + /// Read a tuple: two or more comma-separated expressions in + /// parentheses. A single element (or none) isn't a tuple; use unit + /// `()` for an empty value. + fn read_tuple_literal(&mut self) -> Result, ParsingError> { + let start = self.offset; + let elements = self.take_block_chars("a tuple", '(', ')', true, |outer| { + outer.take_elements(false, |inner| inner.read_expression()) + })?; + if elements.len() < 2 { + return Err(ParsingError::InvalidTuple(self.span_since(start))); + } + let span = self.span_since(start); + Ok(Expression::Tuple(elements, span)) + } + fn parse_string_pieces(&mut self, raw: &'i str) -> Result>, ParsingError> { // Quick check: if no braces, just return a single text piece if !raw.contains('{') { @@ -1889,17 +1931,10 @@ impl<'i> Parser<'i> { /// Parse a simple integral number fn read_numeric_integral(&mut self) -> Result, ParsingError> { - let content = self.source; - - if let Ok(amount) = content - .trim_ascii() - .parse::() - { - self.advance(content.len()); - Ok(Numeric::Integral(amount)) - } else { - Err(ParsingError::InvalidIntegral(Span::new(self.offset, 0))) - } + let decimal = self + .read_decimal_part() + .map_err(|_| ParsingError::InvalidIntegral(Span::new(self.offset, 0)))?; + Ok(Numeric::Integral(decimal.number)) } /// Parse a scientific quantity with units @@ -2552,102 +2587,12 @@ impl<'i> Parser<'i> { Ok((lang, result)) } - /// Consume parameters to an invocation or function. Specifically, look - /// for the form - /// - /// ( one, 2, "three", ```bash echo "four"``` ) - /// - /// and return a Vec with an Expression for each parameter in the list. Most however, - /// will either be - /// - /// ( a, b, c ) - /// - /// or - /// - /// ( ```lang some content``` ) - /// + /// Consume parameters to an invocation or function: a parenthesised, + /// comma-separated list of full expressions, e.g. `(a, b, other(c))`. + /// Unlike a list, there's no newline form. fn read_parameters(&mut self) -> Result>, ParsingError> { self.take_block_chars("parameters for a function", '(', ')', true, |outer| { - let mut params = Vec::new(); - - loop { - outer.trim_whitespace(); - - let content = outer.source; - if content.is_empty() { - break; - } - - let param_start = outer.offset; - if content.starts_with("```") { - let (lang, lines) = outer - .take_block_delimited("```", |inner| inner.parse_multiline_content()) - .map_err(|err| match err { - ParsingError::Expected(span, "the corresponding end delimiter") => { - ParsingError::InvalidMultiline(Span::new(span.offset, 0)) - } - _ => err, - })?; - let span = outer.span_since(param_start); - params.push(Expression::Multiline(lang, lines, span)); - } else if content.starts_with("\"") { - let parts = - outer.take_block_chars("a string literal", '"', '"', false, |inner| { - inner.parse_string_pieces(inner.source) - })?; - let span = outer.span_since(param_start); - params.push(Expression::String(parts, span)); - } else if content.starts_with('\'') { - let value = outer.take_block_chars( - "a response literal", - '\'', - '\'', - false, - |inner| Ok(inner.source), - )?; - let span = outer.span_since(param_start); - params.push(Expression::Response(value, span)); - } else if is_numeric_quantity(content) { - let numeric = outer.read_numeric_quantity()?; - let span = outer.span_since(param_start); - params.push(Expression::Number(numeric, span)); - } else if is_numeric_integral(content) - || content - .as_bytes() - .first() - .is_some_and(|b| b.is_ascii_digit()) - || content.starts_with('-') - && content - .as_bytes() - .get(1) - .is_some_and(|b| b.is_ascii_digit()) - { - let decimal = outer.read_decimal_part()?; - let span = outer.span_since(param_start); - params.push(Expression::Number(Numeric::Integral(decimal.number), span)); - } else if content.starts_with('?') { - outer.advance(1); - let span = outer.span_since(param_start); - params.push(Expression::Hole(span)); - } else { - let name = outer.read_identifier()?; - let span = name.span; - params.push(Expression::Variable(name, span)); - } - - // Handle comma separation - outer.trim_whitespace(); - if outer - .source - .starts_with(',') - { - outer.advance(1); - } else { - break; - } - } - - Ok(params) + outer.take_elements(false, |inner| inner.read_expression()) }) } diff --git a/src/problem/messages.rs b/src/problem/messages.rs index 584760f..2e73129 100644 --- a/src/problem/messages.rs +++ b/src/problem/messages.rs @@ -579,15 +579,33 @@ expressions as parameters as required: .to_string(), ) } - ParsingError::InvalidLiteral(_) => ( - "Invalid literal".to_string(), - r#" -This is not a valid value. If you need to pass an empty value you probably -want unit, written `()`. - "# - .trim_ascii() - .to_string(), - ), + ParsingError::InvalidTuple(_) => { + let examples = vec![ + Expression::Tuple( + vec![ + Expression::Number(Numeric::Integral(2), Span::default()), + Expression::String(vec![Piece::Text("mice")], Span::default()), + ], + Span::default(), + ), + Expression::Unit(Span::default()), + ]; + + ( + "Invalid tuple syntax".to_string(), + format!( + r#" +A tuple needs two or more values separated by commas, like {}. +Parentheses around a single value aren't necessary; just write the +value on its own. For an empty value use unit, written {}. + "#, + examples[0].present(renderer), + examples[1].present(renderer), + ) + .trim_ascii() + .to_string(), + ) + } ParsingError::InvalidCodeBlock(_) => { let examples = vec![ Expression::Execution( diff --git a/src/program/types.rs b/src/program/types.rs index 175cfd0..a3c11a9 100644 --- a/src/program/types.rs +++ b/src/program/types.rs @@ -126,6 +126,7 @@ pub enum Operation<'i> { Multiline(Option<&'i str>, Vec<&'i str>), Tablet(Vec>), List(Vec>), + Tuple(Vec>), Invoke(Invocable<'i>), Execute(Executable<'i>), Hole, diff --git a/src/resolution/resolver.rs b/src/resolution/resolver.rs index 6e1d984..c64237b 100644 --- a/src/resolution/resolver.rs +++ b/src/resolution/resolver.rs @@ -105,7 +105,10 @@ fn resolve_operation<'i>( resolve_operation(arg, known, arities, problems); } } - Operation::Sequence(ops) | Operation::List(ops) | Operation::Prologue(ops) => { + Operation::Sequence(ops) + | Operation::List(ops) + | Operation::Tuple(ops) + | Operation::Prologue(ops) => { for op in ops { resolve_operation(op, known, arities, problems); } @@ -175,7 +178,10 @@ fn gather_iterated<'i>(op: &Operation<'i>, iterated: &mut HashSet<&'i str>) { gather_iterated(body, iterated); } Operation::Bind { value, .. } => gather_iterated(value, iterated), - Operation::Sequence(ops) | Operation::List(ops) | Operation::Prologue(ops) => { + Operation::Sequence(ops) + | Operation::List(ops) + | Operation::Tuple(ops) + | Operation::Prologue(ops) => { for op in ops { gather_iterated(op, iterated); } @@ -237,7 +243,10 @@ fn mark_iterated<'i>(op: &mut Operation<'i>, iterated: &HashSet<&str>) { } mark_iterated(body, iterated); } - Operation::Sequence(ops) | Operation::List(ops) | Operation::Prologue(ops) => { + Operation::Sequence(ops) + | Operation::List(ops) + | Operation::Tuple(ops) + | Operation::Prologue(ops) => { for op in ops { mark_iterated(op, iterated); } @@ -331,7 +340,10 @@ fn check_scope<'i>( } check_scope(body, scope, problems); } - Operation::Sequence(ops) | Operation::List(ops) | Operation::Prologue(ops) => { + Operation::Sequence(ops) + | Operation::List(ops) + | Operation::Tuple(ops) + | Operation::Prologue(ops) => { for op in ops { check_scope(op, scope, problems); } diff --git a/src/runner/checks/evaluator.rs b/src/runner/checks/evaluator.rs index bcab131..93927ae 100644 --- a/src/runner/checks/evaluator.rs +++ b/src/runner/checks/evaluator.rs @@ -166,9 +166,51 @@ fn sequence_evaluation() { assert_eq!(v, value::Value::Unitus); } -// Build a Parametriq of three values by reducing a wrapped construction. -// Simplest path: pre-stuff env with a Parametriq, then bind a tuple of names -// to a Variable that looks it up. +#[test] +fn tuple_literal_evaluates_to_parametriq() { + let library = Library::core(); + let context = Context::native(false); + let mut env = Environment::new(); + let tuple = Operation::Tuple(vec![ + Operation::Number(LangNumeric::Integral(2)), + Operation::String(vec![Fragment::Text("mice")]), + ]); + let v = evaluate(&library, &context, &mut env, &tuple).expect("evaluated"); + assert_eq!( + v, + value::Value::Parametriq(vec![ + value::Value::Quanticle(value::Numeric::Integral(2)), + value::Value::Literali("mice".to_string()), + ]) + ); +} + +#[test] +fn tuple_literal_binds_directly() { + // `(2, "mice") ~ (count, kind)` end to end. + let library = Library::core(); + let context = Context::native(false); + let mut env = Environment::new(); + let names = [Identifier::new("count"), Identifier::new("kind")]; + let bind = Operation::Bind { + names: &names, + value: Box::new(Operation::Tuple(vec![ + Operation::Number(LangNumeric::Integral(2)), + Operation::String(vec![Fragment::Text("mice")]), + ])), + inferred: None, + }; + let result = evaluate(&library, &context, &mut env, &bind).expect("evaluated"); + assert_eq!(result, value::Value::Unitus); + assert_eq!( + env.lookup("count"), + Some(&value::Value::Quanticle(value::Numeric::Integral(2))) + ); + assert_eq!( + env.lookup("kind"), + Some(&value::Value::Literali("mice".to_string())) + ); +} #[test] fn multi_name_bind_destructures_parametriq() { diff --git a/src/runner/driver.rs b/src/runner/driver.rs index 93c13fc..cf7caff 100644 --- a/src/runner/driver.rs +++ b/src/runner/driver.rs @@ -620,13 +620,7 @@ impl Driver for Interface { .command(&mut self.out, qualified, script) } - fn action( - &mut self, - qualified: &str, - name: &str, - verb: &str, - value: &Value, - ) -> UserInput { + fn action(&mut self, qualified: &str, name: &str, verb: &str, value: &Value) -> UserInput { self.verdict .action(&mut self.out, qualified, name, verb, value) } @@ -1939,13 +1933,7 @@ impl Driver for Transcript { .command(qualified, script) } - fn action( - &mut self, - qualified: &str, - name: &str, - verb: &str, - value: &Value, - ) -> UserInput { + fn action(&mut self, qualified: &str, name: &str, verb: &str, value: &Value) -> UserInput { self.emit(Trace::Execute { path: qualified.to_string(), script: format!("{} {}", verb, value.label()) diff --git a/src/runner/evaluator.rs b/src/runner/evaluator.rs index a3473e9..89f85d0 100644 --- a/src/runner/evaluator.rs +++ b/src/runner/evaluator.rs @@ -185,6 +185,13 @@ pub fn evaluate<'i>( } Ok(Value::Arraeum(values)) } + Operation::Tuple(items) => { + let mut values = Vec::with_capacity(items.len()); + for item in items { + values.push(evaluate(library, context, env, item)?); + } + Ok(Value::Parametriq(values)) + } Operation::Bind { names, value, .. } => { let v = evaluate(library, context, env, value)?; bind_names(env, names, v)?; diff --git a/src/runner/runner.rs b/src/runner/runner.rs index 367e8c5..a447538 100644 --- a/src/runner/runner.rs +++ b/src/runner/runner.rs @@ -492,6 +492,7 @@ impl<'i, D: Driver> Runner<'i, D> { | Operation::Multiline(_, _) | Operation::Tablet(_) | Operation::List(_) + | Operation::Tuple(_) | Operation::Prose(_) | Operation::Hole | Operation::Unit => { diff --git a/src/translation/translator.rs b/src/translation/translator.rs index e0176bb..2be994f 100644 --- a/src/translation/translator.rs +++ b/src/translation/translator.rs @@ -557,7 +557,8 @@ impl<'i> Translator<'i> { | [expr @ language::Expression::String(..)] | [expr @ language::Expression::Multiline(..)] | [expr @ language::Expression::Pair(..)] - | [expr @ language::Expression::List(..)] => { + | [expr @ language::Expression::List(..)] + | [expr @ language::Expression::Tuple(..)] => { Some(Fragment::Interpolation(self.translate_expression(expr))) } // A multi-statement block, or any executable single statement, @@ -794,6 +795,13 @@ impl<'i> Translator<'i> { Operation::List(items) } } + language::Expression::Tuple(elements, _) => { + let items = elements + .iter() + .map(|element| self.translate_expression(element)) + .collect(); + Operation::Tuple(items) + } language::Expression::Application(invocation, _) => { Operation::Invoke(self.translate_invocation(invocation)) }