summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/eval/mod.rs58
-rw-r--r--src/eval/value.rs6
-rw-r--r--src/parse/mod.rs22
-rw-r--r--src/pretty.rs13
-rw-r--r--src/syntax/expr.rs39
-rw-r--r--src/syntax/visit.rs6
-rw-r--r--tests/ref/control/for-invalid.pngbin1653 -> 0 bytes
-rw-r--r--tests/ref/control/for-value.pngbin1702 -> 0 bytes
-rw-r--r--tests/ref/control/for.pngbin1389 -> 2877 bytes
-rw-r--r--tests/ref/control/if-invalid.pngbin1343 -> 0 bytes
-rw-r--r--tests/ref/control/if.pngbin1227 -> 1681 bytes
-rw-r--r--tests/ref/control/invalid.pngbin0 -> 3743 bytes
-rw-r--r--tests/ref/control/let-invalid.pngbin364 -> 0 bytes
-rw-r--r--tests/ref/control/let.png (renamed from tests/ref/control/let-terminated.png)bin1459 -> 1459 bytes
-rw-r--r--tests/ref/control/while.pngbin0 -> 838 bytes
-rw-r--r--tests/ref/spacing.pngbin3858 -> 5117 bytes
-rw-r--r--tests/typ/control/for-invalid.typ32
-rw-r--r--tests/typ/control/for-value.typ20
-rw-r--r--tests/typ/control/for.typ54
-rw-r--r--tests/typ/control/if-invalid.typ28
-rw-r--r--tests/typ/control/if-value.typ21
-rw-r--r--tests/typ/control/if.typ46
-rw-r--r--tests/typ/control/invalid.typ100
-rw-r--r--tests/typ/control/let-invalid.typ20
-rw-r--r--tests/typ/control/let-terminated.typ28
-rw-r--r--tests/typ/control/let.typ32
-rw-r--r--tests/typ/control/while.typ46
-rw-r--r--tests/typ/spacing.typ10
-rw-r--r--tools/test-helper/extension.js5
29 files changed, 381 insertions, 205 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 596ceb50..3cf97860 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -118,6 +118,7 @@ impl Eval for Expr {
Self::Binary(v) => v.eval(ctx),
Self::Let(v) => v.eval(ctx),
Self::If(v) => v.eval(ctx),
+ Self::While(v) => v.eval(ctx),
Self::For(v) => v.eval(ctx),
}
}
@@ -403,24 +404,56 @@ impl Eval for ExprIf {
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let condition = self.condition.eval(ctx);
-
- if let Value::Bool(boolean) = condition {
- return if boolean {
+ if let Value::Bool(condition) = condition {
+ if condition {
self.if_body.eval(ctx)
} else if let Some(expr) = &self.else_body {
expr.eval(ctx)
} else {
Value::None
- };
- } else if condition != Value::Error {
- ctx.diag(error!(
- self.condition.span(),
- "expected boolean, found {}",
- condition.type_name(),
- ));
+ }
+ } else {
+ if condition != Value::Error {
+ ctx.diag(error!(
+ self.condition.span(),
+ "expected boolean, found {}",
+ condition.type_name(),
+ ));
+ }
+ Value::Error
}
+ }
+}
- Value::Error
+impl Eval for ExprWhile {
+ type Output = Value;
+
+ fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
+ let mut output = vec![];
+ loop {
+ let condition = self.condition.eval(ctx);
+ if let Value::Bool(condition) = condition {
+ if condition {
+ match self.body.eval(ctx) {
+ Value::Template(v) => output.extend(v),
+ Value::Str(v) => output.push(TemplateNode::Str(v)),
+ Value::Error => return Value::Error,
+ _ => {}
+ }
+ } else {
+ return Value::Template(output);
+ }
+ } else {
+ if condition != Value::Error {
+ ctx.diag(error!(
+ self.condition.span(),
+ "expected boolean, found {}",
+ condition.type_name(),
+ ));
+ }
+ return Value::Error;
+ }
+ }
}
}
@@ -438,7 +471,8 @@ impl Eval for ExprFor {
$(ctx.scopes.def_mut($binding.as_str(), $value);)*
match self.body.eval(ctx) {
- Value::Template(new) => output.extend(new),
+ Value::Template(v) => output.extend(v),
+ Value::Str(v) => output.push(TemplateNode::Str(v)),
Value::Error => {
ctx.scopes.pop();
return Value::Error;
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 2a91cf8a..d910155a 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -586,7 +586,11 @@ primitive! { Color: "color", Value::Color }
primitive! { String: "string", Value::Str }
primitive! { ValueArray: "array", Value::Array }
primitive! { ValueDict: "dictionary", Value::Dict }
-primitive! { ValueTemplate: "template", Value::Template }
+primitive! {
+ ValueTemplate: "template",
+ Value::Template,
+ Value::Str(v) => vec![TemplateNode::Str(v)],
+}
primitive! { ValueFunc: "function", Value::Func }
primitive! { ValueArgs: "arguments", Value::Args }
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
index 8d6958cf..d4cf90c7 100644
--- a/src/parse/mod.rs
+++ b/src/parse/mod.rs
@@ -71,7 +71,7 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option<Node> {
Token::UnicodeEscape(t) => Node::Text(unicode_escape(p, t)),
// Hashtag + keyword / identifier.
- Token::Ident(_) | Token::Let | Token::If | Token::For => {
+ Token::Ident(_) | Token::Let | Token::If | Token::While | Token::For => {
*at_start = false;
let stmt = token == Token::Let;
let group = if stmt { Group::Stmt } else { Group::Expr };
@@ -191,6 +191,7 @@ fn primary(p: &mut Parser) -> Option<Expr> {
// Keywords.
Some(Token::Let) => expr_let(p),
Some(Token::If) => expr_if(p),
+ Some(Token::While) => expr_while(p),
Some(Token::For) => expr_for(p),
// Structures.
@@ -382,6 +383,25 @@ fn expr_if(p: &mut Parser) -> Option<Expr> {
expr_if
}
+/// Parse a while expresion.
+fn expr_while(p: &mut Parser) -> Option<Expr> {
+ let start = p.start();
+ p.assert(Token::While);
+
+ let mut expr_while = None;
+ if let Some(condition) = expr(p) {
+ if let Some(body) = body(p) {
+ expr_while = Some(Expr::While(ExprWhile {
+ span: p.span(start),
+ condition: Box::new(condition),
+ body: Box::new(body),
+ }));
+ }
+ }
+
+ expr_while
+}
+
/// Parse a for expression.
fn expr_for(p: &mut Parser) -> Option<Expr> {
let start = p.start();
diff --git a/src/pretty.rs b/src/pretty.rs
index de910b99..0899824a 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -221,6 +221,7 @@ impl Pretty for Expr {
Self::Call(v) => v.pretty(p),
Self::Let(v) => v.pretty(p),
Self::If(v) => v.pretty(p),
+ Self::While(v) => v.pretty(p),
Self::For(v) => v.pretty(p),
}
}
@@ -413,6 +414,15 @@ impl Pretty for ExprIf {
}
}
+impl Pretty for ExprWhile {
+ fn pretty(&self, p: &mut Printer) {
+ p.push_str("while ");
+ self.condition.pretty(p);
+ p.push(' ');
+ self.body.pretty(p);
+ }
+}
+
impl Pretty for ExprFor {
fn pretty(&self, p: &mut Printer) {
p.push_str("for ");
@@ -718,9 +728,10 @@ mod tests {
// Keywords.
roundtrip("#let x = 1 + 2");
+ test_parse("#if x [y] #else [z]", "#if x [y] else [z]");
+ roundtrip("#while x {y}");
roundtrip("#for x in y {z}");
roundtrip("#for k, x in y {z}");
- test_parse("#if x [y] #else [z]", "#if x [y] else [z]");
}
#[test]
diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs
index 5b37bb56..638d9dd3 100644
--- a/src/syntax/expr.rs
+++ b/src/syntax/expr.rs
@@ -19,19 +19,21 @@ pub enum Expr {
Template(ExprTemplate),
/// A grouped expression: `(1 + 2)`.
Group(ExprGroup),
- /// A block expression: `{ #let x = 1; x + 2 }`.
+ /// A block expression: `{ let x = 1; x + 2 }`.
Block(ExprBlock),
/// A unary operation: `-x`.
Unary(ExprUnary),
/// A binary operation: `a + b`.
Binary(ExprBinary),
- /// An invocation of a function: `foo(...)`, `#[foo ...]`.
+ /// An invocation of a function: `foo(...)`.
Call(ExprCall),
- /// A let expression: `#let x = 1`.
+ /// A let expression: `let x = 1`.
Let(ExprLet),
- /// An if expression: `#if x { y } #else { z }`.
+ /// An if expression: `if x { y } else { z }`.
If(ExprIf),
- /// A for expression: `#for x #in y { z }`.
+ /// A while expression: `while x { y }`.
+ While(ExprWhile),
+ /// A for expression: `for x in y { z }`.
For(ExprFor),
}
@@ -51,6 +53,7 @@ impl Expr {
Self::Call(v) => v.span,
Self::Let(v) => v.span,
Self::If(v) => v.span,
+ Self::While(v) => v.span,
Self::For(v) => v.span,
}
}
@@ -62,6 +65,7 @@ impl Expr {
| Expr::Call(_)
| Expr::Let(_)
| Expr::If(_)
+ | Expr::While(_)
| Expr::For(_)
)
}
@@ -154,7 +158,7 @@ pub struct ExprGroup {
pub expr: Box<Expr>,
}
-/// A block expression: `{ #let x = 1; x + 2 }`.
+/// A block expression: `{ let x = 1; x + 2 }`.
#[derive(Debug, Clone, PartialEq)]
pub struct ExprBlock {
/// The source code location.
@@ -365,7 +369,7 @@ pub enum Associativity {
Right,
}
-/// An invocation of a function: `foo(...)`, `#[foo ...]`.
+/// An invocation of a function: `foo(...)`.
#[derive(Debug, Clone, PartialEq)]
pub struct ExprCall {
/// The source code location.
@@ -407,7 +411,7 @@ impl ExprArg {
}
}
-/// A let expression: `#let x = 1`.
+/// A let expression: `let x = 1`.
#[derive(Debug, Clone, PartialEq)]
pub struct ExprLet {
/// The source code location.
@@ -418,7 +422,7 @@ pub struct ExprLet {
pub init: Option<Box<Expr>>,
}
-/// An if expression: `#if x { y } #else { z }`.
+/// An if expression: `if x { y } else { z }`.
#[derive(Debug, Clone, PartialEq)]
pub struct ExprIf {
/// The source code location.
@@ -431,7 +435,18 @@ pub struct ExprIf {
pub else_body: Option<Box<Expr>>,
}
-/// A for expression: `#for x #in y { z }`.
+/// A while expression: `while x { y }`.
+#[derive(Debug, Clone, PartialEq)]
+pub struct ExprWhile {
+ /// The source code location.
+ pub span: Span,
+ /// The condition which selects whether to evaluate the body.
+ pub condition: Box<Expr>,
+ /// The expression to evaluate while the condition is true.
+ pub body: Box<Expr>,
+}
+
+/// A for expression: `for x in y { z }`.
#[derive(Debug, Clone, PartialEq)]
pub struct ExprFor {
/// The source code location.
@@ -447,9 +462,9 @@ pub struct ExprFor {
/// A pattern in a for loop.
#[derive(Debug, Clone, PartialEq)]
pub enum ForPattern {
- /// A value pattern: `#for v #in array`.
+ /// A value pattern: `for v in array`.
Value(Ident),
- /// A key-value pattern: `#for k, v #in dict`.
+ /// A key-value pattern: `for k, v in dict`.
KeyValue(Ident, Ident),
}
diff --git a/src/syntax/visit.rs b/src/syntax/visit.rs
index 2d3c683f..1bf260c7 100644
--- a/src/syntax/visit.rs
+++ b/src/syntax/visit.rs
@@ -61,6 +61,7 @@ visit! {
Expr::Call(e) => v.visit_call(e),
Expr::Let(e) => v.visit_let(e),
Expr::If(e) => v.visit_if(e),
+ Expr::While(e) => v.visit_while(e),
Expr::For(e) => v.visit_for(e),
}
}
@@ -132,6 +133,11 @@ visit! {
}
}
+ fn visit_while(v, node: &ExprWhile) {
+ v.visit_expr(&node.condition);
+ v.visit_expr(&node.body);
+ }
+
fn visit_for(v, node: &ExprFor) {
v.visit_expr(&node.iter);
v.visit_expr(&node.body);
diff --git a/tests/ref/control/for-invalid.png b/tests/ref/control/for-invalid.png
deleted file mode 100644
index d758aa95..00000000
--- a/tests/ref/control/for-invalid.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/control/for-value.png b/tests/ref/control/for-value.png
deleted file mode 100644
index fa323edc..00000000
--- a/tests/ref/control/for-value.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/control/for.png b/tests/ref/control/for.png
index 2f13985a..cfbc8d08 100644
--- a/tests/ref/control/for.png
+++ b/tests/ref/control/for.png
Binary files differ
diff --git a/tests/ref/control/if-invalid.png b/tests/ref/control/if-invalid.png
deleted file mode 100644
index 319fbdbd..00000000
--- a/tests/ref/control/if-invalid.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/control/if.png b/tests/ref/control/if.png
index 75a20d00..7db3a8ae 100644
--- a/tests/ref/control/if.png
+++ b/tests/ref/control/if.png
Binary files differ
diff --git a/tests/ref/control/invalid.png b/tests/ref/control/invalid.png
new file mode 100644
index 00000000..9a119088
--- /dev/null
+++ b/tests/ref/control/invalid.png
Binary files differ
diff --git a/tests/ref/control/let-invalid.png b/tests/ref/control/let-invalid.png
deleted file mode 100644
index 19d4d545..00000000
--- a/tests/ref/control/let-invalid.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/control/let-terminated.png b/tests/ref/control/let.png
index 24f20c69..24f20c69 100644
--- a/tests/ref/control/let-terminated.png
+++ b/tests/ref/control/let.png
Binary files differ
diff --git a/tests/ref/control/while.png b/tests/ref/control/while.png
new file mode 100644
index 00000000..f0baf0af
--- /dev/null
+++ b/tests/ref/control/while.png
Binary files differ
diff --git a/tests/ref/spacing.png b/tests/ref/spacing.png
index 5c3acf9b..fb102e66 100644
--- a/tests/ref/spacing.png
+++ b/tests/ref/spacing.png
Binary files differ
diff --git a/tests/typ/control/for-invalid.typ b/tests/typ/control/for-invalid.typ
deleted file mode 100644
index c8bdebdd..00000000
--- a/tests/typ/control/for-invalid.typ
+++ /dev/null
@@ -1,32 +0,0 @@
-// Test invalid for loop syntax.
-
----
-// Error: 5-5 expected identifier
-#for
-
-// Error: 7-7 expected keyword `in`
-#for v
-
-// Error: 10-10 expected expression
-#for v in
-
-// Error: 15-15 expected body
-#for v in iter
-
----
-// Should output `v in iter`.
-// Error: 5 expected identifier
-#for
-v in iter {}
-
-// Should output `A thing`.
-// Error: 7-10 expected identifier, found string
-A#for "v" thing.
-
-// Should output `in iter`.
-// Error: 6-9 expected identifier, found string
-#for "v" in iter {}
-
-// Should output `+ b in iter`.
-// Error: 7 expected keyword `in`
-#for a + b in iter {}
diff --git a/tests/typ/control/for-value.typ b/tests/typ/control/for-value.typ
deleted file mode 100644
index 3ab80716..00000000
--- a/tests/typ/control/for-value.typ
+++ /dev/null
@@ -1,20 +0,0 @@
-// 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/typ/control/for.typ b/tests/typ/control/for.typ
index 294345b5..36bce447 100644
--- a/tests/typ/control/for.typ
+++ b/tests/typ/control/for.typ
@@ -1,11 +1,10 @@
// Test for loops.
-// Ref: false
---
-// Array.
-
-#for x in () {}
+// Empty array.
+#for x in () [Nope]
+// Array.
#let sum = 0
#for x in (1, 2, 3, 4, 5) {
sum += x
@@ -13,14 +12,12 @@
#test(sum, 15)
----
-// Dictionary.
-// Ref: true
-(\ #for k, v in (name: "Typst", age: 2) [
- #h(0.5cm) {k}: {v}, \
-])
+// Dictionary is not traversed in insertion order.
+// Should output `age: 1, name: Typst,`.
+#for k, v in (name: "Typst", age: 2) [
+ {k}: {v}, \
+]
----
// String.
{
let out = ""
@@ -36,6 +33,33 @@
}
---
+// Block body.
+// 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])
+ } + "]"
+}
+
+// Template body.
+// Should output `234`.
+#for v in (1, 2, 3, 4, 5, 6, 7) [#if v >= 2 and v <= 5 { repr(v) }]
+
+---
+// Value of for loops.
+// Ref: false
+#test(type(for v in () {}), "template")
+#test(type(for v in () []), "template")
+
+---
+// Error: 14-19 unknown variable
+#let error = error
+
// Uniterable expression.
// Error: 11-15 cannot loop over boolean
#for v in true {}
@@ -44,9 +68,7 @@
// Error: 11-18 cannot add integer and string
#for v in 1 + "2" {}
-// Error: 14-17 cannot apply '-' to string
-#let error = -""
-#let result = for v in (1, 2, 3) {
+// A single error stops iteration.
+#test(error, for v in (1, 2, 3) {
if v < 2 [Ok] else {error}
-}
-#test(result, error)
+})
diff --git a/tests/typ/control/if-invalid.typ b/tests/typ/control/if-invalid.typ
deleted file mode 100644
index 6d2deab1..00000000
--- a/tests/typ/control/if-invalid.typ
+++ /dev/null
@@ -1,28 +0,0 @@
-// Test invalid if syntax.
-
----
-// Error: 4 expected expression
-#if
-
-// Error: 4 expected expression
-{if}
-
-// Error: 6 expected body
-#if x
-
-// Error: 1-6 unexpected keyword `else`
-#else {}
-
----
-// Should output `x`.
-// Error: 4 expected expression
-#if
-x {}
-
-// Should output `something`.
-// Error: 6 expected body
-#if x something
-
-// Should output `A thing.`
-// Error: 20 expected body
-A#if false {} #else thing
diff --git a/tests/typ/control/if-value.typ b/tests/typ/control/if-value.typ
deleted file mode 100644
index d7124255..00000000
--- a/tests/typ/control/if-value.typ
+++ /dev/null
@@ -1,21 +0,0 @@
-// 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/typ/control/if.typ b/tests/typ/control/if.typ
index 4ed6b649..8d07e9b8 100644
--- a/tests/typ/control/if.typ
+++ b/tests/typ/control/if.typ
@@ -3,35 +3,61 @@
---
// Test condition evaluation.
#if 1 < 2 [
- Ok.
+ One.
]
#if true == false [
- Bad, but we {dont-care}!
+ {Bad}, but we {dont-care}!
]
---
-// Brace in condition.
+// Braced condition.
#if {true} [
- Ok.
+ One.
+]
+
+// Template in condition.
+#if [] != none [
+ Two.
]
// Multi-line condition with parens.
#if (
1 + 1
== 1
-) {
- nope
-} #else {
- "Ok."
+) [
+ Nope.
+] #else {
+ "Three."
}
// Multiline.
#if false [
Bad.
] #else {
- let pt = "."
- "Ok" + pt
+ let point = "."
+ "Four" + point
+}
+
+---
+// 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/typ/control/invalid.typ b/tests/typ/control/invalid.typ
new file mode 100644
index 00000000..49158a68
--- /dev/null
+++ b/tests/typ/control/invalid.typ
@@ -0,0 +1,100 @@
+// Test invalid control syntax.
+
+---
+// Error: 5 expected identifier
+#let
+
+// Error: 5 expected identifier
+{let}
+
+// Error: 6-9 expected identifier, found string
+#let "v"
+
+// Should output `1`.
+// Error: 7 expected semicolon or line break
+#let v 1
+
+// Error: 9 expected expression
+#let v =
+
+// Should output `= 1`.
+// Error: 6-9 expected identifier, found string
+#let "v" = 1
+
+---
+// Error: 4 expected expression
+#if
+
+// Error: 4 expected expression
+{if}
+
+// Error: 6 expected body
+#if x
+
+// Error: 1-6 unexpected keyword `else`
+#else {}
+
+// Should output `x`.
+// Error: 4 expected expression
+#if
+x {}
+
+// Should output `something`.
+// Error: 6 expected body
+#if x something
+
+// Should output `A thing.`
+// Error: 20 expected body
+A#if false {} #else thing
+
+---
+// Error: 7 expected expression
+#while
+
+// Error: 7 expected expression
+{while}
+
+// Error: 9 expected body
+#while x
+
+// Should output `x`.
+// Error: 7 expected expression
+#while
+x {}
+
+// Should output `something`.
+// Error: 9 expected body
+#while x something
+
+---
+// Error: 5 expected identifier
+#for
+
+// Error: 5 expected identifier
+{for}
+
+// Error: 7 expected keyword `in`
+#for v
+
+// Error: 10 expected expression
+#for v in
+
+// Error: 15 expected body
+#for v in iter
+
+// Should output `v in iter`.
+// Error: 5 expected identifier
+#for
+v in iter {}
+
+// Should output `A thing`.
+// Error: 7-10 expected identifier, found string
+A#for "v" thing
+
+// Should output `in iter`.
+// Error: 6-9 expected identifier, found string
+#for "v" in iter {}
+
+// Should output `+ b in iter`.
+// Error: 7 expected keyword `in`
+#for a + b in iter {}
diff --git a/tests/typ/control/let-invalid.typ b/tests/typ/control/let-invalid.typ
deleted file mode 100644
index f29353ed..00000000
--- a/tests/typ/control/let-invalid.typ
+++ /dev/null
@@ -1,20 +0,0 @@
-// Test invalid let binding syntax.
-
----
-// Error: 5 expected identifier
-#let
-
-// Error: 6-9 expected identifier, found string
-#let "v"
-
-// Should output `1`.
-// Error: 7 expected semicolon or line break
-#let v 1
-
-// Error: 9 expected expression
-#let v =
-
----
-// Should output `= 1`.
-// Error: 6-9 expected identifier, found string
-#let "v" = 1
diff --git a/tests/typ/control/let-terminated.typ b/tests/typ/control/let-terminated.typ
deleted file mode 100644
index 623265e0..00000000
--- a/tests/typ/control/let-terminated.typ
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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 expected semicolon or line break
-#let v4 = 4 Four
-
-// Terminated by semicolon even though we are in a paren group.
-// Error: 2:19 expected expression
-// Error: 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/typ/control/let.typ b/tests/typ/control/let.typ
index e609d3a9..8df29b11 100644
--- a/tests/typ/control/let.typ
+++ b/tests/typ/control/let.typ
@@ -1,7 +1,8 @@
// Test let bindings.
-// Ref: false
---
+// Ref: false
+
// Automatically initialized with none.
#let x
#test(x, none)
@@ -9,3 +10,32 @@
// Manually initialized with one.
#let x = 1
#test(x, 1)
+
+---
+// Termination.
+
+// 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 expected semicolon or line break
+#let v4 = 4 Four
+
+// Terminated by semicolon even though we are in a paren group.
+// Error: 2:19 expected expression
+// Error: 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/typ/control/while.typ b/tests/typ/control/while.typ
new file mode 100644
index 00000000..7ad70372
--- /dev/null
+++ b/tests/typ/control/while.typ
@@ -0,0 +1,46 @@
+// Test while expressions.
+
+---
+// Should output `2 4 6 8 10`.
+#let i = 0
+#while i < 10 [
+ { i += 2 }
+ #i
+]
+
+// Should output `Hi`.
+#let iter = true
+#while iter {
+ iter = false
+ "Hi."
+}
+
+#while false {
+ dont-care
+}
+
+---
+// Value of while loops.
+// Ref: false
+#test(type(while false {}), "template")
+#test(type(while false []), "template")
+
+---
+// Error: 14-19 unknown variable
+#let error = error
+
+// Condition must be boolean.
+// Error: 8-14 expected boolean, found template
+#while [nope] [nope]
+
+// Make sure that we don't complain twice.
+// Error: 8-15 unknown variable
+#while nothing {}
+
+// A single error stops iteration.
+#let i = 0
+#test(error, while i < 10 {
+ i += 1
+ if i < 5 [nope] else { error }
+})
+#test(i, 5)
diff --git a/tests/typ/spacing.typ b/tests/typ/spacing.typ
index d44cd84c..77dac53c 100644
--- a/tests/typ/spacing.typ
+++ b/tests/typ/spacing.typ
@@ -20,8 +20,16 @@ A#if false [] #else [B]C \
A#if true [B] #else [] C \
---
+// Spacing around while loop.
+
+#let c = true; A#while c [{c = false}B]C \
+#let c = true; A#while c [{c = false}B] C \
+#let c = true; A #while c { c = false; "B" }C \
+#let c = true; A #while c { c = false; "B" } C \
+
+---
// Spacing around for loop.
A#for _ in (none,) [B]C \
A#for _ in (none,) [B] C \
-A #for _ in (none,) [B]C \
+A #for _ in (none,) {"B"}C \
diff --git a/tools/test-helper/extension.js b/tools/test-helper/extension.js
index e5325bed..ad157bcb 100644
--- a/tools/test-helper/extension.js
+++ b/tools/test-helper/extension.js
@@ -92,9 +92,9 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) {
<style>
body, html {
width: 100%;
- text-align: center;
margin: 0;
padding: 0;
+ text-align: center;
}
img {
width: 80%;
@@ -102,7 +102,10 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) {
object-fit: contain;
}
pre {
+ display: inline-block;
font-family: var(--vscode-editor-font-family);
+ text-align: left;
+ width: 80%;
}
</style>
</head>