diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index a43219e75..1a2116704 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -317,17 +317,11 @@ impl Spanned<&ExprBinary> {
return Value::Error;
}
- let lhty = lhs.type_name();
- let rhty = rhs.type_name();
+ let (l, r) = (lhs.type_name(), rhs.type_name());
+
let out = op(lhs, rhs);
if out == Value::Error {
- ctx.diag(error!(
- self.span,
- "cannot apply '{}' to {} and {}",
- self.v.op.v.as_str(),
- lhty,
- rhty,
- ));
+ self.error(ctx, l, r);
}
out
@@ -358,13 +352,41 @@ impl Spanned<&ExprBinary> {
}
};
- if let Ok(mut slot) = slot.try_borrow_mut() {
- *slot = op(std::mem::take(&mut slot), rhs);
- return Value::None;
+ let (constant, err, value) = if let Ok(mut inner) = slot.try_borrow_mut() {
+ let lhs = std::mem::take(&mut *inner);
+ let types = (lhs.type_name(), rhs.type_name());
+
+ *inner = op(lhs, rhs);
+ if *inner == Value::Error {
+ (false, Some(types), Value::Error)
+ } else {
+ (false, None, Value::None)
+ }
+ } else {
+ (true, None, Value::Error)
+ };
+
+ if constant {
+ ctx.diag(error!(span, "cannot assign to a constant"));
}
- ctx.diag(error!(span, "cannot assign to a constant"));
- Value::Error
+ if let Some((l, r)) = err {
+ self.error(ctx, l, r);
+ }
+
+ value
+ }
+
+ fn error(&self, ctx: &mut EvalContext, l: &str, r: &str) {
+ let op = self.v.op.v.as_str();
+ let message = match self.v.op.v {
+ BinOp::Add => format!("cannot add {} and {}", l, r),
+ BinOp::Sub => format!("cannot subtract {1} from {0}", l, r),
+ BinOp::Mul => format!("cannot multiply {} with {}", l, r),
+ BinOp::Div => format!("cannot divide {} by {}", l, r),
+ _ => format!("cannot apply '{}' to {} and {}", op, l, r),
+ };
+ ctx.diag(error!(self.span, "{}", message));
}
}
diff --git a/src/eval/ops.rs b/src/eval/ops.rs
index 56d6687b4..c52a62ca7 100644
--- a/src/eval/ops.rs
+++ b/src/eval/ops.rs
@@ -44,10 +44,16 @@ pub fn add(lhs: Value, rhs: Value) -> Value {
(Linear(a), Length(b)) => Linear(a + b),
(Linear(a), Relative(b)) => Linear(a + b),
(Linear(a), Linear(b)) => Linear(a + b),
+
(Str(a), Str(b)) => Str(a + &b),
(Array(a), Array(b)) => Array(concat(a, b)),
(Dict(a), Dict(b)) => Dict(concat(a, b)),
+
+ // TODO: Add string and template.
(Template(a), Template(b)) => Template(concat(a, b)),
+ (Template(a), None) => Template(a),
+ (None, Template(b)) => Template(b),
+
_ => Error,
}
}
diff --git a/src/parse/parser.rs b/src/parse/parser.rs
index 2ca8eb102..b77677727 100644
--- a/src/parse/parser.rs
+++ b/src/parse/parser.rs
@@ -205,7 +205,7 @@ impl<'s> Parser<'s> {
pub fn expect(&mut self, t: Token) -> bool {
let eaten = self.eat_if(t);
if !eaten {
- self.expected(t.name());
+ self.expected_at(t.name(), self.last_end);
}
eaten
}
diff --git a/tests/lang/ref/arrays.png b/tests/lang/ref/array.png
similarity index 100%
rename from tests/lang/ref/arrays.png
rename to tests/lang/ref/array.png
diff --git a/tests/lang/ref/block-invalid.png b/tests/lang/ref/block-invalid.png
new file mode 100644
index 000000000..56471574b
Binary files /dev/null and b/tests/lang/ref/block-invalid.png differ
diff --git a/tests/lang/ref/block-value.png b/tests/lang/ref/block-value.png
new file mode 100644
index 000000000..a3c03698e
Binary files /dev/null and b/tests/lang/ref/block-value.png differ
diff --git a/tests/lang/ref/blocks.png b/tests/lang/ref/blocks.png
deleted file mode 100644
index 8dd7f9404..000000000
Binary files a/tests/lang/ref/blocks.png and /dev/null differ
diff --git a/tests/lang/ref/bracket-call.png b/tests/lang/ref/bracket-call.png
deleted file mode 100644
index fe7f24346..000000000
Binary files a/tests/lang/ref/bracket-call.png and /dev/null differ
diff --git a/tests/lang/ref/call-args.png b/tests/lang/ref/call-args.png
new file mode 100644
index 000000000..0ce9000a6
Binary files /dev/null and b/tests/lang/ref/call-args.png differ
diff --git a/tests/lang/ref/call-bracket.png b/tests/lang/ref/call-bracket.png
new file mode 100644
index 000000000..2f020629d
Binary files /dev/null and b/tests/lang/ref/call-bracket.png differ
diff --git a/tests/lang/ref/call-chain.png b/tests/lang/ref/call-chain.png
new file mode 100644
index 000000000..5c90dbd80
Binary files /dev/null and b/tests/lang/ref/call-chain.png differ
diff --git a/tests/lang/ref/call-invalid.png b/tests/lang/ref/call-invalid.png
new file mode 100644
index 000000000..e03d84c11
Binary files /dev/null and b/tests/lang/ref/call-invalid.png differ
diff --git a/tests/lang/ref/comment.png b/tests/lang/ref/comment.png
new file mode 100644
index 000000000..79f438b2f
Binary files /dev/null and b/tests/lang/ref/comment.png differ
diff --git a/tests/lang/ref/comments.png b/tests/lang/ref/comments.png
deleted file mode 100644
index df5e5b9cb..000000000
Binary files a/tests/lang/ref/comments.png and /dev/null differ
diff --git a/tests/lang/ref/dict.png b/tests/lang/ref/dict.png
new file mode 100644
index 000000000..45d23ad39
Binary files /dev/null and b/tests/lang/ref/dict.png differ
diff --git a/tests/lang/ref/dictionaries.png b/tests/lang/ref/dictionaries.png
deleted file mode 100644
index aa40549e6..000000000
Binary files a/tests/lang/ref/dictionaries.png and /dev/null differ
diff --git a/tests/lang/ref/emph-strong.png b/tests/lang/ref/emph-strong.png
deleted file mode 100644
index 36392f8c0..000000000
Binary files a/tests/lang/ref/emph-strong.png and /dev/null differ
diff --git a/tests/lang/ref/emph.png b/tests/lang/ref/emph.png
new file mode 100644
index 000000000..a3aae7269
Binary files /dev/null and b/tests/lang/ref/emph.png differ
diff --git a/tests/lang/ref/escape.png b/tests/lang/ref/escape.png
new file mode 100644
index 000000000..aafe09eb2
Binary files /dev/null and b/tests/lang/ref/escape.png differ
diff --git a/tests/lang/ref/escaping.png b/tests/lang/ref/escaping.png
deleted file mode 100644
index 575a4b2fc..000000000
Binary files a/tests/lang/ref/escaping.png and /dev/null differ
diff --git a/tests/lang/ref/expr-binary.png b/tests/lang/ref/expr-binary.png
new file mode 100644
index 000000000..6c94ef9b0
Binary files /dev/null and b/tests/lang/ref/expr-binary.png differ
diff --git a/tests/lang/ref/for-invalid.png b/tests/lang/ref/for-invalid.png
new file mode 100644
index 000000000..fa65f4957
Binary files /dev/null and b/tests/lang/ref/for-invalid.png differ
diff --git a/tests/lang/ref/for-loop.png b/tests/lang/ref/for-loop.png
new file mode 100644
index 000000000..2f13985ad
Binary files /dev/null and b/tests/lang/ref/for-loop.png differ
diff --git a/tests/lang/ref/for-value.png b/tests/lang/ref/for-value.png
new file mode 100644
index 000000000..fa323edcd
Binary files /dev/null and b/tests/lang/ref/for-value.png differ
diff --git a/tests/lang/ref/heading.png b/tests/lang/ref/heading.png
new file mode 100644
index 000000000..f72806ec9
Binary files /dev/null and b/tests/lang/ref/heading.png differ
diff --git a/tests/lang/ref/headings.png b/tests/lang/ref/headings.png
deleted file mode 100644
index b1ad33666..000000000
Binary files a/tests/lang/ref/headings.png and /dev/null differ
diff --git a/tests/lang/ref/if-branch.png b/tests/lang/ref/if-branch.png
new file mode 100644
index 000000000..8bb852968
Binary files /dev/null and b/tests/lang/ref/if-branch.png differ
diff --git a/tests/lang/ref/if-invalid.png b/tests/lang/ref/if-invalid.png
new file mode 100644
index 000000000..319fbdbdf
Binary files /dev/null and b/tests/lang/ref/if-invalid.png differ
diff --git a/tests/lang/ref/if.png b/tests/lang/ref/if.png
deleted file mode 100644
index b763034f2..000000000
Binary files a/tests/lang/ref/if.png and /dev/null differ
diff --git a/tests/lang/ref/let-invalid.png b/tests/lang/ref/let-invalid.png
new file mode 100644
index 000000000..19d4d545d
Binary files /dev/null and b/tests/lang/ref/let-invalid.png differ
diff --git a/tests/lang/ref/let-terminated.png b/tests/lang/ref/let-terminated.png
new file mode 100644
index 000000000..24f20c693
Binary files /dev/null and b/tests/lang/ref/let-terminated.png differ
diff --git a/tests/lang/ref/let.png b/tests/lang/ref/let.png
deleted file mode 100644
index c555a9a07..000000000
Binary files a/tests/lang/ref/let.png and /dev/null differ
diff --git a/tests/lang/ref/linebreak.png b/tests/lang/ref/linebreak.png
new file mode 100644
index 000000000..4769e2913
Binary files /dev/null and b/tests/lang/ref/linebreak.png differ
diff --git a/tests/lang/ref/linebreaks.png b/tests/lang/ref/linebreaks.png
deleted file mode 100644
index 5268ed64c..000000000
Binary files a/tests/lang/ref/linebreaks.png and /dev/null differ
diff --git a/tests/lang/ref/raw.png b/tests/lang/ref/raw.png
index 2a5b083db..0da49c1b8 100644
Binary files a/tests/lang/ref/raw.png and b/tests/lang/ref/raw.png differ
diff --git a/tests/lang/ref/repr.png b/tests/lang/ref/repr.png
new file mode 100644
index 000000000..f3bf781eb
Binary files /dev/null and b/tests/lang/ref/repr.png differ
diff --git a/tests/lang/ref/spacing.png b/tests/lang/ref/spacing.png
new file mode 100644
index 000000000..5c3acf9b2
Binary files /dev/null and b/tests/lang/ref/spacing.png differ
diff --git a/tests/lang/ref/strong.png b/tests/lang/ref/strong.png
new file mode 100644
index 000000000..eb5e4d8e7
Binary files /dev/null and b/tests/lang/ref/strong.png differ
diff --git a/tests/lang/ref/text.png b/tests/lang/ref/text.png
index 54479229c..88ce95e33 100644
Binary files a/tests/lang/ref/text.png and b/tests/lang/ref/text.png differ
diff --git a/tests/lang/ref/values.png b/tests/lang/ref/values.png
deleted file mode 100644
index df46bd2d9..000000000
Binary files a/tests/lang/ref/values.png and /dev/null differ
diff --git a/tests/lang/typ/arrays.typ b/tests/lang/typ/array.typ
similarity index 85%
rename from tests/lang/typ/arrays.typ
rename to tests/lang/typ/array.typ
index c91a5ac18..f80cc0cd4 100644
--- a/tests/lang/typ/arrays.typ
+++ b/tests/lang/typ/array.typ
@@ -1,3 +1,6 @@
+// Test arrays.
+
+---
// Empty.
{()}
@@ -16,24 +19,19 @@
, #003
,)}
-// Missing closing paren.
// Error: 3-3 expected closing paren
{(}
-// Not an array.
// Error: 2-3 expected expression, found closing paren
{)}
-// Missing comma and bad expression.
// Error: 2:4-2:4 expected comma
// Error: 1:4-1:6 expected expression, found end of block comment
{(1*/2)}
-// Bad expression.
// Error: 6-8 expected expression, found invalid token
{(1, 1u 2)}
-// Leading comma is not an expression.
// Error: 3-4 expected expression, found comma
{(,1)}
diff --git a/tests/lang/typ/block-invalid.typ b/tests/lang/typ/block-invalid.typ
new file mode 100644
index 000000000..cf51b91b0
--- /dev/null
+++ b/tests/lang/typ/block-invalid.typ
@@ -0,0 +1,37 @@
+// Test invalid code block syntax.
+
+---
+// Multiple unseparated expressions in one line.
+
+// Error: 2-4 expected expression, found invalid token
+{1u}
+
+// Should output `1`.
+// Error: 3-3 expected semicolon or line break
+{0 1}
+
+// Should output `2`.
+// Error: 2:13-2:13 expected semicolon or line break
+// Error: 1:24-1:24 expected semicolon or line break
+{#let x = -1 #let y = 3 x + y}
+
+// Should output `3`.
+{
+ // Error: 10-13 expected identifier, found string
+ #for "v"
+
+ // Error: 11-11 expected keyword `#in`
+ #for v #let z = 1 + 2
+
+ z
+}
+
+---
+// Ref: false
+// Error: 3:1-3:1 expected closing brace
+{
+
+---
+// Ref: false
+// Error: 1-2 unexpected closing brace
+}
diff --git a/tests/lang/typ/block-scoping.typ b/tests/lang/typ/block-scoping.typ
new file mode 100644
index 000000000..bd1f58675
--- /dev/null
+++ b/tests/lang/typ/block-scoping.typ
@@ -0,0 +1,35 @@
+// Test scoping with blocks.
+// Ref: false
+
+---
+// Block in template does not create a scope.
+{ #let x = 1 }
+#[test x, 1]
+
+---
+// Block in expression does create a scope.
+#let a = {
+ #let b = 1
+ b
+}
+
+#[test a, 1]
+
+// Error: 2-3 unknown variable
+{b}
+
+---
+// Multiple nested scopes.
+{
+ #let a = "a1"
+ {
+ #let a = "a2"
+ {
+ test(a, "a2")
+ #let a = "a3"
+ test(a, "a3")
+ }
+ test(a, "a2")
+ }
+ test(a, "a1")
+}
diff --git a/tests/lang/typ/block-value.typ b/tests/lang/typ/block-value.typ
new file mode 100644
index 000000000..62934ce67
--- /dev/null
+++ b/tests/lang/typ/block-value.typ
@@ -0,0 +1,38 @@
+// Test return value of code blocks.
+
+---
+All none
+
+// Nothing evaluates to none.
+{}
+
+// Let evaluates to none.
+{ #let v = 0 }
+
+// Trailing none evaluates to none.
+{
+ type("")
+ none
+}
+
+---
+// Evaluates to single expression.
+{ "Hello" }
+
+// Evaluates to trailing expression.
+{ #let x = "Hel"; x + "lo" }
+
+// Evaluates to concatenation of for loop bodies.
+{
+ #let parts = ("Hel", "lo")
+ #for s #in parts [{s}]
+}
+
+---
+// Works the same way in code environment.
+// Ref: false
+#[test {
+ #let x = 1
+ #let y = 2
+ x + y
+}, 3]
diff --git a/tests/lang/typ/blocks.typ b/tests/lang/typ/blocks.typ
deleted file mode 100644
index b2691ffb1..000000000
--- a/tests/lang/typ/blocks.typ
+++ /dev/null
@@ -1,17 +0,0 @@
-// Empty.
-{}
-
-// Basic expression.
-{1}
-
-// Bad expression.
-// Error: 2-4 expected expression, found invalid token
-{1u}
-
-// Missing closing brace in nested block.
-// Error: 5-5 expected closing brace
-{({1) + 1}
-
-// Missing closing bracket in template expression.
-// Error: 11-11 expected closing bracket
-{[_] + [3_}
diff --git a/tests/lang/typ/bracket-call.typ b/tests/lang/typ/bracket-call.typ
deleted file mode 100644
index a2d563c68..000000000
--- a/tests/lang/typ/bracket-call.typ
+++ /dev/null
@@ -1,107 +0,0 @@
-// Basic call, whitespace insignificant.
-#[f], #[ f ], #[
- f
-]
-
-#[f bold]
-
-#[f 1,]
-
-#[f a:2]
-
-#[f 1, a: (3, 4), 2, b: "5"]
-
----
-// Body and no body.
-#[f][#[f]]
-
-// Lots of potential bodies.
-#[f][f]#[f]
-
-// Multi-paragraph body.
-#[box][
- First
-
- Second
-]
-
----
-// Chained.
-#[f | f]
-
-// Multi-chain.
-#[f|f|f]
-
-// With body.
-// Error: 7-8 expected identifier, found integer
-#[f | 1 | box][💕]
-
-// Error: 2:3-2:3 expected identifier
-// Error: 1:4-1:4 expected identifier
-#[||f true]
-
-// Error: 7-7 expected identifier
-#[f 1|]
-
-// Error: 2:3-2:3 expected identifier
-// Error: 1:4-1:4 expected identifier
-#[|][Nope]
-
-// Error: 2:6-2:6 expected closing paren
-// Error: 1:9-1:10 expected expression, found closing paren
-#[f (|f )]
-
-// With actual functions.
-#[box width: 1cm | image "res/rhino.png"]
-
----
-// Error: 5-7 expected expression, found end of block comment
-#[f */]
-
-// Error: 8-9 expected expression, found colon
-#[f a:1:]
-
-// Error: 6-6 expected comma
-#[f 1 2]
-
-// Error: 2:5-2:6 expected identifier
-// Error: 1:7-1:7 expected expression
-#[f 1:]
-
-// Error: 5-6 expected identifier
-#[f 1:2]
-
-// Error: 5-8 expected identifier
-#[f (x):1]
-
----
-// Ref: false
-// Error: 2:3-2:4 expected function, found string
-#let x = "string"
-#[x]
-
-// Error: 3-4 expected identifier, found invalid token
-#[# 1]
-
-// Error: 4:1-4:1 expected identifier
-// Error: 3:1-3:1 expected closing bracket
-#[
-
----
-// Ref: false
-// Error: 2:3-2:4 expected identifier, found closing paren
-// Error: 3:1-3:1 expected closing bracket
-#[)
-
----
-// Error: 3:1-3:1 expected closing bracket
-#[f [*]
-
----
-// Error: 3:1-3:1 expected closing bracket
-#[f][`a]`
-
----
-// Error: 3:1-3:1 expected quote
-// Error: 2:1-2:1 expected closing bracket
-#[f "]
diff --git a/tests/lang/typ/call-args.typ b/tests/lang/typ/call-args.typ
new file mode 100644
index 000000000..cf79c1f02
--- /dev/null
+++ b/tests/lang/typ/call-args.typ
@@ -0,0 +1,34 @@
+// Test function call arguments.
+
+---
+// One argument.
+#[f bold]
+
+// One argument and trailing comma.
+#[f 1,]
+
+// One named argument.
+#[f a:2]
+
+// Mixed arguments.
+{f(1, a: (3, 4), 2, b: "5")}
+
+---
+// Error: 5-6 expected expression, found colon
+#[f :]
+
+// Error: 8-10 expected expression, found end of block comment
+#[f a:1*/]
+
+// Error: 6-6 expected comma
+#[f 1 2]
+
+// Error: 2:5-2:6 expected identifier
+// Error: 1:7-1:7 expected expression
+#[f 1:]
+
+// Error: 5-6 expected identifier
+#[f 1:2]
+
+// Error: 4-7 expected identifier
+{f((x):1)}
diff --git a/tests/lang/typ/call-bracket.typ b/tests/lang/typ/call-bracket.typ
new file mode 100644
index 000000000..2ee2c5d41
--- /dev/null
+++ b/tests/lang/typ/call-bracket.typ
@@ -0,0 +1,18 @@
+// Test bracketed function calls.
+
+---
+// Whitespace insignificant.
+#[f], #[ f ]
+
+// Body and no body.
+#[f][#[f]]
+
+// Tight functions.
+#[f]#[f]
+
+// Multi-paragraph body.
+#[align right][
+ First
+
+ Second
+]
diff --git a/tests/lang/typ/call-chain.typ b/tests/lang/typ/call-chain.typ
new file mode 100644
index 000000000..72899f958
--- /dev/null
+++ b/tests/lang/typ/call-chain.typ
@@ -0,0 +1,31 @@
+// Test bracket call chaining.
+
+---
+// Chained once.
+#[f | f]
+
+// Chained twice.
+#[f|f|f]
+
+// With body.
+// Error: 7-8 expected identifier, found integer
+#[f | 1 | box][💕]
+
+// With actual functions.
+#[box width: 1cm | image "res/rhino.png"]
+
+---
+// Error: 8-8 expected identifier
+#[f 1 |]
+
+// Error: 4-4 expected identifier
+#[ | f true]
+
+// Error: 2:3-2:3 expected identifier
+// Error: 1:4-1:4 expected identifier
+#[|][Nope]
+
+// Pipe wins over parens.
+// Error: 2:6-2:6 expected closing paren
+// Error: 1:9-1:10 expected expression, found closing paren
+#[f (|f )]
diff --git a/tests/lang/typ/call-invalid.typ b/tests/lang/typ/call-invalid.typ
new file mode 100644
index 000000000..8e3efa328
--- /dev/null
+++ b/tests/lang/typ/call-invalid.typ
@@ -0,0 +1,36 @@
+// Test invalid function calls.
+
+---
+// Error: 4-4 expected closing paren
+{f(}
+
+---
+// Error: 4:1-4:1 expected identifier
+// Error: 3:1-3:1 expected closing bracket
+#[
+
+---
+// Error: 3-3 expected identifier
+#[]
+
+// Error: 3-6 expected identifier, found string
+#["f"]
+
+// Error: 2:3-2:4 expected identifier, found opening paren
+// Error: 1:5-1:6 expected expression, found closing paren
+#[(f)]
+
+---
+#let x = "string"
+
+// Error: 3-4 expected function, found string
+#[x]
+
+---
+// Error: 3:1-3:1 expected closing bracket
+#[f][`a]`
+
+---
+// Error: 3:1-3:1 expected quote
+// Error: 2:1-2:1 expected closing bracket
+#[f "]
diff --git a/tests/lang/typ/call-paren.typ b/tests/lang/typ/call-paren.typ
new file mode 100644
index 000000000..482e20e83
--- /dev/null
+++ b/tests/lang/typ/call-paren.typ
@@ -0,0 +1,11 @@
+// Test parenthesized function calls.
+// Ref: false
+
+---
+// Whitespace insignificant.
+#[test type(1), "integer"]
+#[test type (1), "integer"]
+
+// From variable.
+#let alias = type
+#[test alias(alias), "function"]
diff --git a/tests/lang/typ/comments.typ b/tests/lang/typ/comment.typ
similarity index 65%
rename from tests/lang/typ/comments.typ
rename to tests/lang/typ/comment.typ
index 3cabb5869..524a24e3c 100644
--- a/tests/lang/typ/comments.typ
+++ b/tests/lang/typ/comment.typ
@@ -1,3 +1,6 @@
+// Test line and block comments.
+
+---
// Line comment acts as spacing.
A// you
B
@@ -7,12 +10,13 @@ C/*
/* */
*/D
-// Works in headers.
+// Works in code.
#[f /*1*/ a: "b" //
, 1]
+---
// End should not appear without start.
-// Error: 7-9 unexpected end of block comment
+// Error: 1:7-1:9 unexpected end of block comment
/* */ */
// Unterminated is okay.
diff --git a/tests/lang/typ/dictionaries.typ b/tests/lang/typ/dict.typ
similarity index 67%
rename from tests/lang/typ/dictionaries.typ
rename to tests/lang/typ/dict.typ
index 4f9e0f608..b12dbd590 100644
--- a/tests/lang/typ/dictionaries.typ
+++ b/tests/lang/typ/dict.typ
@@ -1,14 +1,18 @@
+// Test dictionaries.
+
+---
// Empty
{(:)}
// Two pairs.
{(one: 1, two: 2)}
-// Simple expression after this is already identified as a dictionary.
+---
+// Simple expression after already being identified as a dictionary.
// Error: 9-10 expected named pair, found expression
{(a: 1, b)}
-// Identified as dictionary by initial colon.
+// Identified as dictionary due to initial colon.
// Error: 4:4-4:5 expected named pair, found expression
// Error: 3:5-3:5 expected comma
// Error: 2:12-2:16 expected identifier
diff --git a/tests/lang/typ/emph-strong.typ b/tests/lang/typ/emph-strong.typ
deleted file mode 100644
index 2f77c7bbe..000000000
--- a/tests/lang/typ/emph-strong.typ
+++ /dev/null
@@ -1,11 +0,0 @@
-// Basic.
-_Emph_ and *strong*!
-
-// Inside of words.
-Pa_rtl_y emphasized or str*ength*ened.
-
-// Scoped to body.
-#[box][*Sco_ped] to body.
-
-// Unterminated is fine.
-_The End
diff --git a/tests/lang/typ/emph.typ b/tests/lang/typ/emph.typ
new file mode 100644
index 000000000..8e5812a87
--- /dev/null
+++ b/tests/lang/typ/emph.typ
@@ -0,0 +1,14 @@
+// Test emphasis toggle.
+
+---
+// Basic.
+_Emphasized!_
+
+// Inside of words.
+Partly em_phas_ized.
+
+// Scoped to body.
+#[box][_Scoped] to body.
+
+// Unterminated is fine.
+_The End
diff --git a/tests/lang/typ/escaping.typ b/tests/lang/typ/escape.typ
similarity index 76%
rename from tests/lang/typ/escaping.typ
rename to tests/lang/typ/escape.typ
index 57d8ec689..3c0f02f38 100644
--- a/tests/lang/typ/escaping.typ
+++ b/tests/lang/typ/escape.typ
@@ -1,8 +1,11 @@
+// Test escape sequences.
+
+---
// Escapable symbols.
-\\ \/ \[ \] \{ \} \* \_ \# \~ \` \$
+\\ \/ \[ \] \{ \} \# \* \_ \= \~ \` \$
// No need to escape.
-( ) = ;
+( ) ; < >
// Unescapable.
\a \: \; \( \)
@@ -12,7 +15,7 @@
\/\* \*\/
\/* \*/ *
-// Test unicode escape sequence.
+// Unicode escape sequence.
\u{1F3D5} == 🏕
// Escaped escape sequence.
diff --git a/tests/lang/typ/expr-assoc.typ b/tests/lang/typ/expr-assoc.typ
new file mode 100644
index 000000000..64db98c68
--- /dev/null
+++ b/tests/lang/typ/expr-assoc.typ
@@ -0,0 +1,18 @@
+// Test operator associativity.
+// Ref: false
+
+---
+// Math operators are left-associative.
+#[test 10 / 2 / 2 == (10 / 2) / 2, true]
+#[test 10 / 2 / 2 == 10 / (2 / 2), false]
+#[test 1 / 2 * 3, 1.5]
+
+---
+// Assignment is right-associative.
+{
+ #let x = 1
+ #let y = 2
+ x = y = "ok"
+ test(x, none)
+ test(y, "ok")
+}
diff --git a/tests/lang/typ/expr-binary.typ b/tests/lang/typ/expr-binary.typ
new file mode 100644
index 000000000..b02269981
--- /dev/null
+++ b/tests/lang/typ/expr-binary.typ
@@ -0,0 +1,132 @@
+// Test binary expressions.
+// Ref: false
+
+---
+// Test template addition.
+// Ref: true
+{[*Hello ] + [world!*]}
+
+---
+// Test math operators.
+
+// Addition.
+#[test 2 + 4, 6]
+#[test "a" + "b", "ab"]
+#[test (1, 2) + (3, 4), (1, 2, 3, 4)]
+#[test (a: 1) + (b: 2, c: 3), (a: 1, b: 2, c: 3)]
+
+// Subtraction.
+#[test 1-4, 3*-1]
+#[test 4cm - 2cm, 2cm]
+#[test 1e+2-1e-2, 99.99]
+
+// Multiplication.
+#[test 2 * 4, 8]
+
+// Division.
+#[test 12pt/.4, 30pt]
+#[test 7 / 2, 3.5]
+
+// Combination.
+#[test 3-4 * 5 < -10, true]
+#[test { #let x; x = 1 + 4*5 >= 21 and { x = "a"; x + "b" == "ab" }; x }, true]
+
+// Mathematical identities.
+#let nums = (1, 3.14, 12pt, 45deg, 90%, 13% + 10pt)
+#for v #in nums {
+ // Test plus and minus.
+ test(v + v - v, v)
+ test(v - v - v, -v)
+
+ // Test plus/minus and multiplication.
+ test(v - v, 0 * v)
+ test(v + v, 2 * v)
+
+ // Integer addition does not give a float.
+ #if type(v) != "integer" {
+ test(v + v, 2.0 * v)
+ }
+
+ // Linears cannot be divided by themselves.
+ #if type(v) != "linear" {
+ test(v / v, 1.0)
+ test(v / v == 1, true)
+ }
+}
+
+
+// Make sure length, relative and linear
+// - can all be added to / subtracted from each other,
+// - multiplied with integers and floats,
+// - divided by floats.
+#let dims = (10pt, 30%, 50% + 3cm)
+#for a #in dims {
+ #for b #in dims {
+ test(type(a + b), type(a - b))
+ }
+
+ #for b #in (7, 3.14) {
+ test(type(a * b), type(a))
+ test(type(b * a), type(a))
+ test(type(a / b), type(a))
+ }
+}
+
+---
+// Test boolean operators.
+
+// And.
+#[test false and false, false]
+#[test false and true, false]
+#[test true and false, false]
+#[test true and true, true]
+
+// Or.
+#[test false or false, false]
+#[test false or true, true]
+#[test true or false, true]
+#[test true or true, true]
+
+// Short-circuiting.
+#[test false and dont-care, false]
+#[test true or dont-care, true]
+
+---
+// Test equality operators.
+
+#[test 1 == "hi", false]
+#[test 1 == 1.0, true]
+#[test 30% == 30% + 0cm, true]
+#[test 1in == 0% + 72pt, true]
+#[test 30% == 30% + 1cm, false]
+#[test "ab" == "a" + "b", true]
+#[test () == (1,), false]
+#[test (1, 2, 3) == (1, 2.0) + (3,), true]
+#[test (:) == (a: 1), false]
+#[test (a: 2 - 1.0, b: 2) == (b: 2, a: 1), true]
+#[test [*Hi*] == [*Hi*], true]
+
+#[test "a" != "a", false]
+#[test [*] != [_], true]
+
+---
+// Test comparison operators.
+
+#[test 13 * 3 < 14 * 4, true]
+#[test 5 < 10, true]
+#[test 5 > 5, false]
+#[test 5 <= 5, true]
+#[test 5 <= 4, false]
+#[test 45deg < 1rad, true]
+
+---
+// Test assignment operators.
+
+#let x = 0
+{ x = 10 } #[test x, 10]
+{ x -= 5 } #[test x, 5]
+{ x += 1 } #[test x, 6]
+{ x *= x } #[test x, 36]
+{ x /= 2.0 } #[test x, 18.0]
+{ x = "some" } #[test x, "some"]
+{ x += "thing" } #[test x, "something"]
diff --git a/tests/lang/typ/expr-invalid.typ b/tests/lang/typ/expr-invalid.typ
new file mode 100644
index 000000000..2d16034bb
--- /dev/null
+++ b/tests/lang/typ/expr-invalid.typ
@@ -0,0 +1,59 @@
+// Test invalid expressions.
+// Ref: false
+
+---
+// Missing expressions.
+
+// Error: 3-3 expected expression
+{-}
+
+// Error: 11-11 expected expression
+#[test {1+}, 1]
+
+// Error: 11-11 expected expression
+#[test {2*}, 2]
+
+---
+// Mismatched types.
+
+// Error: 2-12 cannot apply '+' to template
+{+([] + [])}
+
+// Error: 2-5 cannot apply '-' to string
+{-""}
+
+// Error: 2-8 cannot apply 'not' to array
+{not ()}
+
+// Error: 1:2-1:12 cannot apply '<=' to relative and relative
+{30% <= 40%}
+
+// Special messages for +, -, * and /.
+// Error: 4:03-4:10 cannot add integer and string
+// Error: 3:12-3:19 cannot subtract integer from relative
+// Error: 2:21-2:29 cannot multiply integer with boolean
+// Error: 1:31-1:39 cannot divide integer by length
+{(1 + "2", 40% - 1, 2 * true, 3 / 12pt)}
+
+// Error: 15-23 cannot apply '+=' to integer and string
+{ #let x = 1; x += "2" }
+
+---
+// Bad left-hand sides of assignment.
+
+// Error: 1:3-1:6 cannot assign to this expression
+{ (x) = "" }
+
+// Error: 1:3-1:8 cannot assign to this expression
+{ 1 + 2 += 3 }
+
+// Error: 1:3-1:4 unknown variable
+{ z = 1 }
+
+// Error: 1:3-1:6 cannot assign to a constant
+{ box = "hi" }
+
+// Works if we define box beforehand
+// (since then it doesn't resolve to the standard library version anymore).
+#let box = ""
+{ box = "hi" }
diff --git a/tests/lang/typ/expr-prec.typ b/tests/lang/typ/expr-prec.typ
new file mode 100644
index 000000000..fea5f7949
--- /dev/null
+++ b/tests/lang/typ/expr-prec.typ
@@ -0,0 +1,30 @@
+// Test operator precedence.
+// Ref: false
+
+---
+// Multiplication binds stronger than addition.
+#[test 1+2*-3, -5]
+
+// Subtraction binds stronger than comparison.
+#[test 3 == 5 - 2, true]
+
+// Boolean operations bind stronger than '=='.
+#[test "a" == "a" and 2 < 3, true]
+#[test not "b" == "b", false]
+
+// Assignment binds stronger than boolean operations.
+// Error: 2-7 cannot assign to this expression
+{not x = "a"}
+
+---
+// Parentheses override precedence.
+#[test (1), 1]
+#[test (1+2)*-3, -9]
+
+// Error: 15-15 expected closing paren
+#[test {(1 + 1}, 2]
+
+---
+// Precedence doesn't matter for chained unary operators.
+// Error: 2-11 cannot apply '-' to boolean
+{-not true}
diff --git a/tests/lang/typ/expr-unary.typ b/tests/lang/typ/expr-unary.typ
new file mode 100644
index 000000000..a1d97a494
--- /dev/null
+++ b/tests/lang/typ/expr-unary.typ
@@ -0,0 +1,23 @@
+// Test unary expressions.
+// Ref: false
+
+---
+// Test plus and minus.
+#for v #in (1, 3.14, 12pt, 45deg, 90%, 13% + 10pt) {
+ // Test plus.
+ test(+v, v)
+
+ // Test minus.
+ test(-v, -1 * v)
+ test(--v, v)
+
+ // Test combination.
+ test(-++ --v, -v)
+}
+
+#[test -(4 + 2), 6-12]
+
+---
+// Test not.
+#[test not true, false]
+#[test not false, true]
diff --git a/tests/lang/typ/expressions.typ b/tests/lang/typ/expressions.typ
deleted file mode 100644
index 167d0d1a7..000000000
--- a/tests/lang/typ/expressions.typ
+++ /dev/null
@@ -1,120 +0,0 @@
-// Ref: false
-
-#let a = 2
-#let b = 4
-
-// Error: 14-17 cannot apply '+' to string
-#let error = +""
-
-// Paren call.
-#[test f(1), "f(1)"]
-#[test type(1), "integer"]
-
-// Unary operations.
-#[test +1, 1]
-#[test -1, 1-2]
-#[test --1, 1]
-
-// Math operations.
-#[test "a" + "b", "ab"]
-#[test 1-4, 3*-1]
-#[test a * b, 8]
-#[test 12pt/.4, 30pt]
-#[test 1e+2-1e-2, 99.99]
-
-// Associativity.
-#[test 1+2+3, 6]
-#[test 1/2*3, 1.5]
-
-// Precedence.
-#[test 1+2*-3, -5]
-
-// Short-circuiting logical operators.
-#[test not "a" == "b", true]
-#[test not 7 < 4 and 10 == 10, true]
-#[test 3 < 2 or 4 < 5, true]
-#[test false and false or true, true]
-
-// Right-hand side not even evaluated.
-#[test false and dont-care, false]
-#[test true or dont-care, true]
-
-// Equality and inequality.
-#[test "ab" == "a" + "b", true]
-#[test [*Hi*] == [*Hi*], true]
-#[test "a" != "a", false]
-#[test [*] != [_], true]
-#[test (1, 2, 3) == (1, 2) + (3,), true]
-#[test () == (1,), false]
-#[test (a: 1, b: 2) == (b: 2, a: 1), true]
-#[test (:) == (a: 1), false]
-#[test 1 == "hi", false]
-#[test 1 == 1.0, true]
-#[test 30% == 30% + 0cm, true]
-#[test 1in == 0% + 72pt, true]
-#[test 30% == 30% + 1cm, false]
-
-// Comparisons.
-#[test 13 * 3 < 14 * 4, true]
-#[test 5 < 10, true]
-#[test 5 > 5, false]
-#[test 5 <= 5, true]
-#[test 5 <= 4, false]
-#[test 45deg < 1rad, true]
-
-// Assignment.
-#let x = "some"
-#let y = "some"
-#[test (x = y = "") == none and x == none and y == "", true]
-
-// Modify-assign operators.
-#let x = 0
-{ x = 10 } #[test x, 10]
-{ x -= 5 } #[test x, 5]
-{ x += 1 } #[test x, 6]
-{ x *= x } #[test x, 36]
-{ x /= 2.0 } #[test x, 18.0]
-{ x = "some" } #[test x, "some"]
-{ x += "thing" } #[test x, "something"]
-
-// Error: 3-4 unknown variable
-{ z = 1 }
-
-// Error: 3-6 cannot assign to this expression
-{ (x) = "" }
-
-// Error: 3-8 cannot assign to this expression
-{ 1 + 2 = 3}
-
-// Error: 3-6 cannot assign to a constant
-{ box = "hi" }
-
-// Works if we define box before (since then it doesn't resolve to the standard
-// library version anymore).
-#let box = ""; { box = "hi" }
-
-// Parentheses.
-#[test (a), 2]
-#[test (2), 2]
-#[test (1+2)*3, 9]
-
-// Error: 3-3 expected expression
-{-}
-
-// Error: 11-11 expected expression
-#[test {1+}, 1]
-
-// Error: 11-11 expected expression
-#[test {2*}, 2]
-
-// Error: 8-17 cannot apply '-' to boolean
-#[test -not true, error]
-
-// Error: 2-8 cannot apply 'not' to array
-{not ()}
-
-// Error: 3-10 cannot apply '+' to integer and string
-{(1 + "2")}
-
-// Error: 2-12 cannot apply '<=' to relative and relative
-{30% <= 40%}
diff --git a/tests/lang/typ/for-invalid.typ b/tests/lang/typ/for-invalid.typ
new file mode 100644
index 000000000..ca83649ab
--- /dev/null
+++ b/tests/lang/typ/for-invalid.typ
@@ -0,0 +1,35 @@
+// Test invalid for loop syntax.
+
+---
+// Error: 5-5 expected identifier
+#for
+
+// Error: 7-7 expected keyword `#in`
+#for v
+
+// Error: 11-11 expected expression
+#for v #in
+
+// Error: 16-16 expected body
+#for v #in iter
+
+---
+// Should output `v iter`.
+// Error: 2:5-2:5 expected identifier
+// Error: 2:3-2:6 unexpected keyword `#in`
+#for
+v #in iter {}
+
+// Should output `A thing`.
+// Error: 7-10 expected identifier, found string
+A#for "v" thing.
+
+// Should output `iter`.
+// Error: 2:6-2:9 expected identifier, found string
+// Error: 1:10-1:13 unexpected keyword `#in`
+#for "v" #in iter {}
+
+// Should output `+ b iter`.
+// Error: 2:7-2:7 expected keyword `#in`
+// Error: 1:12-1:15 unexpected keyword `#in`
+#for a + b #in iter {}
diff --git a/tests/lang/typ/for-loop.typ b/tests/lang/typ/for-loop.typ
new file mode 100644
index 000000000..944cef1ea
--- /dev/null
+++ b/tests/lang/typ/for-loop.typ
@@ -0,0 +1,44 @@
+// Test which things are iterable.
+// Ref: false
+
+---
+// Array.
+
+#for x #in () {}
+
+#let sum = 0
+#for x #in (1, 2, 3, 4, 5) {
+ sum += x
+}
+#[test sum, 15]
+
+---
+// Dictionary.
+// Ref: true
+(\ #for k, v #in (name: "Typst", age: 2) [
+ #[h 0.5cm] {k}: {v}, \
+])
+
+---
+// String.
+{
+ #let out = ""
+ #let first = true
+ #for c #in "abc" {
+ #if not first {
+ out += ", "
+ }
+ first = false
+ out += c
+ }
+ test(out, "a, b, c")
+}
+
+---
+// Uniterable expression.
+// Error: 12-16 cannot loop over boolean
+#for v #in true {}
+
+// Make sure that we don't complain twice.
+// Error: 12-19 cannot add integer and string
+#for v #in 1 + "2" {}
diff --git a/tests/lang/typ/for-pattern.typ b/tests/lang/typ/for-pattern.typ
new file mode 100644
index 000000000..87bf603fe
--- /dev/null
+++ b/tests/lang/typ/for-pattern.typ
@@ -0,0 +1,30 @@
+// Test for loop patterns.
+// Ref: false
+
+---
+#let out = ()
+
+// Values of array.
+#for v #in (1, 2, 3) {
+ out += (v,)
+}
+
+// Values of dictionary.
+#for v #in (a: 4, b: 5) {
+ out += (v,)
+}
+
+// Keys and values of dictionary.
+#for k, v #in (a: 6, b: 7) {
+ out += (k,)
+ out += (v,)
+}
+
+#[test out, (1, 2, 3, 4, 5, "a", 6, "b", 7)]
+
+---
+// Keys and values of array.
+// Error: 6-10 mismatched pattern
+#for k, v #in (-1, -2, -3) {
+ dont-care
+}
diff --git a/tests/lang/typ/for-value.typ b/tests/lang/typ/for-value.typ
new file mode 100644
index 000000000..f0705fc5f
--- /dev/null
+++ b/tests/lang/typ/for-value.typ
@@ -0,0 +1,20 @@
+// Test return value of for loops.
+
+---
+// Template body yields template.
+// Should output `234`.
+#for v #in (1, 2, 3, 4) [#if v >= 2 [{v}]]
+
+---
+// Block body yields template.
+// Should output `[1st, 2nd, 3rd, 4th, 5th, 6th]`.
+{
+ [\[] + #for v #in (1, 2, 3, 4, 5, 6) {
+ (#if v > 1 [, ]
+ + [{v}]
+ + #if v == 1 [st]
+ + #if v == 2 [nd]
+ + #if v == 3 [rd]
+ + #if v >= 4 [th])
+ } + [\]]
+}
diff --git a/tests/lang/typ/headings.typ b/tests/lang/typ/heading.typ
similarity index 56%
rename from tests/lang/typ/headings.typ
rename to tests/lang/typ/heading.typ
index 2fd2d2aca..9bff2e6e8 100644
--- a/tests/lang/typ/headings.typ
+++ b/tests/lang/typ/heading.typ
@@ -1,41 +1,44 @@
-// Test different numbers of hashtags.
-
-// Valid levels.
-= One
-=== Three
-====== Six
-
-// Too many hashtags.
-// Warning: 1-8 should not exceed depth 6
-======= Seven
+// Test headings.
---
-// Test heading vs. no heading.
+// Different number of hashtags.
-// Parsed as headings if at start of the context.
-/**/ = Heading
-{[== Heading]}
-#[box][=== Heading]
+// Valid levels.
+=1
+===2
+======6
-// Not at the start of the context.
-Text = with=sign
-
-// Escaped.
-\= No heading
+// Too many hashtags.
+// Warning: 1:1-1:8 should not exceed depth 6
+=======7
---
// Heading continuation over linebreak.
// Code blocks continue heading.
-= This {
- "continues"
+= A{
+ "B"
}
// Function call continues heading.
= #[box][
- This,
-] too
+ A
+] B
// Without some kind of block, headings end at a line break.
-= This
-not
+= A
+B
+
+---
+// Heading vs. no heading.
+
+// Parsed as headings if at start of the context.
+/**/ = Ok
+{[== Ok]}
+#[box][=== Ok]
+
+// Not at the start of the context.
+No = heading
+
+// Escaped.
+\= No heading
diff --git a/tests/lang/typ/if-branch.typ b/tests/lang/typ/if-branch.typ
new file mode 100644
index 000000000..8399d6741
--- /dev/null
+++ b/tests/lang/typ/if-branch.typ
@@ -0,0 +1,45 @@
+// Test conditions of if-else expressions.
+
+---
+// Test condition evaluation.
+#if 1 < 2 [
+ Ok.
+]
+
+#if true == false [
+ Bad, but we {dont-care}!
+]
+
+---
+// Brace in condition.
+#if {true} [
+ Ok.
+]
+
+// Multi-line condition with parens.
+#if (
+ 1 + 1
+ == 1
+) {
+ nope
+} #else {
+ "Ok."
+}
+
+// Multiline.
+#if false [
+ Bad.
+] #else {
+ #let pt = "."
+ "Ok" + pt
+}
+
+---
+// Condition must be boolean.
+// If it isn't, neither branch is evaluated.
+// Error: 5-14 expected boolean, found string
+#if "a" + "b" { nope } #else { nope }
+
+// Make sure that we don't complain twice.
+// Error: 5-12 cannot add integer and string
+#if 1 + "2" {}
diff --git a/tests/lang/typ/if-invalid.typ b/tests/lang/typ/if-invalid.typ
new file mode 100644
index 000000000..c7ead226c
--- /dev/null
+++ b/tests/lang/typ/if-invalid.typ
@@ -0,0 +1,28 @@
+// Test invalid if syntax.
+
+---
+// Error: 4-4 expected expression
+#if
+
+// Error: 5-5 expected expression
+{#if}
+
+// Error: 6-6 expected body
+#if x
+
+// Error: 1-6 unexpected keyword `#else`
+#else {}
+
+---
+// Should output `x`.
+// Error: 4-4 expected expression
+#if
+x {}
+
+// Should output `something`.
+// Error: 6-6 expected body
+#if x something
+
+// Should output `A thing.`
+// Error: 20-20 expected body
+A#if false {} #else thing
diff --git a/tests/lang/typ/if-value.typ b/tests/lang/typ/if-value.typ
new file mode 100644
index 000000000..f9e95ff6c
--- /dev/null
+++ b/tests/lang/typ/if-value.typ
@@ -0,0 +1,21 @@
+// Test return value of if expressions.
+// Ref: false
+
+---
+{
+ #let x = 1
+ #let y = 2
+ #let z
+
+ // Returns if branch.
+ z = #if x < y { "ok" }
+ test(z, "ok")
+
+ // Returns else branch.
+ z = #if x > y { "bad" } #else { "ok" }
+ test(z, "ok")
+
+ // Missing else evaluates to none.
+ z = #if x > y { "bad" }
+ test(z, none)
+}
diff --git a/tests/lang/typ/if.typ b/tests/lang/typ/if.typ
deleted file mode 100644
index 0e0d03d17..000000000
--- a/tests/lang/typ/if.typ
+++ /dev/null
@@ -1,64 +0,0 @@
-#let x = true
-
-// The two different bodies.
-#if true [_1_,] #if x {"2"}
-
-// Braced condition is fine.
-#if {true} {"3"}
-
-// Newlines.
-#if false [
-
-] #else [
- 4
-]
-
-// Multiline (condition needs parens because it's terminated by the line break,
-// just like the right-hand side of a let-binding).
-#if (
- x
-) {
- "Fi" + "ve"
-}
-
-// Spacing is somewhat delicate. We only want to have spacing in the output if
-// there was whitespace before/after the full if-else statement. In particular,
-// spacing after a simple if should be retained, but spacing between the first
-// body and the else should be ignored.
-a#if true[b]c \
-a#if true[b] c \
-a #if true{"b"}c \
-a #if true{"b"} c \
-a#if false [?] #else [b]c \
-a#if true {"b"} #else {"?"} c \
-
-// Body not evaluated at all if condition is false.
-#if false { dont-care-about-undefined-variables }
-
----
-#let x = true
-
-// Needs condition.
-// Error: 6-7 expected expression, found closing brace
-a#if }
-
-// Needs if-body.
-// Error: 2:7-2:7 expected body
-// Error: 1:16-1:16 expected body
-a#if x b#if (x)c
-
-// Needs else-body.
-// Error: 20-20 expected body
-a#if true [b] #else c
-
-// Lone else.
-// Error: 1-6 unexpected keyword `#else`
-#else []
-
-// Condition must be boolean. If it isn't, neither branch is evaluated.
-// Error: 5-14 expected boolean, found string
-#if "a" + "b" { "nope" } #else { "nope" }
-
-// No coercing from empty array or or stuff like that.
-// Error: 5-7 expected boolean, found array
-#if () { "nope" } #else { "nope" }
diff --git a/tests/lang/typ/let-invalid.typ b/tests/lang/typ/let-invalid.typ
new file mode 100644
index 000000000..3e32e2cf4
--- /dev/null
+++ b/tests/lang/typ/let-invalid.typ
@@ -0,0 +1,20 @@
+// Test invalid let binding syntax.
+
+---
+// Error: 5-5 expected identifier
+#let
+
+// Error: 6-9 expected identifier, found string
+#let "v"
+
+// Should output `1`.
+// Error: 7-7 expected semicolon or line break
+#let v 1
+
+// Error: 9-9 expected expression
+#let v =
+
+---
+// Should output `= 1`.
+// Error: 6-9 expected identifier, found string
+#let "v" = 1
diff --git a/tests/lang/typ/let-terminated.typ b/tests/lang/typ/let-terminated.typ
new file mode 100644
index 000000000..6dda7bb21
--- /dev/null
+++ b/tests/lang/typ/let-terminated.typ
@@ -0,0 +1,28 @@
+// Test termination of let statements.
+
+---
+// Terminated by line break.
+#let v1 = 1
+One
+
+// Terminated by semicolon.
+#let v2 = 2; Two
+
+// Terminated by semicolon and line break.
+#let v3 = 3;
+Three
+
+// Terminated because expression ends.
+// Error: 12-12 expected semicolon or line break
+#let v4 = 4 Four
+
+// Terminated by semicolon even though we are in a paren group.
+// Error: 2:19-2:19 expected expression
+// Error: 1:19-1:19 expected closing paren
+#let v5 = (1, 2 + ; Five
+
+#[test v1, 1]
+#[test v2, 2]
+#[test v3, 3]
+#[test v4, 4]
+#[test v5, (1, 2)]
diff --git a/tests/lang/typ/let-value.typ b/tests/lang/typ/let-value.typ
new file mode 100644
index 000000000..12e1ba78c
--- /dev/null
+++ b/tests/lang/typ/let-value.typ
@@ -0,0 +1,11 @@
+// Test value of let binding.
+// Ref: false
+
+---
+// Automatically initialized with none.
+#let x
+#[test x, none]
+
+// Manually initialized with one.
+#let x = 1
+#[test x, 1]
diff --git a/tests/lang/typ/let.typ b/tests/lang/typ/let.typ
deleted file mode 100644
index 4f42e44c6..000000000
--- a/tests/lang/typ/let.typ
+++ /dev/null
@@ -1,66 +0,0 @@
-// Automatically initialized with `none`.
-#let x
-#[test x, none]
-
-// Initialized with `1`.
-#let y = 1
-#[test y, 1]
-
-// Initialize with template, not terminated by semicolon in template.
-#let v = [Hello; there]
-
-// Not terminated by line break due to parens.
-#let x = (
- 1,
- 2,
- 3,
-)
-#[test x, (1, 2, 3)]
-
-// Multiple bindings in one line.
-#let x = "a"; #let y = "b"; #[test x + y, "ab"]
-
-// Invalid name.
-// Error: 6-7 expected identifier, found integer
-#let 1
-
-// Invalid name.
-// Error: 6-7 expected identifier, found integer
-#let 1 = 2
-
-// Missing binding name.
-// Error: 5-5 expected identifier
-#let
-x = 5
-
-// Missing right-hand side.
-// Error: 9-9 expected expression
-#let a =
-
-// No name at all.
-// Error: 11-11 expected identifier
-The Fi#let;rst
-
-// Terminated with just a line break.
-#let v = "a"
-The Second #[test v, "a"]
-
-// Terminated with semicolon + line break.
-#let v = "a";
-The Third #[test v, "a"]
-
-// Terminated with just a semicolon.
-The#let v = "a"; Fourth #[test v, "a"]
-
-// Terminated by semicolon even though we are in a paren group.
-// Error: 2:25-2:25 expected expression
-// Error: 1:25-1:25 expected closing paren
-The#let array = (1, 2 + ;Fifth #[test array, (1, 2)]
-
-// Not terminated.
-// Error: 16-16 expected semicolon or line break
-The#let v = "a"Sixth #[test v, "a"]
-
-// Not terminated.
-// Error: 16-16 expected semicolon or line break
-The#let v = "a" #[test v, "a"] Seventh
diff --git a/tests/lang/typ/linebreaks.typ b/tests/lang/typ/linebreak.typ
similarity index 75%
rename from tests/lang/typ/linebreaks.typ
rename to tests/lang/typ/linebreak.typ
index ee2f453ae..e63929924 100644
--- a/tests/lang/typ/linebreaks.typ
+++ b/tests/lang/typ/linebreak.typ
@@ -1,6 +1,6 @@
-// Leading line break.
-\ Leading
+// Test forced line breaks.
+---
// Directly after word.
Line\ Break
@@ -10,10 +10,14 @@ Line \ Break
// Directly before word does not work.
No \Break
-// Trailing before paragraph break.
-Paragraph 1 \
+---
+// Leading line break.
+\ Leading
-Paragraph 2
+// Trailing before paragraph break.
+Trailing 1 \
+
+Trailing 2
// Trailing before end of document.
-Paragraph 3 \
+Trailing 3 \
diff --git a/tests/lang/typ/nbsp.typ b/tests/lang/typ/nbsp.typ
index b2f099503..5af6c84f9 100644
--- a/tests/lang/typ/nbsp.typ
+++ b/tests/lang/typ/nbsp.typ
@@ -1,2 +1,5 @@
+// Test the non breaking space.
+
+---
// Parsed correctly, but not actually doing anything at the moment.
The non-breaking~space does not work.
diff --git a/tests/lang/typ/raw.typ b/tests/lang/typ/raw.typ
index 36149dc89..753f1a09c 100644
--- a/tests/lang/typ/raw.typ
+++ b/tests/lang/typ/raw.typ
@@ -1,12 +1,17 @@
-#[font 8pt]
+// Test raw blocks.
+---
// No extra space.
`A``B`
+---
// Typst syntax inside.
`#let x = 1` \
`#[f 1]`
+---
+// Trimming.
+
// Space between "rust" and "let" is trimmed.
The keyword ```rust let```.
@@ -19,15 +24,17 @@ The keyword ```rust let```.
```py
import this
-def say_hi():
- print("Hi!")
+def hi():
+ print("Hi!")
```
+---
// Lots of backticks inside.
````
```backticks```
````
+---
// Unterminated.
// Error: 2:1-2:1 expected backtick(s)
`endless
diff --git a/tests/lang/typ/values.typ b/tests/lang/typ/repr.typ
similarity index 95%
rename from tests/lang/typ/values.typ
rename to tests/lang/typ/repr.typ
index 7260cad10..1975a8e68 100644
--- a/tests/lang/typ/values.typ
+++ b/tests/lang/typ/repr.typ
@@ -1,10 +1,12 @@
// Test representation of values in the document.
+---
+// Variables.
+
#let name = "Typst"
#let ke-bab = "Kebab!"
#let α = "Alpha"
-// Variables.
{name} \
{ke-bab} \
{α} \
@@ -12,11 +14,13 @@
// Error: 2-3 unknown variable
{_}
+---
// Literal values.
{none} (empty) \
{true} \
{false} \
+---
// Numerical values.
{1} \
{1.0e-4} \
@@ -29,12 +33,15 @@
{2.5rad} \
{45deg} \
+---
// Colors.
{#f7a20500} \
+---
// Strings and escaping.
{"hi"} \
{"a\n[]\"\u{1F680}string"} \
+---
// Templates.
{[*{"Hi"} #[f 1]*]}
diff --git a/tests/lang/typ/spacing.typ b/tests/lang/typ/spacing.typ
new file mode 100644
index 000000000..54e49f2d7
--- /dev/null
+++ b/tests/lang/typ/spacing.typ
@@ -0,0 +1,27 @@
+// Test spacing around control flow structures.
+
+---
+// Spacing around let.
+
+// Error: 6-6 expected identifier
+A#let;B \
+A#let x = 1;B #[test x, 1] \
+A #let x = 2;B #[test x, 2] \
+A#let x = 3; B #[test x, 3] \
+
+---
+// Spacing around if-else.
+
+A#if true[B]C \
+A#if true[B] C \
+A #if true{"B"}C \
+A #if true{"B"} C \
+A#if false [] #else [B]C \
+A#if true [B] #else [] C \
+
+---
+// Spacing around for loop.
+
+A#for _ #in (none,) [B]C \
+A#for _ #in (none,) [B] C \
+A #for _ #in (none,) [B]C \
diff --git a/tests/lang/typ/strong.typ b/tests/lang/typ/strong.typ
new file mode 100644
index 000000000..63e6eb35b
--- /dev/null
+++ b/tests/lang/typ/strong.typ
@@ -0,0 +1,14 @@
+// Test strong toggle.
+
+---
+// Basic.
+*Strong!*
+
+// Inside of words.
+Partly str*ength*ened.
+
+// Scoped to body.
+#[box][*Scoped] to body.
+
+// Unterminated is fine.
+*The End
diff --git a/tests/lang/typ/text.typ b/tests/lang/typ/text.typ
index 9c1e79770..d86f48957 100644
--- a/tests/lang/typ/text.typ
+++ b/tests/lang/typ/text.typ
@@ -1 +1,8 @@
+// Test simple text.
+
+---
Hello 🌏!
+
+---
+// Some code stuff in text.
+let f() , ; : | + - /= == 12 "string"
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 76436ae84..63831c229 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -141,12 +141,29 @@ fn test(
let mut ok = true;
let mut frames = vec![];
-
let mut lines = 0;
- for (i, part) in src.split("---").enumerate() {
- let (part_ok, part_frames) = test_part(part, i, lines, env);
- ok &= part_ok;
- frames.extend(part_frames);
+ let mut compare_ref = true;
+
+ let parts: Vec<_> = src.split("---").collect();
+ for (i, part) in parts.iter().enumerate() {
+ let is_header = i == 0
+ && parts.len() > 1
+ && part
+ .lines()
+ .all(|s| s.starts_with("//") || s.chars().all(|c| c.is_whitespace()));
+
+ if is_header {
+ for line in part.lines() {
+ if line.starts_with("// Ref: false") {
+ compare_ref = false;
+ }
+ }
+ } else {
+ let (part_ok, part_frames) = test_part(part, i, compare_ref, lines, env);
+ ok &= part_ok;
+ frames.extend(part_frames);
+ }
+
lines += part.lines().count() as u32;
}
@@ -179,9 +196,16 @@ fn test(
ok
}
-fn test_part(src: &str, i: usize, lines: u32, env: &mut Env) -> (bool, Vec) {
+fn test_part(
+ src: &str,
+ i: usize,
+ compare_ref: bool,
+ lines: u32,
+ env: &mut Env,
+) -> (bool, Vec) {
let map = LineMap::new(src);
- let (compare_ref, ref_diags) = parse_metadata(src, &map);
+ let (local_compare_ref, ref_diags) = parse_metadata(src, &map);
+ let compare_ref = local_compare_ref.unwrap_or(compare_ref);
let mut scope = library::new();
@@ -242,12 +266,20 @@ fn test_part(src: &str, i: usize, lines: u32, env: &mut Env) -> (bool, Vec (bool, SpanVec) {
+fn parse_metadata(src: &str, map: &LineMap) -> (Option, SpanVec) {
let mut diags = vec![];
- let mut compare_ref = true;
+ let mut compare_ref = None;
for (i, line) in src.lines().enumerate() {
- compare_ref &= !line.starts_with("// Ref: false");
+ let line = line.trim();
+
+ if line.starts_with("// Ref: false") {
+ compare_ref = Some(false);
+ }
+
+ if line.starts_with("// Ref: true") {
+ compare_ref = Some(true);
+ }
let (level, rest) = if let Some(rest) = line.strip_prefix("// Warning: ") {
(Level::Warning, rest)