diff --git a/src/domain/engine.rs b/src/domain/engine.rs index 8b0b3af..cf10d97 100644 --- a/src/domain/engine.rs +++ b/src/domain/engine.rs @@ -369,6 +369,7 @@ fn render_expression(expr: &Expression) -> String { 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 ab5b614..5eec653 100644 --- a/src/editor/server.rs +++ b/src/editor/server.rs @@ -774,6 +774,9 @@ impl TechniqueLanguageServer { "Invalid function call".to_string(), DiagnosticSeverity::ERROR, ), + ParsingError::InvalidLiteral(_) => { + ("Invalid literal".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 2daaffa..869c0c3 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -1324,7 +1324,10 @@ impl<'i> Formatter<'i> { Expression::Pair(pair, _) => self.append_pair(pair), Expression::List(elements, _) => self.append_list(elements), Expression::Hole(_) => { - self.add_fragment_reference(Syntax::Variable, "?"); + self.add_fragment_reference(Syntax::Hole, "?"); + } + Expression::Unit(_) => { + self.add_fragment_reference(Syntax::Unit, "()"); } Expression::Separator => {} } diff --git a/src/formatting/syntax.rs b/src/formatting/syntax.rs index 98cd117..575ddf5 100644 --- a/src/formatting/syntax.rs +++ b/src/formatting/syntax.rs @@ -13,6 +13,8 @@ pub enum Syntax { StepItem, CodeBlock, Variable, + Hole, + Unit, Section, String, Numeric, diff --git a/src/highlighting/terminal.rs b/src/highlighting/terminal.rs index 3cbdacc..e896c8b 100644 --- a/src/highlighting/terminal.rs +++ b/src/highlighting/terminal.rs @@ -37,6 +37,14 @@ impl Render for Terminal { .color(owo_colors::Rgb(0x72, 0x9f, 0xcf)) .bold() .to_string(), + Syntax::Hole => content // variable.hole.technique - #f57900 (orange) bold + .color(owo_colors::Rgb(0xf5, 0x79, 0x00)) + .bold() + .to_string(), + Syntax::Unit => content // variable.unit.technique - #f57900 (orange) bold + .color(owo_colors::Rgb(0xf5, 0x79, 0x00)) + .bold() + .to_string(), Syntax::Section => content // markup.heading.technique .to_string(), Syntax::String => content // string - #4e9a06 (green) bold diff --git a/src/highlighting/typst.rs b/src/highlighting/typst.rs index c520bfc..2baea81 100644 --- a/src/highlighting/typst.rs +++ b/src/highlighting/typst.rs @@ -23,6 +23,8 @@ impl Render for Typst { Syntax::StepItem => markup("weight: \"bold\"", &content), Syntax::CodeBlock => markup("fill: rgb(0x99, 0x99, 0x99), weight: \"bold\"", &content), Syntax::Variable => markup("fill: rgb(0x72, 0x9f, 0xcf), weight: \"bold\"", &content), + Syntax::Hole => markup("fill: rgb(0xf5, 0x79, 0x00), weight: \"bold\"", &content), + Syntax::Unit => markup("fill: rgb(0xf5, 0x79, 0x00), weight: \"bold\"", &content), Syntax::Section => markup("", &content), Syntax::String => markup("fill: rgb(0x4e, 0x9a, 0x06), weight: \"bold\"", &content), Syntax::Numeric => markup("fill: rgb(0xad, 0x7f, 0xa8), weight: \"bold\"", &content), diff --git a/src/language/types.rs b/src/language/types.rs index 365082f..ff4d7b9 100644 --- a/src/language/types.rs +++ b/src/language/types.rs @@ -447,6 +447,7 @@ pub enum Expression<'i> { Pair(Box>, Span), List(Vec>, Span), Hole(Span), + Unit(Span), Separator, } @@ -471,6 +472,7 @@ impl PartialEq for Expression<'_> { (Expression::Pair(a, _), Expression::Pair(b, _)) => a == b, (Expression::List(a, _), Expression::List(b, _)) => a == b, (Expression::Hole(_), Expression::Hole(_)) => true, + (Expression::Unit(_), Expression::Unit(_)) => true, (Expression::Separator, Expression::Separator) => true, _ => false, } diff --git a/src/linking/linker.rs b/src/linking/linker.rs index 398fdb8..df4aabf 100644 --- a/src/linking/linker.rs +++ b/src/linking/linker.rs @@ -115,7 +115,8 @@ fn link_operation<'i>( | Operation::Number(_) | Operation::Multiline(_, _) | Operation::Prose(_) - | Operation::Hole => {} + | Operation::Hole + | Operation::Unit => {} } } diff --git a/src/parsing/checks/errors.rs b/src/parsing/checks/errors.rs index 21f5b91..1bf51f6 100644 --- a/src/parsing/checks/errors.rs +++ b/src/parsing/checks/errors.rs @@ -263,6 +263,32 @@ making_coffee : ); } +#[test] +fn invalid_literal_whitespace_parens() { + expect_error( + r#" +making_coffee : + + 1. Do something { ( ) } + "# + .trim_ascii(), + ParsingError::InvalidLiteral(Span::new(39, 3)), + ); +} + +#[test] +fn invalid_literal_parenthesised_expression() { + expect_error( + r#" +making_coffee : + + 1. Do something { (x) } + "# + .trim_ascii(), + ParsingError::InvalidLiteral(Span::new(39, 3)), + ); +} + #[test] fn invalid_invocation_in_repeat() { expect_error( diff --git a/src/parsing/checks/parser.rs b/src/parsing/checks/parser.rs index 8b0e061..d943b42 100644 --- a/src/parsing/checks/parser.rs +++ b/src/parsing/checks/parser.rs @@ -234,6 +234,43 @@ fn hole_as_expression() { ); } +#[test] +fn unit_as_expression() { + // Exactly `()` is the unit literal. It must not be confused with a + // function call `name()`, an invocation `()`, or parens that + // enclose whitespace or content. + let mut input = Parser::new(); + input.initialize("()"); + assert_eq!( + input.read_expression(), + Ok(Expression::Unit(Span::default())) + ); + + input.initialize("name()"); + assert_eq!( + input.read_expression(), + Ok(Expression::Execution( + Function { + target: Identifier::new("name"), + parameters: vec![] + }, + Span::default() + )) + ); + + input.initialize("()"); + assert_eq!( + input.read_expression(), + Ok(Expression::Application( + Invocation { + target: Target::Local(Identifier::new("name")), + parameters: Some(vec![]) + }, + Span::default() + )) + ); +} + #[test] fn declaration_simple() { let mut input = Parser::new(); @@ -2757,6 +2794,22 @@ fn descriptive_binding_parenthesised_tuple() { } } +#[test] +fn descriptive_prose_parens_stay_text() { + // A bare `()` in ordinary prose is plain text, not the unit literal. + let mut input = Parser::new(); + input.initialize("Some prose with () in it.\n"); + let paragraphs = input + .read_descriptive() + .unwrap(); + + let descriptives = ¶graphs[0].0; + let Descriptive::Text(text) = &descriptives[0] else { + panic!("expected Text, got {:?}", descriptives[0]); + }; + assert_eq!(*text, "Some prose with () in it."); +} + #[test] fn descriptive_binding_naked_tuple_requires_parentheses() { let mut input = Parser::new(); diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index e5dc928..d36c146 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -46,6 +46,7 @@ pub enum ParsingError { MixedSectionContent(Span), InvalidInvocation(Span), InvalidFunction(Span), + InvalidLiteral(Span), InvalidCodeBlock(Span), InvalidStep(Span), InvalidSubstep(Span), @@ -81,6 +82,7 @@ impl ParsingError { | ParsingError::MixedSectionContent(span) | ParsingError::InvalidInvocation(span) | ParsingError::InvalidFunction(span) + | ParsingError::InvalidLiteral(span) | ParsingError::InvalidCodeBlock(span) | ParsingError::InvalidStep(span) | ParsingError::InvalidSubstep(span) @@ -1511,6 +1513,10 @@ impl<'i> Parser<'i> { self.advance(1); let span = self.span_since(start); Ok(Expression::Hole(span)) + } else if is_unit(content) { + self.advance(2); + let span = self.span_since(start); + Ok(Expression::Unit(span)) } else if is_numeric(content) { let numeric = self.read_numeric()?; let span = self.span_since(start); @@ -1547,6 +1553,18 @@ 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, @@ -3256,6 +3274,10 @@ fn is_repeat_keyword(content: &str) -> bool { re.is_match(content) } +fn is_unit(content: &str) -> bool { + content.starts_with("()") +} + fn is_function(content: &str) -> bool { // The head before `(` must not cross a `;` statement separator. A greedy // `.+?` matched across one — `b ; three(` looked like a call named diff --git a/src/problem/messages.rs b/src/problem/messages.rs index dd8f4dd..584760f 100644 --- a/src/problem/messages.rs +++ b/src/problem/messages.rs @@ -579,6 +579,15 @@ 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::InvalidCodeBlock(_) => { let examples = vec![ Expression::Execution( diff --git a/src/program/types.rs b/src/program/types.rs index c9bf713..22a27e4 100644 --- a/src/program/types.rs +++ b/src/program/types.rs @@ -128,6 +128,7 @@ pub enum Operation<'i> { Invoke(Invocable<'i>), Execute(Executable<'i>), Hole, + Unit, Prose(&'i str), Sequence(Vec>), /// The executable work hoisted from a procedure's description is an diff --git a/src/resolution/resolver.rs b/src/resolution/resolver.rs index a16d8d6..d102167 100644 --- a/src/resolution/resolver.rs +++ b/src/resolution/resolver.rs @@ -145,7 +145,8 @@ fn resolve_operation<'i>( | Operation::Number(_) | Operation::Multiline(_, _) | Operation::Prose(_) - | Operation::Hole => {} + | Operation::Hole + | Operation::Unit => {} } } @@ -211,7 +212,8 @@ fn gather_iterated<'i>(op: &Operation<'i>, iterated: &mut HashSet<&'i str>) { | Operation::Number(_) | Operation::Multiline(_, _) | Operation::Prose(_) - | Operation::Hole => {} + | Operation::Hole + | Operation::Unit => {} } } @@ -271,7 +273,8 @@ fn mark_iterated<'i>(op: &mut Operation<'i>, iterated: &HashSet<&str>) { | Operation::Number(_) | Operation::Multiline(_, _) | Operation::Prose(_) - | Operation::Hole => {} + | Operation::Hole + | Operation::Unit => {} } } @@ -362,7 +365,8 @@ fn check_scope<'i>( Operation::Number(_) | Operation::Multiline(_, _) | Operation::Prose(_) - | Operation::Hole => {} + | Operation::Hole + | Operation::Unit => {} } } diff --git a/src/runner/evaluator.rs b/src/runner/evaluator.rs index 3d0c3fa..646cb1a 100644 --- a/src/runner/evaluator.rs +++ b/src/runner/evaluator.rs @@ -199,6 +199,7 @@ pub fn evaluate<'i>( // A `?` reached outside a procedure invocation has no parameter name // to defer against; it stands for an as-yet-unsupplied value. Operation::Hole => Ok(Value::Futurae(String::new())), + Operation::Unit => Ok(Value::Unitus), Operation::Prose(_) | Operation::Prologue(_) | Operation::Section { .. } diff --git a/src/runner/runner.rs b/src/runner/runner.rs index c38d9b3..a83b26f 100644 --- a/src/runner/runner.rs +++ b/src/runner/runner.rs @@ -492,7 +492,8 @@ impl<'i, D: Driver> Runner<'i, D> { | Operation::Tablet(_) | Operation::List(_) | Operation::Prose(_) - | Operation::Hole => { + | Operation::Hole + | Operation::Unit => { let value = super::evaluator::evaluate(&self.library, &self.context, env, op)?; Ok(Conclusion::Completed(Outcome::Done(value))) } diff --git a/src/translation/checks/translate.rs b/src/translation/checks/translate.rs index 13845fe..c8d5906 100644 --- a/src/translation/checks/translate.rs +++ b/src/translation/checks/translate.rs @@ -612,6 +612,31 @@ run : assert_eq!(*n, 42); } +#[test] +fn expression_unit_translates() { + // The unit literal `()` lowers to Operation::Unit. + let source = r#" +% technique v1 + +run : + +{ + () +} + "# + .trim_ascii(); + let path = Path::new("Test.tq"); + let document = parsing::parse(path, source).expect("parse"); + let program = translate(&document).expect("translate"); + + let Operation::Sequence(ops) = &program.subroutines[0].body else { + panic!("expected Sequence"); + }; + let Operation::Unit = &ops[0] else { + panic!("expected Unit, got {:?}", ops[0]); + }; +} + #[test] fn expression_string_text_fragment_translates() { // A plain string literal becomes a single Text fragment. diff --git a/src/translation/translator.rs b/src/translation/translator.rs index 24b7b8c..a59fe9d 100644 --- a/src/translation/translator.rs +++ b/src/translation/translator.rs @@ -832,6 +832,7 @@ impl<'i> Translator<'i> { } } language::Expression::Hole(_) => Operation::Hole, + language::Expression::Unit(_) => Operation::Unit, language::Expression::Separator => Operation::Sequence(Vec::new()), } }