summaryrefslogtreecommitdiff
path: root/tests/typ/compiler
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-29 13:37:25 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-29 14:18:13 +0100
commit0efe669278a5e1c3f2985eba2f3360e91159c54a (patch)
tree502712857c48f0decb5e698257c0a96d358a436e /tests/typ/compiler
parent836692e73cff0356e409a9ba5b4887b86809d4ca (diff)
Reorganize library and tests
Diffstat (limited to 'tests/typ/compiler')
-rw-r--r--tests/typ/compiler/array.typ98
-rw-r--r--tests/typ/compiler/bench.typ45
-rw-r--r--tests/typ/compiler/block.typ144
-rw-r--r--tests/typ/compiler/break-continue.typ147
-rw-r--r--tests/typ/compiler/call.typ106
-rw-r--r--tests/typ/compiler/closure.typ158
-rw-r--r--tests/typ/compiler/comment.typ33
-rw-r--r--tests/typ/compiler/construct.typ31
-rw-r--r--tests/typ/compiler/dict.typ75
-rw-r--r--tests/typ/compiler/field.typ40
-rw-r--r--tests/typ/compiler/for.typ123
-rw-r--r--tests/typ/compiler/if.typ136
-rw-r--r--tests/typ/compiler/import.typ116
-rw-r--r--tests/typ/compiler/include.typ32
-rw-r--r--tests/typ/compiler/label.typ58
-rw-r--r--tests/typ/compiler/let.typ68
-rw-r--r--tests/typ/compiler/methods-collection.typ115
-rw-r--r--tests/typ/compiler/methods-color.typ22
-rw-r--r--tests/typ/compiler/methods-str.typ117
-rw-r--r--tests/typ/compiler/methods.typ46
-rw-r--r--tests/typ/compiler/module.typ12
-rw-r--r--tests/typ/compiler/modules/chap1.typ9
-rw-r--r--tests/typ/compiler/modules/chap2.typ11
-rw-r--r--tests/typ/compiler/modules/cycle1.typ6
-rw-r--r--tests/typ/compiler/modules/cycle2.typ6
-rw-r--r--tests/typ/compiler/ops-assoc.typ18
-rw-r--r--tests/typ/compiler/ops-invalid.typ107
-rw-r--r--tests/typ/compiler/ops-prec.typ30
-rw-r--r--tests/typ/compiler/ops.typ230
-rw-r--r--tests/typ/compiler/repr.typ48
-rw-r--r--tests/typ/compiler/return.typ82
-rw-r--r--tests/typ/compiler/set.typ66
-rw-r--r--tests/typ/compiler/shorthand.typ20
-rw-r--r--tests/typ/compiler/show-bare.typ33
-rw-r--r--tests/typ/compiler/show-node.typ104
-rw-r--r--tests/typ/compiler/show-recursive.typ51
-rw-r--r--tests/typ/compiler/show-selector.typ36
-rw-r--r--tests/typ/compiler/show-text.typ59
-rw-r--r--tests/typ/compiler/spread.typ93
-rw-r--r--tests/typ/compiler/while.typ58
40 files changed, 2789 insertions, 0 deletions
diff --git a/tests/typ/compiler/array.typ b/tests/typ/compiler/array.typ
new file mode 100644
index 00000000..cb8433cb
--- /dev/null
+++ b/tests/typ/compiler/array.typ
@@ -0,0 +1,98 @@
+// Test arrays.
+// Ref: false
+
+---
+// Ref: true
+
+#set page(width: 150pt)
+
+// Empty.
+{()}
+
+// Not an array, just a parenthesized expression.
+{(1)}
+
+// One item and trailing comma.
+{(-1,)}
+
+// No trailing comma.
+{(true, false)}
+
+// Multiple lines and items and trailing comma.
+{("1"
+ , rgb("002")
+ ,)}
+
+---
+// Test lvalue and rvalue access.
+{
+ let array = (1, 2)
+ array(1) += 5 + array(0)
+ test(array, (1, 8))
+}
+
+---
+// Test rvalue out of bounds.
+// Error: 2-14 array index out of bounds (index: 5, len: 3)
+{(1, 2, 3)(5)}
+
+---
+// Test lvalue out of bounds.
+{
+ let array = (1, 2, 3)
+ // Error: 3-11 array index out of bounds (index: 3, len: 3)
+ array(3) = 5
+}
+
+---
+// Test negative indices.
+{
+ let array = (1, 2, 3, 4)
+ test(array(0), 1)
+ test(array(-1), 4)
+ test(array(-2), 3)
+ test(array(-3), 2)
+ test(array(-4), 1)
+}
+
+---
+// Error: 2-15 array index out of bounds (index: -4, len: 3)
+{(1, 2, 3)(-4)}
+
+---
+// Test non-collection indexing.
+
+{
+ let x = 10pt
+ // Error: 3-4 expected collection, found length
+ x() = 1
+}
+
+---
+// Error: 3 expected closing paren
+{(}
+
+// Error: 2-3 expected expression, found closing paren
+{)}
+
+// Error: 4 expected comma
+// Error: 4-6 unexpected end of block comment
+{(1*/2)}
+
+// Error: 6-8 invalid number suffix
+{(1, 1u 2)}
+
+// Error: 3-4 expected expression, found comma
+{(,1)}
+
+// Missing expression makes named pair incomplete, making this an empty array.
+// Error: 5 expected expression
+{(a:)}
+
+// Named pair after this is already identified as an array.
+// Error: 6-10 expected expression, found named pair
+{(1, b: 2)}
+
+// Keyed pair after this is already identified as an array.
+// Error: 6-14 expected expression, found keyed pair
+{(1, "key": 2)}
diff --git a/tests/typ/compiler/bench.typ b/tests/typ/compiler/bench.typ
new file mode 100644
index 00000000..6aff1ac2
--- /dev/null
+++ b/tests/typ/compiler/bench.typ
@@ -0,0 +1,45 @@
+// Configuration with `page` and `font` functions.
+#set page(width: 450pt, margin: 1cm)
+
+// There are variables and they can take normal values like strings, ...
+#let city = "Berlin"
+
+// ... but also "content" values. While these contain markup,
+// they are also values and can be summed, stored in arrays etc.
+// There are also more standard control flow structures, like #if and #for.
+#let university = [*Technische Universität {city}*]
+#let faculty = [*Fakultät II, Institut for Mathematik*]
+
+// The `box` function just places content into a rectangular container. When
+// the only argument to a function is a content block, the parentheses can be
+// omitted (i.e. `f[a]` is the same as `f([a])`).
+#box[
+ // Backslash adds a forced line break.
+ #university \
+ #faculty \
+ Sekretariat MA \
+ Dr. Max Mustermann \
+ Ola Nordmann, John Doe
+]
+#align(right, box[*WiSe 2019/2020* \ Woche 3])
+
+// Adds vertical spacing.
+#v(6mm)
+
+// If the last argument to a function is a content block, we can also place it
+// behind the parentheses.
+#align(center)[
+ // Markdown-like syntax for headings.
+ ==== 3. Übungsblatt Computerorientierte Mathematik II #v(4mm)
+ *Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(4mm)
+ *Alle Antworten sind zu beweisen.*
+]
+
+*1. Aufgabe* #align(right)[(1 + 1 + 2 Punkte)]
+
+Ein _Binärbaum_ ist ein Wurzelbaum, in dem jeder Knoten ≤ 2 Kinder hat.
+Die Tiefe eines Knotens _v_ ist die Länge des eindeutigen Weges von der Wurzel
+zu _v_, und die Höhe von _v_ ist die Länge eines längsten (absteigenden) Weges
+von _v_ zu einem Blatt. Die Höhe des Baumes ist die Höhe der Wurzel.
+
+#v(6mm)
diff --git a/tests/typ/compiler/block.typ b/tests/typ/compiler/block.typ
new file mode 100644
index 00000000..45f63f8e
--- /dev/null
+++ b/tests/typ/compiler/block.typ
@@ -0,0 +1,144 @@
+// Test code blocks.
+// Ref: false
+
+---
+// Ref: true
+
+// 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] + "?"
+}
+
+---
+// Nothing evaluates to none.
+#test({}, none)
+
+// Let evaluates to none.
+#test({ let v = 0 }, none)
+
+// Evaluates to single expression.
+#test({ "hello" }, "hello")
+
+// Evaluates to string.
+#test({ let x = "m"; x + "y" }, "my")
+
+// Evaluated to int.
+#test({
+ let x = 1
+ let y = 2
+ x + y
+}, 3)
+
+// String is joined with trailing none, evaluates to string.
+#test({
+ type("")
+ none
+}, "string")
+
+---
+// Some things can't be joined.
+{
+ [A]
+ // Error: 3-4 cannot join content with integer
+ 1
+ [B]
+}
+
+---
+// Block directly in markup also creates a scope.
+{ let x = 1 }
+
+// Error: 7-8 unknown variable
+#test(x, 1)
+
+---
+// Block in expression does create a scope.
+#let a = {
+ let b = 1
+ b
+}
+
+#test(a, 1)
+
+// Error: 2-3 unknown variable
+{b}
+
+---
+// Double block creates a scope.
+{{
+ import b from "module.typ"
+ test(b, 1)
+}}
+
+// Error: 2-3 unknown variable
+{b}
+
+---
+// Multiple nested scopes.
+{
+ let a = "a1"
+ {
+ let a = "a2"
+ {
+ test(a, "a2")
+ let a = "a3"
+ test(a, "a3")
+ }
+ test(a, "a2")
+ }
+ test(a, "a1")
+}
+
+---
+// Content blocks also create a scope.
+[#let x = 1]
+
+// Error: 2-3 unknown variable
+{x}
+
+---
+// Multiple unseparated expressions in one line.
+
+// Error: 2-4 invalid number suffix
+{1u}
+
+// Should output `1`.
+// Error: 3 expected semicolon or line break
+{1 2}
+
+// Should output `2`.
+// Error: 12 expected semicolon or line break
+// Error: 22 expected semicolon or line break
+{let x = -1 let y = 3 x + y}
+
+// Should output `3`.
+{
+ // Error: 7-10 expected identifier, found string
+ for "v"
+
+ // Error: 8 expected keyword `in`
+ for v let z = 1 + 2
+
+ z
+}
+
+---
+// Error: 2 expected closing brace
+{
+
+---
+// Error: 1-2 unexpected closing brace
+}
diff --git a/tests/typ/compiler/break-continue.typ b/tests/typ/compiler/break-continue.typ
new file mode 100644
index 00000000..fb3222fa
--- /dev/null
+++ b/tests/typ/compiler/break-continue.typ
@@ -0,0 +1,147 @@
+// Test break and continue in loops.
+// Ref: false
+
+---
+// 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)
+
+---
+// Test joining with break.
+
+#let i = 0
+#let x = while true {
+ i += 1
+ str(i)
+ if i >= 5 {
+ "."
+ break
+ }
+}
+
+#test(x, "12345.")
+
+---
+// Test continue.
+
+#let i = 0
+#let x = 0
+
+#while x < 8 {
+ i += 1
+ if mod(i, 3) == 0 {
+ continue
+ }
+ x += i
+}
+
+// If continue did not work, this would equal 10.
+#test(x, 12)
+
+---
+// Test joining with continue.
+
+#let x = for i in range(5) {
+ "a"
+ if mod(i, 3) == 0 {
+ "_"
+ continue
+ }
+ str(i)
+}
+
+#test(x, "a_a1a2a_a4")
+
+---
+// Test break outside of loop.
+#let f() = {
+ // Error: 3-8 cannot break outside of loop
+ break
+}
+
+#for i in range(1) {
+ f()
+}
+
+---
+// Test break in function call.
+#let identity(x) = x
+#let out = for i in range(5) {
+ "A"
+ identity({
+ "B"
+ break
+ })
+ "C"
+}
+
+#test(out, "AB")
+
+---
+// Test continue outside of loop.
+
+// Error: 12-20 cannot continue outside of loop
+#let x = { continue }
+
+---
+// Error: 1-10 cannot continue outside of loop
+#continue
+
+---
+// Ref: true
+// Should output `Hello World 🌎`.
+#for _ in range(10) {
+ [Hello ]
+ [World {
+ [🌎]
+ break
+ }]
+}
+
+---
+// Ref: true
+// 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("Roboto")
+ #show it => text(fill: color, it)
+ #smallcaps(if color != green [
+ Some
+ ] else [
+ Last
+ #break
+ ])
+]
+
+---
+// Ref: true
+// Test break in set rule.
+// Should output `Hi` in blue.
+#for i in range(10) {
+ [Hello]
+ set text(blue, ..break)
+ [Not happening]
+}
+
+---
+// Test second block during break flow.
+// Ref: true
+
+#for i in range(10) {
+ table(
+ { [A]; break },
+ for _ in range(3) [B]
+ )
+}
diff --git a/tests/typ/compiler/call.typ b/tests/typ/compiler/call.typ
new file mode 100644
index 00000000..dc582c9c
--- /dev/null
+++ b/tests/typ/compiler/call.typ
@@ -0,0 +1,106 @@
+// Test function calls.
+// Ref: false
+
+---
+// Ref: true
+
+// Ommitted 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]
+
+---
+// Trailing comma.
+#test(1 + 1, 2,)
+
+// Call function assigned to variable.
+#let alias = type
+#test(alias(alias), "function")
+
+// Callee expressions.
+{
+ // Wrapped in parens.
+ test((type)("hi"), "string")
+
+ // Call the return value of a function.
+ let adder(dx) = x => x + dx
+ test(adder(2)(5), 7)
+}
+
+---
+// Error: 28-47 duplicate argument
+#set text(family: "Arial", family: "Helvetica")
+
+---
+// Error: 2-6 expected callable or collection, found boolean
+{true()}
+
+---
+#let x = "x"
+
+// Error: 1-3 expected callable or collection, found string
+#x()
+
+---
+#let f(x) = x
+
+// Error: 1-6 expected callable or collection, found integer
+#f(1)(2)
+
+---
+#let f(x) = x
+
+// Error: 1-6 expected callable or collection, found content
+#f[1](2)
+
+---
+// Error: 7-8 expected expression, found colon
+#func(:)
+
+// Error: 10-12 unexpected end of block comment
+#func(a:1*/)
+
+// Error: 8 expected comma
+#func(1 2)
+
+// Error: 7-8 expected identifier, found integer
+// Error: 9 expected expression
+#func(1:)
+
+// Error: 7-8 expected identifier, found integer
+#func(1:2)
+
+// Error: 7-12 expected identifier, found string
+#func("abc": 2)
+
+// Error: 7-10 expected identifier, found group
+{func((x):1)}
+
+---
+// Error: 2:1 expected closing bracket
+#func[`a]`
+
+---
+// Error: 7 expected closing paren
+{func(}
+
+---
+// Error: 2:1 expected quote
+// Error: 2:1 expected closing paren
+#func("]
diff --git a/tests/typ/compiler/closure.typ b/tests/typ/compiler/closure.typ
new file mode 100644
index 00000000..2c6c1ea0
--- /dev/null
+++ b/tests/typ/compiler/closure.typ
@@ -0,0 +1,158 @@
+// Test closures.
+// Ref: false
+
+---
+// Don't parse closure directly in content.
+// Ref: true
+
+#let x = "x"
+
+// Should output `x => y`.
+#x => y
+
+---
+// Basic closure without captures.
+{
+ let adder = (x, y) => x + y
+ test(adder(2, 3), 5)
+}
+
+---
+// 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)
+}
+
+---
+// 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!")
+}
+
+---
+// Redefined variable.
+{
+ let x = 1
+ let f() = {
+ let x = x + 2
+ x
+ }
+ test(f(), 3)
+}
+
+---
+// Import bindings.
+{
+ let b = "module.typ"
+ let f() = {
+ import b from b
+ b
+ }
+ test(f(), 1)
+}
+
+---
+// For loop bindings.
+{
+ let v = (1, 2, 3)
+ let f() = {
+ let s = 0
+ for v in v { s += v }
+ s
+ }
+ test(f(), 6)
+}
+
+---
+// Let + closure bindings.
+{
+ let g = "hi"
+ let f() = {
+ let g() = "bye"
+ g()
+ }
+ test(f(), "bye")
+}
+
+---
+// Parameter bindings.
+{
+ let x = 5
+ let g() = {
+ let f(x, y: x) = x + y
+ f
+ }
+
+ test(g()(8), 13)
+}
+
+---
+// Don't leak environment.
+{
+ // Error: 16-17 unknown variable
+ let func() = x
+ let x = "hi"
+ func()
+}
+
+---
+// Too few arguments.
+{
+ let types(x, y) = "[" + type(x) + ", " + type(y) + "]"
+ test(types(14%, 12pt), "[ratio, length]")
+
+ // Error: 13-21 missing argument: y
+ test(types("nope"), "[string, none]")
+}
+
+---
+// Too many arguments.
+{
+ let f(x) = x + 1
+
+ // Error: 8-13 unexpected argument
+ f(1, "two", () => x)
+}
+
+---
+// 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
+ test(greet("Typst", whatever: 10))
+}
+
+---
+// Error: 6-16 expected identifier, named pair or argument sink, found keyed pair
+{(a, "named": b) => none}
+
+---
+// Error: 10-15 expected identifier, found string
+#let foo("key": b) = key
+
+---
+// Error: 10-14 expected identifier, found `none`
+#let foo(none: b) = key
diff --git a/tests/typ/compiler/comment.typ b/tests/typ/compiler/comment.typ
new file mode 100644
index 00000000..4a4dc7ab
--- /dev/null
+++ b/tests/typ/compiler/comment.typ
@@ -0,0 +1,33 @@
+// Test line and block comments.
+
+---
+// Line comment acts as spacing.
+A// you
+B
+
+// Block comment does not act as spacing, nested block comments.
+C/*
+ /* */
+*/D
+
+// Works in code.
+#test(type(/*1*/ 1) //
+, "integer")
+
+// End of block comment in line comment.
+// Hello */
+
+// Nested line comment.
+/*//*/
+Still comment.
+*/
+
+E
+
+---
+// End should not appear without start.
+// Error: 7-9 unexpected end of block comment
+/* */ */
+
+// Unterminated is okay.
+/*
diff --git a/tests/typ/compiler/construct.typ b/tests/typ/compiler/construct.typ
new file mode 100644
index 00000000..f535184c
--- /dev/null
+++ b/tests/typ/compiler/construct.typ
@@ -0,0 +1,31 @@
+// Test node functions.
+
+---
+// Ensure that constructor styles aren't passed down the tree.
+// The inner list should have no extra indent.
+#set par(leading: 2pt)
+#list(body-indent: 20pt, [First], list[A][B])
+
+---
+// Ensure that constructor styles win, but not over outer styles.
+// The outer paragraph should be right-aligned,
+// but the B should be center-aligned.
+#set par(align: center)
+#par(align: right)[
+ A #rect(width: 2cm, fill: conifer, inset: 4pt)[B]
+]
+
+---
+// The inner rectangle should also be yellow here.
+// (and therefore invisible)
+[#set rect(fill: yellow);#text(1em, rect(inset: 5pt, rect()))]
+
+---
+// The inner rectangle should not be yellow here.
+A #rect(fill: yellow, inset: 5pt, rect()) B
+
+---
+// The constructor property should still work
+// when there are recursive show rules.
+#show list: set text(blue)
+#list(label: "(a)", [A], list[B])
diff --git a/tests/typ/compiler/dict.typ b/tests/typ/compiler/dict.typ
new file mode 100644
index 00000000..d791f77b
--- /dev/null
+++ b/tests/typ/compiler/dict.typ
@@ -0,0 +1,75 @@
+// Test dictionaries.
+// Ref: false
+
+---
+// Ref: true
+
+// Empty
+{(:)}
+
+// Two pairs and string key.
+#let dict = (normal: 1, "spacy key": 2)
+#dict
+
+#test(dict.normal, 1)
+#test(dict("spacy key"), 2)
+
+---
+// Test lvalue and rvalue access.
+{
+ let dict = (a: 1, "b b": 1)
+ dict("b b") += 1
+ dict.state = (ok: true, err: false)
+ test(dict, (a: 1, "b b": 2, state: (ok: true, err: false)))
+ test(dict.state.ok, true)
+ dict("state").ok = false
+ test(dict.state.ok, false)
+ test(dict.state.err, false)
+}
+
+---
+// Test rvalue missing key.
+{
+ let dict = (a: 1, b: 2)
+ // Error: 11-20 dictionary does not contain key "c"
+ let x = dict("c")
+}
+
+---
+// Missing lvalue is automatically none-initialized.
+{
+ let dict = (:)
+ dict("b") += 1
+ test(dict, (b: 1))
+}
+
+---
+// Error: 24-32 pair has duplicate key
+{(first: 1, second: 2, first: 3)}
+
+---
+// Error: 17-23 pair has duplicate key
+{(a: 1, "b": 2, "a": 3)}
+
+---
+// Simple expression after already being identified as a dictionary.
+// Error: 9-10 expected named or keyed pair, found identifier
+{(a: 1, b)}
+
+// Identified as dictionary due to initial colon.
+// Error: 4-5 expected named or keyed pair, found integer
+// Error: 5 expected comma
+// Error: 12-16 expected identifier or string, found boolean
+// Error: 17-18 expected expression, found colon
+{(:1 b:"", true::)}
+
+---
+// Error: 3-15 cannot mutate a temporary value
+{ (key: value).other = "some" }
+
+---
+{
+ let object = none
+ // Error: 3-9 expected dictionary, found none
+ object.property = "value"
+}
diff --git a/tests/typ/compiler/field.typ b/tests/typ/compiler/field.typ
new file mode 100644
index 00000000..abea87fb
--- /dev/null
+++ b/tests/typ/compiler/field.typ
@@ -0,0 +1,40 @@
+// Test field access.
+// Ref: false
+
+---
+// Test field on dictionary.
+#let dict = (nothing: "ness", hello: "world")
+#test(dict.nothing, "ness")
+{
+ let world = dict
+ .hello
+
+ test(world, "world")
+}
+
+---
+// Test field on node.
+#show list: node => {
+ test(node.items.len(), 3)
+}
+
+- A
+- B
+- C
+
+---
+// Error: 6-13 dictionary does not contain key "invalid"
+{(:).invalid}
+
+---
+// Error: 2-7 cannot access field on boolean
+{false.ok}
+
+---
+// Error: 29-32 unknown field "fun"
+#show heading: node => node.fun
+= A
+
+---
+// Error: 8-12 expected identifier, found boolean
+{false.true}
diff --git a/tests/typ/compiler/for.typ b/tests/typ/compiler/for.typ
new file mode 100644
index 00000000..822f7423
--- /dev/null
+++ b/tests/typ/compiler/for.typ
@@ -0,0 +1,123 @@
+// Test for loops.
+// Ref: false
+
+---
+// Ref: true
+
+// Empty array.
+#for x in () [Nope]
+
+// Dictionary is not traversed in insertion order.
+// Should output `Age: 2. Name: Typst.`.
+#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.positional().map(repr)
+#let f2(..args) = args.named().pairs((k, v) => repr(k) + ": " + repr(v))
+#let f(..args) = (f1(..args) + f2(..args)).join(", ")
+#f(1, a: 2)
+
+---
+#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") {
+ test(repr(i + 1), v)
+}
+
+// Values of dictionary.
+#for v in (a: 4, b: 5) {
+ out += (v,)
+}
+
+// Keys and values of dictionary.
+#for k, v in (a: 6, b: 7) {
+ out += (k,)
+ out += (v,)
+}
+
+#test(out, (1, 2, 3, 4, 5, "a", 6, "b", 7))
+
+// 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")
+
+---
+// Uniterable expression.
+// Error: 11-15 cannot loop over boolean
+#for v in true {}
+
+---
+// Keys and values of strings.
+// Error: 6-10 mismatched pattern
+#for k, v in "hi" {
+ dont-care
+}
+
+---
+// Error: 5 expected identifier
+#for
+
+// Error: 7 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
+
+// Error: 5 expected identifier
+#for
+v in iter {}
+
+// Error: 7-10 expected identifier, found string
+A#for "v" thing
+
+// Error: 6-9 expected identifier, found string
+#for "v" in iter {}
+
+// Error: 7 expected keyword `in`
+#for a + b in iter {}
diff --git a/tests/typ/compiler/if.typ b/tests/typ/compiler/if.typ
new file mode 100644
index 00000000..0d87c689
--- /dev/null
+++ b/tests/typ/compiler/if.typ
@@ -0,0 +1,136 @@
+// Test if-else expressions.
+
+---
+// Test condition evaluation.
+#if 1 < 2 [
+ One.
+]
+
+#if true == false [
+ {Bad}, but we {dont-care}!
+]
+
+---
+// 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.
+]
+
+---
+// Test else if.
+// Ref: false
+
+#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")
+
+---
+// 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)
+}
+
+---
+// Condition must be boolean.
+// If it isn't, neither branch is evaluated.
+// Error: 5-14 expected boolean, found string
+#if "a" + "b" { nope } else { nope }
+
+---
+// Make sure that we don't complain twice.
+// Error: 5-12 cannot add integer and string
+#if 1 + "2" {}
+
+---
+// 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: 19 expected body
+A#if false {} else thing
+
+#if a []else [b]
+#if a [] else [b]
+#if a {} else [b]
diff --git a/tests/typ/compiler/import.typ b/tests/typ/compiler/import.typ
new file mode 100644
index 00000000..0620403d
--- /dev/null
+++ b/tests/typ/compiler/import.typ
@@ -0,0 +1,116 @@
+// Test module imports.
+
+---
+// Test importing semantics.
+
+// A named import.
+#import item from "module.typ"
+#test(item(1, 2), 3)
+
+// Test that this will be overwritten.
+#let value = [foo]
+
+// Import multiple things.
+#import fn, value from "module.typ"
+#fn[Like and Subscribe!]
+#value
+
+// Code mode
+{
+ import b from "module.typ"
+ test(b, 1)
+}
+
+// A wildcard import.
+#import * from "module.typ"
+
+// It exists now!
+#d
+
+// Who needs whitespace anyways?
+#import*from"module.typ"
+
+// Should output `bye`.
+// Stop at semicolon.
+#import a, c from "module.typ";bye
+
+// Allow the trailing comma.
+#import a, c, from "module.typ"
+
+---
+// Error: 19-21 failed to load file (is a directory)
+#import name from ""
+
+---
+// Error: 16-27 file not found (searched at typ/compiler/lib/0.2.1)
+#import * from "lib/0.2.1"
+
+---
+// Some non-text stuff.
+// Error: 16-37 file is not valid utf-8
+#import * from "../../res/rhino.png"
+
+---
+// Unresolved import.
+// Error: 9-21 unresolved import
+#import non_existing from "module.typ"
+
+---
+// Cyclic import of this very file.
+// Error: 16-30 cyclic import
+#import * from "./import.typ"
+
+---
+// Cyclic import in other file.
+#import * from "./modules/cycle1.typ"
+
+This is never reached.
+
+---
+// Error: 8 expected import items
+// Error: 8 expected keyword `from`
+#import
+
+// Error: 9-19 expected identifier, found string
+// Error: 19 expected keyword `from`
+#import "file.typ"
+
+// Error: 16-19 expected identifier, found string
+// Error: 22 expected keyword `from`
+#import afrom, "b", c
+
+// Error: 9 expected import items
+#import from "module.typ"
+
+// Error: 9-10 expected expression, found assignment operator
+// Error: 10 expected import items
+#import = from "module.typ"
+
+// Error: 15 expected expression
+#import * from
+
+// An additional trailing comma.
+// Error: 17-18 expected expression, found comma
+#import a, b, c,, from "module.typ"
+
+// Error: 1-6 unexpected keyword `from`
+#from "module.typ"
+
+// Error: 2:2 expected semicolon or line break
+#import * from "module.typ
+"target
+
+// Error: 28 expected semicolon or line break
+#import * from "module.typ" § 0.2.1
+
+// A star in the list.
+// Error: 12-13 expected expression, found star
+#import a, *, b from "module.typ"
+
+// An item after a star.
+// Error: 10 expected keyword `from`
+#import *, a from "module.typ"
+
+---
+// Error: 9-13 expected identifier, found named pair
+#import a: 1 from ""
diff --git a/tests/typ/compiler/include.typ b/tests/typ/compiler/include.typ
new file mode 100644
index 00000000..289fea21
--- /dev/null
+++ b/tests/typ/compiler/include.typ
@@ -0,0 +1,32 @@
+// Test module includes.
+
+---
+#set page(width: 200pt)
+
+= Document
+
+// Include a file
+#include "/typ/compiler/modules/chap1.typ"
+
+// Expression as a file name.
+#let chap2 = include "modu" + "les/chap" + "2.typ"
+
+-- _Intermission_ --
+#chap2
+
+---
+{
+ // Error: 19-38 file not found (searched at typ/compiler/modules/chap3.typ)
+ let x = include "modules/chap3.typ"
+}
+
+---
+#include "modules/chap1.typ"
+
+// The variables of the file should not appear in this scope.
+// Error: 1-6 unknown variable
+#name
+
+---
+// Error: 18 expected semicolon or line break
+#include "hi.typ" Hi
diff --git a/tests/typ/compiler/label.typ b/tests/typ/compiler/label.typ
new file mode 100644
index 00000000..795c0435
--- /dev/null
+++ b/tests/typ/compiler/label.typ
@@ -0,0 +1,58 @@
+// Test labels.
+
+---
+// Test labelled headings.
+#show heading: set text(10pt)
+#show heading.where(label: <intro>): underline
+
+= Introduction <intro>
+The beginning.
+
+= Conclusion
+The end.
+
+---
+// Test label after expression.
+#show strong.where(label: <v>): set text(red)
+
+#let a = [*A*]
+#let b = [*B*]
+#a <v> #b
+
+---
+// Test labelled text.
+#show "t": it => {
+ set text(blue) if it.label == <last>
+ it
+}
+
+This is a thing [that <last>] happened.
+
+---
+// Test abusing dynamic labels for styling.
+#show <red>: set text(red)
+#show <blue>: set text(blue)
+
+*A* *B* <red> *C* #label("bl" + "ue") *D*
+
+---
+// Test that label ignores parbreak.
+#show <hide>: none
+
+_Hidden_
+<hide>
+
+_Hidden_
+
+<hide>
+_Visible_
+
+---
+// Test that label only works within one content block.
+#show <strike>: strike
+*This is* [<strike>] *protected.*
+*This is not.* <strike>
+
+---
+// Test that incomplete label is text.
+1 < 2 is #if 1 < 2 [not] a label.
diff --git a/tests/typ/compiler/let.typ b/tests/typ/compiler/let.typ
new file mode 100644
index 00000000..c3be64a5
--- /dev/null
+++ b/tests/typ/compiler/let.typ
@@ -0,0 +1,68 @@
+// Test let bindings.
+
+---
+// 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 rect(body) = rect(width: 2cm, fill: fill, inset: 5pt, body)
+#rect[Hi!]
+
+---
+// 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)
+
+---
+// Error: 5 expected identifier
+#let
+
+// Error: 5 expected identifier
+{let}
+
+// Error: 6-9 expected identifier, found string
+#let "v"
+
+// Error: 7 expected semicolon or line break
+#let v 1
+
+// Error: 9 expected expression
+#let v =
+
+// Error: 6-9 expected identifier, 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: 18 expected closing paren
+#let v5 = (1, 2 + ; Five
+
+---
+// Error: 13 expected body
+#let func(x)
+
+// Error: 15 expected expression
+#let func(x) =
diff --git a/tests/typ/compiler/methods-collection.typ b/tests/typ/compiler/methods-collection.typ
new file mode 100644
index 00000000..fcebf640
--- /dev/null
+++ b/tests/typ/compiler/methods-collection.typ
@@ -0,0 +1,115 @@
+// Test the collection methods.
+// Ref: false
+
+---
+// Test the `len` method.
+#test(().len(), 0)
+#test(("A", "B", "C").len(), 3)
+#test("Hello World!".len(), 12)
+#test((a: 1, b: 2).len(), 2)
+
+---
+// The the `first` and `last` methods.
+#test(().first(), none)
+#test(().last(), none)
+#test((1,).first(), 1)
+#test((2,).last(), 2)
+#test((1, 2, 3).first(), 1)
+#test((1, 2, 3).last(), 3)
+
+---
+// Test the `push` and `pop` methods.
+{
+ let tasks = (a: (1, 2, 3), b: (4, 5, 6))
+ tasks("a").pop()
+ tasks("b").push(7)
+ test(tasks("a"), (1, 2))
+ test(tasks("b"), (4, 5, 6, 7))
+}
+
+---
+// Test the `insert` and `remove` methods.
+{
+ let array = (0, 1, 2, 4, 5)
+ array.insert(3, 3)
+ test(array, range(6))
+ array.remove(1)
+ test(array, (0, 2, 3, 4, 5))
+}
+
+---
+// Error: 2:17-2:19 missing argument: index
+#let numbers = ()
+{ numbers.insert() }
+
+---
+// Test the `slice` method.
+#test((1, 2, 3, 4).slice(2), (3, 4))
+#test(range(10).slice(2, 6), (2, 3, 4, 5))
+#test(range(10).slice(4, count: 3), (4, 5, 6))
+#test(range(10).slice(-5, count: 2), (5, 6))
+#test((1, 2, 3).slice(2, -2), ())
+#test((1, 2, 3).slice(-2, 2), (2,))
+#test((1, 2, 3).slice(-3, 2), (1, 2))
+#test("ABCD".split("").slice(1, -1).join("-"), "A-B-C-D")
+
+---
+// Error: 3-31 array index out of bounds (index: 12, len: 10)
+{ range(10).slice(9, count: 3) }
+
+---
+// Error: 3-25 array index out of bounds (index: -4, len: 3)
+{ (1, 2, 3).slice(0, -4) }
+
+---
+// Test the `position` method.
+#test(("Hi", "❤️", "Love").position(s => s == "❤️"), 1)
+#test(("Bye", "💘", "Apart").position(s => s == "❤️"), none)
+#test(("A", "B", "CDEF", "G").position(v => v.len() > 2), 2)
+
+---
+// Test the `rev` method.
+#test(range(3).rev(), (2, 1, 0))
+
+---
+// Test the `join` method.
+#test(().join(), none)
+#test((1,).join(), 1)
+#test(("a", "b", "c").join(), "abc")
+#test("(" + ("a", "b", "c").join(", ") + ")", "(a, b, c)")
+
+---
+// Error: 2-22 cannot join boolean with boolean
+{(true, false).join()}
+
+---
+// Error: 2-20 cannot join string with integer
+{("a", "b").join(1)}
+
+---
+// Test joining content.
+// Ref: true
+{([One], [Two], [Three]).join([, ], last: [ and ])}.
+
+---
+// Test the `sorted` method.
+#test(().sorted(), ())
+#test(((true, false) * 10).sorted(), (false,) * 10 + (true,) * 10)
+#test(("it", "the", "hi", "text").sorted(), ("hi", "it", "text", "the"))
+#test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10))
+
+---
+// Error: 2-26 cannot order content and content
+{([Hi], [There]).sorted()}
+
+---
+// Test dictionary methods.
+#let dict = (a: 3, c: 2, b: 1)
+#test("c" in dict, true)
+#test(dict.len(), 3)
+#test(dict.values(), (3, 1, 2))
+#test(dict.pairs((k, v) => k + str(v)).join(), "a3b1c2")
+
+{ dict.remove("c") }
+#test("c" in dict, false)
+#test(dict, (a: 3, b: 1))
diff --git a/tests/typ/compiler/methods-color.typ b/tests/typ/compiler/methods-color.typ
new file mode 100644
index 00000000..1188030a
--- /dev/null
+++ b/tests/typ/compiler/methods-color.typ
@@ -0,0 +1,22 @@
+// Test color modification methods.
+
+---
+// Test gray color modification.
+#test(luma(20%).lighten(50%), luma(60%))
+#test(luma(80%).darken(20%), luma(63.9%))
+#test(luma(80%).negate(), luma(20%))
+
+---
+// Test CMYK color conversion.
+// Ref: true
+#let c = cmyk(50%, 64%, 16%, 17%)
+#rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%))
+#rect(width: 1cm, fill: c)
+#rect(width: 1cm, fill: c.negate())
+
+#for x in range(0, 11) {
+ square(width: 9pt, fill: c.lighten(x * 10%))
+}
+#for x in range(0, 11) {
+ square(width: 9pt, fill: c.darken(x * 10%))
+}
diff --git a/tests/typ/compiler/methods-str.typ b/tests/typ/compiler/methods-str.typ
new file mode 100644
index 00000000..aead4aa4
--- /dev/null
+++ b/tests/typ/compiler/methods-str.typ
@@ -0,0 +1,117 @@
+// Test the string methods.
+// Ref: false
+
+---
+// Test the `slice` method.
+#test("abc".slice(1, 2), "b")
+#test("abc🏡def".slice(2, 7), "c🏡")
+#test("abc🏡def".slice(2, -2), "c🏡d")
+#test("abc🏡def".slice(-3, -1), "de")
+
+---
+// Test the `contains` method.
+#test("abc".contains("b"), true)
+#test("b" in "abc", true)
+#test("1234f".contains(regex("\d")), true)
+#test(regex("\d") in "1234f", true)
+#test("abc".contains("d"), false)
+#test("1234g" in "1234f", false)
+#test("abc".contains(regex("^[abc]$")), false)
+#test("abc".contains(regex("^[abc]+$")), true)
+
+---
+// Test the `starts-with` and `ends-with` methods.
+#test("Typst".starts-with("Ty"), true)
+#test("Typst".starts-with(regex("[Tt]ys")), false)
+#test("Typst".starts-with("st"), false)
+#test("Typst".ends-with("st"), true)
+#test("Typst".ends-with(regex("\d*")), true)
+#test("Typst".ends-with(regex("\d+")), false)
+#test("Typ12".ends-with(regex("\d+")), true)
+
+---
+// Test the `find` and `position` methods.
+#let date = regex("\d{2}:\d{2}")
+#test("Hello World".find("World"), "World")
+#test("Hello World".position("World"), 6)
+#test("It's 12:13 now".find(date), "12:13")
+#test("It's 12:13 now".position(date), 5)
+
+---
+// Test the `match` method.
+#test("Is there a".match("for this?"), none)
+#test(
+ "The time of my life.".match(regex("[mit]+e")),
+ (start: 4, end: 8, text: "time", captures: ()),
+)
+
+// Test the `matches` method.
+#test("Hello there".matches("\d"), ())
+#test("Day by Day.".matches("Day"), (
+ (start: 0, end: 3, text: "Day", captures: ()),
+ (start: 7, end: 10, text: "Day", captures: ()),
+))
+
+// Compute the sum of all timestamps in the text.
+#let timesum(text) = {
+ let time = 0
+ for match in text.matches(regex("(\d+):(\d+)")) {
+ let caps = match.captures
+ time += 60 * int(caps(0)) + int(caps(1))
+ }
+ str(int(time / 60)) + ":" + str(mod(time, 60))
+}
+
+#test(timesum(""), "0:0")
+#test(timesum("2:70"), "3:10")
+#test(timesum("1:20, 2:10, 0:40"), "4:10")
+
+---
+// Test the `replace` method.
+#test("ABC".replace("", "-"), "-A-B-C-")
+#test("Ok".replace("Ok", "Nope", count: 0), "Ok")
+#test("to add?".replace("", "How ", count: 1), "How to add?")
+#test("AB C DEF GH J".replace(" ", ",", count: 2), "AB,C,DEF GH J")
+#test("Walcemo"
+ .replace("o", "k")
+ .replace("e", "o")
+ .replace("k", "e")
+ .replace("a", "e"),
+ "Welcome"
+)
+#test("123".replace(regex("\d$"), "_"), "12_")
+#test("123".replace(regex("\d{1,2}$"), "__"), "1__")
+
+---
+// Test the `trim` method.
+#let str = "Typst, LaTeX, Word, InDesign"
+#let array = ("Typst", "LaTeX", "Word", "InDesign")
+#test(str.split(",").map(s => s.trim()), array)
+#test("".trim(), "")
+#test(" abc ".trim(at: start), "abc ")
+#test(" abc ".trim(at: end, repeat: true), " abc")
+#test(" abc".trim(at: start, repeat: false), "abc")
+#test("aabcaa".trim("a", repeat: false), "abca")
+#test("aabca".trim("a", at: start), "bca")
+#test("aabcaa".trim("a", at: end, repeat: false), "aabca")
+#test("".trim(regex(".")), "")
+#test("123abc456".trim(regex("\d")), "abc")
+#test("123abc456".trim(regex("\d"), repeat: false), "23abc45")
+#test("123a4b5c678".trim(regex("\d"), repeat: true), "a4b5c")
+#test("123a4b5c678".trim(regex("\d"), repeat: false), "23a4b5c67")
+#test("123abc456".trim(regex("\d"), at: start), "abc456")
+#test("123abc456".trim(regex("\d"), at: end), "123abc")
+#test("123abc456".trim(regex("\d+"), at: end, repeat: false), "123abc")
+#test("123abc456".trim(regex("\d{1,2}$"), repeat: false), "123abc4")
+#test("hello world".trim(regex(".")), "")
+
+---
+// Error: 17-21 expected either `start` or `end`
+{"abc".trim(at: left)}
+
+---
+// Test the `split` method.
+#test("abc".split(""), ("", "a", "b", "c", ""))
+#test("abc".split("b"), ("a", "c"))
+#test("a123c".split(regex("\d")), ("a", "", "", "c"))
+#test("a123c".split(regex("\d+")), ("a", "c"))
diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ
new file mode 100644
index 00000000..07f6e410
--- /dev/null
+++ b/tests/typ/compiler/methods.typ
@@ -0,0 +1,46 @@
+// Test method calls.
+// Ref: false
+
+---
+// Test whitespace around dot.
+#test( "Hi there" . split() , ("Hi", "there"))
+
+---
+// Test mutating indexed value.
+{
+ let matrix = (((1,), (2,)), ((3,), (4,)))
+ matrix(1)(0).push(5)
+ test(matrix, (((1,), (2,)), ((3, 5), (4,))))
+}
+
+---
+// 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!")
+}
+
+---
+// Error: 2:3-2:16 type array has no method `fun`
+#let numbers = ()
+{ numbers.fun() }
+
+---
+// Error: 2:3-2:44 cannot mutate a temporary value
+#let numbers = (1, 2, 3)
+{ numbers.map(v => v / 2).sorted().map(str).remove(4) }
+
+---
+// Error: 2:3-2:19 cannot mutate a temporary value
+#let numbers = (1, 2, 3)
+{ numbers.sorted() = 1 }
+
+---
+// Error: 3-6 cannot mutate a constant
+{ box.push(1) }
diff --git a/tests/typ/compiler/module.typ b/tests/typ/compiler/module.typ
new file mode 100644
index 00000000..b0a3fbf3
--- /dev/null
+++ b/tests/typ/compiler/module.typ
@@ -0,0 +1,12 @@
+// A file to import in import / include tests.
+// Ref: false
+
+#let a
+#let b = 1
+#let c = 2
+#let d = 3
+#let value = [hi]
+#let item(a, b) = a + b
+#let fn = rect.with(fill: conifer, inset: 5pt)
+
+Some _includable_ text.
diff --git a/tests/typ/compiler/modules/chap1.typ b/tests/typ/compiler/modules/chap1.typ
new file mode 100644
index 00000000..06a4c1a1
--- /dev/null
+++ b/tests/typ/compiler/modules/chap1.typ
@@ -0,0 +1,9 @@
+// Ref: false
+
+#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/typ/compiler/modules/chap2.typ b/tests/typ/compiler/modules/chap2.typ
new file mode 100644
index 00000000..d4aedc60
--- /dev/null
+++ b/tests/typ/compiler/modules/chap2.typ
@@ -0,0 +1,11 @@
+// Ref: false
+
+#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/typ/compiler/modules/cycle1.typ b/tests/typ/compiler/modules/cycle1.typ
new file mode 100644
index 00000000..a9c00f5e
--- /dev/null
+++ b/tests/typ/compiler/modules/cycle1.typ
@@ -0,0 +1,6 @@
+// Ref: false
+
+#import * from "cycle2.typ"
+#let inaccessible = "wow"
+
+This is the first element of an import cycle.
diff --git a/tests/typ/compiler/modules/cycle2.typ b/tests/typ/compiler/modules/cycle2.typ
new file mode 100644
index 00000000..204da519
--- /dev/null
+++ b/tests/typ/compiler/modules/cycle2.typ
@@ -0,0 +1,6 @@
+// Ref: false
+
+#import * from "cycle1.typ"
+#let val = "much cycle"
+
+This is the second element of an import cycle.
diff --git a/tests/typ/compiler/ops-assoc.typ b/tests/typ/compiler/ops-assoc.typ
new file mode 100644
index 00000000..ec128c61
--- /dev/null
+++ b/tests/typ/compiler/ops-assoc.typ
@@ -0,0 +1,18 @@
+// Test operator associativity.
+// Ref: false
+
+---
+// Math operators are left-associative.
+#test(10 / 2 / 2 == (10 / 2) / 2, true)
+#test(10 / 2 / 2 == 10 / (2 / 2), false)
+#test(1 / 2 * 3, 1.5)
+
+---
+// Assignment is right-associative.
+{
+ let x = 1
+ let y = 2
+ x = y = "ok"
+ test(x, none)
+ test(y, "ok")
+}
diff --git a/tests/typ/compiler/ops-invalid.typ b/tests/typ/compiler/ops-invalid.typ
new file mode 100644
index 00000000..3e9e5478
--- /dev/null
+++ b/tests/typ/compiler/ops-invalid.typ
@@ -0,0 +1,107 @@
+// Test invalid operations.
+// Ref: false
+
+---
+// Error: 3 expected expression
+{-}
+
+---
+// Error: 10 expected expression
+#test({1+}, 1)
+
+---
+// Error: 10 expected expression
+#test({2*}, 2)
+
+---
+// Error: 2-12 cannot apply '+' to content
+{+([] + [])}
+
+---
+// Error: 2-5 cannot apply '-' to string
+{-""}
+
+---
+// Error: 2-8 cannot apply 'not' to array
+{not ()}
+
+---
+// Error: 2-18 cannot apply '<=' to relative length and ratio
+{30% + 1pt <= 40%}
+
+---
+// Error: 2-13 cannot apply '<=' to length and length
+{1em <= 10pt}
+
+---
+// Special messages for +, -, * and /.
+// Error: 03-10 cannot add integer and string
+{(1 + "2", 40% - 1)}
+
+---
+// Error: 14-22 cannot add integer and string
+{ let x = 1; x += "2" }
+
+---
+// Error: 3-12 cannot divide ratio by length
+{ 10% / 5pt }
+
+---
+// Error: 3-12 cannot divide these two lengths
+{ 1em / 5pt }
+
+---
+// Error: 3-19 cannot divide relative length by ratio
+{ (10% + 1pt) / 5% }
+
+---
+// Error: 3-28 cannot divide these two relative lengths
+{ (10% + 1pt) / (20% + 1pt) }
+
+---
+// Error: 12-19 cannot subtract integer from ratio
+{(1234567, 40% - 1)}
+
+---
+// Error: 2-10 cannot multiply integer with boolean
+{2 * true}
+
+---
+// Error: 2-10 cannot divide integer by length
+{3 / 12pt}
+
+---
+// Error: 2-9 cannot repeat this string -1 times
+{-1 * ""}
+
+---
+{
+ let x = 2
+ for _ in range(61) {
+ x *= 2
+ }
+ // Error: 4-18 cannot repeat this string 4611686018427387904 times
+ {x * "abcdefgh"}
+}
+
+---
+// Error: 3-6 cannot mutate a temporary value
+{ (x) = "" }
+
+---
+// Error: 3-8 cannot mutate a temporary value
+{ 1 + 2 += 3 }
+
+---
+// Error: 3-4 unknown variable
+{ z = 1 }
+
+---
+// Error: 3-7 cannot mutate a constant
+{ rect = "hi" }
+
+---
+// 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/typ/compiler/ops-prec.typ b/tests/typ/compiler/ops-prec.typ
new file mode 100644
index 00000000..23afcc5f
--- /dev/null
+++ b/tests/typ/compiler/ops-prec.typ
@@ -0,0 +1,30 @@
+// Test operator precedence.
+// Ref: false
+
+---
+// Multiplication binds stronger than addition.
+#test(1+2*-3, -5)
+
+// Subtraction binds stronger than comparison.
+#test(3 == 5 - 2, true)
+
+// Boolean operations bind stronger than '=='.
+#test("a" == "a" and 2 < 3, true)
+#test(not "b" == "b", false)
+
+// Assignment binds stronger than boolean operations.
+// Error: 2-7 cannot mutate a temporary value
+{not x = "a"}
+
+---
+// Parentheses override precedence.
+#test((1), 1)
+#test((1+2)*-3, -9)
+
+// Error: 14 expected closing paren
+#test({(1 + 1}, 2)
+
+---
+// Precedence doesn't matter for chained unary operators.
+// Error: 2-11 cannot apply '-' to boolean
+{-not true}
diff --git a/tests/typ/compiler/ops.typ b/tests/typ/compiler/ops.typ
new file mode 100644
index 00000000..a38a527c
--- /dev/null
+++ b/tests/typ/compiler/ops.typ
@@ -0,0 +1,230 @@
+// Test binary expressions.
+// Ref: false
+
+---
+// Test adding content.
+// Ref: true
+{[*Hello* ] + [world!]}
+
+---
+// 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))
+
+// 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) != "integer" {
+ test(v + v, 2.0 * v)
+ }
+
+ if "relative" not in type(v) 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)
+ }
+}
+
+---
+// 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)
+
+---
+// 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 by hash equality.
+#let t = [a]
+#test(t == t, true)
+#test([] == [], true)
+#test([a] == [a], true)
+#test([[a]] == [a], true)
+#test([] == [a], false)
+#test(box[] == box[], true)
+#test(box[a] == box[], false)
+
+---
+// 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 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")
+
+---
+// Error: 3-6 cannot mutate a constant
+{ box = 1 }
+
+---
+// 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)
+
+---
+// Error: 9 expected keyword `in`
+{"a" not}
+
+---
+// Test `with` method.
+
+// Apply positional arguments.
+#let add(x, y) = x + y
+#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)
diff --git a/tests/typ/compiler/repr.typ b/tests/typ/compiler/repr.typ
new file mode 100644
index 00000000..bdea9c82
--- /dev/null
+++ b/tests/typ/compiler/repr.typ
@@ -0,0 +1,48 @@
+// Test representation of values in the document.
+
+---
+// Literal values.
+{auto} \
+{none} (empty) \
+{true} \
+{false}
+
+---
+// Numerical values.
+{1} \
+{1.0e-4} \
+{3.15} \
+{1e-10} \
+{50.368%} \
+{0.0000012345pt} \
+{4.5cm} \
+{12e1pt} \
+{2.5rad} \
+{45deg} \
+{1.7em} \
+{1cm + 0em} \
+{2em + 10pt} \
+{2.3fr}
+
+---
+// Colors and strokes.
+#set text(0.8em)
+#rgb("f7a205") \
+{2pt + rgb("f7a205")}
+
+---
+// Strings and escaping.
+#raw(repr("hi"), lang: "typc") \
+#repr("a\n[]\"\u{1F680}string")
+
+---
+// Content.
+#raw(repr[*{"H" + "i"} there*])
+
+---
+// Functions are invisible.
+Nothing
+#let f(x) = x
+{f}
+{rect}
+{() => none}
diff --git a/tests/typ/compiler/return.typ b/tests/typ/compiler/return.typ
new file mode 100644
index 00000000..0eea394e
--- /dev/null
+++ b/tests/typ/compiler/return.typ
@@ -0,0 +1,82 @@
+// Test return out of functions.
+// Ref: false
+
+---
+// Test return with value.
+#let f(x) = {
+ return x + 1
+}
+
+#test(f(1), 2)
+
+---
+// 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")
+
+---
+// Test return with joining and content.
+// Ref: true
+
+#let f(text, caption: none) = {
+ text
+ if caption == none [\.#return]
+ [, ]
+ emph(caption)
+ [\.]
+}
+
+#f(caption: [with caption])[My figure]
+
+#f[My other figure]
+
+---
+// Test return outside of function.
+
+#for x in range(5) {
+ // Error: 3-9 cannot return outside of function
+ return
+}
+
+---
+// Test that the expression is evaluated to the end.
+#let sum(..args) = {
+ let s = 0
+ for v in args.positional() {
+ s += v
+ }
+ s
+}
+
+#let f() = {
+ sum(..return, 1, 2, 3)
+ "nope"
+}
+
+#test(f(), 6)
+
+---
+// Test value return from content.
+#let x = 3
+#let f() = [
+ Hello 😀
+ #return "nope"
+ World
+]
+
+#test(f(), "nope")
diff --git a/tests/typ/compiler/set.typ b/tests/typ/compiler/set.typ
new file mode 100644
index 00000000..fc5053b1
--- /dev/null
+++ b/tests/typ/compiler/set.typ
@@ -0,0 +1,66 @@
+// General tests for set.
+
+---
+// Test that text is affected by instantiation-site bold.
+#let x = [World]
+Hello *{x}*
+
+---
+// Test that lists are affected by correct indents.
+#let fruit = [
+ - Apple
+ - Orange
+ #list(body-indent: 20pt, [Pear])
+]
+
+- Fruit
+[#set list(indent: 10pt)
+ #fruit]
+- No more fruit
+
+---
+// Test that that block spacing and text style are respected from
+// the outside, but the more specific fill is respected.
+#set block(spacing: 4pt)
+#set text(style: "italic", fill: eastern)
+#let x = [And the forest #parbreak() lay silent!]
+#text(fill: forest, x)
+
+---
+// Test that scoping works as expected.
+{
+ if true {
+ set text(blue)
+ [Blue ]
+ }
+ [Not blue]
+}
+
+---
+// Test relative path resolving in layout phase.
+#let choice = ("monkey.svg", "rhino.png", "tiger.jpg")
+#set enum(label: n => {
+ let path = "../../res/" + choice(n - 1)
+ move(dy: -0.15em, image(path, width: 1em, height: 1em))
+})
+
++ Monkey
++ Rhino
++ Tiger
+
+---
+// Test conditional set.
+#show ref: it => {
+ set text(red) if it.target == "unknown"
+ it
+}
+
+@hello from the @unknown
+
+---
+// Error: 19-24 expected boolean, found integer
+#set text(red) if 1 + 2
+
+---
+// Error: 11-25 set is only allowed directly in code and content blocks
+{ let x = set text(blue) }
diff --git a/tests/typ/compiler/shorthand.typ b/tests/typ/compiler/shorthand.typ
new file mode 100644
index 00000000..5c94dab0
--- /dev/null
+++ b/tests/typ/compiler/shorthand.typ
@@ -0,0 +1,20 @@
+// Test shorthands for unicode codepoints.
+
+---
+The non-breaking~space does work.
+
+---
+// Make sure non-breaking and normal space always
+// have the same width. Even if the font decided
+// differently.
+#set text("Latin Modern Roman")
+a b \
+a~b
+
+---
+- En dash: --
+- Em dash: ---
+
+---
+#set text("Roboto")
+A... vs {"A..."}
diff --git a/tests/typ/compiler/show-bare.typ b/tests/typ/compiler/show-bare.typ
new file mode 100644
index 00000000..8b8d0852
--- /dev/null
+++ b/tests/typ/compiler/show-bare.typ
@@ -0,0 +1,33 @@
+// Test bare show without selector.
+
+---
+#set page(height: 130pt)
+#set text(0.7em)
+
+#align(center)[
+ #text(1.3em)[*Essay on typography*] \
+ T. Ypst
+]
+
+#show columns.with(2)
+Great typography is at the essence of great storytelling. It is the medium that
+transports meaning from parchment to reader, the wave that sparks a flame
+in booklovers and the great fulfiller of human need.
+
+---
+// Test bare show in content block.
+A [_B #show c => [*#c*]; C_] D
+
+---
+// Test style precedence.
+#set text(fill: eastern, size: 1.5em)
+#show text.with(fill: forest)
+Forest
+
+---
+#show [Shown]
+Ignored
+
+---
+// Error: 4-18 show is only allowed directly in code and content blocks
+{ (show body => 2) * body }
diff --git a/tests/typ/compiler/show-node.typ b/tests/typ/compiler/show-node.typ
new file mode 100644
index 00000000..98f36f13
--- /dev/null
+++ b/tests/typ/compiler/show-node.typ
@@ -0,0 +1,104 @@
+// Test node show rules.
+
+---
+// Override lists.
+#show list: it => "(" + it.items.join(", ") + ")"
+
+- A
+ - B
+ - C
+- D
+- E
+
+---
+// Test full reset.
+#show heading: [B]
+#show heading: set text(size: 10pt, weight: 400)
+A [= Heading] C
+
+---
+// Test full removal.
+#show heading: none
+
+Where is
+= There are no headings around here!
+my heading?
+
+---
+// Test integrated example.
+#show heading: it => block({
+ set text(10pt)
+ move(dy: -1pt)[📖]
+ h(5pt)
+ if it.level == 1 {
+ underline(text(1.25em, blue, it.body))
+ } else {
+ text(red, it.body)
+ }
+})
+
+= Task 1
+Some text.
+
+== Subtask
+Some more text.
+
+= Task 2
+Another text.
+
+---
+// Test set and show in code blocks.
+#show heading: it => {
+ set text(red)
+ show "ding": [🛎]
+ it.body
+}
+
+= Heading
+
+---
+// Test that scoping works as expected.
+{
+ let world = [ World ]
+ show "W": strong
+ world
+ {
+ set text(blue)
+ show it => {
+ show "o": "Ø"
+ it
+ }
+ world
+ }
+ world
+}
+
+---
+#show heading: [1234]
+= Heading
+
+---
+// Error: 25-29 unknown field "page"
+#show heading: it => it.page
+= Heading
+
+---
+// Error: 7-12 this function is not selectable
+#show upper: it => {}
+
+---
+// Error: 7-11 to select text, please use a string or regex instead
+#show text: it => {}
+
+---
+// Error: 16-20 expected content or function, found integer
+#show heading: 1234
+= Heading
+
+---
+// Error: 7-10 expected selector, found color
+#show red: []
+
+---
+// Error: 7-25 show is only allowed directly in code and content blocks
+{ 1 + show heading: none }
diff --git a/tests/typ/compiler/show-recursive.typ b/tests/typ/compiler/show-recursive.typ
new file mode 100644
index 00000000..91a295f2
--- /dev/null
+++ b/tests/typ/compiler/show-recursive.typ
@@ -0,0 +1,51 @@
+// Test recursive show rules.
+
+---
+// Test basic identity.
+#show heading: it => it
+= Heading
+
+---
+// Test more recipes down the chain.
+#show list: scale.with(origin: left, x: 80%)
+#show heading: []
+#show enum: []
+- Actual
+- Tight
+- List
+= Nope
+
+---
+// Test show rule in function.
+#let starwars(body) = {
+ show list: it => block({
+ stack(dir: ltr,
+ text(red, it),
+ 1fr,
+ scale(x: -100%, text(blue, it)),
+ )
+ })
+ body
+}
+
+- Normal list
+
+#starwars[
+ - Star
+ - Wars
+ - List
+]
+
+- Normal list
+
+---
+// Test multi-recursion with nested lists.
+#set rect(inset: 3pt)
+#show list: rect.with(stroke: blue)
+#show list: rect.with(stroke: red)
+#show list: block
+
+- List
+ - Nested
+ - List
+- Recursive!
diff --git a/tests/typ/compiler/show-selector.typ b/tests/typ/compiler/show-selector.typ
new file mode 100644
index 00000000..0e9823a5
--- /dev/null
+++ b/tests/typ/compiler/show-selector.typ
@@ -0,0 +1,36 @@
+// Test show rule patterns.
+
+---
+// Inline code.
+#show raw.where(block: false): rect.with(
+ radius: 2pt,
+ outset: (y: 3pt),
+ inset: (x: 3pt),
+ fill: luma(230),
+)
+
+// Code blocks.
+#show raw.where(block: true): rect.with(
+ outset: -3pt,
+ inset: 11pt,
+ fill: luma(230),
+ stroke: (left: 1.5pt + luma(180)),
+)
+
+#set page(margin: (top: 12pt))
+#set par(justify: true)
+
+This code tests `code`
+with selectors and justification.
+
+```rs
+code!("it");
+```
+
+---
+#show heading.where(level: 1): set text(red)
+#show heading.where(level: 2): set text(blue)
+#show heading: set text(green)
+= Red
+== Blue
+=== Green
diff --git a/tests/typ/compiler/show-text.typ b/tests/typ/compiler/show-text.typ
new file mode 100644
index 00000000..124d2ed2
--- /dev/null
+++ b/tests/typ/compiler/show-text.typ
@@ -0,0 +1,59 @@
+// Test text replacement show rules.
+
+---
+// Test classic example.
+#set text("Roboto")
+#show "Der Spiegel": smallcaps
+Die Zeitung Der Spiegel existiert.
+
+---
+// Another classic example.
+#show "TeX": [T#h(-0.145em)#move(dy: 0.233em)[E]#h(-0.135em)X]
+#show regex("(Lua)?(La)?TeX"): name => box(text("Latin Modern Roman")[#name])
+
+TeX, LaTeX, LuaTeX and LuaLaTeX!
+
+---
+// Test that replacements happen exactly once.
+#show "A": [BB]
+#show "B": [CC]
+AA (8)
+
+---
+// Test caseless match and word boundaries.
+#show regex("(?i)\bworld\b"): [🌍]
+
+Treeworld, the World of worlds, is a world.
+
+---
+// This is a fun one.
+#set par(justify: true)
+#show regex("\S"): letter => rect(inset: 2pt)[#upper(letter)]
+#lorem(5)
+
+---
+// See also: https://github.com/mTvare6/hello-world.rs
+#show regex("(?i)rust"): it => [#it (🚀)]
+Rust is memory-safe and blazingly fast. Let's rewrite everything in rust.
+
+---
+// Test accessing the string itself.
+#show "hello": it => it.text.split("").map(upper).join("|")
+Oh, hello there!
+
+---
+// Replace worlds but only in lists.
+#show list: it => [
+ #show "World": [🌎]
+ #it
+]
+
+World
+- World
+
+---
+// Test absolute path in layout phase.
+
+#show "GRAPH": image("/res/graph.png")
+
+The GRAPH has nodes.
diff --git a/tests/typ/compiler/spread.typ b/tests/typ/compiler/spread.typ
new file mode 100644
index 00000000..ff661ead
--- /dev/null
+++ b/tests/typ/compiler/spread.typ
@@ -0,0 +1,93 @@
+// Test argument sinks and spreading.
+// Ref: false
+
+---
+// 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)")
+}
+
+---
+// Test multiple calls.
+{
+ let f(b, c: "!") = b + c
+ let g(a, ..sink) = a + f(..sink)
+ test(g("a", "b", c: "c"), "abc")
+}
+
+---
+// Test doing things with arguments.
+{
+ let save(..args) = {
+ test(type(args), "arguments")
+ test(repr(args), "(1, 2, three: true)")
+ }
+
+ save(1, 2, three: true)
+}
+
+---
+// Test spreading array and dictionary.
+{
+ let more = (3, -3, 6, 10)
+ test(min(1, 2, ..more), -3)
+ test(max(..more, 9), 10)
+ test(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)")
+}
+
+---
+// None is spreadable.
+#let f() = none
+#f(..none)
+#f(..if false {})
+#f(..for x in () [])
+
+---
+// Error: 8-14 cannot spread string
+#min(.."nope")
+
+---
+// Error: 8-14 expected identifier, named pair or argument sink, found spread
+#let f(..true) = none
+
+---
+// Error: 15-16 only one argument sink is allowed
+#let f(..a, ..b) = none
+
+---
+// Test spreading into array and dictionary.
+{
+ let l = (1, 2, 3)
+ let r = (5, 6, 7)
+ test((..l, 4, ..r), range(1, 8))
+ test((..none), ())
+}
+
+{
+ let x = (a: 1)
+ let y = (b: 2)
+ let z = (a: 3)
+ test((:..x, ..y, ..z), (a: 3, b: 2))
+ test((..(a: 1), b: 2), (a: 1, b: 2))
+}
+
+---
+// Error: 11-17 cannot spread dictionary into array
+{(1, 2, ..(a: 1))}
+
+---
+// Error: 5-11 cannot spread array into dictionary
+{(..(1, 2), a: 1)}
diff --git a/tests/typ/compiler/while.typ b/tests/typ/compiler/while.typ
new file mode 100644
index 00000000..5dc5ae41
--- /dev/null
+++ b/tests/typ/compiler/while.typ
@@ -0,0 +1,58 @@
+// 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(while false {}, none)
+
+#let i = 0
+#test(type(while i < 1 [{ i += 1 }]), "content")
+
+---
+// Condition must be boolean.
+// Error: 8-14 expected boolean, found content
+#while [nope] [nope]
+
+---
+// Make sure that we terminate and don't complain multiple times.
+#while true {
+ // Error: 3-7 unknown variable
+ nope
+}
+
+---
+// Error: 7 expected expression
+#while
+
+// Error: 7 expected expression
+{while}
+
+// Error: 9 expected body
+#while x
+
+// Error: 7 expected expression
+#while
+x {}
+
+// Error: 9 expected body
+#while x something