Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/domain/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/editor/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
5 changes: 4 additions & 1 deletion src/formatting/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {}
}
Expand Down
2 changes: 2 additions & 0 deletions src/formatting/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub enum Syntax {
StepItem,
CodeBlock,
Variable,
Hole,
Unit,
Section,
String,
Numeric,
Expand Down
8 changes: 8 additions & 0 deletions src/highlighting/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/highlighting/typst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions src/language/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ pub enum Expression<'i> {
Pair(Box<Pair<'i>>, Span),
List(Vec<Expression<'i>>, Span),
Hole(Span),
Unit(Span),
Separator,
}

Expand All @@ -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,
}
Expand Down
3 changes: 2 additions & 1 deletion src/linking/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ fn link_operation<'i>(
| Operation::Number(_)
| Operation::Multiline(_, _)
| Operation::Prose(_)
| Operation::Hole => {}
| Operation::Hole
| Operation::Unit => {}
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/parsing/checks/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
53 changes: 53 additions & 0 deletions src/parsing/checks/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<name>()`, 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("<name>()");
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();
Expand Down Expand Up @@ -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 = &paragraphs[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();
Expand Down
22 changes: 22 additions & 0 deletions src/parsing/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub enum ParsingError {
MixedSectionContent(Span),
InvalidInvocation(Span),
InvalidFunction(Span),
InvalidLiteral(Span),
InvalidCodeBlock(Span),
InvalidStep(Span),
InvalidSubstep(Span),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/problem/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/program/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ pub enum Operation<'i> {
Invoke(Invocable<'i>),
Execute(Executable<'i>),
Hole,
Unit,
Prose(&'i str),
Sequence(Vec<Operation<'i>>),
/// The executable work hoisted from a procedure's description is an
Expand Down
12 changes: 8 additions & 4 deletions src/resolution/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ fn resolve_operation<'i>(
| Operation::Number(_)
| Operation::Multiline(_, _)
| Operation::Prose(_)
| Operation::Hole => {}
| Operation::Hole
| Operation::Unit => {}
}
}

Expand Down Expand Up @@ -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 => {}
}
}

Expand Down Expand Up @@ -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 => {}
}
}

Expand Down Expand Up @@ -362,7 +365,8 @@ fn check_scope<'i>(
Operation::Number(_)
| Operation::Multiline(_, _)
| Operation::Prose(_)
| Operation::Hole => {}
| Operation::Hole
| Operation::Unit => {}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/runner/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. }
Expand Down
3 changes: 2 additions & 1 deletion src/runner/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
Expand Down
25 changes: 25 additions & 0 deletions src/translation/checks/translate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/translation/translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
}
Expand Down