summaryrefslogtreecommitdiff
path: root/tests/suite/foundations
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-04-13 10:39:45 +0200
committerGitHub <noreply@github.com>2024-04-13 08:39:45 +0000
commit020294fca9a7065d4b9cf4e677f606ebaaa29b00 (patch)
treec0027ad22046e2726c22298461327823d6b88d53 /tests/suite/foundations
parent72dd79210602ecc799726fc096b078afbb47f299 (diff)
Better test runner (#3922)
Diffstat (limited to 'tests/suite/foundations')
-rw-r--r--tests/suite/foundations/array.typ494
-rw-r--r--tests/suite/foundations/assert.typ40
-rw-r--r--tests/suite/foundations/bytes.typ31
-rw-r--r--tests/suite/foundations/calc.typ261
-rw-r--r--tests/suite/foundations/content.typ120
-rw-r--r--tests/suite/foundations/context.typ65
-rw-r--r--tests/suite/foundations/datetime.typ93
-rw-r--r--tests/suite/foundations/dict.typ266
-rw-r--r--tests/suite/foundations/duration.typ103
-rw-r--r--tests/suite/foundations/eval.typ54
-rw-r--r--tests/suite/foundations/float.typ66
-rw-r--r--tests/suite/foundations/int.typ73
-rw-r--r--tests/suite/foundations/label.typ70
-rw-r--r--tests/suite/foundations/panic.typ14
-rw-r--r--tests/suite/foundations/plugin.typ47
-rw-r--r--tests/suite/foundations/repr.typ57
-rw-r--r--tests/suite/foundations/str.typ315
-rw-r--r--tests/suite/foundations/type.typ25
-rw-r--r--tests/suite/foundations/version.typ47
19 files changed, 2241 insertions, 0 deletions
diff --git a/tests/suite/foundations/array.typ b/tests/suite/foundations/array.typ
new file mode 100644
index 00000000..3992d75e
--- /dev/null
+++ b/tests/suite/foundations/array.typ
@@ -0,0 +1,494 @@
+// Test arrays.
+
+--- array-basic-syntax ---
+#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")
+ ,)
+
+--- array-bad-token ---
+// Error: 4-6 unexpected end of block comment
+#(1*/2)
+
+--- array-bad-number-suffix ---
+// Error: 6-8 invalid number suffix: u
+#(1, 1u 2)
+
+--- array-leading-comma ---
+// Error: 3-4 unexpected comma
+#(,1)
+
+--- array-incomplete-pair ---
+// Missing expression makes named pair incomplete, making this an empty array.
+// Error: 5 expected expression
+#(a:)
+
+--- array-named-pair ---
+// Named pair after this is already identified as an array.
+// Error: 6-10 expected expression, found named pair
+#(1, b: 2)
+
+--- array-keyed-pair ---
+// Keyed pair after this is already identified as an array.
+// Error: 6-14 expected expression, found keyed pair
+#(1, "key": 2)
+
+--- array-bad-conversion-from-string ---
+// Error: 8-15 expected array, bytes, or version, found string
+#array("hello")
+
+--- spread-into-array ---
+// 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), ())
+}
+
+--- spread-dict-into-array ---
+// Error: 9-17 cannot spread dictionary into array
+#(1, 2, ..(a: 1))
+
+--- array-len ---
+// Test the `len` method.
+#test(().len(), 0)
+#test(("A", "B", "C").len(), 3)
+
+--- array-at-lvalue ---
+// Test lvalue and rvalue access.
+#{
+ let array = (1, 2)
+ array.at(1) += 5 + array.at(0)
+ test(array, (1, 8))
+}
+
+--- array-first-and-at-lvalue ---
+// Test different lvalue method.
+#{
+ let array = (1, 2, 3)
+ array.first() = 7
+ array.at(1) *= 8
+ test(array, (7, 16, 3))
+}
+
+--- array-at-out-of-bounds ---
+// Test rvalue out of bounds.
+// Error: 2-17 array index out of bounds (index: 5, len: 3) and no default value was specified
+#(1, 2, 3).at(5)
+
+--- array-at-out-of-bounds-negative ---
+// Error: 2-18 array index out of bounds (index: -4, len: 3) and no default value was specified
+#(1, 2, 3).at(-4)
+
+--- array-at-out-of-bounds-lvalue ---
+// Test lvalue out of bounds.
+#{
+ let array = (1, 2, 3)
+ // Error: 3-14 array index out of bounds (index: 3, len: 3)
+ array.at(3) = 5
+}
+
+--- array-at-with-default ---
+// Test default value.
+#test((1, 2, 3).at(2, default: 5), 3)
+#test((1, 2, 3).at(3, default: 5), 5)
+
+--- array-remove-with-default ---
+// Test remove with default value.
+
+#{
+ let array = (1, 2, 3)
+ test(array.remove(2, default: 5), 3)
+}
+
+#{
+ let array = (1, 2, 3)
+ test(array.remove(3, default: 5), 5)
+}
+
+--- array-range ---
+// Test the `range` function.
+#test(range(4), (0, 1, 2, 3))
+#test(range(1, 4), (1, 2, 3))
+#test(range(-4, 2), (-4, -3, -2, -1, 0, 1))
+#test(range(10, 5), ())
+#test(range(10, step: 3), (0, 3, 6, 9))
+#test(range(1, 4, step: 1), (1, 2, 3))
+#test(range(1, 8, step: 2), (1, 3, 5, 7))
+#test(range(5, 2, step: -1), (5, 4, 3))
+#test(range(10, 0, step: -3), (10, 7, 4, 1))
+
+--- array-range-end-missing ---
+// Error: 2-9 missing argument: end
+#range()
+
+--- array-range-float-invalid ---
+// Error: 11-14 expected integer, found float
+#range(1, 2.0)
+
+--- array-range-bad-step-type ---
+// Error: 17-22 expected integer, found string
+#range(4, step: "one")
+
+--- array-range-step-zero ---
+// Error: 18-19 number must not be zero
+#range(10, step: 0)
+
+--- array-bad-method-lvalue ---
+// Test bad lvalue.
+// Error: 2:3-2:14 cannot mutate a temporary value
+#let array = (1, 2, 3)
+#(array.len() = 4)
+
+--- array-unknown-method-lvalue ---
+// Test bad lvalue.
+// Error: 2:9-2:13 type array has no method `yolo`
+#let array = (1, 2, 3)
+#(array.yolo() = 4)
+
+--- array-negative-indices ---
+// Test negative indices.
+#{
+ let array = (1, 2, 3, 4)
+ test(array.at(0), 1)
+ test(array.at(-1), 4)
+ test(array.at(-2), 3)
+ test(array.at(-3), 2)
+ test(array.at(-4), 1)
+}
+
+--- array-first-and-last ---
+// The the `first` and `last` methods.
+#test((1,).first(), 1)
+#test((2,).last(), 2)
+#test((1, 2, 3).first(), 1)
+#test((1, 2, 3).last(), 3)
+
+--- array-first-empty ---
+// Error: 2-12 array is empty
+#().first()
+
+--- array-last-empty ---
+// Error: 2-11 array is empty
+#().last()
+
+--- array-push-and-pop ---
+// Test the `push` and `pop` methods.
+#{
+ let tasks = (a: (1, 2, 3), b: (4, 5, 6))
+ test(tasks.at("a").pop(), 3)
+ tasks.b.push(7)
+ test(tasks.a, (1, 2))
+ test(tasks.at("b"), (4, 5, 6, 7))
+}
+
+--- array-insert-and-remove ---
+// 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))
+}
+
+--- array-insert-missing-index ---
+// Error: 2:2-2:18 missing argument: index
+#let numbers = ()
+#numbers.insert()
+
+--- array-slice ---
+// 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")
+
+--- array-slice-out-of-bounds ---
+// Error: 2-30 array index out of bounds (index: 12, len: 10)
+#range(10).slice(9, count: 3)
+
+--- array-slice-out-of-bounds-negative ---
+// Error: 2-24 array index out of bounds (index: -4, len: 3)
+#(1, 2, 3).slice(0, -4)
+
+--- array-position ---
+// 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)
+
+--- array-filter ---
+// Test the `filter` method.
+#test(().filter(calc.even), ())
+#test((1, 2, 3, 4).filter(calc.even), (2, 4))
+#test((7, 3, 2, 5, 1).filter(x => x < 5), (3, 2, 1))
+
+--- array-map ---
+// Test the `map` method.
+#test(().map(x => x * 2), ())
+#test((2, 3).map(x => x * 2), (4, 6))
+
+--- array-fold ---
+// Test the `fold` method.
+#test(().fold("hi", grid), "hi")
+#test((1, 2, 3, 4).fold(0, (s, x) => s + x), 10)
+
+--- array-fold-closure-without-params ---
+// Error: 20-22 unexpected argument
+#(1, 2, 3).fold(0, () => none)
+
+--- array-sum ---
+// Test the `sum` method.
+#test(().sum(default: 0), 0)
+#test(().sum(default: []), [])
+#test((1, 2, 3).sum(), 6)
+
+--- array-sum-empty ---
+// Error: 2-10 cannot calculate sum of empty array with no default
+#().sum()
+
+--- array-product ---
+// Test the `product` method.
+#test(().product(default: 0), 0)
+#test(().product(default: []), [])
+#test(([ab], 3).product(), [ab]*3)
+#test((1, 2, 3).product(), 6)
+
+--- array-product-empty ---
+// Error: 2-14 cannot calculate product of empty array with no default
+#().product()
+
+--- array-rev ---
+// Test the `rev` method.
+#test(range(3).rev(), (2, 1, 0))
+
+--- array-join ---
+// 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)")
+
+--- array-join-bad-values ---
+// Error: 2-22 cannot join boolean with boolean
+#(true, false).join()
+
+--- array-join-bad-separator ---
+// Error: 2-20 cannot join string with integer
+#("a", "b").join(1)
+
+--- array-join-content ---
+// Test joining content.
+#([One], [Two], [Three]).join([, ], last: [ and ]).
+
+--- array-intersperse ---
+// Test the `intersperse` method
+#test(().intersperse("a"), ())
+#test((1,).intersperse("a"), (1,))
+#test((1, 2).intersperse("a"), (1, "a", 2))
+#test((1, 2, "b").intersperse("a"), (1, "a", 2, "a", "b"))
+
+--- array-chunks ---
+// Test the `chunks` method.
+#test(().chunks(10), ())
+#test((1, 2, 3).chunks(10), ((1, 2, 3),))
+#test((1, 2, 3, 4, 5, 6).chunks(3), ((1, 2, 3), (4, 5, 6)))
+#test((1, 2, 3, 4, 5, 6, 7, 8).chunks(3), ((1, 2, 3), (4, 5, 6), (7, 8)))
+
+#test(().chunks(10, exact: true), ())
+#test((1, 2, 3).chunks(10, exact: true), ())
+#test((1, 2, 3, 4, 5, 6).chunks(3, exact: true), ((1, 2, 3), (4, 5, 6)))
+#test((1, 2, 3, 4, 5, 6, 7, 8).chunks(3, exact: true), ((1, 2, 3), (4, 5, 6)))
+
+--- array-chunks-size-zero ---
+// Error: 19-20 number must be positive
+#(1, 2, 3).chunks(0)
+
+--- array-chunks-size-negative ---
+// Error: 19-21 number must be positive
+#(1, 2, 3).chunks(-5)
+
+--- array-sorted ---
+// Test the `sorted` method.
+#test(().sorted(), ())
+#test(().sorted(key: x => x), ())
+#test(((true, false) * 10).sorted(), (false,) * 10 + (true,) * 10)
+#test(("it", "the", "hi", "text").sorted(), ("hi", "it", "text", "the"))
+#test(("I", "the", "hi", "text").sorted(key: x => x), ("I", "hi", "text", "the"))
+#test(("I", "the", "hi", "text").sorted(key: x => x.len()), ("I", "hi", "the", "text"))
+#test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10))
+#test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x), (-10, -7, -5, 1, 2, 2, 3, 6, 8))
+#test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x * x), (1, 2, 2, 3, -5, 6, -7, 8, -10))
+
+--- array-sorted-key-function-positional-1 ---
+// Error: 12-18 unexpected argument
+#().sorted(x => x)
+
+--- array-zip ---
+// Test the `zip` method.
+#test(().zip(()), ())
+#test((1,).zip(()), ())
+#test((1,).zip((2,)), ((1, 2),))
+#test((1, 2).zip((3, 4)), ((1, 3), (2, 4)))
+#test((1, 2, 3, 4).zip((5, 6)), ((1, 5), (2, 6)))
+#test(((1, 2), 3).zip((4, 5)), (((1, 2), 4), (3, 5)))
+#test((1, "hi").zip((true, false)), ((1, true), ("hi", false)))
+#test((1, 2, 3).zip((3, 4, 5), (6, 7, 8)), ((1, 3, 6), (2, 4, 7), (3, 5, 8)))
+#test(().zip((), ()), ())
+#test((1,).zip((2,), (3,)), ((1, 2, 3),))
+#test((1, 2, 3).zip(), ((1,), (2,), (3,)))
+#test(array.zip(()), ())
+
+--- array-enumerate ---
+// Test the `enumerate` method.
+#test(().enumerate(), ())
+#test(().enumerate(start: 5), ())
+#test(("a", "b", "c").enumerate(), ((0, "a"), (1, "b"), (2, "c")))
+#test(("a", "b", "c").enumerate(start: 1), ((1, "a"), (2, "b"), (3, "c")))
+#test(("a", "b", "c").enumerate(start: 42), ((42, "a"), (43, "b"), (44, "c")))
+#test(("a", "b", "c").enumerate(start: -7), ((-7, "a"), (-6, "b"), (-5, "c")))
+
+--- array-dedup ---
+// Test the `dedup` method.
+#test(().dedup(), ())
+#test((1,).dedup(), (1,))
+#test((1, 1).dedup(), (1,))
+#test((1, 2, 1).dedup(), (1, 2))
+#test(("Jane", "John", "Eric").dedup(), ("Jane", "John", "Eric"))
+#test(("Jane", "John", "Eric", "John").dedup(), ("Jane", "John", "Eric"))
+
+--- array-dedup-key ---
+// Test the `dedup` method with the `key` argument.
+#test((1, 2, 3, 4, 5, 6).dedup(key: x => calc.rem(x, 2)), (1, 2))
+#test((1, 2, 3, 4, 5, 6).dedup(key: x => calc.rem(x, 3)), (1, 2, 3))
+#test(("Hello", "World", "Hi", "There").dedup(key: x => x.len()), ("Hello", "Hi"))
+#test(("Hello", "World", "Hi", "There").dedup(key: x => x.at(0)), ("Hello", "World", "There"))
+
+--- array-to-dict ---
+// Test the `to-dict` method.
+#test(().to-dict(), (:))
+#test((("a", 1), ("b", 2), ("c", 3)).to-dict(), (a: 1, b: 2, c: 3))
+#test((("a", 1), ("b", 2), ("c", 3), ("b", 4)).to-dict(), (a: 1, b: 4, c: 3))
+
+--- array-to-dict-bad-item-type ---
+// Error: 2-16 expected (str, any) pairs, found integer
+#(1,).to-dict()
+
+--- array-to-dict-bad-pair-length-1 ---
+// Error: 2-19 expected pairs of length 2, found length 1
+#((1,),).to-dict()
+
+--- array-to-dict-bad-pair-length-3 ---
+// Error: 2-26 expected pairs of length 2, found length 3
+#(("key",1,2),).to-dict()
+
+--- array-to-dict-bad-key-type ---
+// Error: 2-21 expected key of type str, found integer
+#((1, 2),).to-dict()
+
+--- array-zip-positional-and-named-argument ---
+// Error: 13-30 unexpected argument: val
+#().zip((), val: "applicable")
+
+--- array-sorted-bad-key ---
+// Error: 32-37 cannot divide by zero
+#(1, 2, 0, 3).sorted(key: x => 5 / x)
+
+--- array-sorted-uncomparable ---
+// Error: 2-26 cannot compare content and content
+#([Hi], [There]).sorted()
+
+--- array-sorted-uncomparable-lengths ---
+// Error: 2-26 cannot compare 3em with 2pt
+#(1pt, 2pt, 3em).sorted()
+
+--- array-sorted-key-function-positional-2 ---
+// Error: 42-52 unexpected argument
+#((k: "a", v: 2), (k: "b", v: 1)).sorted(it => it.v)
+
+--- issue-3014-mix-array-dictionary ---
+// Error: 8-17 expected expression, found named pair
+#(box, fill: red)
+
+--- issue-3154-array-first-empty ---
+#{
+ let array = ()
+ // Error: 3-16 array is empty
+ array.first()
+}
+
+--- issue-3154-array-first-mutable-empty ---
+#{
+ let array = ()
+ // Error: 3-16 array is empty
+ array.first() = 9
+}
+
+--- issue-3154-array-last-empty ---
+#{
+ let array = ()
+ // Error: 3-15 array is empty
+ array.last()
+}
+
+--- issue-3154-array-last-mutable-empty ---
+#{
+ let array = ()
+ // Error: 3-15 array is empty
+ array.last() = 9
+}
+
+--- issue-3154-array-at-out-of-bounds ---
+#{
+ let array = (1,)
+ // Error: 3-14 array index out of bounds (index: 1, len: 1) and no default value was specified
+ array.at(1)
+}
+
+--- issue-3154-array-at-out-of-bounds-default ---
+#{
+ let array = (1,)
+ test(array.at(1, default: 0), 0)
+}
+
+--- issue-3154-array-at-out-of-bounds-mutable ---
+#{
+ let array = (1,)
+ // Error: 3-14 array index out of bounds (index: 1, len: 1)
+ array.at(1) = 9
+}
+
+--- issue-3154-array-at-out-of-bounds-mutable-default ---
+#{
+ let array = (1,)
+ // Error: 3-26 array index out of bounds (index: 1, len: 1)
+ array.at(1, default: 0) = 9
+}
+
+--- array-unopened ---
+// Error: 2-3 unclosed delimiter
+#{)}
+
+--- array-unclosed ---
+// Error: 3-4 unclosed delimiter
+#{(}
diff --git a/tests/suite/foundations/assert.typ b/tests/suite/foundations/assert.typ
new file mode 100644
index 00000000..5de0f387
--- /dev/null
+++ b/tests/suite/foundations/assert.typ
@@ -0,0 +1,40 @@
+--- assert-fail ---
+// Test failing assertions.
+// Error: 2-16 assertion failed
+#assert(1 == 2)
+
+--- assert-fail-message ---
+// Test failing assertions.
+// Error: 2-51 assertion failed: two is smaller than one
+#assert(2 < 1, message: "two is smaller than one")
+
+--- assert-bad-type ---
+// Test failing assertions.
+// Error: 9-15 expected boolean, found string
+#assert("true")
+
+--- assert-eq-fail ---
+// Test failing assertions.
+// Error: 2-19 equality assertion failed: value 10 was not equal to 11
+#assert.eq(10, 11)
+
+--- assert-eq-fail-message ---
+// Test failing assertions.
+// Error: 2-55 equality assertion failed: 10 and 12 are not equal
+#assert.eq(10, 12, message: "10 and 12 are not equal")
+
+--- assert-ne-fail ---
+// Test failing assertions.
+// Error: 2-19 inequality assertion failed: value 11 was equal to 11
+#assert.ne(11, 11)
+
+--- assert-ne-fail-message ---
+// Test failing assertions.
+// Error: 2-57 inequality assertion failed: must be different from 11
+#assert.ne(11, 11, message: "must be different from 11")
+
+--- assert-success ---
+// Test successful assertions.
+#assert(5 > 3)
+#assert.eq(15, 15)
+#assert.ne(10, 12)
diff --git a/tests/suite/foundations/bytes.typ b/tests/suite/foundations/bytes.typ
new file mode 100644
index 00000000..c7089278
--- /dev/null
+++ b/tests/suite/foundations/bytes.typ
@@ -0,0 +1,31 @@
+// Test the bytes type.
+
+--- bytes-basic ---
+#let data = read("/assets/images/rhino.png", encoding: none)
+#test(data.len(), 232243)
+#test(data.slice(0, count: 5), bytes((137, 80, 78, 71, 13)))
+#test(str(data.slice(1, 4)), "PNG")
+#test(repr(data), "bytes(232243)")
+
+--- bytes-string-conversion ---
+#test(str(bytes(range(0x41, 0x50))), "ABCDEFGHIJKLMNO")
+
+--- bytes-array-conversion ---
+#test(array(bytes("Hello")), (0x48, 0x65, 0x6C, 0x6C, 0x6F))
+
+--- bytes-addition ---
+// Test addition and joining.
+#test(bytes((1, 2)) + bytes(()), bytes((1, 2)))
+#test(bytes((1, 2)) + bytes((3, 4)), bytes((1, 2, 3, 4)))
+#test(bytes(()) + bytes((3, 4)), bytes((3, 4)))
+
+--- bytes-joining ---
+#test(str({
+ bytes("Hello")
+ bytes((0x20,))
+ bytes("World")
+}), "Hello World")
+
+--- bytes-bad-conversion-from-dict ---
+// Error: 8-14 expected string, array, or bytes, found dictionary
+#bytes((a: 1))
diff --git a/tests/suite/foundations/calc.typ b/tests/suite/foundations/calc.typ
new file mode 100644
index 00000000..e702be9f
--- /dev/null
+++ b/tests/suite/foundations/calc.typ
@@ -0,0 +1,261 @@
+--- calc-round ---
+#test(calc.round(calc.e, digits: 2), 2.72)
+#test(calc.round(calc.pi, digits: 2), 3.14)
+
+--- calc-abs ---
+// Test the `abs` function.
+#test(calc.abs(-3), 3)
+#test(calc.abs(3), 3)
+#test(calc.abs(-0.0), 0.0)
+#test(calc.abs(0.0), -0.0)
+#test(calc.abs(-3.14), 3.14)
+#test(calc.abs(50%), 50%)
+#test(calc.abs(-25%), 25%)
+
+--- cals-abs-bad-type ---
+// Error: 11-22 expected integer, float, length, angle, ratio, or fraction, found string
+#calc.abs("no number")
+
+--- calc-even-and-odd ---
+// Test the `even` and `odd` functions.
+#test(calc.even(2), true)
+#test(calc.odd(2), false)
+#test(calc.odd(-1), true)
+#test(calc.even(-11), false)
+
+--- calc-rem ---
+// Test the `rem` function.
+#test(calc.rem(1, 1), 0)
+#test(calc.rem(5, 3), 2)
+#test(calc.rem(5, -3), 2)
+#test(calc.rem(22.5, 10), 2.5)
+#test(calc.rem(9, 4.5), 0)
+
+--- calc-rem-divisor-zero-1 ---
+// Error: 14-15 divisor must not be zero
+#calc.rem(5, 0)
+
+--- calc-rem-divisor-zero-2 ---
+// Error: 16-19 divisor must not be zero
+#calc.rem(3.0, 0.0)
+
+--- calc-div-euclid ---
+// Test the `div-euclid` function.
+#test(calc.div-euclid(7, 3), 2)
+#test(calc.div-euclid(7, -3), -2)
+#test(calc.div-euclid(-7, 3), -3)
+#test(calc.div-euclid(-7, -3), 3)
+#test(calc.div-euclid(2.5, 2), 1)
+
+--- calc-div-euclid-divisor-zero-1 ---
+// Error: 21-22 divisor must not be zero
+#calc.div-euclid(5, 0)
+
+--- calc-div-euclid-divisor-zero-2 ---
+// Error: 23-26 divisor must not be zero
+#calc.div-euclid(3.0, 0.0)
+
+--- calc-rem-euclid ---
+// Test the `rem-euclid` function.
+#test(calc.rem-euclid(7, 3), 1)
+#test(calc.rem-euclid(7, -3), 1)
+#test(calc.rem-euclid(-7, 3), 2)
+#test(calc.rem-euclid(-7, -3), 2)
+#test(calc.rem-euclid(2.5, 2), 0.5)
+
+--- calc-rem-euclid-divisor-zero-1 ---
+// Error: 21-22 divisor must not be zero
+#calc.rem-euclid(5, 0)
+
+--- calc-rem-euclid-divisor-zero-2 ---
+// Error: 23-26 divisor must not be zero
+#calc.rem-euclid(3.0, 0.0)
+
+--- calc-quo ---
+// Test the `quo` function.
+#test(calc.quo(1, 1), 1)
+#test(calc.quo(5, 3), 1)
+#test(calc.quo(5, -3), -1)
+#test(calc.quo(22.5, 10), 2)
+#test(calc.quo(9, 4.5), 2)
+
+--- calc-quo-divisor-zero-1 ---
+// Error: 14-15 divisor must not be zero
+#calc.quo(5, 0)
+
+--- calc-quo-divisor-zero-2 ---
+// Error: 16-19 divisor must not be zero
+#calc.quo(3.0, 0.0)
+
+--- calc-min-and-max ---
+// Test the `min` and `max` functions.
+#test(calc.min(2, -4), -4)
+#test(calc.min(3.5, 1e2, -0.1, 3), -0.1)
+#test(calc.max(-3, 11), 11)
+#test(calc.min("hi"), "hi")
+
+--- calc-pow-log-exp-ln ---
+// Test the `pow`, `log`, `exp`, and `ln` functions.
+#test(calc.pow(10, 0), 1)
+#test(calc.pow(2, 4), 16)
+#test(calc.exp(2), calc.pow(calc.e, 2))
+#test(calc.ln(10), calc.log(10, base: calc.e))
+
+--- calc-bit-logical ---
+// Test the `bit-not`, `bit-and`, `bit-or` and `bit-xor` functions.
+#test(64.bit-not(), -65)
+#test(0.bit-not(), -1)
+#test((-56).bit-not(), 55)
+#test(128.bit-and(192), 128)
+#test(192.bit-and(224), 192)
+#test((-1).bit-and(325532), 325532)
+#test(0.bit-and(-53), 0)
+#test(0.bit-or(-1), -1)
+#test(5.bit-or(3), 7)
+#test((-50).bit-or(3), -49)
+#test(64.bit-or(32), 96)
+#test((-1).bit-xor(1), -2)
+#test(64.bit-xor(96), 32)
+#test((-1).bit-xor(-7), 6)
+#test(0.bit-xor(492), 492)
+
+--- calc-bit-shift ---
+// Test the `bit-lshift` and `bit-rshift` functions.
+#test(32.bit-lshift(2), 128)
+#test(694.bit-lshift(0), 694)
+#test(128.bit-rshift(2), 32)
+#test(128.bit-rshift(12345), 0)
+#test((-7).bit-rshift(2), -2)
+#test((-7).bit-rshift(12345), -1)
+#test(128.bit-rshift(2, logical: true), 32)
+#test((-7).bit-rshift(61, logical: true), 7)
+#test(128.bit-rshift(12345, logical: true), 0)
+#test((-7).bit-rshift(12345, logical: true), 0)
+
+--- calc-bit-shift-too-large ---
+// Error: 2-18 the result is too large
+#1.bit-lshift(64)
+
+--- calc-bit-lshift-negative ---
+// Error: 15-17 number must be at least zero
+#1.bit-lshift(-1)
+
+--- calc-bit-rshift-negative ---
+// Error: 15-17 number must be at least zero
+#1.bit-rshift(-1)
+
+--- calc-pow-zero-to-power-of-zero ---
+// Error: 2-16 zero to the power of zero is undefined
+#calc.pow(0, 0)
+
+--- calc-pow-exponent-too-large ---
+// Error: 14-31 exponent is too large
+#calc.pow(2, 10000000000000000)
+
+--- calc-pow-too-large ---
+// Error: 2-25 the result is too large
+#calc.pow(2, 2147483647)
+
+--- calc-pow-bad-exponent ---
+// Error: 14-36 exponent may not be infinite, subnormal, or NaN
+#calc.pow(2, calc.pow(2.0, 10000.0))
+
+--- calc-pow-not-real ---
+// Error: 2-19 the result is not a real number
+#calc.pow(-1, 0.5)
+
+--- calc-sqrt-not-real ---
+// Error: 12-14 cannot take square root of negative number
+#calc.sqrt(-1)
+
+--- calc-root ---
+#test(calc.root(12.0, 1), 12.0)
+#test(calc.root(9.0, 2), 3.0)
+#test(calc.root(27.0, 3), 3.0)
+#test(calc.root(-27.0, 3), -3.0)
+// 100^(-1/2) = (100^(1/2))^-1 = 1/sqrt(100)
+#test(calc.root(100.0, -2), 0.1)
+
+--- calc-root-zeroth ---
+// Error: 17-18 cannot take the 0th root of a number
+#calc.root(1.0, 0)
+
+--- calc-root-negative-even ---
+// Error: 24-25 negative numbers do not have a real nth root when n is even
+#test(calc.root(-27.0, 4), -3.0)
+
+--- calc-log-negative ---
+// Error: 11-13 value must be strictly positive
+#calc.log(-1)
+
+--- calc-log-bad-base ---
+// Error: 20-21 base may not be zero, NaN, infinite, or subnormal
+#calc.log(1, base: 0)
+
+--- calc-log-not-real ---
+// Error: 2-24 the result is not a real number
+#calc.log(10, base: -1)
+
+--- calc-fact ---
+// Test the `fact` function.
+#test(calc.fact(0), 1)
+#test(calc.fact(5), 120)
+
+--- calc-fact-too-large ---
+// Error: 2-15 the result is too large
+#calc.fact(21)
+
+--- calc-perm ---
+// Test the `perm` function.
+#test(calc.perm(0, 0), 1)
+#test(calc.perm(5, 3), 60)
+#test(calc.perm(5, 5), 120)
+#test(calc.perm(5, 6), 0)
+
+--- calc-perm-too-large ---
+// Error: 2-19 the result is too large
+#calc.perm(21, 21)
+
+--- calc-binom ---
+// Test the `binom` function.
+#test(calc.binom(0, 0), 1)
+#test(calc.binom(5, 3), 10)
+#test(calc.binom(5, 5), 1)
+#test(calc.binom(5, 6), 0)
+#test(calc.binom(6, 2), 15)
+
+--- calc-gcd ---
+// Test the `gcd` function.
+#test(calc.gcd(112, 77), 7)
+#test(calc.gcd(12, 96), 12)
+#test(calc.gcd(13, 9), 1)
+#test(calc.gcd(13, -9), 1)
+#test(calc.gcd(272557, 272557), 272557)
+#test(calc.gcd(0, 0), 0)
+#test(calc.gcd(7, 0), 7)
+
+--- calc-lcm ---
+// Test the `lcm` function.
+#test(calc.lcm(112, 77), 1232)
+#test(calc.lcm(12, 96), 96)
+#test(calc.lcm(13, 9), 117)
+#test(calc.lcm(13, -9), 117)
+#test(calc.lcm(272557, 272557), 272557)
+#test(calc.lcm(0, 0), 0)
+#test(calc.lcm(8, 0), 0)
+
+--- calc-lcm-too-large ---
+// Error: 2-41 the result is too large
+#calc.lcm(15486487489457, 4874879896543)
+
+--- calc-min-nothing ---
+// Error: 2-12 expected at least one value
+#calc.min()
+
+--- calc-min-uncomparable ---
+// Error: 14-18 cannot compare string and integer
+#calc.min(1, "hi")
+
+--- calc-max-uncomparable ---
+// Error: 16-19 cannot compare 1pt with 1em
+#calc.max(1em, 1pt)
diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ
new file mode 100644
index 00000000..afecc124
--- /dev/null
+++ b/tests/suite/foundations/content.typ
@@ -0,0 +1,120 @@
+--- content-at-default ---
+// Test .at() default values for content.
+#test(auto, [a].at("doesn't exist", default: auto))
+
+--- content-field-syntax ---
+// Test fields on elements.
+#show list: it => {
+ test(it.children.len(), 3)
+}
+
+- A
+- B
+- C
+
+--- content-field-missing ---
+// Error: 25-28 content does not contain field "fun"
+#show heading: it => it.fun
+= A
+
+--- content-fields ---
+// Test content fields method.
+#test([a].fields(), (text: "a"))
+#test([a *b*].fields(), (children: ([a], [ ], strong[b])))
+
+--- content-fields-mutable-invalid ---
+#{
+ let object = [hi]
+ // Error: 3-9 cannot mutate fields on content
+ object.property = "value"
+}
+
+--- content-field-materialized-table ---
+// Ensure that fields from set rules are materialized into the element before
+// a show rule runs.
+#set table(columns: (10pt, auto))
+#show table: it => it.columns
+#table[A][B][C][D]
+
+--- content-field-materialized-heading ---
+// Test it again with a different element.
+#set heading(numbering: "(I)")
+#show heading: set text(size: 11pt, weight: "regular")
+#show heading: it => it.numbering
+= Heading
+
+--- content-field-materialized-query ---
+// Test it with query.
+#set raw(lang: "rust")
+#context query(<myraw>).first().lang
+`raw` <myraw>
+
+--- content-fields-complex ---
+// Integrated test for content fields.
+#let compute(equation, ..vars) = {
+ let vars = vars.named()
+ let f(elem) = {
+ let func = elem.func()
+ if func == text {
+ let text = elem.text
+ if regex("^\d+$") in text {
+ int(text)
+ } else if text in vars {
+ int(vars.at(text))
+ } else {
+ panic("unknown math variable: " + text)
+ }
+ } else if func == math.attach {
+ let value = f(elem.base)
+ if elem.has("t") {
+ value = calc.pow(value, f(elem.t))
+ }
+ value
+ } else if elem.has("children") {
+ elem
+ .children
+ .filter(v => v != [ ])
+ .split[+]
+ .map(xs => xs.fold(1, (prod, v) => prod * f(v)))
+ .fold(0, (sum, v) => sum + v)
+ }
+ }
+ let result = f(equation.body)
+ [With ]
+ vars
+ .pairs()
+ .map(p => $#p.first() = #p.last()$)
+ .join(", ", last: " and ")
+ [ we have:]
+ $ equation = result $
+}
+
+#compute($x y + y^2$, x: 2, y: 3)
+
+--- content-label-has-method ---
+// Test whether the label is accessible through the `has` method.
+#show heading: it => {
+ assert(it.has("label"))
+ it
+}
+
+= Hello, world! <my-label>
+
+--- content-label-field-access ---
+// Test whether the label is accessible through field syntax.
+#show heading: it => {
+ assert(str(it.label) == "my-label")
+ it
+}
+
+= Hello, world! <my-label>
+
+--- content-label-fields-method ---
+// Test whether the label is accessible through the fields method.
+#show heading: it => {
+ assert("label" in it.fields())
+ assert(str(it.fields().label) == "my-label")
+ it
+}
+
+= Hello, world! <my-label>
diff --git a/tests/suite/foundations/context.typ b/tests/suite/foundations/context.typ
new file mode 100644
index 00000000..fea9f544
--- /dev/null
+++ b/tests/suite/foundations/context.typ
@@ -0,0 +1,65 @@
+// Test context expressions.
+
+--- context-body-atomic-in-markup ---
+// Test that context body is parsed as atomic expression.
+#let c = [#context "hello".]
+#test(c.children.first().func(), (context none).func())
+#test(c.children.last(), [.])
+
+--- context-element-constructor-forbidden ---
+// Test that manual construction is forbidden.
+// Error: 2-25 cannot be constructed manually
+#(context none).func()()
+
+--- context-in-show-rule ---
+// Test that show rule establishes context.
+#set heading(numbering: "1.")
+#show heading: it => test(
+ counter(heading).get(),
+ (intro: (1,), back: (2,)).at(str(it.label)),
+)
+
+= Introduction <intro>
+= Background <back>
+
+--- context-in-show-rule-query ---
+// Test that show rule on non-locatable element allows `query`.
+// Error: 18-47 Assertion failed: 2 != 3
+#show emph: _ => test(query(heading).len(), 3)
+#show strong: _ => test(query(heading).len(), 2)
+= Introduction
+= Background
+*Hi* _there_
+
+--- context-assign-to-captured-variable ---
+// Test error when captured variable is assigned to.
+#let i = 0
+// Error: 11-12 variables from outside the context expression are read-only and cannot be modified
+#context (i = 1)
+
+--- context-compatibility-locate ---
+#let s = state("x", 0)
+#let compute(expr) = [
+ #s.update(x =>
+ eval(expr.replace("x", str(x)))
+ )
+ New value is #s.display().
+]
+
+#locate(loc => {
+ let elem = query(<here>, loc).first()
+ test(s.at(elem.location()), 13)
+})
+
+#compute("10") \
+#compute("x + 3") \
+*Here.* <here> \
+#compute("x * 2") \
+#compute("x - 5")
+
+--- context-compatibility-styling ---
+#style(styles => measure([it], styles).width < 20pt)
+
+--- context-compatibility-counter-display ---
+#counter(heading).update(10)
+#counter(heading).display(n => test(n, 10))
diff --git a/tests/suite/foundations/datetime.typ b/tests/suite/foundations/datetime.typ
new file mode 100644
index 00000000..b54c11f3
--- /dev/null
+++ b/tests/suite/foundations/datetime.typ
@@ -0,0 +1,93 @@
+--- datetime-constructor-empty ---
+// Error: 2-12 at least one of date or time must be fully specified
+#datetime()
+
+--- datetime-constructor-time-invalid ---
+// Error: 2-42 time is invalid
+#datetime(hour: 25, minute: 0, second: 0)
+
+--- datetime-constructor-date-invalid ---
+// Error: 2-41 date is invalid
+#datetime(year: 2000, month: 2, day: 30)
+
+--- datetime-display ---
+// Test displaying of dates.
+#test(datetime(year: 2023, month: 4, day: 29).display(), "2023-04-29")
+#test(datetime(year: 2023, month: 4, day: 29).display("[year]"), "2023")
+#test(
+ datetime(year: 2023, month: 4, day: 29)
+ .display("[year repr:last_two]"),
+ "23",
+)
+#test(
+ datetime(year: 2023, month: 4, day: 29)
+ .display("[year] [month repr:long] [day] [week_number] [weekday]"),
+ "2023 April 29 17 Saturday",
+)
+
+// Test displaying of times
+#test(datetime(hour: 14, minute: 26, second: 50).display(), "14:26:50")
+#test(datetime(hour: 14, minute: 26, second: 50).display("[hour]"), "14")
+#test(
+ datetime(hour: 14, minute: 26, second: 50)
+ .display("[hour repr:12 padding:none]"),
+ "2",
+)
+#test(
+ datetime(hour: 14, minute: 26, second: 50)
+ .display("[hour], [minute], [second]"), "14, 26, 50",
+)
+
+// Test displaying of datetimes
+#test(
+ datetime(year: 2023, month: 4, day: 29, hour: 14, minute: 26, second: 50).display(),
+ "2023-04-29 14:26:50",
+)
+
+// Test getting the year/month/day etc. of a datetime
+#let d = datetime(year: 2023, month: 4, day: 29, hour: 14, minute: 26, second: 50)
+#test(d.year(), 2023)
+#test(d.month(), 4)
+#test(d.weekday(), 6)
+#test(d.day(), 29)
+#test(d.hour(), 14)
+#test(d.minute(), 26)
+#test(d.second(), 50)
+
+#let e = datetime(year: 2023, month: 4, day: 29)
+#test(e.hour(), none)
+#test(e.minute(), none)
+#test(e.second(), none)
+
+// Test today
+#test(datetime.today().display(), "1970-01-01")
+#test(datetime.today(offset: auto).display(), "1970-01-01")
+#test(datetime.today(offset: 2).display(), "1970-01-01")
+
+--- datetime-ordinal ---
+// Test date methods.
+#test(datetime(day: 1, month: 1, year: 2000).ordinal(), 1);
+#test(datetime(day: 1, month: 3, year: 2000).ordinal(), 31 + 29 + 1);
+#test(datetime(day: 31, month: 12, year: 2000).ordinal(), 366);
+#test(datetime(day: 1, month: 3, year: 2001).ordinal(), 31 + 28 + 1);
+#test(datetime(day: 31, month: 12, year: 2001).ordinal(), 365);
+
+--- datetime-display-missing-closing-bracket ---
+// Error: 27-34 missing closing bracket for bracket at index 0
+#datetime.today().display("[year")
+
+--- datetime-display-invalid-component ---
+// Error: 27-38 invalid component name 'nothing' at index 1
+#datetime.today().display("[nothing]")
+
+--- datetime-display-invalid-modifier ---
+// Error: 27-50 invalid modifier 'wrong' at index 6
+#datetime.today().display("[year wrong:last_two]")
+
+--- datetime-display-expected-component ---
+// Error: 27-33 expected component name at index 2
+#datetime.today().display(" []")
+
+--- datetime-display-insufficient-information ---
+// Error: 2-36 failed to format datetime (insufficient information)
+#datetime.today().display("[hour]")
diff --git a/tests/suite/foundations/dict.typ b/tests/suite/foundations/dict.typ
new file mode 100644
index 00000000..2c2d2a41
--- /dev/null
+++ b/tests/suite/foundations/dict.typ
@@ -0,0 +1,266 @@
+// Test dictionaries.
+
+--- dict-basic-syntax ---
+
+// Empty
+#(:)
+
+// Two pairs and string key.
+#let dict = (normal: 1, "spacy key": 2)
+#dict
+
+#test(dict.normal, 1)
+#test(dict.at("spacy key"), 2)
+
+--- dict-fields ---
+// Test field on dictionary.
+#let dict = (nothing: "ness", hello: "world")
+#test(dict.nothing, "ness")
+#{
+ let world = dict
+ .hello
+
+ test(world, "world")
+}
+
+--- dict-missing-field ---
+// Error: 6-13 dictionary does not contain key "invalid"
+#(:).invalid
+
+--- dict-bad-key ---
+// Error: 3-7 expected string, found boolean
+// Error: 16-18 expected string, found integer
+#(true: false, 42: 3)
+
+--- dict-duplicate-key ---
+// Error: 24-29 duplicate key: first
+#(first: 1, second: 2, first: 3)
+
+--- dict-duplicate-key-stringy ---
+// Error: 17-20 duplicate key: a
+#(a: 1, "b": 2, "a": 3)
+
+--- dict-bad-expression ---
+// Simple expression after already being identified as a dictionary.
+// Error: 9-10 expected named or keyed pair, found identifier
+#(a: 1, b)
+
+--- dict-leading-colon ---
+// Identified as dictionary due to initial colon.
+// The boolean key is allowed for now since it will only cause an error at the evaluation stage.
+// Error: 4-5 expected named or keyed pair, found integer
+// Error: 17 expected expression
+#(:1 b:"", true:)
+
+--- spread-into-dict ---
+#{
+ 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))
+}
+
+--- spread-array-into-dict ---
+// Error: 3-11 cannot spread array into dictionary
+#(..(1, 2), a: 1)
+
+--- dict-at-lvalue ---
+// Test lvalue and rvalue access.
+#{
+ let dict = (a: 1, "b b": 1)
+ dict.at("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.at("state").ok = false
+ test(dict.state.ok, false)
+ test(dict.state.err, false)
+}
+
+--- dict-at-missing-key ---
+// Test rvalue missing key.
+#{
+ let dict = (a: 1, b: 2)
+ // Error: 11-23 dictionary does not contain key "c" and no default value was specified
+ let x = dict.at("c")
+}
+
+--- dict-at-default ---
+// Test default value.
+#test((a: 1, b: 2).at("b", default: 3), 2)
+#test((a: 1, b: 2).at("c", default: 3), 3)
+
+--- dict-insert ---
+// Test insert.
+#{
+ let dict = (a: 1, b: 2)
+ dict.insert("b", 3)
+ test(dict, (a: 1, b: 3))
+ dict.insert("c", 5)
+ test(dict, (a: 1, b: 3, c: 5))
+}
+
+--- dict-remove-with-default ---
+// Test remove with default value.
+#{
+ let dict = (a: 1, b: 2)
+ test(dict.remove("b", default: 3), 2)
+}
+
+#{
+ let dict = (a: 1, b: 2)
+ test(dict.remove("c", default: 3), 3)
+}
+
+--- dict-missing-lvalue ---
+// Missing lvalue is not automatically none-initialized.
+#{
+ let dict = (:)
+ // Error: 3-9 dictionary does not contain key "b"
+ // Hint: 3-9 use `insert` to add or update values
+ dict.b += 1
+}
+
+--- dict-basic-methods ---
+// Test dictionary methods.
+#let dict = (a: 3, c: 2, b: 1)
+#test("c" in dict, true)
+#test(dict.len(), 3)
+#test(dict.values(), (3, 2, 1))
+#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3c2b1")
+
+#dict.remove("c")
+#test("c" in dict, false)
+#test(dict, (a: 3, b: 1))
+
+--- dict-from-module ---
+// Test dictionary constructor
+#dictionary(sys).at("version")
+#dictionary(sys).at("no-crash", default: none)
+
+--- dict-remove-order ---
+// Test that removal keeps order.
+#let dict = (a: 1, b: 2, c: 3, d: 4)
+#dict.remove("b")
+#test(dict.keys(), ("a", "c", "d"))
+
+--- dict-temporary-lvalue ---
+// Error: 3-15 cannot mutate a temporary value
+#((key: "val").other = "some")
+
+--- dict-function-item-not-a-method ---
+#{
+ let dict = (
+ call-me: () => 1,
+ )
+ // Error: 8-15 type dictionary has no method `call-me`
+ // Hint: 8-15 to call the function stored in the dictionary, surround the field access with parentheses, e.g. `(dict.call-me)(..)`
+ dict.call-me()
+}
+
+--- dict-item-missing-method ---
+#{
+ let dict = (
+ nonfunc: 1
+ )
+
+ // Error: 8-15 type dictionary has no method `nonfunc`
+ // Hint: 8-15 did you mean to access the field `nonfunc`?
+ dict.nonfunc()
+}
+
+--- dict-dynamic-uplicate-key ---
+#let a = "hello"
+#let b = "world"
+#let c = "value"
+#let d = "conflict"
+
+#assert.eq(((a): b), ("hello": "world"))
+#assert.eq(((a): 1, (a): 2), ("hello": 2))
+#assert.eq((hello: 1, (a): 2), ("hello": 2))
+#assert.eq((a + b: c, (a + b): d, (a): "value2", a: "value3"), ("helloworld": "conflict", "hello": "value2", "a": "value3"))
+
+--- issue-1338-dictionary-underscore ---
+#let foo = "foo"
+#let bar = "bar"
+// Error: 8-9 expected expression, found underscore
+// Error: 16-17 expected expression, found underscore
+#(foo: _, bar: _)
+
+--- issue-1342-dictionary-bare-expressions ---
+// Error: 5-8 expected named or keyed pair, found identifier
+// Error: 10-13 expected named or keyed pair, found identifier
+#(: foo, bar)
+
+--- issue-3154-dict-at-not-contained ---
+#{
+ let dict = (a: 1)
+ // Error: 3-15 dictionary does not contain key "b" and no default value was specified
+ dict.at("b")
+}
+
+--- issue-3154-dict-at-missing-default ---
+#{
+ let dict = (a: 1)
+ test(dict.at("b", default: 0), 0)
+}
+
+--- issue-3154-dict-at-missing-mutable ---
+#{
+ let dict = (a: 1)
+ // Error: 3-15 dictionary does not contain key "b"
+ // Hint: 3-15 use `insert` to add or update values
+ dict.at("b") = 9
+}
+
+--- issue-3154-dict-at-missing-mutable-default ---
+#{
+ let dict = (a: 1)
+ // Error: 3-27 dictionary does not contain key "b"
+ // Hint: 3-27 use `insert` to add or update values
+ dict.at("b", default: 0) = 9
+}
+
+--- issue-3154-dict-syntax-missing ---
+#{
+ let dict = (a: 1)
+ // Error: 8-9 dictionary does not contain key "b"
+ dict.b
+}
+
+--- issue-3154-dict-syntax-missing-mutable ---
+#{
+ let dict = (a: 1)
+ dict.b = 9
+ test(dict, (a: 1, b: 9))
+}
+
+--- issue-3154-dict-syntax-missing-add-assign ---
+#{
+ let dict = (a: 1)
+ // Error: 3-9 dictionary does not contain key "b"
+ // Hint: 3-9 use `insert` to add or update values
+ dict.b += 9
+}
+
+--- issue-3232-dict-unexpected-keys-sides ---
+// Confusing "expected relative length or dictionary, found dictionary"
+// Error: 16-58 unexpected keys "unexpected" and "unexpected-too"
+#block(outset: (unexpected: 0.5em, unexpected-too: 0.2em), [Hi])
+
+--- issue-3232-dict-unexpected-keys-corners ---
+// Error: 14-56 unexpected keys "unexpected" and "unexpected-too"
+#box(radius: (unexpected: 0.5em, unexpected-too: 0.5em), [Hi])
+
+--- issue-3232-dict-unexpected-key-sides ---
+// Error: 16-49 unexpected key "unexpected", valid keys are "left", "top", "right", "bottom", "x", "y", and "rest"
+#block(outset: (unexpected: 0.2em, right: 0.5em), [Hi]) // The 1st key is unexpected
+
+--- issue-3232-dict-unexpected-key-corners ---
+// Error: 14-50 unexpected key "unexpected", valid keys are "top-left", "top-right", "bottom-right", "bottom-left", "left", "top", "right", "bottom", and "rest"
+#box(radius: (top-left: 0.5em, unexpected: 0.5em), [Hi]) // The 2nd key is unexpected
+
+--- issue-3232-dict-empty ---
+#block(outset: (:), [Hi]) // Ok
+#box(radius: (:), [Hi]) // Ok
diff --git a/tests/suite/foundations/duration.typ b/tests/suite/foundations/duration.typ
new file mode 100644
index 00000000..7e53f703
--- /dev/null
+++ b/tests/suite/foundations/duration.typ
@@ -0,0 +1,103 @@
+// Test durations.
+
+--- duration-negate ---
+// Test negating durations.
+#test(-duration(hours: 2), duration(hours: -2))
+
+--- duration-add-and-subtract ---
+// Test adding and subtracting durations.
+#test(duration(weeks: 1, hours: 1), duration(weeks: 1) + duration(hours: 1))
+#test(duration(weeks: 1, hours: -1), duration(weeks: 1) - duration(hours: 1))
+#test(duration(days: 6, hours: 23), duration(weeks: 1) - duration(hours: 1))
+
+--- duration-add-and-subtract-dates ---
+// Test adding and subtracting durations and dates.
+#let d = datetime(day: 1, month: 1, year: 2000)
+#let d2 = datetime(day: 1, month: 2, year: 2000)
+#test(d + duration(weeks: 2), datetime(day: 15, month: 1, year: 2000))
+#test(d + duration(days: 3), datetime(day: 4, month: 1, year: 2000))
+#test(d + duration(weeks: 1, days: 3), datetime(day: 11, month: 1, year: 2000))
+#test(d2 + duration(days: -1), datetime(day: 31, month: 1, year: 2000))
+#test(d2 + duration(days: -3), datetime(day: 29, month: 1, year: 2000))
+#test(d2 + duration(weeks: -1), datetime(day: 25, month: 1, year: 2000))
+#test(d + duration(days: -1), datetime(day: 31, month: 12, year: 1999))
+#test(d + duration(weeks: 1, days: -7), datetime(day: 1, month: 1, year: 2000))
+#test(d2 - duration(days: 1), datetime(day: 31, month: 1, year: 2000))
+#test(d2 - duration(days: 3), datetime(day: 29, month: 1, year: 2000))
+#test(d2 - duration(weeks: 1), datetime(day: 25, month: 1, year: 2000))
+#test(d - duration(days: 1), datetime(day: 31, month: 12, year: 1999))
+#test(datetime(day: 31, month: 1, year: 2000) + duration(days: 1), d2)
+#test(
+ datetime(day: 31, month: 12, year: 2000) + duration(days: 1),
+ datetime(day: 1, month: 1, year: 2001),
+)
+
+--- duration-add-and-subtract-times ---
+// Test adding and subtracting durations and times.
+#let a = datetime(hour: 12, minute: 0, second: 0)
+#test(a + duration(hours: 1, minutes: -60), datetime(hour: 12, minute: 0, second: 0))
+#test(a + duration(hours: 2), datetime(hour: 14, minute: 0, second: 0))
+#test(a + duration(minutes: 10), datetime(hour: 12, minute: 10, second: 0))
+#test(a + duration(seconds: 30), datetime(hour: 12, minute: 0, second: 30))
+#test(a + duration(hours: -2), datetime(hour: 10, minute: 0, second: 0))
+#test(a - duration(hours: 2), datetime(hour: 10, minute: 0, second: 0))
+#test(a + duration(minutes: -10), datetime(hour: 11, minute: 50, second: 0))
+#test(a - duration(minutes: 10), datetime(hour: 11, minute: 50, second: 0))
+#test(a + duration(seconds: -30), datetime(hour: 11, minute: 59, second: 30))
+#test(a - duration(seconds: 30), datetime(hour: 11, minute: 59, second: 30))
+#test(
+ a + duration(hours: 1, minutes: 13, seconds: 13),
+ datetime(hour: 13, minute: 13, second: 13),
+)
+
+--- duration-add-and-subtract-datetimes ---
+// Test adding and subtracting durations and datetimes.
+#test(
+ datetime(day: 1, month: 1, year: 2000, hour: 12, minute: 0, second: 0)
+ + duration(weeks: 1, days: 3, hours: -13, minutes: 10, seconds: -10 ),
+ datetime(day: 10, month: 1, year: 2000, hour: 23, minute: 9, second: 50),
+)
+#test(
+ datetime(day: 1, month: 1, year: 2000, hour: 12, minute: 0, second: 0)
+ + duration(weeks: 1, days: 3, minutes: 10)
+ - duration(hours: 13, seconds: 10),
+ datetime(day: 10, month: 1, year: 2000, hour: 23, minute: 9, second: 50),
+)
+
+--- duration-from-date-subtraction ---
+// Test subtracting dates.
+#let a = datetime(hour: 12, minute: 0, second: 0)
+#let b = datetime(day: 1, month: 1, year: 2000)
+#test(datetime(hour: 14, minute: 0, second: 0) - a, duration(hours: 2))
+#test(datetime(hour: 14, minute: 0, second: 0) - a, duration(minutes: 120))
+#test(datetime(hour: 13, minute: 0, second: 0) - a, duration(seconds: 3600))
+#test(datetime(day: 1, month: 2, year: 2000) - b, duration(days: 31))
+#test(datetime(day: 15, month: 1, year: 2000) - b, duration(weeks: 2))
+
+--- duration-multiply-with-number ---
+// Test multiplying and dividing durations with numbers.
+#test(duration(minutes: 10) * 6, duration(hours: 1))
+#test(duration(minutes: 10) * 2, duration(minutes: 20))
+#test(duration(minutes: 10) * 2.5, duration(minutes: 25))
+#test(duration(minutes: 10) / 2, duration(minutes: 5))
+#test(duration(minutes: 10) / 2.5, duration(minutes: 4))
+
+--- duration-divide ---
+// Test dividing durations with durations
+#test(duration(minutes: 20) / duration(hours: 1), 1 / 3)
+#test(duration(minutes: 20) / duration(minutes: 10), 2)
+#test(duration(minutes: 20) / duration(minutes: 8), 2.5)
+
+--- duration-compare ---
+// Test comparing durations
+#test(duration(minutes: 20) > duration(minutes: 10), true)
+#test(duration(minutes: 20) >= duration(minutes: 10), true)
+#test(duration(minutes: 10) < duration(minutes: 20), true)
+#test(duration(minutes: 10) <= duration(minutes: 20), true)
+#test(duration(minutes: 10) == duration(minutes: 10), true)
+#test(duration(minutes: 10) != duration(minutes: 20), true)
+#test(duration(minutes: 10) <= duration(minutes: 10), true)
+#test(duration(minutes: 10) >= duration(minutes: 10), true)
+#test(duration(minutes: 20) < duration(minutes: 10), false)
+#test(duration(minutes: 20) <= duration(minutes: 10), false)
+#test(duration(minutes: 20) == duration(minutes: 10), false)
diff --git a/tests/suite/foundations/eval.typ b/tests/suite/foundations/eval.typ
new file mode 100644
index 00000000..f85146b2
--- /dev/null
+++ b/tests/suite/foundations/eval.typ
@@ -0,0 +1,54 @@
+--- eval ---
+// Test the eval function.
+#test(eval("1 + 2"), 3)
+#test(eval("1 + x", scope: (x: 3)), 4)
+#test(eval("let x = x + 1; x + 1", scope: (x: 1)), 3)
+
+--- eval-mode ---
+// Test evaluation in other modes.
+#eval("[_Hello" + " World!_]") \
+#eval("_Hello" + " World!_", mode: "markup") \
+#eval("RR_1^NN", mode: "math", scope: (RR: math.NN, NN: math.RR))
+
+--- eval-syntax-error-1 ---
+// Error: 7-12 expected pattern
+#eval("let")
+
+--- eval-in-show-rule ---
+#show raw: it => text(font: "PT Sans", eval("[" + it.text + "]"))
+
+Interacting
+```
+#set text(blue)
+Blue #move(dy: -0.15em)[🌊]
+```
+
+--- eval-runtime-error ---
+// Error: 7-17 cannot continue outside of loop
+#eval("continue")
+
+--- eval-syntax-error-2 ---
+// Error: 7-12 expected semicolon or line break
+#eval("1 2")
+
+--- eval-path-resolve ---
+// Test absolute path.
+#eval("image(\"/assets/images/tiger.jpg\", width: 50%)")
+
+--- eval-path-resolve-in-show-rule ---
+#show raw: it => eval(it.text, mode: "markup")
+
+```
+#show emph: image("/assets/images/tiger.jpg", width: 50%)
+_Tiger!_
+```
+
+--- eval-path-resolve-relative ---
+// Test relative path.
+#test(eval(`"HELLO" in read("./eval.typ")`.text), true)
+
+--- issue-2055-math-eval ---
+// Evaluating a math expr should renders the same as an equation
+#eval(mode: "math", "f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)")
+
+$f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)$
diff --git a/tests/suite/foundations/float.typ b/tests/suite/foundations/float.typ
new file mode 100644
index 00000000..770533b9
--- /dev/null
+++ b/tests/suite/foundations/float.typ
@@ -0,0 +1,66 @@
+--- float-constructor ---
+#test(float(10), 10.0)
+#test(float(50% * 30%), 0.15)
+#test(float("31.4e-1"), 3.14)
+#test(float("31.4e\u{2212}1"), 3.14)
+#test(float("3.1415"), 3.1415)
+#test(float("-7654.321"), -7654.321)
+#test(float("\u{2212}7654.321"), -7654.321)
+#test(type(float(10)), float)
+
+--- float-constructor-bad-type ---
+// Error: 8-13 expected float, boolean, integer, ratio, or string, found type
+#float(float)
+
+--- float-constructor-bad-value ---
+// Error: 8-15 invalid float: 1.2.3
+#float("1.2.3")
+
+--- float-is-nan ---
+// Test float `is-nan()`.
+#test(float(calc.nan).is-nan(), true)
+#test(float(10).is-nan(), false)
+
+--- float-is-infinite ---
+// Test float `is-infinite()`.
+#test(float(calc.inf).is-infinite(), true)
+#test(float(-calc.inf).is-infinite(), true)
+#test(float(10).is-infinite(), false)
+#test(float(-10).is-infinite(), false)
+
+--- float-signum ---
+// Test float `signum()`
+#test(float(0.0).signum(), 1.0)
+#test(float(1.0).signum(), 1.0)
+#test(float(-1.0).signum(), -1.0)
+#test(float(10.0).signum(), 1.0)
+#test(float(-10.0).signum(), -1.0)
+#test(float(calc.nan).signum().is-nan(), true)
+
+--- float-repr ---
+// Test the `repr` function with floats.
+#repr(12.0) \
+#repr(3.14) \
+#repr(1234567890.0) \
+#repr(0123456789.0) \
+#repr(0.0) \
+#repr(-0.0) \
+#repr(-1.0) \
+#repr(-9876543210.0) \
+#repr(-0987654321.0) \
+#repr(-3.14) \
+#repr(4.0 - 8.0)
+
+--- float-display ---
+// Test floats.
+#12.0 \
+#3.14 \
+#1234567890.0 \
+#0123456789.0 \
+#0.0 \
+#(-0.0) \
+#(-1.0) \
+#(-9876543210.0) \
+#(-0987654321.0) \
+#(-3.14) \
+#(4.0 - 8.0)
diff --git a/tests/suite/foundations/int.typ b/tests/suite/foundations/int.typ
new file mode 100644
index 00000000..0c85dcab
--- /dev/null
+++ b/tests/suite/foundations/int.typ
@@ -0,0 +1,73 @@
+--- int-base-alternative ---
+// Test numbers with alternative bases.
+#test(0x10, 16)
+#test(0b1101, 13)
+#test(0xA + 0xa, 0x14)
+
+--- int-base-binary-invalid ---
+// Error: 2-7 invalid binary number: 0b123
+#0b123
+
+--- int-base-hex-invalid ---
+// Error: 2-8 invalid hexadecimal number: 0x123z
+#0x123z
+
+--- int-constructor ---
+// Test conversion to numbers.
+#test(int(false), 0)
+#test(int(true), 1)
+#test(int(10), 10)
+#test(int("150"), 150)
+#test(int("-834"), -834)
+#test(int("\u{2212}79"), -79)
+#test(int(10 / 3), 3)
+
+--- int-constructor-bad-type ---
+// Error: 6-10 expected integer, boolean, float, or string, found length
+#int(10pt)
+
+--- int-constructor-bad-value ---
+// Error: 6-12 invalid integer: nope
+#int("nope")
+
+--- int-signum ---
+// Test int `signum()`
+#test(int(0).signum(), 0)
+#test(int(1.0).signum(), 1)
+#test(int(-1.0).signum(), -1)
+#test(int(10.0).signum(), 1)
+#test(int(-10.0).signum(), -1)
+
+--- int-repr ---
+// Test the `repr` function with integers.
+#repr(12) \
+#repr(1234567890) \
+#repr(0123456789) \
+#repr(0) \
+#repr(-0) \
+#repr(-1) \
+#repr(-9876543210) \
+#repr(-0987654321) \
+#repr(4 - 8)
+
+--- int-display ---
+// Test integers.
+#12 \
+#1234567890 \
+#0123456789 \
+#0 \
+#(-0) \
+#(-1) \
+#(-9876543210) \
+#(-0987654321) \
+#(4 - 8)
+
+--- issue-int-constructor ---
+// Test that integer -> integer conversion doesn't do a roundtrip through float.
+#let x = 9223372036854775800
+#test(type(x), int)
+#test(int(x), x)
+
+--- number-invalid-suffix ---
+// Error: 2-4 invalid number suffix: u
+#1u
diff --git a/tests/suite/foundations/label.typ b/tests/suite/foundations/label.typ
new file mode 100644
index 00000000..2cde102c
--- /dev/null
+++ b/tests/suite/foundations/label.typ
@@ -0,0 +1,70 @@
+// Test labels.
+
+--- label-show-where-selector ---
+// Test labelled headings.
+#show heading: set text(10pt)
+#show heading.where(label: <intro>): underline
+
+= Introduction <intro>
+The beginning.
+
+= Conclusion
+The end.
+
+--- label-after-expression ---
+// Test label after expression.
+#show strong.where(label: <v>): set text(red)
+
+#let a = [*A*]
+#let b = [*B*]
+#a <v> #b
+
+--- label-on-text ---
+// Test labelled text.
+#show "t": it => {
+ set text(blue) if it.has("label") and it.label == <last>
+ it
+}
+
+This is a thing #[that <last>] happened.
+
+--- label-dynamic-show-set ---
+// Test abusing dynamic labels for styling.
+#show <red>: set text(red)
+#show <blue>: set text(blue)
+
+*A* *B* <red> *C* #label("bl" + "ue") *D*
+
+--- label-after-parbreak ---
+// Test that label ignores parbreak.
+#show <hide>: none
+
+_Hidden_
+<hide>
+
+_Hidden_
+
+<hide>
+_Visible_
+
+--- label-in-block ---
+// Test that label only works within one content block.
+#show <strike>: strike
+*This is* #[<strike>] *protected.*
+*This is not.* <strike>
+
+--- label-unclosed-is-text ---
+// Test that incomplete label is text.
+1 < 2 is #if 1 < 2 [not] a label.
+
+--- label-text-styled-and-sequence ---
+// Test label on text, styled, and sequence.
+#test([Hello<hi>].label, <hi>)
+#test([#[A *B* C]<hi>].label, <hi>)
+#test([#text(red)[Hello]<hi>].label, <hi>)
+
+--- label-string-conversion ---
+// Test getting the name of a label.
+#test(str(<hey>), "hey")
+#test(str(label("hey")), "hey")
+#test(str([Hmm<hey>].label), "hey")
diff --git a/tests/suite/foundations/panic.typ b/tests/suite/foundations/panic.typ
new file mode 100644
index 00000000..5d9d4046
--- /dev/null
+++ b/tests/suite/foundations/panic.typ
@@ -0,0 +1,14 @@
+--- panic ---
+// Test panic.
+// Error: 2-9 panicked
+#panic()
+
+--- panic-with-int ---
+// Test panic.
+// Error: 2-12 panicked with: 123
+#panic(123)
+
+--- panic-with-str ---
+// Test panic.
+// Error: 2-24 panicked with: "this is wrong"
+#panic("this is wrong")
diff --git a/tests/suite/foundations/plugin.typ b/tests/suite/foundations/plugin.typ
new file mode 100644
index 00000000..0842980e
--- /dev/null
+++ b/tests/suite/foundations/plugin.typ
@@ -0,0 +1,47 @@
+// Test WebAssembly plugins.
+
+--- plugin-basic ---
+#let p = plugin("/assets/plugins/hello.wasm")
+#test(p.hello(), bytes("Hello from wasm!!!"))
+#test(p.double_it(bytes("hey!")), bytes("hey!.hey!"))
+#test(
+ p.shuffle(bytes("value1"), bytes("value2"), bytes("value3")),
+ bytes("value3-value1-value2"),
+)
+
+--- plugin-wrong-number-of-arguments ---
+#let p = plugin("/assets/plugins/hello.wasm")
+
+// Error: 2-20 plugin function takes 0 arguments, but 1 was given
+#p.hello(bytes(""))
+
+--- plugin-wrong-argument-type ---
+#let p = plugin("/assets/plugins/hello.wasm")
+
+// Error: 10-14 expected bytes, found boolean
+// Error: 27-29 expected bytes, found integer
+#p.hello(true, bytes(()), 10)
+
+--- plugin-error ---
+#let p = plugin("/assets/plugins/hello.wasm")
+
+// Error: 2-17 plugin errored with: This is an `Err`
+#p.returns_err()
+
+--- plugin-panic ---
+#let p = plugin("/assets/plugins/hello.wasm")
+
+// Error: 2-16 plugin panicked: wasm `unreachable` instruction executed
+#p.will_panic()
+
+--- plugin-out-of-bounds-read ---
+#let p = plugin("/assets/plugins/plugin-oob.wasm")
+
+// Error: 2-14 plugin tried to read out of bounds: pointer 0x40000000 is out of bounds for read of length 1
+#p.read_oob()
+
+--- plugin-out-of-bounds-write ---
+#let p = plugin("/assets/plugins/plugin-oob.wasm")
+
+// Error: 2-27 plugin tried to write out of bounds: pointer 0x40000000 is out of bounds for write of length 3
+#p.write_oob(bytes("xyz"))
diff --git a/tests/suite/foundations/repr.typ b/tests/suite/foundations/repr.typ
new file mode 100644
index 00000000..7f03209b
--- /dev/null
+++ b/tests/suite/foundations/repr.typ
@@ -0,0 +1,57 @@
+--- repr ---
+#test(repr(ltr), "ltr")
+#test(repr((1, 2, false, )), "(1, 2, false)")
+
+--- repr-literals ---
+// Literal values.
+#auto \
+#none (empty) \
+#true \
+#false
+
+--- repr-numerical ---
+// 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) \
+#(100% + (2em + 2pt)) \
+#(100% + 0pt) \
+#(100% - 2em + 2pt) \
+#(100% - 2pt) \
+#2.3fr
+
+--- repr-misc ---
+// 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(lang: "typc", repr[*Hey*]) \
+#raw(lang: "typc", repr[A _sequence_]) \
+#raw(lang: "typc", repr[A _longer_ *sequence*!])
+
+// Functions.
+#let f(x) = x
+#f \
+#rect \
+#(() => none)
+
+// Types.
+#int \
+#type("hi") \
+#type((a: 1))
diff --git a/tests/suite/foundations/str.typ b/tests/suite/foundations/str.typ
new file mode 100644
index 00000000..025ec53d
--- /dev/null
+++ b/tests/suite/foundations/str.typ
@@ -0,0 +1,315 @@
+// Test the string methods.
+
+--- str-constructor ---
+// Test conversion to string.
+#test(str(123), "123")
+#test(str(123, base: 3), "11120")
+#test(str(-123, base: 16), "βˆ’7b")
+#test(str(9223372036854775807, base: 36), "1y2p0ij32e8e7")
+#test(str(50.14), "50.14")
+#test(str(10 / 3).len() > 10, true)
+
+--- str-from-float ---
+// Test the `str` function with floats.
+#test(str(12.0), "12")
+#test(str(3.14), "3.14")
+#test(str(1234567890.0), "1234567890")
+#test(str(0123456789.0), "123456789")
+#test(str(0.0), "0")
+#test(str(-0.0), "0")
+#test(str(-1.0), "βˆ’1")
+#test(str(-9876543210.0), "βˆ’9876543210")
+#test(str(-0987654321.0), "βˆ’987654321")
+#test(str(-3.14), "βˆ’3.14")
+#test(str(4.0 - 8.0), "βˆ’4")
+
+--- str-from-int ---
+// Test the `str` function with integers.
+#test(str(12), "12")
+#test(str(1234567890), "1234567890")
+#test(str(0123456789), "123456789")
+#test(str(0), "0")
+#test(str(-0), "0")
+#test(str(-1), "βˆ’1")
+#test(str(-9876543210), "βˆ’9876543210")
+#test(str(-0987654321), "βˆ’987654321")
+#test(str(4 - 8), "βˆ’4")
+
+--- str-constructor-bad-type ---
+// Error: 6-8 expected integer, float, version, bytes, label, type, or string, found content
+#str([])
+
+--- str-constructor-bad-base ---
+// Error: 17-19 base must be between 2 and 36
+#str(123, base: 99)
+
+--- str-constructor-unsupported-base ---
+// Error: 18-19 base is only supported for integers
+#str(1.23, base: 2)
+
+--- str-from-and-to-unicode ---
+// Test the unicode function.
+#test(str.from-unicode(97), "a")
+#test(str.to-unicode("a"), 97)
+
+--- str-from-unicode-bad-type ---
+// Error: 19-22 expected integer, found content
+#str.from-unicode([a])
+
+--- str-to-unicode-bad-type ---
+// Error: 17-21 expected exactly one character
+#str.to-unicode("ab")
+
+--- str-from-unicode-negative ---
+// Error: 19-21 number must be at least zero
+#str.from-unicode(-1)
+
+--- str-from-unicode-bad-value ---
+// Error: 2-28 0x110000 is not a valid codepoint
+#str.from-unicode(0x110000) // 0x10ffff is the highest valid code point
+
+--- string-len ---
+// Test the `len` method.
+#test("Hello World!".len(), 12)
+
+--- string-first-and-last ---
+// Test the `first` and `last` methods.
+#test("Hello".first(), "H")
+#test("Hello".last(), "o")
+#test("πŸ³οΈβ€πŸŒˆAπŸ³οΈβ€βš§οΈ".first(), "πŸ³οΈβ€πŸŒˆ")
+#test("πŸ³οΈβ€πŸŒˆAπŸ³οΈβ€βš§οΈ".last(), "πŸ³οΈβ€βš§οΈ")
+
+--- string-first-empty ---
+// Error: 2-12 string is empty
+#"".first()
+
+--- string-last-empty ---
+// Error: 2-11 string is empty
+#"".last()
+
+--- string-at ---
+// Test the `at` method.
+#test("Hello".at(1), "e")
+#test("Hello".at(4), "o")
+#test("Hello".at(-1), "o")
+#test("Hello".at(-2), "l")
+#test("Hey: πŸ³οΈβ€πŸŒˆ there!".at(5), "πŸ³οΈβ€πŸŒˆ")
+
+--- string-at-default ---
+// Test `at`'s 'default' parameter.
+#test("z", "Hello".at(5, default: "z"))
+
+--- string-at-not-a-char-boundary ---
+// Error: 2-14 string index 2 is not a character boundary
+#"πŸ³οΈβ€πŸŒˆ".at(2)
+
+--- string-at-out-of-bounds ---
+// Error: 2-15 no default value was specified and string index out of bounds (index: 5, len: 5)
+#"Hello".at(5)
+
+--- string-at-at-default-other-type ---
+#test("Hello".at(5, default: (a: 10)), (a: 10))
+
+--- string-slice ---
+// 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")
+
+--- string-slice-not-a-char-boundary ---
+// Error: 2-21 string index -1 is not a character boundary
+#"πŸ³οΈβ€πŸŒˆ".slice(0, -1)
+
+--- string-clusters ---
+// Test the `clusters` and `codepoints` methods.
+#test("abc".clusters(), ("a", "b", "c"))
+#test("abc".clusters(), ("a", "b", "c"))
+#test("πŸ³οΈβ€πŸŒˆ!".clusters(), ("πŸ³οΈβ€πŸŒˆ", "!"))
+
+--- string-codepoints ---
+#test("πŸ³οΈβ€πŸŒˆ!".codepoints(), ("🏳", "\u{fe0f}", "\u{200d}", "🌈", "!"))
+
+--- string-contains ---
+// 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)
+
+--- string-starts-with ---
+// 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)
+
+--- string-ends-with ---
+#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("typst13".ends-with(regex("1[0-9]")), true)
+#test("typst113".ends-with(regex("1[0-9]")), true)
+#test("typst23".ends-with(regex("1[0-9]")), false)
+
+--- string-find-and-position ---
+// 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)
+
+--- string-match ---
+// 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: ()),
+)
+
+--- string-matches ---
+// 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.at(0)) + int(caps.at(1))
+ }
+ str(int(time / 60)) + ":" + str(calc.rem(time, 60))
+}
+
+#test(timesum(""), "0:0")
+#test(timesum("2:70"), "3:10")
+#test(timesum("1:20, 2:10, 0:40"), "4:10")
+
+--- stgring-replace ---
+// Test the `replace` method with `Str` replacements.
+#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__")
+
+--- string-replace-function ---
+// Test the `replace` method with `Func` replacements.
+
+#test("abc".replace(regex("[a-z]"), m => {
+ str(m.start) + m.text + str(m.end)
+}), "0a11b22c3")
+#test("abcd, efgh".replace(regex("\w+"), m => {
+ upper(m.text)
+}), "ABCD, EFGH")
+#test("hello : world".replace(regex("^(.+)\s*(:)\s*(.+)$"), m => {
+ upper(m.captures.at(0)) + m.captures.at(1) + " " + upper(m.captures.at(2))
+}), "HELLO : WORLD")
+#test("hello world, lorem ipsum".replace(regex("(\w+) (\w+)"), m => {
+ m.captures.at(1) + " " + m.captures.at(0)
+}), "world hello, ipsum lorem")
+#test("hello world, lorem ipsum".replace(regex("(\w+) (\w+)"), count: 1, m => {
+ m.captures.at(1) + " " + m.captures.at(0)
+}), "world hello, lorem ipsum")
+#test("123 456".replace(regex("[a-z]+"), "a"), "123 456")
+
+#test("abc".replace("", m => "-"), "-a-b-c-")
+#test("abc".replace("", m => "-", count: 1), "-abc")
+#test("123".replace("abc", m => ""), "123")
+#test("123".replace("abc", m => "", count: 2), "123")
+#test("a123b123c".replace("123", m => {
+ str(m.start) + "-" + str(m.end)
+}), "a1-4b5-8c")
+#test("halla warld".replace("a", m => {
+ if m.start == 1 { "e" }
+ else if m.start == 4 or m.start == 7 { "o" }
+}), "hello world")
+#test("aaa".replace("a", m => str(m.captures.len())), "000")
+
+--- string-replace-function-bad-type ---
+// Error: 23-24 expected string, found integer
+#"123".replace("123", m => 1)
+
+--- string-replace-bad-type ---
+// Error: 23-32 expected string or function, found array
+#"123".replace("123", (1, 2, 3))
+
+--- string-trim-basic ---
+// Test the `trim` method; the pattern is not provided.
+#let str = "Typst, LaTeX, Word, InDesign"
+#let array = ("Typst", "LaTeX", "Word", "InDesign")
+#test(str.split(",").map(s => s.trim()), array)
+#test("".trim(), "")
+#test(" ".trim(), "")
+#test("\t".trim(), "")
+#test("\n".trim(), "")
+#test("\t \n".trim(), "")
+#test(" abc ".trim(at: start), "abc ")
+#test("\tabc ".trim(at: start), "abc ")
+#test("abc\n".trim(at: end), "abc")
+#test(" abc ".trim(at: end, repeat: true), " abc")
+#test(" abc".trim(at: start, repeat: false), "abc")
+
+--- string-trim-pattern-str ---
+// Test the `trim` method; the pattern is a string.
+#test("aabcaa".trim("a", repeat: false), "abca")
+#test("aabca".trim("a", at: start), "bca")
+#test("aabcaa".trim("a", at: end, repeat: false), "aabca")
+#test(" abc\n".trim("\n"), " abc")
+#test("whole".trim("whole", at: start), "")
+
+--- string-trim-pattern-regex ---
+// Test the `trim` method; the pattern is a regex.
+#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(".")), "")
+#test("12306".trim(regex("\d"), at: start), "")
+#test("12306abc".trim(regex("\d"), at: start), "abc")
+#test("whole".trim(regex("whole"), at: start), "")
+#test("12306".trim(regex("\d"), at: end), "")
+#test("abc12306".trim(regex("\d"), at: end), "abc")
+#test("whole".trim(regex("whole"), at: end), "")
+
+--- string-trim-at-bad-alignment ---
+// Error: 17-21 expected either `start` or `end`
+#"abc".trim(at: left)
+
+--- string-split ---
+// 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"))
+
+--- string-rev ---
+// Test the `rev` method.
+#test("abc".rev(), "cba")
+#test("axΜ‚e".rev(), "exΜ‚a")
+
+--- string-unclosed ---
+// Error: 2-2:1 unclosed string
+#"hello\"
diff --git a/tests/suite/foundations/type.typ b/tests/suite/foundations/type.typ
new file mode 100644
index 00000000..f2a98845
--- /dev/null
+++ b/tests/suite/foundations/type.typ
@@ -0,0 +1,25 @@
+--- type ---
+#test(type(1), int)
+#test(type(ltr), direction)
+#test(type(10 / 3), float)
+
+--- type-string-compatibility ---
+#test(type(10), int)
+#test(type(10), "integer")
+#test("is " + type(10), "is integer")
+#test(int in ("integer", "string"), true)
+#test(int in "integers or strings", true)
+#test(str in "integers or strings", true)
+
+--- issue-3110-type-constructor ---
+// Let the error message report the type name.
+// Error: 2-9 type content does not have a constructor
+#content()
+
+--- issue-3110-associated-field ---
+// Error: 6-12 type integer does not contain field `MAXVAL`
+#int.MAXVAL
+
+--- issue-3110-associated-function ---
+// Error: 6-18 type string does not contain field `from-unΓ―code`
+#str.from-unΓ―code(97)
diff --git a/tests/suite/foundations/version.typ b/tests/suite/foundations/version.typ
new file mode 100644
index 00000000..bf2cadb1
--- /dev/null
+++ b/tests/suite/foundations/version.typ
@@ -0,0 +1,47 @@
+// Test versions.
+
+--- version-constructor ---
+// Test version constructor.
+
+// Empty.
+#version()
+
+// Plain.
+#test(version(1, 2).major, 1)
+
+// Single Array argument.
+#test(version((1, 2)).minor, 2)
+
+// Mixed arguments.
+#test(version(1, (2, 3), 4, (5, 6), 7).at(5), 6)
+
+--- version-equality ---
+// Test equality of different-length versions
+#test(version(), version(0))
+#test(version(0), version(0, 0))
+#test(version(1, 2), version(1, 2, 0, 0, 0, 0))
+
+--- version-at ---
+// Test `version.at`.
+
+// Non-negative index in bounds
+#test(version(1, 2).at(1), 2)
+
+// Non-negative index out of bounds
+#test(version(1, 2).at(4), 0)
+
+// Negative index in bounds
+#test(version(1, 2).at(-2), 1)
+
+// Error: 2-22 component index out of bounds (index: -3, len: 2)
+#version(1, 2).at(-3)
+
+--- version-fields ---
+// Test version fields.
+#test(version(1, 2, 3).major, 1)
+#test(version(1, 2, 3).minor, 2)
+#test(version(1, 2, 3).patch, 3)
+
+--- version-type ---
+// Test the type of `sys.version`
+#test(type(sys.version), version)