aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-25 15:14:08 +0100
committereunhhu <[email protected]>2026-09-25 15:14:08 +0100
commitc298c2eec0b7ec0fcdacb8f661a9da3c184d2960 (patch)
treeaffe490db2b070f0b064bbb9bf5ade49d45a9293 /tests
parent91aa0afcd34cff345f1f7293d82fec7d00082d8b (diff)
downloadaup-c298c2eec0b7ec0fcdacb8f661a9da3c184d2960.tar.gz
aup-c298c2eec0b7ec0fcdacb8f661a9da3c184d2960.zip
Add fns and libraries
fn name(params) = value declares one expression, inlined where it is called: arguments are computed once onto the stack and the body reads them with PICK, so a fn costs no runtime call and keeps Pure's bound on work. Parameters take any kind of value; recursion is a compile error. use name reads name.aup beside the source or from AUP_PATH. A library declares only fns and uses, sees only its parameters, and is called as name.fn(...). Diagnostics now carry the file they are in, and the compiler takes a loader so that sources come from the host. examples/words.aup is such a library; examples/tasks.aup uses it and still matches its reference model.
Diffstat (limited to 'tests')
-rw-r--r--tests/check.py67
1 files changed, 66 insertions, 1 deletions
diff --git a/tests/check.py b/tests/check.py
index 73b7572..90124f2 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -146,6 +146,17 @@ DIAGNOSTICS = [
(LIST + 'view t = each at in xs {\n a = 1\n}\n', '6:15', "'at' is already declared as a list index"),
(LIST + 'view t = each x in xs {\n c = x\n}\n', '7:7', 'a column is a number or text, not an index'),
('view each = 1\n', '1:6', "'each' is reserved"),
+ # fns
+ ('fn f(x) = x\nview v = f(1, 2)\n', '2:10', 'f expects 1 argument'),
+ ('fn f(x) = f(x)\nview v = f(1)\n', '1:11', "fn 'f' calls itself (in fn 'f' called at 2:10)"),
+ ('fn f(x, x) = 1\n', '1:9', "duplicate parameter 'x'"),
+ ('view v = g(1)\n', '1:10', "unknown fn 'g'"),
+ ('fn f(x) = x + "a"\nview v = f(1)\n', '1:15', "expected a number, not text (in fn 'f' called at 2:10)"),
+ ('fn text(x) = x\n', '1:4', "'text' is a built-in conversion"),
+ ('state f = 0\nfn f = 1\n', '2:4', "'f' is already declared as a state"),
+ ('state n = 0\nview v = n(1)\n', '2:10', "'n' is a state, not a fn"),
+ ('use missing\n', '1:5', "cannot find library 'missing' (missing.aup)"),
+ ('view v = shapes.area(1, 2)\n', '1:17', "unknown library 'shapes'; write use shapes first"),
]
ACCEPTED = [
@@ -183,6 +194,12 @@ ACCEPTED = [
'view grid = each cell in cells {\n label = cell.label\n heavy = cell.weight > 2\n'
' here = if cell.position == at.position { ">" } else { " " }\n}\n'
'view other = each cell in cells { weight = cell.weight }\n',
+ # fns: parameters of any kind, constants, calls inside fns, tables and conditionals
+ 'record T {\n a = 0\n s: text\n}\nstate xs: list of T\nstate at: index of xs\nstate note: text\n'
+ 'state c: cursor in note\nfn tag(i) = "{i.s}#{i.a}"\nfn limit = 3\nfn clip(x) = min(x, limit)\n'
+ 'fn later(cursor, n) = cursor + n char\ninput k = key KeyK\n'
+ 'on k.down {\n xs.append(T { a: clip(xs.count), s: "q" })\n c = later(c, 2)\n}\n'
+ 'view board = each r in xs {\n tag = tag(r)\n}\nview cur = if xs.count > 0 { tag(at) } else { "-" }\n',
]
@@ -499,7 +516,8 @@ class TasksModel:
{'count': len(self.tasks), 'position': self.at,
'current': item['title'] if item else '(empty)',
'status': f'{self.at + 1}/{len(self.tasks)}' if item else 'empty',
- 'finished': item and item['done'], 'board': board})
+ 'finished': item and item['done'],
+ 'summary': f"{len(self.tasks)} {'task' if len(self.tasks) == 1 else 'tasks'}", 'board': board})
def tasks_parity(aup, pure_run, work, seeds, steps=400):
@@ -544,6 +562,52 @@ def tasks_parity(aup, pure_run, work, seeds, steps=400):
return compared, failed
+def libraries(aup, pure_run, work):
+ """fn calls compute each argument once, libraries are found beside the
+ source or on AUP_PATH, a library sees only its parameters, and errors in a
+ library name its file."""
+ lib, other = work / 'libs', work / 'libs' / 'far'
+ other.mkdir(parents=True, exist_ok=True)
+ (lib / 'shapes.aup').write_text('fn area(w, h) = w * h\nfn square(s) = area(s, s)\nfn unit = 1\n')
+ (lib / 'loopa.aup').write_text('use loopb\nfn a = 1\n')
+ (lib / 'loopb.aup').write_text('use loopa\nfn b = 2\n')
+ (lib / 'stateful.aup').write_text('fn f = 1\nstate x = 0\n')
+ (lib / 'peek.aup').write_text('fn f(a) = a + x\n')
+ (other / 'far.aup').write_text('fn twice(x) = x * 2\n')
+ env = {'AUP_PATH': f'{lib / "missing"}:{other}', 'PATH': '/usr/bin:/bin'}
+
+ def build(source):
+ path = lib / 'main.aup'
+ path.write_text(source)
+ return subprocess.run([str(aup), 'build', str(path), '-o', str(lib / 'main.pmod')], capture_output=True,
+ text=True, env=env, timeout=60)
+
+ good = [
+ ('use shapes\nuse far\nstate n = 3\nview a = shapes.square(n)\nview b = shapes.area(2, n + 1) + shapes.unit\n'
+ 'view c = far.twice(n)\n', {'a': 9, 'b': 9, 'c': 6}),
+ ('fn sq(x) = x * x\nstate n = 3\nview v = sq(n + 1)\nview t = sq(sq(2))\n', {'v': 16, 't': 16}),
+ ('fn first(a, b) = a\nview v = first(1, 1 / 0)\n', {'v': None}), # arguments are always computed
+ ]
+ for source, views in good:
+ result = build(source)
+ assert result.returncode == 0, (source, result.stderr)
+ first = (lib / 'main.pmod').read_bytes()
+ assert build(source).returncode == 0 and (lib / 'main.pmod').read_bytes() == first, source
+ shown = json.loads(run(pure_run, lib / 'main.pmod', stdin='show\n').stdout)
+ assert shown['views'] == views, (source, shown['views'])
+ bad = [
+ ('use loopa\n', 'loopb.aup:1:5', "library 'loopa' uses itself through its uses"),
+ ('use stateful\n', 'stateful.aup:2:1', 'a library declares only fn and use'),
+ ('use peek\nstate x = 1\nview v = peek.f(1)\n', 'peek.aup:1:15',
+ "'x' is not a parameter; a library fn reads only its parameters (in fn 'f' called at 3:15)"),
+ ('use shapes\nview v = shapes.volume(1)\n', 'main.aup:2:17', "library 'shapes' has no fn 'volume'"),
+ ]
+ for source, where, message in bad:
+ result = build(source)
+ assert result.returncode == 1 and f'{where}: {message}' in result.stderr, (source, result.stderr)
+ return len(good), len(bad)
+
+
def conditionals(aup, pure_run, work):
"""Only the chosen branch runs (10 / x is never computed at x = 0), else-if
chains pick the first true arm, and conditionals nest inside arithmetic."""
@@ -672,6 +736,7 @@ def main():
report['list_model_snapshots'], report['list_model_failed_events'] = \
tasks_parity(aup, pure_run, work, range(args.text_seeds))
report['conditional_snapshots'] = conditionals(aup, pure_run, work)
+ report['library_programs'], report['library_diagnostics'] = libraries(aup, pure_run, work)
report['editor_parity_operations'], report['editor_parity_outcomes'] = \
editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds))
print(json.dumps(report))