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
7 changes: 7 additions & 0 deletions src/domain/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions src/editor/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 17 additions & 2 deletions src/formatting/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, "?");
}
Expand Down Expand Up @@ -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<Expression>) {
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> {
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> {
Binding(Box<Expression<'i>>, Vec<Identifier<'i>>, Span),
Pair(Box<Pair<'i>>, Span),
List(Vec<Expression<'i>>, Span),
Tuple(Vec<Expression<'i>>, Span),
Hole(Span),
Unit(Span),
Separator,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/linking/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
8 changes: 4 additions & 4 deletions src/parsing/checks/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,28 +264,28 @@ making_coffee :
}

#[test]
fn invalid_literal_whitespace_parens() {
fn invalid_tuple_whitespace_parens() {
expect_error(
r#"
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 :

1. Do something { (x) }
"#
.trim_ascii(),
ParsingError::InvalidLiteral(Span::new(39, 3)),
ParsingError::InvalidTuple(Span::new(39, 3)),
);
}

Expand Down
224 changes: 224 additions & 0 deletions src/parsing/checks/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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]
Expand Down
Loading