summaryrefslogtreecommitdiff
path: root/tests/suite/scripting
diff options
context:
space:
mode:
Diffstat (limited to 'tests/suite/scripting')
-rw-r--r--tests/suite/scripting/blocks.typ143
-rw-r--r--tests/suite/scripting/call.typ200
-rw-r--r--tests/suite/scripting/closure.typ223
-rw-r--r--tests/suite/scripting/destructuring.typ357
-rw-r--r--tests/suite/scripting/field.typ76
-rw-r--r--tests/suite/scripting/for.typ135
-rw-r--r--tests/suite/scripting/get-rule.typ67
-rw-r--r--tests/suite/scripting/if.typ134
-rw-r--r--tests/suite/scripting/import.typ334
-rw-r--r--tests/suite/scripting/include.typ32
-rw-r--r--tests/suite/scripting/let.typ143
-rw-r--r--tests/suite/scripting/loop.typ142
-rw-r--r--tests/suite/scripting/methods.typ51
-rw-r--r--tests/suite/scripting/module.typ13
-rw-r--r--tests/suite/scripting/modules/chap1.typ8
-rw-r--r--tests/suite/scripting/modules/chap2.typ10
-rw-r--r--tests/suite/scripting/modules/cycle1.typ5
-rw-r--r--tests/suite/scripting/modules/cycle2.typ5
-rw-r--r--tests/suite/scripting/ops.typ465
-rw-r--r--tests/suite/scripting/params.typ69
-rw-r--r--tests/suite/scripting/recursion.typ55
-rw-r--r--tests/suite/scripting/return.typ87
-rw-r--r--tests/suite/scripting/while.typ59
23 files changed, 2813 insertions, 0 deletions
diff --git a/tests/suite/scripting/blocks.typ b/tests/suite/scripting/blocks.typ
new file mode 100644
index 00000000..f139b8c6
--- /dev/null
+++ b/tests/suite/scripting/blocks.typ
@@ -0,0 +1,143 @@
+// Test code blocks.
+
+--- code-block-basic-syntax ---
+
+// Evaluates to join of none, [My ] and the two loop bodies.
+#{
+ let parts = ("my fri", "end.")
+ [Hello, ]
+ for s in parts [#s]
+}
+
+// Evaluates to join of the content and strings.
+#{
+ [How]
+ if true {
+ " are"
+ }
+ [ ]
+ if false [Nope]
+ [you] + "?"
+}
+
+--- code-block-empty ---
+// Nothing evaluates to none.
+#test({}, none)
+
+--- code-block-let ---
+// Let evaluates to none.
+#test({ let v = 0 }, none)
+
+--- code-block-single-expression ---
+// Evaluates to single expression.
+#test({ "hello" }, "hello")
+
+--- code-block-multiple-expressions-single-line ---
+// Evaluates to string.
+#test({ let x = "m"; x + "y" }, "my")
+
+--- code-block-join-let-with-expression ---
+// Evaluated to int.
+#test({
+ let x = 1
+ let y = 2
+ x + y
+}, 3)
+
+--- code-block-join-expression-with-none ---
+// String is joined with trailing none, evaluates to string.
+#test({
+ type("")
+ none
+}, str)
+
+--- code-block-join-int-with-content ---
+// Some things can't be joined.
+#{
+ [A]
+ // Error: 3-4 cannot join content with integer
+ 1
+ [B]
+}
+
+--- code-block-scope-in-markup ---
+// Block directly in markup also creates a scope.
+#{ let x = 1 }
+
+// Error: 7-8 unknown variable: x
+#test(x, 1)
+
+--- code-block-scope-in-let ---
+// Block in expression does create a scope.
+#let a = {
+ let b = 1
+ b
+}
+
+#test(a, 1)
+
+// Error: 3-4 unknown variable: b
+#{b}
+
+--- code-block-double-scope ---
+// Double block creates a scope.
+#{{
+ import "module.typ": b
+ test(b, 1)
+}}
+
+// Error: 2-3 unknown variable: b
+#b
+
+--- code-block-nested-scopes ---
+// Multiple nested scopes.
+#{
+ let a = "a1"
+ {
+ let a = "a2"
+ {
+ test(a, "a2")
+ let a = "a3"
+ test(a, "a3")
+ }
+ test(a, "a2")
+ }
+ test(a, "a1")
+}
+
+--- code-block-multiple-literals-without-semicolon ---
+// Multiple unseparated expressions in one line.
+// Error: 4 expected semicolon or line break
+#{1 2}
+
+--- code-block-multiple-expressions-without-semicolon ---
+// Error: 13 expected semicolon or line break
+// Error: 23 expected semicolon or line break
+#{let x = -1 let y = 3 x + y}
+
+--- code-block-incomplete-expressions ---
+#{
+ // Error: 7-10 expected pattern, found string
+ for "v"
+
+ // Error: 8 expected keyword `in`
+ // Error: 22 expected block
+ for v let z = 1 + 2
+
+ z
+}
+
+--- code-block-unclosed ---
+// Error: 2-3 unclosed delimiter
+#{
+
+--- code-block-unopened ---
+// Error: 2-3 unexpected closing brace
+#}
+
+--- content-block-in-markup-scope ---
+// Content blocks also create a scope.
+#[#let x = 1]
+
+// Error: 2-3 unknown variable: x
+#x
diff --git a/tests/suite/scripting/call.typ b/tests/suite/scripting/call.typ
new file mode 100644
index 00000000..e79fc949
--- /dev/null
+++ b/tests/suite/scripting/call.typ
@@ -0,0 +1,200 @@
+// Test function calls.
+
+--- call-basic ---
+
+// Omitted space.
+#let f() = {}
+#[#f()*Bold*]
+
+// Call return value of function with body.
+#let f(x, body) = (y) => [#x] + body + [#y]
+#f(1)[2](3)
+
+// Don't parse this as a function.
+#test (it)
+
+#let f(body) = body
+#f[A]
+#f()[A]
+#f([A])
+
+#let g(a, b) = a + b
+#g[A][B]
+#g([A], [B])
+#g()[A][B]
+
+--- call-aliased-function ---
+// Call function assigned to variable.
+#let alias = type
+#test(alias(alias), type)
+
+--- call-complex-callee-expression ---
+// Callee expressions.
+#{
+ // Wrapped in parens.
+ test((type)("hi"), str)
+
+ // Call the return value of a function.
+ let adder(dx) = x => x + dx
+ test(adder(2)(5), 7)
+}
+
+--- call-bad-type-bool-literal ---
+// Error: 2-6 expected function, found boolean
+#true()
+
+--- call-bad-type-string-var ---
+#let x = "x"
+
+// Error: 2-3 expected function, found string
+#x()
+
+--- call-bad-type-int-expr ---
+#let f(x) = x
+
+// Error: 2-6 expected function, found integer
+#f(1)(2)
+
+--- call-bad-type-content-expr ---
+#let f(x) = x
+
+// Error: 2-6 expected function, found content
+#f[1](2)
+
+--- call-args-trailing-comma ---
+// Trailing comma.
+#test(1 + 1, 2,)
+
+--- call-args-duplicate ---
+// Error: 26-30 duplicate argument: font
+#set text(font: "Arial", font: "Helvetica")
+
+--- call-args-bad-positional-as-named ---
+// Error: 4-15 the argument `amount` is positional
+// Hint: 4-15 try removing `amount:`
+#h(amount: 0.5)
+
+--- call-args-bad-colon ---
+// Error: 7-8 unexpected colon
+#func(:)
+
+--- call-args-bad-token ---
+// Error: 10-12 unexpected end of block comment
+#func(a:1*/)
+
+--- call-args-missing-comma ---
+// Error: 8 expected comma
+#func(1 2)
+
+--- call-args-bad-name-and-incomplete-pair ---
+// Error: 7-8 expected identifier, found integer
+// Error: 9 expected expression
+#func(1:)
+
+--- call-args-bad-name-int ---
+// Error: 7-8 expected identifier, found integer
+#func(1:2)
+
+--- call-args-bad-name-string ---
+// Error: 7-12 expected identifier, found string
+#func("abc": 2)
+
+--- call-args-bad-name-group ---
+// Error: 7-10 expected identifier, found group
+#func((x):1)
+
+--- call-args-lone-underscore ---
+// Test that lone underscore works.
+#test((1, 2, 3).map(_ => {}).len(), 3)
+
+--- call-args-spread-override ---
+// Test standard argument overriding.
+#{
+ let f(style: "normal", weight: "regular") = {
+ "(style: " + style + ", weight: " + weight + ")"
+ }
+
+ let myf(..args) = f(weight: "bold", ..args)
+ test(myf(), "(style: normal, weight: bold)")
+ test(myf(weight: "black"), "(style: normal, weight: black)")
+ test(myf(style: "italic"), "(style: italic, weight: bold)")
+}
+
+--- call-args-spread-forward ---
+// Test multiple calls.
+#{
+ let f(b, c: "!") = b + c
+ let g(a, ..sink) = a + f(..sink)
+ test(g("a", "b", c: "c"), "abc")
+}
+
+--- call-args-spread-type-repr ---
+// Test doing things with arguments.
+#{
+ let save(..args) = {
+ test(type(args), arguments)
+ test(repr(args), "(three: true, 1, 2)")
+ }
+
+ save(1, 2, three: true)
+}
+
+--- call-args-spread-array-and-dict ---
+// Test spreading array and dictionary.
+#{
+ let more = (3, -3, 6, 10)
+ test(calc.min(1, 2, ..more), -3)
+ test(calc.max(..more, 9), 10)
+ test(calc.max(..more, 11), 11)
+}
+
+#{
+ let more = (c: 3, d: 4)
+ let tostr(..args) = repr(args)
+ test(tostr(a: 1, ..more, b: 2), "(a: 1, c: 3, d: 4, b: 2)")
+}
+
+--- call-args-spread-none ---
+// None is spreadable.
+#let f() = none
+#f(..none)
+#f(..if false {})
+#f(..for x in () [])
+
+--- call-args-spread-string-invalid ---
+// Error: 11-19 cannot spread string
+#calc.min(.."nope")
+
+--- call-args-content-block-unclosed ---
+// Error: 6-7 unclosed delimiter
+#func[`a]`
+
+--- issue-886-args-sink ---
+// Test bugs with argument sinks.
+#let foo(..body) = repr(body.pos())
+#foo(a: "1", b: "2", 1, 2, 3, 4, 5, 6)
+
+--- issue-3144-unexpected-arrow ---
+#let f(a: 10) = a(1) + 1
+#test(f(a: _ => 5), 6)
+
+--- issue-3502-space-and-comments-around-destructuring-colon ---
+#let ( key : /* hi */ binding ) = ( key: "ok" )
+#test(binding, "ok")
+
+--- issue-3502-space-around-dict-colon ---
+#test(( key : "value" ).key, "value")
+
+--- issue-3502-space-around-param-colon ---
+// Test that a space after a named parameter is permissible.
+#let f( param : v ) = param
+#test(f( param /* ok */ : 2 ), 2)
+
+--- call-args-unclosed ---
+// Error: 7-8 unclosed delimiter
+#{func(}
+
+--- call-args-unclosed-string ---
+// Error: 6-7 unclosed delimiter
+// Error: 1:7-2:1 unclosed string
+#func("]
diff --git a/tests/suite/scripting/closure.typ b/tests/suite/scripting/closure.typ
new file mode 100644
index 00000000..e3677d33
--- /dev/null
+++ b/tests/suite/scripting/closure.typ
@@ -0,0 +1,223 @@
+// Test closures.
+
+--- closure-without-params-non-atomic ---
+// Don't parse closure directly in content.
+
+#let x = "x"
+
+// Should output `x => y`.
+#x => y
+
+--- closure-without-captures ---
+// Basic closure without captures.
+#{
+ let adder = (x, y) => x + y
+ test(adder(2, 3), 5)
+}
+
+--- closure-as-arg ---
+// Pass closure as argument and return closure.
+// Also uses shorthand syntax for a single argument.
+#{
+ let chain = (f, g) => (x) => f(g(x))
+ let f = x => x + 1
+ let g = x => 2 * x
+ let h = chain(f, g)
+ test(h(2), 5)
+}
+
+--- closure-capture-from-popped-stack-frame ---
+// Capture environment.
+#{
+ let mark = "!"
+ let greet = {
+ let hi = "Hi"
+ name => {
+ hi + ", " + name + mark
+ }
+ }
+
+ test(greet("Typst"), "Hi, Typst!")
+
+ // Changing the captured variable after the closure definition has no effect.
+ mark = "?"
+ test(greet("Typst"), "Hi, Typst!")
+}
+
+--- closure-shadows-outer-var ---
+// Redefined variable.
+#{
+ let x = 1
+ let f() = {
+ let x = x + 2
+ x
+ }
+ test(f(), 3)
+}
+
+--- closure-shadows-outer-var-import ---
+// Import bindings.
+#{
+ let b = "module.typ"
+ let f() = {
+ import b: b
+ b
+ }
+ test(f(), 1)
+}
+
+--- closure-shadows-outer-var-for-loop ---
+// For loop bindings.
+#{
+ let v = (1, 2, 3)
+ let f() = {
+ let s = 0
+ for v in v { s += v }
+ s
+ }
+ test(f(), 6)
+}
+
+--- closure-let-basic ---
+// Let + closure bindings.
+#{
+ let g = "hi"
+ let f() = {
+ let g() = "bye"
+ g()
+ }
+ test(f(), "bye")
+}
+
+--- closure-let-args ---
+// Parameter bindings.
+#{
+ let x = 5
+ let g() = {
+ let f(x, y: x) = x + y
+ f
+ }
+
+ test(g()(8), 13)
+}
+
+--- closure-bad-capture ---
+// Don't leak environment.
+#{
+ // Error: 16-17 unknown variable: x
+ let func() = x
+ let x = "hi"
+ func()
+}
+
+--- closure-missing-arg-positional ---
+// Too few arguments.
+#{
+ let types(x, y) = "[" + str(type(x)) + ", " + str(type(y)) + "]"
+ test(types(14%, 12pt), "[ratio, length]")
+
+ // Error: 8-21 missing argument: y
+ test(types("nope"), "[string, none]")
+}
+
+--- closure-too-many-args-positional ---
+// Too many arguments.
+#{
+ let f(x) = x + 1
+
+ // Error: 8-13 unexpected argument
+ f(1, "two", () => x)
+}
+
+--- closure-capture-in-lvalue ---
+// Mutable method with capture in argument.
+#let x = "b"
+#let f() = {
+ let a = (b: 5)
+ a.at(x) = 10
+ a
+}
+#f()
+
+--- closure-capture-mutate ---
+#let x = ()
+#let f() = {
+ // Error: 3-4 variables from outside the function are read-only and cannot be modified
+ x.at(1) = 2
+}
+#f()
+
+--- closure-named-args-basic ---
+// Named arguments.
+#{
+ let greet(name, birthday: false) = {
+ if birthday { "Happy Birthday, " } else { "Hey, " } + name + "!"
+ }
+
+ test(greet("Typst"), "Hey, Typst!")
+ test(greet("Typst", birthday: true), "Happy Birthday, Typst!")
+
+ // Error: 23-35 unexpected argument: whatever
+ test(greet("Typst", whatever: 10))
+}
+
+--- closure-args-sink ---
+// Parameter unpacking.
+#let f((a, b), ..c) = (a, b, c)
+#test(f((1, 2), 3, 4), (1, 2, (3, 4)))
+
+#let f((k: a, b), c: 3, (d,)) = (a, b, c, d)
+#test(f((k: 1, b: 2), (4,)), (1, 2, 3, 4))
+
+// Error: 8-14 expected identifier, found destructuring pattern
+#let f((a, b): 0) = none
+
+// Error: 10-19 expected pattern, found array
+#let f(..(a, b: c)) = none
+
+// Error: 10-16 expected pattern, found array
+#let f(..(a, b)) = none
+
+--- closure-param-duplicate-positional ---
+// Error: 11-12 duplicate parameter: x
+#let f(x, x) = none
+
+--- closure-body-multiple-expressions ---
+// Error: 21 expected comma
+// Error: 22-23 expected pattern, found integer
+// Error: 24-25 unexpected plus
+// Error: 26-27 expected pattern, found integer
+#let f = (x: () => 1 2 + 3) => 4
+
+--- closure-param-duplicate-mixed ---
+// Error: 14-15 duplicate parameter: a
+// Error: 23-24 duplicate parameter: b
+// Error: 35-36 duplicate parameter: b
+#let f(a, b, a: none, b: none, c, b) = none
+
+--- closure-param-duplicate-spread ---
+// Error: 13-14 duplicate parameter: a
+#let f(a, ..a) = none
+
+--- closure-pattern-bad-string ---
+// Error: 7-14 expected pattern, found string
+#((a, "named": b) => none)
+
+--- closure-let-pattern-bad-string ---
+// Error: 10-15 expected pattern, found string
+#let foo("key": b) = key
+
+--- closure-param-keyword ---
+// Error: 10-14 expected pattern, found `none`
+// Hint: 10-14 keyword `none` is not allowed as an identifier; try `none_` instead
+#let foo(none: b) = key
+
+--- closure-param-named-underscore ---
+// Error: 10-11 expected identifier, found underscore
+#let foo(_: 3) = none
+
+--- issue-non-atomic-closure ---
+// Ensure that we can't have non-atomic closures.
+#let x = 1
+#let c = [#(x) => (1, 2)]
+#test(c.children.last(), [(1, 2)]))
diff --git a/tests/suite/scripting/destructuring.typ b/tests/suite/scripting/destructuring.typ
new file mode 100644
index 00000000..0a3c1c54
--- /dev/null
+++ b/tests/suite/scripting/destructuring.typ
@@ -0,0 +1,357 @@
+--- destructuring-group-1 ---
+// This wasn't allowed.
+#let ((x)) = 1
+#test(x, 1)
+
+--- destructuring-group-2 ---
+// This also wasn't allowed.
+#let ((a, b)) = (1, 2)
+#test(a, 1)
+#test(b, 2)
+
+--- destructuring-dict-underscore ---
+// Here, `best` was accessed as a variable, where it shouldn't have.
+#{
+ (best: _) = (best: "brr")
+}
+
+--- destructuring-dict-array-at ---
+// Same here.
+#{
+ let array = (1, 2, 3, 4)
+ (test: array.at(1), best: _) = (test: "baz", best: "brr")
+ test(array, (1, "baz", 3, 4))
+}
+
+--- destructuring-dict-bad ---
+// Error: 7-10 expected identifier, found group
+// Error: 12-14 expected pattern, found integer
+#let ((a): 10) = "world"
+
+--- destructuring-bad-duplicate ---
+// Here, `a` is not duplicate, where it was previously identified as one.
+#let f((a: b), (c,), a) = (a, b, c)
+#test(f((a: 1), (2,), 3), (3, 1, 2))
+
+--- destructuring-non-atomic ---
+// Ensure that we can't have non-atomic destructuring.
+#let x = 1
+#let c = [#() = ()]
+#test(c.children.last(), [()])
+
+--- destructuring-let-array ---
+// Simple destructuring.
+#let (a, b) = (1, 2)
+#test(a, 1)
+#test(b, 2)
+
+--- destructuring-let-array-single-item ---
+#let (a,) = (1,)
+#test(a, 1)
+
+--- destructuring-let-array-placeholders ---
+// Destructuring with multiple placeholders.
+#let (a, _, c, _) = (1, 2, 3, 4)
+#test(a, 1)
+#test(c, 3)
+
+--- destructuring-let-array-with-sink-at-end ---
+// Destructuring with a sink.
+#let (a, b, ..c) = (1, 2, 3, 4, 5, 6)
+#test(a, 1)
+#test(b, 2)
+#test(c, (3, 4, 5, 6))
+
+--- destructuring-let-array-with-sink-in-middle ---
+// Destructuring with a sink in the middle.
+#let (a, ..b, c) = (1, 2, 3, 4, 5, 6)
+#test(a, 1)
+#test(b, (2, 3, 4, 5))
+#test(c, 6)
+
+--- destructuring-let-array-with-sink-at-start-empty ---
+// Destructuring with an empty sink.
+#let (..a, b, c) = (1, 2)
+#test(a, ())
+#test(b, 1)
+#test(c, 2)
+
+--- destructuring-let-array-with-sink-in-middle-empty ---
+// Destructuring with an empty sink.
+#let (a, ..b, c) = (1, 2)
+#test(a, 1)
+#test(b, ())
+#test(c, 2)
+
+--- destructuring-let-array-with-sink-at-end-empty ---
+// Destructuring with an empty sink.
+#let (a, b, ..c) = (1, 2)
+#test(a, 1)
+#test(b, 2)
+#test(c, ())
+
+--- destructuring-let-array-with-sink-empty ---
+// Destructuring with an empty sink and empty array.
+#let (..a) = ()
+#test(a, ())
+
+--- destructuring-let-array-with-unnamed-sink ---
+// Destructuring with unnamed sink.
+#let (a, .., b) = (1, 2, 3, 4)
+#test(a, 1)
+#test(b, 4)
+
+// Error: 10-11 duplicate binding: a
+#let (a, a) = (1, 2)
+
+// Error: 12-15 only one destructuring sink is allowed
+#let (..a, ..a) = (1, 2)
+
+// Error: 12-13 duplicate binding: a
+#let (a, ..a) = (1, 2)
+
+// Error: 13-14 duplicate binding: a
+#let (a: a, a) = (a: 1, b: 2)
+
+// Error: 13-20 expected pattern, found function call
+#let (a, b: b.at(0)) = (a: 1, b: 2)
+
+// Error: 7-14 expected pattern, found function call
+#let (a.at(0),) = (1,)
+
+--- destructuring-let-array-too-few-elements ---
+// Error: 13-14 not enough elements to destructure
+#let (a, b, c) = (1, 2)
+
+--- destructuring-let-array-too-few-elements-with-sink ---
+// Error: 7-10 not enough elements to destructure
+#let (..a, b, c, d) = (1, 2)
+
+--- destructuring-let-array-bool-invalid ---
+// Error: 6-12 cannot destructure boolean
+#let (a, b) = true
+
+--- destructuring-let-dict ---
+// Simple destructuring.
+#let (a: a, b, x: c) = (a: 1, b: 2, x: 3)
+#test(a, 1)
+#test(b, 2)
+#test(c, 3)
+
+--- destructuring-let-dict-with-sink-at-end ---
+// Destructuring with a sink.
+#let (a: _, ..b) = (a: 1, b: 2, c: 3)
+#test(b, (b: 2, c: 3))
+
+--- destructuring-let-dict-with-sink-in-middle ---
+// Destructuring with a sink in the middle.
+#let (a: _, ..b, c: _) = (a: 1, b: 2, c: 3)
+#test(b, (b: 2))
+
+--- destructuring-let-dict-with-sink-at-end-empty ---
+// Destructuring with an empty sink.
+#let (a: _, ..b) = (a: 1)
+#test(b, (:))
+
+--- destructuring-let-dict-with-sink-empty ---
+// Destructuring with an empty sink and empty dict.
+#let (..a) = (:)
+#test(a, (:))
+
+--- destructuring-let-dict-with-unnamed-sink ---
+// Destructuring with unnamed sink.
+#let (a, ..) = (a: 1, b: 2)
+#test(a, 1)
+
+--- destructuring-let-nested ---
+// Nested destructuring.
+#let ((a, b), (key: c)) = ((1, 2), (key: 3))
+#test((a, b, c), (1, 2, 3))
+
+--- destructuring-let-dict-key-string-invalid ---
+// Keyed destructuring is not currently supported.
+// Error: 7-18 expected pattern, found string
+#let ("spacy key": val) = ("spacy key": 123)
+#val
+
+--- destructuring-let-dict-key-expr-invalid ---
+// Keyed destructuring is not currently supported.
+#let x = "spacy key"
+// Error: 7-10 expected identifier, found group
+#let ((x): v) = ("spacy key": 123)
+
+--- destructuring-let-array-trailing-placeholders ---
+// Trailing placeholders.
+// Error: 10-11 not enough elements to destructure
+#let (a, _, _, _, _) = (1,)
+#test(a, 1)
+
+--- destructuring-let-dict-patterns-invalid ---
+// Error: 10-13 expected pattern, found string
+// Error: 18-19 expected pattern, found integer
+#let (a: "a", b: 2) = (a: 1, b: 2)
+
+--- destructuring-let-dict-shorthand-missing-key ---
+// Error: 10-11 dictionary does not contain key "b"
+#let (a, b) = (a: 1)
+
+--- destructuring-let-dict-missing-key ---
+// Error: 10-11 dictionary does not contain key "b"
+#let (a, b: b) = (a: 1)
+
+--- destructuring-let-dict-from-array ---
+// Error: 7-11 cannot destructure named pattern from an array
+#let (a: a, b) = (1, 2, 3)
+
+--- destructuring-during-loop-continue ---
+// Test continue while destructuring.
+// Should output "one = I \ two = II \ one = I".
+#for num in (1, 2, 3, 1) {
+ let (word, roman) = if num == 1 {
+ ("one", "I")
+ } else if num == 2 {
+ ("two", "II")
+ } else {
+ continue
+ }
+ [#word = #roman \ ]
+}
+
+--- destructuring-assign ---
+// Test destructuring assignments.
+
+#let a = none
+#let b = none
+#let c = none
+#((a,) = (1,))
+#test(a, 1)
+
+#((_, a, b, _) = (1, 2, 3, 4))
+#test(a, 2)
+#test(b, 3)
+
+#((a, b, ..c) = (1, 2, 3, 4, 5, 6))
+#test(a, 1)
+#test(b, 2)
+#test(c, (3, 4, 5, 6))
+
+#((a: a, b, x: c) = (a: 1, b: 2, x: 3))
+#test(a, 1)
+#test(b, 2)
+#test(c, 3)
+
+#let a = (1, 2)
+#((a: a.at(0), b) = (a: 3, b: 4))
+#test(a, (3, 2))
+#test(b, 4)
+
+#let a = (1, 2)
+#((a.at(0), b) = (3, 4))
+#test(a, (3, 2))
+#test(b, 4)
+
+#((a, ..b) = (1, 2, 3, 4))
+#test(a, 1)
+#test(b, (2, 3, 4))
+
+#let a = (1, 2)
+#((b, ..a.at(0)) = (1, 2, 3, 4))
+#test(a, ((2, 3, 4), 2))
+#test(b, 1)
+
+--- destructuring-assign-commas ---
+// Test comma placement in destructuring assignment.
+#let array = (1, 2, 3)
+#((key: array.at(1)) = (key: "hi"))
+#test(array, (1, "hi", 3))
+
+#let array = (1, 2, 3)
+#((array.at(1)) = ("hi"))
+#test(array, (1, "hi", 3))
+
+#let array = (1, 2, 3)
+#((array.at(1),) = ("hi",))
+#test(array, (1, "hi", 3))
+
+#let array = (1, 2, 3)
+#((array.at(1)) = ("hi",))
+#test(array, (1, ("hi",), 3))
+
+--- destructuring-assign-nested ---
+// Test nested destructuring assignment.
+#let a
+#let b
+#let c
+#(((a, b), (key: c)) = ((1, 2), (key: 3)))
+#test((a, b, c), (1, 2, 3))
+
+--- destructuring-assign-nested-invalid ---
+#let array = (1, 2, 3)
+// Error: 3-17 cannot destructure string
+#((array.at(1),) = ("hi"))
+#test(array, (1, ("hi",), 3))
+
+--- issue-3275-normal-variable ---
+// Normal variable.
+#for x in (1, 2) {}
+#for x in (a: 1, b: 2) {}
+#for x in "foo" {}
+#for x in bytes("😊") {}
+
+--- issue-3275-placeholder ---
+// Placeholder.
+#for _ in (1, 2) {}
+#for _ in (a: 1, b: 2) {}
+#for _ in "foo" {}
+#for _ in bytes("😊") {}
+
+--- issue-3275-destructuring ---
+// Destructuring.
+#for (a,b,c) in (("a", 1, bytes(())), ("b", 2, bytes(""))) {}
+#for (a, ..) in (("a", 1, bytes(())), ("b", 2, bytes(""))) {}
+#for (k, v) in (a: 1, b: 2, c: 3) {}
+#for (.., v) in (a: 1, b: 2, c: 3) {}
+
+--- issue-3275-loop-over-content ---
+// Error: 11-17 cannot loop over content
+#for x in [1, 2] {}
+
+--- issue-3275-loop-over-arguments ---
+// Error: 11-25 cannot loop over arguments
+#for _ in arguments("a") {}
+
+--- issue-3275-loop-over-integer ---
+// Error: 16-21 cannot loop over integer
+#for (x, y) in 12306 {}
+
+--- issue-3275-destructuring-loop-over-content ---
+// Error: 16-22 cannot loop over content
+#for (x, y) in [1, 2] {}
+
+--- issue-3275-destructuring-loop-over-string ---
+// Error: 6-12 cannot destructure values of string
+#for (x, y) in "foo" {}
+
+--- issue-3275-destructuring-loop-over-string-array ---
+// Error: 6-12 cannot destructure string
+#for (x, y) in ("foo", "bar") {}
+
+--- issue-3275-destructuring-loop-over-bytes ---
+// Error: 6-12 cannot destructure values of bytes
+#for (x, y) in bytes("😊") {}
+
+--- issue-3275-destructuring-loop-over-bytes-array ---
+// Error: 6-12 cannot destructure bytes
+#for (x, y) in (bytes((1,2)), bytes((1,2))) {}
+
+--- issue-3275-destructuring-loop-over-int-array ---
+// Error: 6-12 cannot destructure integer
+#for (x, y) in (1, 2) {}
+
+--- issue-3275-destructuring-loop-over-2d-array-1 ---
+// Error: 10-11 not enough elements to destructure
+#for (x, y) in ((1,), (2,)) {}
+
+--- issue-3275-destructuring-loop-over-2d-array-2 ---
+// Error: 6-12 too many elements to destructure
+#for (x, y) in ((1,2,3), (4,5,6)) {}
diff --git a/tests/suite/scripting/field.typ b/tests/suite/scripting/field.typ
new file mode 100644
index 00000000..7b2427e3
--- /dev/null
+++ b/tests/suite/scripting/field.typ
@@ -0,0 +1,76 @@
+// Test field access.
+
+--- field-function ---
+// Test fields on function scopes.
+#enum.item
+#assert.eq
+#assert.ne
+
+--- field-normal-function-invalid ---
+// Error: 9-16 function `assert` does not contain field `invalid`
+#assert.invalid
+
+--- field-elem-function-invalid ---
+// Error: 7-14 function `enum` does not contain field `invalid`
+#enum.invalid
+
+--- field-elem-function-invalid-call ---
+// Error: 7-14 function `enum` does not contain field `invalid`
+#enum.invalid()
+
+--- field-closure-invalid ---
+// Closures cannot have fields.
+#let f(x) = x
+// Error: 4-11 cannot access fields on user-defined functions
+#f.invalid
+
+--- field-bool-invalid ---
+// Error: 8-10 cannot access fields on type boolean
+#false.ok
+
+--- field-bool-keyword-invalid ---
+// Error: 9-13 cannot access fields on type boolean
+#{false.true}
+
+--- field-invalid-none ---
+#{
+ let object = none
+ // Error: 3-9 none does not have accessible fields
+ object.property = "value"
+}
+
+--- field-invalid-int ---
+#{
+ let object = 10
+ // Error: 3-9 integer does not have accessible fields
+ object.property = "value"
+}
+
+--- field-mutable-invalid-symbol ---
+#{
+ let object = sym.eq.not
+ // Error: 3-9 cannot mutate fields on symbol
+ object.property = "value"
+}
+
+--- field-mutable-invalid-module ---
+#{
+ let object = calc
+ // Error: 3-9 cannot mutate fields on module
+ object.property = "value"
+}
+
+--- field-mutable-invalid-function ---
+#{
+ let object = calc.sin
+ // Error: 3-9 cannot mutate fields on function
+ object.property = "value"
+}
+
+--- field-mutable-invalid-stroke ---
+#{
+ let s = 1pt + red
+ // Error: 3-4 fields on stroke are not yet mutable
+ // Hint: 3-4 try creating a new stroke with the updated field value instead
+ s.thickness = 5pt
+}
diff --git a/tests/suite/scripting/for.typ b/tests/suite/scripting/for.typ
new file mode 100644
index 00000000..e98b3c72
--- /dev/null
+++ b/tests/suite/scripting/for.typ
@@ -0,0 +1,135 @@
+// Test for loops.
+
+--- for-loop-basic ---
+
+// Empty array.
+#for x in () [Nope]
+
+// Dictionary is traversed in insertion order.
+// Should output `Name: Typst. Age: 2.`.
+#for (k, v) in (Name: "Typst", Age: 2) [
+ #k: #v.
+]
+
+// Block body.
+// Should output `[1st, 2nd, 3rd, 4th]`.
+#{
+ "["
+ for v in (1, 2, 3, 4) {
+ if v > 1 [, ]
+ [#v]
+ if v == 1 [st]
+ if v == 2 [nd]
+ if v == 3 [rd]
+ if v >= 4 [th]
+ }
+ "]"
+}
+
+// Content block body.
+// Should output `2345`.
+#for v in (1, 2, 3, 4, 5, 6, 7) [#if v >= 2 and v <= 5 { repr(v) }]
+
+// Map captured arguments.
+#let f1(..args) = args.pos().map(repr)
+#let f2(..args) = args.named().pairs().map(p => repr(p.first()) + ": " + repr(p.last()))
+#let f(..args) = (f1(..args) + f2(..args)).join(", ")
+#f(1, a: 2)
+
+--- for-loop-integrated ---
+#let out = ()
+
+// Values of array.
+#for v in (1, 2, 3) {
+ out += (v,)
+}
+
+// Indices and values of array.
+#for (i, v) in ("1", "2", "3").enumerate() {
+ test(repr(i + 1), v)
+}
+
+// Pairs 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, ("a", 4), ("b", 5), "a", 6, "b", 7))
+
+// Grapheme clusters of string.
+#let first = true
+#let joined = for c in "abc👩‍👩‍👦‍👦" {
+ if not first { ", " }
+ first = false
+ c
+}
+
+#test(joined, "a, b, c, 👩‍👩‍👦‍👦")
+
+// Return value.
+#test(for v in "" [], none)
+#test(type(for v in "1" []), content)
+
+--- for-loop-over-bool ---
+// Uniterable expression.
+// Error: 11-15 cannot loop over boolean
+#for v in true {}
+
+--- for-loop-over-string ---
+// Keys and values of strings.
+// Error: 6-12 cannot destructure values of string
+#for (k, v) in "hi" {
+ dont-care
+}
+
+--- for-loop-destructuring-without-parentheses ---
+// Destructuring without parentheses.
+// Error: 7-8 unexpected comma
+// Hint: 7-8 destructuring patterns must be wrapped in parentheses
+#for k, v in (a: 4, b: 5) {
+ dont-care
+}
+
+--- for-loop-destructuring-half ---
+// Error: 7-8 unexpected comma
+// Hint: 7-8 destructuring patterns must be wrapped in parentheses
+#for k, in () {}
+
+--- for-loop-incomplete ---
+// Error: 5 expected pattern
+#for
+
+// Error: 5 expected pattern
+#for//
+
+// Error: 6 expected pattern
+#{for}
+
+// Error: 7 expected keyword `in`
+#for v
+
+// Error: 10 expected expression
+#for v in
+
+// Error: 15 expected block
+#for v in iter
+
+// Error: 5 expected pattern
+#for
+v in iter {}
+
+// Error: 7-10 expected pattern, found string
+// Error: 16 expected block
+A#for "v" thing
+
+// Error: 6-9 expected pattern, found string
+#for "v" in iter {}
+
+// Error: 7 expected keyword `in`
+#for a + b in iter {}
diff --git a/tests/suite/scripting/get-rule.typ b/tests/suite/scripting/get-rule.typ
new file mode 100644
index 00000000..24d4e5db
--- /dev/null
+++ b/tests/suite/scripting/get-rule.typ
@@ -0,0 +1,67 @@
+--- get-rule-basic ---
+// Test basic get rule.
+#context test(text.lang, "en")
+#set text(lang: "de")
+#context test(text.lang, "de")
+#text(lang: "es", context test(text.lang, "es"))
+
+--- get-rule-in-function ---
+// Test whether context is retained in nested function.
+#let translate(..args) = args.named().at(text.lang)
+#set text(lang: "de")
+#context test(translate(de: "Inhalt", en: "Contents"), "Inhalt")
+
+--- get-rule-in-array-callback ---
+// Test whether context is retained in built-in callback.
+#set text(lang: "de")
+#context test(
+ ("en", "de", "fr").sorted(key: v => v != text.lang),
+ ("de", "en", "fr"),
+)
+
+--- get-rule-folding ---
+// Test folding.
+#set rect(stroke: red)
+#context {
+ test(type(rect.stroke), stroke)
+ test(rect.stroke.paint, red)
+}
+#[
+ #set rect(stroke: 4pt)
+ #context test(rect.stroke, 4pt + red)
+]
+#context test(rect.stroke, stroke(red))
+
+--- get-rule-figure-caption-collision ---
+// We have one collision: `figure.caption` could be both the element and a get
+// rule for the `caption` field, which is settable. We always prefer the
+// element. It's unfortunate, but probably nobody writes
+// `set figure(caption: ..)` anyway.
+#test(type(figure.caption), function)
+#context test(type(figure.caption), function)
+
+--- get-rule-assertion-failure ---
+// Error: 10-31 Assertion failed: "en" != "de"
+#context test(text.lang, "de")
+
+--- get-rule-unknown-field ---
+// Error: 15-20 function `text` does not contain field `langs`
+#context text.langs
+
+--- get-rule-inherent-field ---
+// Error: 18-22 function `heading` does not contain field `body`
+#context heading.body
+
+--- get-rule-missing-context-no-context ---
+// Error: 7-11 can only be used when context is known
+// Hint: 7-11 try wrapping this in a `context` expression
+// Hint: 7-11 the `context` expression should wrap everything that depends on this function
+#text.lang
+
+--- get-rule-unknown-field-no-context ---
+// Error: 7-12 function `text` does not contain field `langs`
+#text.langs
+
+--- get-rule-inherent-field-no-context ---
+// Error: 10-14 function `heading` does not contain field `body`
+#heading.body
diff --git a/tests/suite/scripting/if.typ b/tests/suite/scripting/if.typ
new file mode 100644
index 00000000..cc88925f
--- /dev/null
+++ b/tests/suite/scripting/if.typ
@@ -0,0 +1,134 @@
+// Test if-else expressions.
+
+--- if-markup ---
+// Test condition evaluation.
+#if 1 < 2 [
+ One.
+]
+
+#if true == false [
+ {Bad}, but we {dont-care}!
+]
+
+--- if-condition-complex ---
+// Braced condition.
+#if {true} [
+ One.
+]
+
+// Content block in condition.
+#if [] != none [
+ Two.
+]
+
+// Multi-line condition with parens.
+#if (
+ 1 + 1
+ == 1
+) [
+ Nope.
+] else {
+ "Three."
+}
+
+// Multiline.
+#if false [
+ Bad.
+] else {
+ let point = "."
+ "Four" + point
+}
+
+// Content block can be argument or body depending on whitespace.
+#{
+ if content == type[b] [Fi] else [Nope]
+ if content == type [Nope] else [ve.]
+}
+
+#let i = 3
+#if i < 2 [
+ Five.
+] else if i < 4 [
+ Six.
+] else [
+ Seven.
+]
+
+--- if-else-if-else ---
+// Test else if.
+
+#let nth(n) = {
+ str(n)
+ if n == 1 { "st" }
+ else if n == 2 { "nd" }
+ else if n == 3 { "rd" }
+ else { "th" }
+}
+
+#test(nth(1), "1st")
+#test(nth(2), "2nd")
+#test(nth(3), "3rd")
+#test(nth(4), "4th")
+#test(nth(5), "5th")
+
+--- if-expression ---
+// Value of if expressions.
+
+#{
+ 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)
+}
+
+--- if-condition-string-invalid ---
+// 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 }
+
+--- if-condition-invalid-and-wrong-type ---
+// Make sure that we don't complain twice.
+// Error: 5-12 cannot add integer and string
+#if 1 + "2" {}
+
+--- if-incomplete ---
+// Error: 4 expected expression
+#if
+
+// Error: 5 expected expression
+#{if}
+
+// Error: 6 expected block
+#if x
+
+// Error: 2-6 unexpected keyword `else`
+#else {}
+
+// Should output `x`.
+// Error: 4 expected expression
+#if
+x {}
+
+// Should output `something`.
+// Error: 6 expected block
+#if x something
+
+// Should output `A thing.`
+// Error: 19 expected block
+A#if false {} else thing
+
+#if a []else [b]
+#if a [] else [b]
+#if a {} else [b]
diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ
new file mode 100644
index 00000000..820f81d6
--- /dev/null
+++ b/tests/suite/scripting/import.typ
@@ -0,0 +1,334 @@
+// Test function and module imports.
+
+--- import-basic ---
+// Test basic syntax and semantics.
+
+// Test that this will be overwritten.
+#let value = [foo]
+
+// Import multiple things.
+#import "module.typ": fn, value
+#fn[Like and Subscribe!]
+#value
+
+// Should output `bye`.
+// Stop at semicolon.
+#import "module.typ": a, c;bye
+
+--- import-item-markup ---
+// An item import.
+#import "module.typ": item
+#test(item(1, 2), 3)
+
+--- import-item-in-code ---
+// Code mode
+#{
+ import "module.typ": b
+ test(b, 1)
+}
+
+--- import-wildcard-in-markup ---
+// A wildcard import.
+#import "module.typ": *
+
+// It exists now!
+#test(d, 3)
+
+--- import-item-renamed ---
+// A renamed item import.
+#import "module.typ": item as something
+#test(something(1, 2), 3)
+
+--- import-items-renamed-mixed ---
+// Mixing renamed and not renamed items.
+#import "module.typ": fn, b as val, item as other
+#test(val, 1)
+#test(other(1, 2), 3)
+
+--- import-from-function-scope ---
+// Test importing from function scopes.
+
+#import enum: item
+#import assert.with(true): *
+
+#enum(
+ item(1)[First],
+ item(5)[Fifth]
+)
+#eq(10, 10)
+#ne(5, 6)
+
+--- import-from-function-scope-item-renamed ---
+// Test renaming items imported from function scopes.
+#import assert: eq as aseq
+#aseq(10, 10)
+
+--- import-from-file-bare ---
+// A module import without items.
+#import "module.typ"
+#test(module.b, 1)
+#test(module.item(1, 2), 3)
+#test(module.push(2), 3)
+
+--- import-from-file-renamed ---
+// A renamed module import without items.
+#import "module.typ" as other
+#test(other.b, 1)
+#test(other.item(1, 2), 3)
+#test(other.push(2), 3)
+
+--- import-from-file-items-renamed-mixed ---
+// Mixing renamed module and items.
+#import "module.typ" as newname: b as newval, item
+#test(newname.b, 1)
+#test(newval, 1)
+#test(item(1, 2), 3)
+#test(newname.item(1, 2), 3)
+
+--- import-from-function-scope-renamed ---
+// Renamed module import with function scopes.
+#import enum as othernum
+#test(enum, othernum)
+
+--- import-from-function-scope-renamed-twice ---
+// Mixing renamed module import from function with renamed item import.
+#import assert as asrt
+#import asrt: ne as asne
+#asne(1, 2)
+
+--- import-module-item-name-mutating ---
+// Edge case for module access that isn't fixed.
+#import "module.typ"
+
+// Works because the method name isn't categorized as mutating.
+#test((module,).at(0).item(1, 2), 3)
+
+// Doesn't work because of mutating name.
+// Error: 2-11 cannot mutate a temporary value
+#(module,).at(0).push()
+
+--- import-no-whitespace ---
+// Who needs whitespace anyways?
+#import"module.typ":*
+
+--- import-trailing-comma ---
+// Allow the trailing comma.
+#import "module.typ": a, c,
+
+--- import-source-field-access ---
+// Usual importing syntax also works for function scopes
+#let d = (e: enum)
+#import d.e
+#import d.e as renamed
+#import d.e: item
+#item(2)[a]
+
+--- import-item-rename-unnecessary ---
+// Warning: 23-27 unnecessary import rename to same name
+#import enum: item as item
+
+--- import-rename-unnecessary ---
+// Warning: 17-21 unnecessary import rename to same name
+#import enum as enum
+
+--- import-rename-unnecessary-mixed ---
+// Warning: 17-21 unnecessary import rename to same name
+#import enum as enum: item
+
+// Warning: 17-21 unnecessary import rename to same name
+// Warning: 31-35 unnecessary import rename to same name
+#import enum as enum: item as item
+
+--- import-item-rename-unnecessary-but-ok ---
+// No warning on a case that isn't obviously pathological
+#import "module.typ" as module
+
+--- import-from-closure-invalid ---
+// Can't import from closures.
+#let f(x) = x
+// Error: 9-10 cannot import from user-defined functions
+#import f: x
+
+--- import-from-closure-renamed-invalid ---
+// Can't import from closures, despite renaming.
+#let f(x) = x
+// Error: 9-10 cannot import from user-defined functions
+#import f as g
+
+--- import-from-with-closure-invalid ---
+// Can't import from closures, despite modifiers.
+#let f(x) = x
+// Error: 9-18 cannot import from user-defined functions
+#import f.with(5): x
+
+--- import-from-with-closure-literal-invalid ---
+// Error: 9-18 cannot import from user-defined functions
+#import () => {5}: x
+
+--- import-from-int-invalid ---
+// Error: 9-10 expected path, module, function, or type, found integer
+#import 5: something
+
+--- import-from-int-renamed-invalid ---
+// Error: 9-10 expected path, module, function, or type, found integer
+#import 5 as x
+
+--- import-from-string-invalid ---
+// Error: 9-11 failed to load file (is a directory)
+#import "": name
+
+--- import-from-string-renamed-invalid ---
+// Error: 9-11 failed to load file (is a directory)
+#import "" as x
+
+--- import-file-not-found-invalid ---
+// Error: 9-20 file not found (searched at tests/suite/scripting/lib/0.2.1)
+#import "lib/0.2.1"
+
+--- import-file-not-found-renamed-invalid ---
+// Error: 9-20 file not found (searched at tests/suite/scripting/lib/0.2.1)
+#import "lib/0.2.1" as x
+
+--- import-file-not-valid-utf-8 ---
+// Some non-text stuff.
+// Error: 9-35 file is not valid utf-8
+#import "/assets/images/rhino.png"
+
+--- import-item-not-found ---
+// Unresolved import.
+// Error: 23-35 unresolved import
+#import "module.typ": non_existing
+
+--- import-cyclic ---
+// Cyclic import of this very file.
+// Error: 9-23 cyclic import
+#import "./import.typ"
+
+--- import-cyclic-in-other-file ---
+// Cyclic import in other file.
+#import "./modules/cycle1.typ": *
+
+This is never reached.
+
+--- import-renamed-old-name ---
+// Renaming does not import the old name (without items).
+#import "./modules/chap1.typ" as something
+#test(something.name, "Klaus")
+// Error: 7-12 unknown variable: chap1
+#test(chap1.name, "Klaus")
+
+--- import-items-renamed-old-name ---
+// Renaming does not import the old name (with items).
+#import "./modules/chap1.typ" as something: name as other
+#test(other, "Klaus")
+#test(something.name, "Klaus")
+// Error: 7-12 unknown variable: chap1
+#test(chap1.b, "Klaus")
+
+--- import-incomplete ---
+// Error: 8 expected expression
+#import
+
+--- import-item-string-invalid ---
+// Error: 26-29 unexpected string
+#import "module.typ": a, "b", c
+
+--- import-bad-token ---
+// Error: 23-24 unexpected equals sign
+#import "module.typ": =
+
+--- import-duplicate-comma ---
+// An additional trailing comma.
+// Error: 31-32 unexpected comma
+#import "module.typ": a, b, c,,
+
+--- import-no-colon ---
+// Error: 2:2 expected semicolon or line break
+#import "module.typ
+"stuff
+
+--- import-bad-token-star ---
+// A star in the list.
+// Error: 26-27 unexpected star
+#import "module.typ": a, *, b
+
+--- import-item-after-star ---
+// An item after a star.
+// Error: 24 expected semicolon or line break
+#import "module.typ": *, a
+
+--- import-bad-colon-in-items ---
+// Error: 14-15 unexpected colon
+// Error: 16-17 unexpected integer
+#import "": a: 1
+
+--- import-missing-comma ---
+// Error: 14 expected comma
+#import "": a b
+
+--- import-from-package-bare ---
+// Test import without items.
+#import "@test/adder:0.1.0"
+#test(adder.add(2, 8), 10)
+
+--- import-from-package-items ---
+// Test import with items.
+#import "@test/adder:0.1.0": add
+#test(add(2, 8), 10)
+
+--- import-from-package-required-compiler-version ---
+// Test too high required compiler version.
+// Error: 9-29 package requires typst 1.0.0 or newer (current version is VERSION)
+#import "@test/future:0.1.0": future
+
+--- import-from-package-namespace-invalid-1 ---
+// Error: 9-13 `@` is not a valid package namespace
+#import "@@": *
+
+--- import-from-package-name-missing-1 ---
+// Error: 9-16 package specification is missing name
+#import "@heya": *
+
+--- import-from-package-namespace-invalid-2 ---
+// Error: 9-15 `123` is not a valid package namespace
+#import "@123": *
+
+--- import-from-package-name-missing-2 ---
+// Error: 9-17 package specification is missing name
+#import "@test/": *
+
+--- import-from-package-version-missing-1 ---
+// Error: 9-22 package specification is missing version
+#import "@test/mypkg": *
+
+--- import-from-package-name-invalid ---
+// Error: 9-20 `$$$` is not a valid package name
+#import "@test/$$$": *
+
+--- import-from-package-version-missing-2 ---
+// Error: 9-23 package specification is missing version
+#import "@test/mypkg:": *
+
+--- import-from-package-version-missing-minor ---
+// Error: 9-24 version number is missing minor version
+#import "@test/mypkg:0": *
+
+--- import-from-package-version-major-invalid-1 ---
+// Error: 9-29 `latest` is not a valid major version
+#import "@test/mypkg:latest": *
+
+--- import-from-package-version-major-invalid-2 ---
+// Error: 9-29 `-3` is not a valid major version
+#import "@test/mypkg:-3.0.0": *
+
+--- import-from-package-version-missing-patch-1 ---
+// Error: 9-26 version number is missing patch version
+#import "@test/mypkg:0.3": *
+
+--- import-from-package-version-missing-patch-2 ---
+// Error: 9-27 version number is missing patch version
+#import "@test/mypkg:0.3.": *
+
+--- import-from-file-package-lookalike ---
+// Error: 9-28 file not found (searched at tests/suite/scripting/#test/mypkg:1.0.0)
+#import "#test/mypkg:1.0.0": *
diff --git a/tests/suite/scripting/include.typ b/tests/suite/scripting/include.typ
new file mode 100644
index 00000000..e4da6d19
--- /dev/null
+++ b/tests/suite/scripting/include.typ
@@ -0,0 +1,32 @@
+// Test module includes.
+
+--- include-file ---
+#set page(width: 200pt)
+
+= Document
+
+// Include a file
+#include "modules/chap1.typ"
+
+// Expression as a file name.
+#let chap2 = include "modu" + "les/chap" + "2.typ"
+
+-- _Intermission_ --
+#chap2
+
+--- include-file-not-found ---
+#{
+ // Error: 19-38 file not found (searched at tests/suite/scripting/modules/chap3.typ)
+ let x = include "modules/chap3.typ"
+}
+
+--- include-no-bindings ---
+#include "modules/chap1.typ"
+
+// The variables of the file should not appear in this scope.
+// Error: 2-6 unknown variable: name
+#name
+
+--- include-semicolon-or-linebreak ---
+// Error: 18 expected semicolon or line break
+#include "hi.typ" Hi
diff --git a/tests/suite/scripting/let.typ b/tests/suite/scripting/let.typ
new file mode 100644
index 00000000..2604c4ea
--- /dev/null
+++ b/tests/suite/scripting/let.typ
@@ -0,0 +1,143 @@
+// Test let bindings.
+
+--- let-basic ---
+// Automatically initialized with none.
+#let x
+#test(x, none)
+
+// Manually initialized with one.
+#let z = 1
+#test(z, 1)
+
+// Syntax sugar for function definitions.
+#let fill = conifer
+#let f(body) = rect(width: 2cm, fill: fill, inset: 5pt, body)
+#f[Hi!]
+
+--- let-termination ---
+// 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
+
+#test(v1, 1)
+#test(v2, 2)
+#test(v3, 3)
+
+--- let-valid-idents ---
+// Test what constitutes a valid Typst identifier.
+#let name = 1
+#test(name, 1)
+#let name_ = 1
+#test(name_, 1)
+#let name-2 = 1
+#test(name-2, 1)
+#let name_2 = 1
+#test(name_2, 1)
+#let __name = 1
+#test(__name, 1)
+#let ůñıćóðė = 1
+#test(ůñıćóðė, 1)
+
+--- let-binding-keyword-in-markup ---
+// Error: 6-8 expected pattern, found keyword `as`
+// Hint: 6-8 keyword `as` is not allowed as an identifier; try `as_` instead
+#let as = 1 + 2
+
+--- let-binding-keyword-in-code ---
+#{
+ // Error: 7-9 expected pattern, found keyword `as`
+ // Hint: 7-9 keyword `as` is not allowed as an identifier; try `as_` instead
+ let as = 10
+}
+
+--- let-ident-parenthesized ---
+// Test parenthesised assignments.
+#let (a) = (1, 2)
+
+--- let-incomplete ---
+// Error: 5 expected pattern
+#let
+
+// Error: 6 expected pattern
+#{let}
+
+// Error: 6-9 expected pattern, found string
+#let "v"
+
+// Error: 7 expected semicolon or line break
+#let v 1
+
+// Error: 9 expected expression
+#let v =
+
+// Error: 6-9 expected pattern, found string
+#let "v" = 1
+
+// 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: 18 expected expression
+// Error: 11-12 unclosed delimiter
+#let v5 = (1, 2 + ; Five
+
+// Error: 9-13 expected pattern, found boolean
+#let (..true) = false
+
+--- underscore-invalid ---
+#let _ = 4
+
+#for _ in range(2) []
+
+// Error: 2-3 unexpected underscore
+#_
+
+// Error: 8-9 expected expression, found underscore
+#lorem(_)
+
+// Error: 3-4 expected expression, found underscore
+#(_,)
+
+// Error: 3-4 expected expression, found underscore
+#{_}
+
+// Error: 8-9 expected expression, found underscore
+#{ 1 + _ }
+
+--- let-function-incomplete ---
+// Error: 13 expected equals sign
+#let func(x)
+
+// Error: 15 expected expression
+#let func(x) =
+
+--- let-function-parenthesized ---
+// This is not yet parsed in the ideal way.
+// Error: 12 expected equals sign
+#let (func)(x)
+
+--- let-function-parenthesized-with-init ---
+// These errors aren't great.
+// Error: 12 expected equals sign
+// Error: 15-15 expected semicolon or line break
+#let (func)(x) = 3
+
+--- let-with-no-init-group ---
+// This was unintentionally allowed ...
+// Error: 9 expected equals sign
+#let (a)
+
+--- let-with-no-init-destructuring ---
+// ... where this wasn't.
+// Error: 12 expected equals sign
+#let (a, b)
diff --git a/tests/suite/scripting/loop.typ b/tests/suite/scripting/loop.typ
new file mode 100644
index 00000000..689c1c93
--- /dev/null
+++ b/tests/suite/scripting/loop.typ
@@ -0,0 +1,142 @@
+// Test break and continue in loops.
+
+--- loop-break-basic ---
+// Test break.
+
+#let var = 0
+#let error = false
+
+#for i in range(10) {
+ var += i
+ if i > 5 {
+ break
+ error = true
+ }
+}
+
+#test(var, 21)
+#test(error, false)
+
+--- loop-break-join-basic ---
+// Test joining with break.
+
+#let i = 0
+#let x = while true {
+ i += 1
+ str(i)
+ if i >= 5 {
+ "."
+ break
+ }
+}
+
+#test(x, "12345.")
+
+--- loop-continue-basic ---
+// Test continue.
+
+#let i = 0
+#let x = 0
+
+#while x < 8 {
+ i += 1
+ if calc.rem(i, 3) == 0 {
+ continue
+ }
+ x += i
+}
+
+// If continue did not work, this would equal 10.
+#test(x, 12)
+
+--- loop-continue-join ---
+// Test joining with continue.
+
+#let x = for i in range(5) {
+ "a"
+ if calc.rem(i, 3) == 0 {
+ "_"
+ continue
+ }
+ str(i)
+}
+
+#test(x, "a_a1a2a_a4")
+
+--- loop-break-outside-of-loop ---
+// Test break outside of loop.
+#let f() = {
+ // Error: 3-8 cannot break outside of loop
+ break
+}
+
+#for i in range(1) {
+ f()
+}
+
+--- loop-break-join-in-last-arg ---
+// Test break in function call.
+#let identity(x) = x
+#let out = for i in range(5) {
+ "A"
+ identity({
+ "B"
+ break
+ })
+ "C"
+}
+
+#test(out, "AB")
+
+--- loop-continue-outside-of-loop-in-block ---
+// Test continue outside of loop.
+
+// Error: 12-20 cannot continue outside of loop
+#let x = { continue }
+
+--- loop-continue-outside-of-loop-in-markup ---
+// Error: 2-10 cannot continue outside of loop
+#continue
+
+--- loop-break-join-in-nested-blocks ---
+// Should output `Hello World 🌎`.
+#for _ in range(10) {
+ [Hello ]
+ [World #{
+ [🌎]
+ break
+ }]
+}
+
+--- loop-break-join-set-and-show ---
+// Should output `Some` in red, `Some` in blue and `Last` in green.
+// Everything should be in smallcaps.
+#for color in (red, blue, green, yellow) [
+ #set text(font: "Roboto")
+ #show: it => text(fill: color, it)
+ #smallcaps(if color != green [
+ Some
+ ] else [
+ Last
+ #break
+ ])
+]
+
+--- loop-break-join-in-set-rule-args ---
+// Test break in set rule.
+// Should output `Hi` in blue.
+#for i in range(10) {
+ [Hello]
+ set text(blue, ..break)
+ [Not happening]
+}
+
+--- loop-break-join-in-first-arg ---
+// Test second block during break flow.
+
+#for i in range(10) {
+ table(
+ { [A]; break },
+ for _ in range(3) [B]
+ )
+}
diff --git a/tests/suite/scripting/methods.typ b/tests/suite/scripting/methods.typ
new file mode 100644
index 00000000..5deea2cf
--- /dev/null
+++ b/tests/suite/scripting/methods.typ
@@ -0,0 +1,51 @@
+// Test method calls.
+
+--- method-whitespace ---
+// Test whitespace around dot.
+#test( "Hi there" . split() , ("Hi", "there"))
+
+--- method-mutating ---
+// Test mutating indexed value.
+#{
+ let matrix = (((1,), (2,)), ((3,), (4,)))
+ matrix.at(1).at(0).push(5)
+ test(matrix, (((1,), (2,)), ((3, 5), (4,))))
+}
+
+--- method-multiline ---
+// Test multiline chain in code block.
+#{
+ let rewritten = "Hello. This is a sentence. And one more."
+ .split(".")
+ .map(s => s.trim())
+ .filter(s => s != "")
+ .map(s => s + "!")
+ .join("\n ")
+
+ test(rewritten, "Hello!\n This is a sentence!\n And one more!")
+}
+
+--- method-unknown ---
+// Error: 2:10-2:13 type array has no method `fun`
+#let numbers = ()
+#numbers.fun()
+
+--- method-unknown-but-field-exists ---
+// Error: 2:4-2:10 type content has no method `stroke`
+// Hint: 2:4-2:10 did you mean to access the field `stroke`?
+#let l = line(stroke: red)
+#l.stroke()
+
+--- method-mutate-on-temporary ---
+// Error: 2:2-2:43 cannot mutate a temporary value
+#let numbers = (1, 2, 3)
+#numbers.map(v => v / 2).sorted().map(str).remove(4)
+
+--- assign-to-method-invalid ---
+// Error: 2:3-2:19 cannot mutate a temporary value
+#let numbers = (1, 2, 3)
+#(numbers.sorted() = 1)
+
+--- method-mutate-on-std-constant ---
+// Error: 2-5 cannot mutate a constant: box
+#box.push(1)
diff --git a/tests/suite/scripting/module.typ b/tests/suite/scripting/module.typ
new file mode 100644
index 00000000..8a67d225
--- /dev/null
+++ b/tests/suite/scripting/module.typ
@@ -0,0 +1,13 @@
+// SKIP
+// A file to import in import / include tests.
+
+#let a
+#let b = 1
+#let c = 2
+#let d = 3
+#let value = [hi]
+#let item(a, b) = a + b
+#let push(a) = a + 1
+#let fn = rect.with(fill: conifer, inset: 5pt)
+
+Some _includable_ text.
diff --git a/tests/suite/scripting/modules/chap1.typ b/tests/suite/scripting/modules/chap1.typ
new file mode 100644
index 00000000..13d0acf8
--- /dev/null
+++ b/tests/suite/scripting/modules/chap1.typ
@@ -0,0 +1,8 @@
+// SKIP
+#let name = "Klaus"
+
+== Chapter 1
+#name stood in a field of wheat. There was nothing of particular interest about
+the field #name just casually surveyed for any paths on which the corn would not
+totally ruin his semi-new outdorsy jacket but then again, most of us spend
+considerable time in non-descript environments.
diff --git a/tests/suite/scripting/modules/chap2.typ b/tests/suite/scripting/modules/chap2.typ
new file mode 100644
index 00000000..9c9d12d7
--- /dev/null
+++ b/tests/suite/scripting/modules/chap2.typ
@@ -0,0 +1,10 @@
+// SKIP
+#let name = "Klaus"
+
+== Chapter 2
+Their motivations, however, were pretty descript, so to speak. #name had not yet
+conceptualized their consequences, but that should change pretty quickly. #name
+approached the center of the field and picked up a 4-foot long disk made from
+what could only be cow manure. The hair on the back of #name' neck bristled as
+he stared at the unusual sight. After studying the object for a while, he
+promptly popped the question, "How much?"
diff --git a/tests/suite/scripting/modules/cycle1.typ b/tests/suite/scripting/modules/cycle1.typ
new file mode 100644
index 00000000..0f924ac7
--- /dev/null
+++ b/tests/suite/scripting/modules/cycle1.typ
@@ -0,0 +1,5 @@
+// SKIP
+#import "cycle2.typ": *
+#let inaccessible = "wow"
+
+This is the first element of an import cycle.
diff --git a/tests/suite/scripting/modules/cycle2.typ b/tests/suite/scripting/modules/cycle2.typ
new file mode 100644
index 00000000..69eb2033
--- /dev/null
+++ b/tests/suite/scripting/modules/cycle2.typ
@@ -0,0 +1,5 @@
+// SKIP
+#import "cycle1.typ": *
+#let val = "much cycle"
+
+This is the second element of an import cycle.
diff --git a/tests/suite/scripting/ops.typ b/tests/suite/scripting/ops.typ
new file mode 100644
index 00000000..0f13d212
--- /dev/null
+++ b/tests/suite/scripting/ops.typ
@@ -0,0 +1,465 @@
+// Test binary expressions.
+
+--- ops-add-content ---
+// Test adding content.
+#([*Hello* ] + [world!])
+
+--- ops-unary-basic ---
+// Test math operators.
+
+// Test plus and minus.
+#for v in (1, 3.14, 12pt, 45deg, 90%, 13% + 10pt, 6.3fr) {
+ // Test plus.
+ test(+v, v)
+
+ // Test minus.
+ test(-v, -1 * v)
+ test(--v, v)
+
+ // Test combination.
+ test(-++ --v, -v)
+}
+
+#test(-(4 + 2), 6-12)
+
+// Addition.
+#test(2 + 4, 6)
+#test("a" + "b", "ab")
+#test("a" + if false { "b" }, "a")
+#test("a" + if true { "b" }, "ab")
+#test(13 * "a" + "bbbbbb", "aaaaaaaaaaaaabbbbbb")
+#test((1, 2) + (3, 4), (1, 2, 3, 4))
+#test((a: 1) + (b: 2, c: 3), (a: 1, b: 2, c: 3))
+
+--- ops-add-too-large ---
+// Error: 3-26 value is too large
+#(9223372036854775807 + 1)
+
+--- ops-binary-basic ---
+// 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)
+
+// With block.
+#test(if true {
+ 1
+} + 2, 3)
+
+// Mathematical identities.
+#let nums = (
+ 1, 3.14,
+ 12pt, 3em, 12pt + 3em,
+ 45deg,
+ 90%,
+ 13% + 10pt, 5% + 1em + 3pt,
+ 2.3fr,
+)
+
+#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) != int {
+ test(v + v, 2.0 * v)
+ }
+
+ if type(v) != relative and ("pt" not in repr(v) or "em" not in repr(v)) {
+ test(v / v, 1.0)
+ }
+}
+
+// Make sure length, ratio and relative length
+// - can all be added to / subtracted from each other,
+// - multiplied with integers and floats,
+// - divided by integers and floats.
+#let dims = (10pt, 1em, 10pt + 1em, 30%, 50% + 3cm, 40% + 2em + 1cm)
+#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 division of different numeric types with zero components.
+#for a in (0pt, 0em, 0%) {
+ for b in (10pt, 10em, 10%) {
+ test((2 * b) / b, 2)
+ test((a + b * 2) / b, 2)
+ test(b / (b * 2 + a), 0.5)
+ }
+}
+
+--- ops-multiply-inf-with-length ---
+// Test that multiplying infinite numbers by certain units does not crash.
+#(float("inf") * 1pt)
+#(float("inf") * 1em)
+#(float("inf") * (1pt + 1em))
+
+--- ops-attempt-nan-length ---
+// Test that trying to produce a NaN scalar (such as in lengths) does not crash.
+#let infpt = float("inf") * 1pt
+#test(infpt - infpt, 0pt)
+#test(infpt + (-infpt), 0pt)
+// TODO: this result is surprising
+#test(infpt / float("inf"), 0pt)
+
+--- ops-unary-bool ---
+// Test boolean operators.
+
+// Test not.
+#test(not true, false)
+#test(not false, true)
+
+// 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)
+
+--- ops-equality ---
+// Test equality operators.
+
+// Most things compare by value.
+#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("a" != "a", false)
+
+// Functions compare by identity.
+#test(test == test, true)
+#test((() => {}) == (() => {}), false)
+
+// Content compares field by field.
+#let t = [a]
+#test(t == t, true)
+#test([] == [], true)
+#test([a] == [a], true)
+#test(grid[a] == grid[a], true)
+#test(grid[a] == grid[b], false)
+
+--- ops-compare ---
+// 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(10% < 20%, true)
+#test(50% < 40% + 0pt, false)
+#test(40% + 0pt < 50% + 0pt, true)
+#test(1em < 2em, true)
+#test((0, 1, 2, 4) < (0, 1, 2, 5), true)
+#test((0, 1, 2, 4) < (0, 1, 2, 3), false)
+#test((0, 1, 2, 3.3) > (0, 1, 2, 4), false)
+#test((0, 1, 2) < (0, 1, 2, 3), true)
+#test((0, 1, "b") > (0, 1, "a", 3), true)
+#test((0, 1.1, 3) >= (0, 1.1, 3), true)
+#test((0, 1, datetime(day: 1, month: 12, year: 2023)) <= (0, 1, datetime(day: 1, month: 12, year: 2023), 3), true)
+#test(("a", 23, 40, "b") > ("a", 23, 40), true)
+#test(() <= (), true)
+#test(() >= (), true)
+#test(() <= (1,), true)
+#test((1,) <= (), false)
+
+--- ops-in ---
+// Test `in` operator.
+#test("hi" in "worship", true)
+#test("hi" in ("we", "hi", "bye"), true)
+#test("Hey" in "abHeyCd", true)
+#test("Hey" in "abheyCd", false)
+#test(5 in range(10), true)
+#test(12 in range(10), false)
+#test("" in (), false)
+#test("key" in (key: "value"), true)
+#test("value" in (key: "value"), false)
+#test("Hey" not in "abheyCd", true)
+#test("a" not
+/* fun comment? */ in "abc", false)
+
+--- ops-not-trailing ---
+// Error: 10 expected keyword `in`
+#("a" not)
+
+--- func-with ---
+// Test `with` method.
+
+// Apply positional arguments.
+#let add(x, y) = x + y
+#test(add.with(2)(3), 5)
+#test(add.with(2, 3)(), 5)
+#test(add.with(2).with(3)(), 5)
+#test((add.with(2))(4), 6)
+#test((add.with(2).with(3))(), 5)
+
+// Make sure that named arguments are overridable.
+#let inc(x, y: 1) = x + y
+#test(inc(1), 2)
+
+#let inc2 = inc.with(y: 2)
+#test(inc2(2), 4)
+#test(inc2(2, y: 4), 6)
+
+// Apply arguments to an argument sink.
+#let times(..sink) = {
+ let res = sink.pos().product()
+ if sink.named().at("negate", default: false) { res *= -1 }
+ res
+}
+#test((times.with(2, negate: true).with(5))(), -10)
+#test((times.with(2).with(5).with(negate: true))(), -10)
+#test((times.with(2).with(5, negate: true))(), -10)
+#test((times.with(2).with(negate: true))(5), -10)
+
+--- ops-precedence-basic ---
+// 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)
+
+--- ops-precedence-boolean-ops ---
+// Assignment binds stronger than boolean operations.
+// Error: 2:3-2:8 cannot mutate a temporary value
+#let x = false
+#(not x = "a")
+
+--- ops-precedence-unary ---
+// Precedence doesn't matter for chained unary operators.
+// Error: 3-12 cannot apply '-' to boolean
+#(-not true)
+
+--- ops-precedence-not-in ---
+// Not in handles precedence.
+#test(-1 not in (1, 2, 3), true)
+
+--- ops-precedence-parentheses ---
+// Parentheses override precedence.
+#test((1), 1)
+#test((1+2)*-3, -9)
+
+// Error: 8-9 unclosed delimiter
+#test({(1 + 1}, 2)
+
+--- ops-associativity-left ---
+// 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)
+
+--- ops-associativity-right ---
+// Assignment is right-associative.
+#{
+ let x = 1
+ let y = 2
+ x = y = "ok"
+ test(x, none)
+ test(y, "ok")
+}
+
+--- ops-unary-minus-missing-expr ---
+// Error: 4 expected expression
+#(-)
+
+--- ops-add-missing-rhs ---
+// Error: 10 expected expression
+#test({1+}, 1)
+
+--- ops-mul-missing-rhs ---
+// Error: 10 expected expression
+#test({2*}, 2)
+
+--- ops-unary-plus-on-content ---
+// Error: 3-13 cannot apply unary '+' to content
+#(+([] + []))
+
+--- ops-unary-plus-on-string ---
+// Error: 3-6 cannot apply '-' to string
+#(-"")
+
+--- ops-not-on-array ---
+// Error: 3-9 cannot apply 'not' to array
+#(not ())
+
+--- ops-compare-relative-length-and-ratio ---
+// Error: 3-19 cannot compare relative length and ratio
+#(30% + 1pt <= 40%)
+
+--- ops-compare-em-with-abs ---
+// Error: 3-14 cannot compare 1em with 10pt
+#(1em <= 10pt)
+
+--- ops-compare-normal-float-with-nan ---
+// Error: 3-22 cannot compare 2.2 with NaN
+#(2.2 <= float("nan"))
+
+--- ops-compare-int-and-str ---
+// Error: 3-26 cannot compare integer and string
+#((0, 1, 3) > (0, 1, "a"))
+
+--- ops-compare-array-nested-failure ---
+// Error: 3-42 cannot compare 3.5 with NaN
+#((0, "a", 3.5) <= (0, "a", float("nan")))
+
+--- ops-divide-by-zero-float ---
+// Error: 3-12 cannot divide by zero
+#(1.2 / 0.0)
+
+--- ops-divide-by-zero-int ---
+// Error: 3-8 cannot divide by zero
+#(1 / 0)
+
+--- ops-divide-by-zero-angle ---
+// Error: 3-15 cannot divide by zero
+#(15deg / 0deg)
+
+--- ops-binary-arithmetic-error-message ---
+// Special messages for +, -, * and /.
+// Error: 3-10 cannot add integer and string
+#(1 + "2", 40% - 1)
+
+--- add-assign-int-and-str ---
+// Error: 15-23 cannot add integer and string
+#{ let x = 1; x += "2" }
+
+--- ops-divide-ratio-by-length ---
+// Error: 4-13 cannot divide ratio by length
+#( 10% / 5pt )
+
+--- ops-divide-em-by-abs ---
+// Error: 3-12 cannot divide these two lengths
+#(1em / 5pt)
+
+--- ops-divide-relative-length-by-ratio ---
+// Error: 3-19 cannot divide relative length by ratio
+#((10% + 1pt) / 5%)
+
+--- ops-divide-relative-lengths ---
+// Error: 3-28 cannot divide these two relative lengths
+#((10% + 1pt) / (20% + 1pt))
+
+--- ops-subtract-int-from-ratio ---
+// Error: 13-20 cannot subtract integer from ratio
+#((1234567, 40% - 1))
+
+--- ops-multiply-int-with-bool ---
+// Error: 3-11 cannot multiply integer with boolean
+#(2 * true)
+
+--- ops-divide-int-by-length ---
+// Error: 3-11 cannot divide integer by length
+#(3 / 12pt)
+
+--- multiply-negative-int-with-str ---
+// Error: 3-10 number must be at least zero
+#(-1 * "")
+
+--- ops-assign ---
+// 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")
+
+--- ops-assign-unknown-var-lhs ---
+#{
+ // Error: 3-6 unknown variable: a-1
+ // Hint: 3-6 if you meant to use subtraction, try adding spaces around the minus sign
+ a-1 = 2
+}
+
+--- ops-assign-unknown-var-rhs ---
+#{
+ let a = 2
+ a = 1-a
+ a = a -1
+
+ // Error: 7-10 unknown variable: a-1
+ // Hint: 7-10 if you meant to use subtraction, try adding spaces around the minus sign
+ a = a-1
+}
+
+--- ops-assign-unknown-parenthesized-variable ---
+// Error: 4-5 unknown variable: x
+#((x) = "")
+
+--- ops-assign-destructuring-unknown-variable ---
+// Error: 4-5 unknown variable: x
+#((x,) = (1,))
+
+--- ops-assign-to-temporary ---
+// Error: 3-8 cannot mutate a temporary value
+#(1 + 2 += 3)
+
+--- ops-assign-to-invalid-unary-op ---
+// Error: 2:3-2:8 cannot apply 'not' to string
+#let x = "Hey"
+#(not x = "a")
+
+--- ops-assign-to-invalid-binary-op ---
+// Error: 7-8 unknown variable: x
+#(1 + x += 3)
+
+--- ops-assign-unknown-variable ---
+// Error: 3-4 unknown variable: z
+#(z = 1)
+
+--- ops-assign-to-std-constant ---
+// Error: 3-7 cannot mutate a constant: rect
+#(rect = "hi")
+
+--- ops-assign-to-shadowed-std-constant ---
+// Works if we define rect beforehand
+// (since then it doesn't resolve to the standard library version anymore).
+#let rect = ""
+#(rect = "hi")
diff --git a/tests/suite/scripting/params.typ b/tests/suite/scripting/params.typ
new file mode 100644
index 00000000..688124f2
--- /dev/null
+++ b/tests/suite/scripting/params.typ
@@ -0,0 +1,69 @@
+--- param-underscore-missing-argument ---
+// Error: 17-20 missing argument: pattern parameter
+#let f(a: 10) = a() + 1
+#f(a: _ => 5)
+
+--- params-sink-named ---
+// ... but this was.
+#let f(..x) = {}
+#f(arg: 1)
+
+--- params-sink-unnamed ---
+// unnamed spread
+#let f(.., a) = a
+#test(f(1, 2, 3), 3)
+
+// This wasn't allowed before the bug fix ...
+#let f(..) = 2
+#test(f(arg: 1), 2)
+
+--- params-sink-bool-invalid ---
+// Error: 10-14 expected pattern, found boolean
+#let f(..true) = none
+
+--- params-sink-multiple-invalid ---
+// Error: 13-16 only one argument sink is allowed
+#let f(..a, ..b) = none
+
+--- params-sink-at-start ---
+// Spread at beginning.
+#{
+ let f(..a, b) = (a, b)
+ test(repr(f(1)), "((), 1)")
+ test(repr(f(1, 2, 3)), "((1, 2), 3)")
+ test(repr(f(1, 2, 3, 4, 5)), "((1, 2, 3, 4), 5)")
+}
+
+--- params-sink-in-middle ---
+// Spread in the middle.
+#{
+ let f(a, ..b, c) = (a, b, c)
+ test(repr(f(1, 2)), "(1, (), 2)")
+ test(repr(f(1, 2, 3, 4, 5)), "(1, (2, 3, 4), 5)")
+}
+
+--- params-sink-unnamed-empty ---
+// Unnamed sink should just ignore any extra arguments.
+#{
+ let f(a, b: 5, ..) = (a, b)
+ test(f(4), (4, 5))
+ test(f(10, b: 11), (10, 11))
+ test(f(13, 20, b: 12), (13, 12))
+ test(f(15, b: 16, c: 13), (15, 16))
+}
+
+--- params-sink-missing-arguments ---
+#{
+ let f(..a, b, c, d) = none
+
+ // Error: 3-10 missing argument: d
+ f(1, 2)
+}
+
+--- issue-1029-parameter-destructuring ---
+// Test that underscore works in parameter patterns.
+#test((1, 2, 3).zip((1, 2, 3)).map(((_, x)) => x), (1, 2, 3))
+
+--- issue-1351-parameter-dictionary ---
+// Error: 17-22 expected pattern, found string
+#let foo((test: "bar")) = {}
diff --git a/tests/suite/scripting/recursion.typ b/tests/suite/scripting/recursion.typ
new file mode 100644
index 00000000..43fe848e
--- /dev/null
+++ b/tests/suite/scripting/recursion.typ
@@ -0,0 +1,55 @@
+// Test recursive function calls.
+
+--- recursion-named ---
+// Test with named function.
+#let fib(n) = {
+ if n <= 2 {
+ 1
+ } else {
+ fib(n - 1) + fib(n - 2)
+ }
+}
+
+#test(fib(10), 55)
+
+--- recursion-unnamed-invalid ---
+// Test with unnamed function.
+// Error: 17-18 unknown variable: f
+#let f = (n) => f(n - 1)
+#f(10)
+
+--- recursion-named-returns-itself ---
+// Test capturing with named function.
+#let f = 10
+#let f() = f
+#test(type(f()), function)
+
+--- recursion-unnamed-does-not-return-itself ---
+// Test capturing with unnamed function.
+#let f = 10
+#let f = () => f
+#test(type(f()), int)
+
+--- recursion-shadowing ---
+// Test redefinition.
+#let f(x) = "hello"
+#let f(x) = if x != none { f(none) } else { "world" }
+#test(f(1), "world")
+
+--- recursion-maximum-depth ---
+// Error: 15-21 maximum function call depth exceeded
+#let rec(n) = rec(n) + 1
+#rec(1)
+
+--- recursion-via-include-in-layout ---
+// Test cyclic imports during layout.
+// Error: 2-38 maximum show rule depth exceeded
+// Hint: 2-38 check whether the show rule matches its own output
+#layout(_ => include "recursion.typ")
+
+--- recursion-show-math ---
+// Test recursive show rules.
+// Error: 22-25 maximum show rule depth exceeded
+// Hint: 22-25 check whether the show rule matches its own output
+#show math.equation: $x$
+$ x $
diff --git a/tests/suite/scripting/return.typ b/tests/suite/scripting/return.typ
new file mode 100644
index 00000000..63e1c0b9
--- /dev/null
+++ b/tests/suite/scripting/return.typ
@@ -0,0 +1,87 @@
+// Test return out of functions.
+
+--- return-with-value ---
+// Test return with value.
+#let f(x) = {
+ return x + 1
+}
+
+#test(f(1), 2)
+
+--- return-join ---
+// Test return with joining.
+
+#let f(x) = {
+ "a"
+ if x == 0 {
+ return "b"
+ } else if x == 1 {
+ "c"
+ } else {
+ "d"
+ return
+ "e"
+ }
+}
+
+#test(f(0), "b")
+#test(f(1), "ac")
+#test(f(2), "ad")
+
+--- return-in-nested-content-block ---
+// Test return with joining and content.
+
+#let f(text, caption: none) = {
+ text
+ if caption == none [\.#return]
+ [, ]
+ emph(caption)
+ [\.]
+}
+
+#f(caption: [with caption])[My figure]
+
+#f[My other figure]
+
+--- return-outside-of-function ---
+// Test return outside of function.
+
+#for x in range(5) {
+ // Error: 3-9 cannot return outside of function
+ return
+}
+
+--- return-in-first-arg ---
+// Test that the expression is evaluated to the end.
+#let sum(..args) = {
+ let s = 0
+ for v in args.pos() {
+ s += v
+ }
+ s
+}
+
+#let f() = {
+ sum(..return, 1, 2, 3)
+ "nope"
+}
+
+#test(f(), 6)
+
+--- return-in-content-block ---
+// Test value return from content.
+#let x = 3
+#let f() = [
+ Hello 😀
+ #return "nope"
+ World
+]
+
+#test(f(), "nope")
+
+--- return-semicolon-or-linebreak ---
+// Test rejection of extra value
+#let f() = [
+ // Error: 16-16 expected semicolon or line break
+ #return a + b Hello World
+]
diff --git a/tests/suite/scripting/while.typ b/tests/suite/scripting/while.typ
new file mode 100644
index 00000000..5e452a89
--- /dev/null
+++ b/tests/suite/scripting/while.typ
@@ -0,0 +1,59 @@
+// Test while expressions.
+
+--- while-loop-basic ---
+// 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
+}
+
+--- while-loop-expr ---
+// Value of while loops.
+
+#test(while false {}, none)
+
+#let i = 0
+#test(type(while i < 1 [#(i += 1)]), content)
+
+--- while-loop-condition-content-invalid ---
+// Condition must be boolean.
+// Error: 8-14 expected boolean, found content
+#while [nope] [nope]
+
+--- while-loop-condition-always-true ---
+// Error: 8-25 condition is always true
+#while 2 < "hello".len() {}
+
+--- while-loop-limit ---
+// Error: 2:2-2:24 loop seems to be infinite
+#let i = 1
+#while i > 0 { i += 1 }
+
+--- while-loop-incomplete ---
+// Error: 7 expected expression
+#while
+
+// Error: 8 expected expression
+#{while}
+
+// Error: 9 expected block
+#while x
+
+// Error: 7 expected expression
+#while
+x {}
+
+// Error: 9 expected block
+#while x something