Skip to content

Commit 9919fa8

Browse files
hyperpolymathclaude
andcommitted
feat(levels): L4-L10 per-level test suites (Track C complete)
Extends the per-level node.mjs test pattern (established for L1-L3) across the full 10-level checked core. Each level file imports parseModule + checkModule and asserts a mix of clean-check and diagnostic-fire outcomes on small inline .twasm fixtures. Coverage: L4 Null safety (opt<T>, &region<X>, is_null guards) 6 cases L5 Bounds-proof (indexed access, where-constraints) 5 cases L6 Result-type (field-path resolution, striated ban) 6 cases L7 Aliasing safety (QTT-erased; surface carrier only) 5 cases L8 Effect-tracking (flat, split, region-qualified) 7 cases L9 Lifetime safety (QTT-erased; surface carrier only) 5 cases L10 Linearity (QTT-erased; surface carrier only) 5 cases L7 / L9 / L10 have no ReScript diagnostic surface — enforcement is Idris2-side and QTT-erased. Those files assert the canonical surface shapes parse + check clean and point at the relevant Idris2 proof file (Pointer.idr / Lifetime.idr / Linear.idr), rather than fabricate diagnostics that do not actually fire. Justfile test recipe extended to run L4-L10 after L1-L3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c70d281 commit 9919fa8

8 files changed

Lines changed: 1223 additions & 1 deletion

File tree

Justfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,17 @@ test *args:
106106
@echo "Running E2E driver (parse + check every example)..."
107107
node tests/e2e/e2e-driver.mjs
108108
@echo ""
109-
@echo "Running per-level test suites (L1-L3)..."
109+
@echo "Running per-level test suites (L1-L10)..."
110110
node tests/levels/L1.mjs
111111
node tests/levels/L2.mjs
112112
node tests/levels/L3.mjs
113+
node tests/levels/L4.mjs
114+
node tests/levels/L5.mjs
115+
node tests/levels/L6.mjs
116+
node tests/levels/L7.mjs
117+
node tests/levels/L8.mjs
118+
node tests/levels/L9.mjs
119+
node tests/levels/L10.mjs
113120
@echo ""
114121
@echo "Running Zig FFI tests..."
115122
cd ffi/zig && zig build test

tests/levels/L10.mjs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// Level 10 — Linearity (exactly-once resource usage, QTT-erased)
5+
//
6+
// L10 attests that every linearly-typed resource — an `own region<X>`
7+
// handle, a `unique<T>` owning pointer — is consumed EXACTLY ONCE
8+
// along every execution path. Dropping it is forbidden (would leak),
9+
// and consuming it twice is forbidden (double-free). The Idris2 proof
10+
// layer (`src/abi/TypedWasm/ABI/Linear.idr`) carries the QTT-native
11+
// q=1 quantity reasoning plus the propositional state-machine layer
12+
// added A3 (2026-04-18): `LinHandleU Fresh/Consumed tok`, the
13+
// `consume` state transition, and the theorems `distinctUsage`,
14+
// `consumePreservesData`, `noReuse`, `noReuseEcho`.
15+
//
16+
// L10 is the third of the three QTT-erased levels (L7 + L9 + L10).
17+
// As with those, the ReScript checker has NO diagnostic that fires
18+
// on a "handle consumed twice" or "handle dropped without free"
19+
// program text today: the analysis is Idris2-side, lifted by QTT,
20+
// and entirely erased before code generation. Asserting such a
21+
// diagnostic here would give false confidence.
22+
//
23+
// What this file's role IS:
24+
// - Certify that the canonical L10 surface shapes — `own region<X>`
25+
// round-trip through a function pair (spawn → despawn), with the
26+
// handle exactly-once-threaded — parse + check clean.
27+
// - Exercise the `region.alloc` / `region.free` statement pair, the
28+
// two sides of the linearity contract.
29+
//
30+
// Run: node tests/levels/L10.mjs
31+
32+
import { parseModule } from "../../src/parser/Parser.mjs";
33+
import { checkModule } from "../../src/parser/Checker.mjs";
34+
35+
let passed = 0;
36+
let failed = 0;
37+
38+
function run(source, expect, label) {
39+
const pr = parseModule(source);
40+
if (pr.TAG !== "Ok") {
41+
failed++;
42+
console.log(` FAIL ${label} — parse error: ${pr._0.message}`);
43+
return;
44+
}
45+
const diags = checkModule(pr._0);
46+
if (expect.kind === "clean") {
47+
if (diags.length === 0) {
48+
passed++;
49+
console.log(` OK ${label} — clean check`);
50+
} else {
51+
failed++;
52+
const msgs = diags.map(d => d.message).join("; ");
53+
console.log(` FAIL ${label} — expected clean, got ${diags.length}: ${msgs}`);
54+
}
55+
} else {
56+
const allMsgs = diags.map(d => d.message).join(" | ");
57+
const missing = expect.needles.filter(n => !allMsgs.includes(n));
58+
if (missing.length === 0 && diags.length > 0) {
59+
passed++;
60+
console.log(` OK ${label} — diagnostic fired as expected`);
61+
} else {
62+
failed++;
63+
console.log(` FAIL ${label} — expected needles ${expect.needles.join(", ")}, got "${allMsgs}"`);
64+
}
65+
}
66+
}
67+
68+
console.log("=== Level 10: Linearity (exactly-once, QTT-erased) ===\n");
69+
70+
// ── Positive: region.alloc → owning handle → region.free sequence ────
71+
// Classic L10 shape: the handle is created, threaded, consumed.
72+
// At proof-time, QTT q=1 discharges the exactly-once obligation.
73+
74+
run(
75+
`region Particle {
76+
hp: i32;
77+
}
78+
fn spawn() -> own region<Particle>
79+
effects { Alloc }
80+
{
81+
region.alloc Particle { hp = 100 } -> p;
82+
return p;
83+
}
84+
fn despawn(p: own region<Particle>)
85+
effects { Free }
86+
{
87+
region.free $p;
88+
}`,
89+
{ kind: "clean" },
90+
"spawn/despawn pair — canonical linear handle thread",
91+
);
92+
93+
// ── Positive: unique<T> field — owning-once pointer inside a region ──
94+
// `unique<ptr<Inner>>` carries the same q=1 discipline for non-region
95+
// owning pointers.
96+
97+
run(
98+
`region Inner {
99+
v: i32;
100+
}
101+
region Outer {
102+
owned: unique<ptr<Inner>>;
103+
}`,
104+
{ kind: "clean" },
105+
"unique<ptr<Inner>> field — owning-once pointer",
106+
);
107+
108+
// ── Positive: own handle passed to a function (consumed) ─────────────
109+
// A function can take an owning handle as a parameter — the callee
110+
// is obliged to consume it (free, forward, or store). Surface carrier.
111+
112+
run(
113+
`region Resource {
114+
id: i32;
115+
}
116+
fn make() -> own region<Resource>
117+
effects { Alloc }
118+
{
119+
region.alloc Resource { id = 1 } -> r;
120+
return r;
121+
}
122+
fn release(r: own region<Resource>)
123+
effects { Free }
124+
{
125+
region.free $r;
126+
}
127+
fn pipeline()
128+
effects { Alloc, Free }
129+
{
130+
let r : own region<Resource> = make();
131+
release(r);
132+
}`,
133+
{ kind: "clean" },
134+
"pipeline: alloc-in-callee, consume-in-callee",
135+
);
136+
137+
// ── Positive: alloc with field init block + multiple typed fields ────
138+
// Exercises the `region.alloc T { f = v, g = w } -> handle;` shape.
139+
140+
run(
141+
`region Bundle {
142+
a: i32;
143+
b: f32;
144+
c: bool;
145+
}
146+
fn mint() -> own region<Bundle>
147+
effects { Alloc }
148+
{
149+
region.alloc Bundle { a = 1, b = 2.0, c = true } -> b;
150+
return b;
151+
}`,
152+
{ kind: "clean" },
153+
"region.alloc with multi-field init block",
154+
);
155+
156+
// ── Positive: linearly-typed free-list self-reference ────────────────
157+
// A linked free list: each slot's `next` is a linearly owned pointer
158+
// to the next free slot. Classic linear-types arena shape.
159+
160+
run(
161+
`region FreeSlot {
162+
next: opt<ptr<FreeSlot>>;
163+
}
164+
region Arena[256] {
165+
free_head: opt<ptr<FreeSlot>>;
166+
}`,
167+
{ kind: "clean" },
168+
"linear free-list arena (linearly-typed self-reference)",
169+
);
170+
171+
// ── Summary ──────────────────────────────────────────────────────────
172+
173+
console.log(`\n--- L10: ${passed} passed, ${failed} failed ---`);
174+
process.exit(failed > 0 ? 1 : 0);

tests/levels/L4.mjs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// Level 4 — Null safety (opt<T> tracking)
5+
//
6+
// L4 enforces that nullable pointers (`opt<T>`, `opt<@R>`, `opt<ptr<T>>`)
7+
// are distinct from non-nullable forms in the type system: a function
8+
// taking `&region<X>` receives a witness that the handle is non-null,
9+
// while `opt<...>` fields in a region force an explicit `is_null` check
10+
// before use.
11+
//
12+
// Surface exposure in the ReScript checker: L4 is almost entirely
13+
// structural. The AST distinguishes `OptionalType(...)` from bare
14+
// `PointerType(...)` / `RegionRef(...)` at parse time; no ReScript
15+
// diagnostic fires on a "forgot to null-check" pattern today (that
16+
// analysis lives in the Idris2 `Pointer.idr` / `Levels.idr` Ptr-NonNull
17+
// witness). What IS testable at this layer is:
18+
//
19+
// - `opt<T>` fields parse + check clean (positive: nullability is a
20+
// first-class type constructor).
21+
// - `&region<X>` and `&mut region<X>` parameter syntax parses + checks
22+
// clean (positive: non-null handle types are well-formed).
23+
// - Nested nullable forms — `opt<ptr<T>>`, `opt<@R>` — round-trip
24+
// through parser + checker without diagnostics.
25+
// - `is_null(...)` as a predicate expression threads through the
26+
// checker without firing.
27+
//
28+
// The L4 "proven enforcement" — that a read through a non-null handle
29+
// never dereferences null — lives in the Idris2 proof layer
30+
// (src/abi/TypedWasm/ABI/Pointer.idr and Levels.idr, Ptr-NonNull
31+
// witness). This file's role is to prove the ReScript surface accepts
32+
// all well-formed L4 shapes without false positives.
33+
//
34+
// Run: node tests/levels/L4.mjs
35+
36+
import { parseModule } from "../../src/parser/Parser.mjs";
37+
import { checkModule } from "../../src/parser/Checker.mjs";
38+
39+
let passed = 0;
40+
let failed = 0;
41+
42+
function run(source, expect, label) {
43+
const pr = parseModule(source);
44+
if (pr.TAG !== "Ok") {
45+
if (expect.kind === "parseErr") {
46+
passed++;
47+
console.log(` OK ${label} — parse rejected as expected`);
48+
return;
49+
}
50+
failed++;
51+
console.log(` FAIL ${label} — parse error: ${pr._0.message}`);
52+
return;
53+
}
54+
if (expect.kind === "parseErr") {
55+
failed++;
56+
console.log(` FAIL ${label} — should have failed to parse`);
57+
return;
58+
}
59+
const diags = checkModule(pr._0);
60+
if (expect.kind === "clean") {
61+
if (diags.length === 0) {
62+
passed++;
63+
console.log(` OK ${label} — clean check`);
64+
} else {
65+
failed++;
66+
const msgs = diags.map(d => d.message).join("; ");
67+
console.log(` FAIL ${label} — expected clean, got ${diags.length}: ${msgs}`);
68+
}
69+
} else {
70+
const allMsgs = diags.map(d => d.message).join(" | ");
71+
const missing = expect.needles.filter(n => !allMsgs.includes(n));
72+
if (missing.length === 0 && diags.length > 0) {
73+
passed++;
74+
console.log(` OK ${label} — diagnostic fired as expected`);
75+
} else {
76+
failed++;
77+
console.log(` FAIL ${label} — expected needles ${expect.needles.join(", ")}, got "${allMsgs}"`);
78+
}
79+
}
80+
}
81+
82+
console.log("=== Level 4: Null safety (opt<T> tracking) ===\n");
83+
84+
// ── Positive: opt<@R> field round-trips parser + checker clean ───────
85+
86+
run(
87+
`region Vec2 {
88+
x: f32;
89+
y: f32;
90+
}
91+
region Enemies[32] {
92+
target: opt<@Vec2>;
93+
hp: i32;
94+
}`,
95+
{ kind: "clean" },
96+
"region field typed opt<@R> (nullable embedded region handle)",
97+
);
98+
99+
// ── Positive: opt<ptr<T>> — nested nullability on a raw pointer ──────
100+
101+
run(
102+
`region FreeSlot {
103+
next: opt<ptr<FreeSlot>>;
104+
}`,
105+
{ kind: "clean" },
106+
"opt<ptr<T>> — nested nullable pointer field",
107+
);
108+
109+
// ── Positive: non-null handle parameter — &region<X> shape ───────────
110+
111+
run(
112+
`region Players[8] {
113+
hp: i32;
114+
}
115+
fn read_hp(players: &region<Players>, idx: i32) -> i32
116+
effects { ReadRegion(Players) }
117+
{
118+
region.get $players[idx] .hp -> hp;
119+
return hp;
120+
}`,
121+
{ kind: "clean" },
122+
"&region<X> parameter (non-null shared handle)",
123+
);
124+
125+
// ── Positive: mutable non-null handle — &mut region<X> ───────────────
126+
127+
run(
128+
`region Players[8] {
129+
hp: i32;
130+
}
131+
fn set_hp(players: &mut region<Players>, idx: i32, new_hp: i32)
132+
effects { ReadRegion(Players), WriteRegion(Players) }
133+
{
134+
region.set $players[idx] .hp, new_hp;
135+
}`,
136+
{ kind: "clean" },
137+
"&mut region<X> parameter (non-null exclusive handle)",
138+
);
139+
140+
// ── Positive: is_null predicate on opt-typed field threads clean ─────
141+
142+
run(
143+
`region Vec2 { x: f32; y: f32; }
144+
region Enemies[16] {
145+
target: opt<@Vec2>;
146+
}
147+
fn check_target(enemies: &region<Enemies>, i: i32) -> i32
148+
effects { ReadRegion(Enemies) }
149+
{
150+
region.get $enemies[i] .target -> maybe_target;
151+
if is_null(maybe_target) {
152+
return -1;
153+
}
154+
return 0;
155+
}`,
156+
{ kind: "clean" },
157+
"is_null() guard on opt<@R> field",
158+
);
159+
160+
// ── Positive: opt<primitive> — nullable primitive (u32 example) ──────
161+
162+
run(
163+
`region Slot[4] {
164+
value: opt<u32>;
165+
}`,
166+
{ kind: "clean" },
167+
"opt<u32> — nullable primitive field",
168+
);
169+
170+
// ── Summary ──────────────────────────────────────────────────────────
171+
172+
console.log(`\n--- L4: ${passed} passed, ${failed} failed ---`);
173+
process.exit(failed > 0 ? 1 : 0);

0 commit comments

Comments
 (0)