Skip to content

Commit cbec06f

Browse files
committed
feat: ground same-cube at the typed-wasm level (real finding: 2 classes)
Built affinescript and actually ran the verifier (it previously only ever SKIPped). Upgraded verify-same-cube.sh from normalised-text comparison to compiling each face to typed-wasm and sha256-comparing the modules — the wasm IS the cube, so this is the rigorous bar and avoids text-only false-positives. Grounded result for the 'greet' corpus (affinescript @ main, 457-test suite green, per-face snapshot net 6/6 green): the six faces compile to TWO wasm classes — {canonical, jaffa, cafe} vs {rattle, pseudo, lucid}. The split is a real lowering-style divergence (trailing call as statement vs tail-expression); observationally identical (same output, same unit return) but not byte-identical wasm. So 'different faces, same cube' holds observationally but not byte-level for this corpus. Docs (profile + corpus README) record the honest finding; the verifier reports wasm equivalence classes with preview diffs as diagnostics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPG9mEQXFyA3k7NWAzMNMr
1 parent 95fb310 commit cbec06f

3 files changed

Lines changed: 174 additions & 135 deletions

File tree

examples/same-cube/README.md

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Each subdirectory here is **one program written in every AffineScript face**.
66
The files are deliberately the *same* program (identical strings, identical
77
structure) — not the demonstrative, different-string examples in
88
`affinescript/examples/faces/`. That is the whole point: if they are the same
9-
program, every face must lower to the **same canonical cube**.
9+
program, every face should lower to the **same cube**.
1010

1111
```
1212
greet/
@@ -25,23 +25,46 @@ scripts/verify-same-cube.sh examples/same-cube/greet \
2525
--out .machine_readable/audits/same-cube.jsonl
2626
```
2727

28-
The verifier detects each file's face from its `face:` pragma, runs the
29-
matching `preview-*` transformer, normalises the result (modulo comments and
30-
whitespace), and compares it to `canonical.affine`. Output is a per-face
31-
table plus invariant-path claim records (`Grounded` / `Ungrounded`).
28+
The verifier detects each file's face from its `face:` pragma, **compiles it to
29+
typed-wasm** (`compile --face <face>`), and sha256-compares the modules — the
30+
wasm *is* the cube, so byte-identical wasm is the rigorous bar. For any face
31+
outside the canonical class it also prints the `preview-*` text diff as a
32+
diagnostic of *where* the lowering diverges. Output is a per-face table, the
33+
wasm equivalence classes, and invariant-path claim records (`Grounded` /
34+
`Ungrounded`). Needs an `affinescript` binary (PATH, `--affinescript`,
35+
`AFFINESCRIPT`, or `../affinescript/_build`); without one it SKIPs.
3236

33-
This is a **claim-path debugger**, not a rubber stamp: a `DIFF` line does not
34-
mean the tool failed — it means the tool located the face that diverges from
35-
the cube, and prints exactly where. Grounding requires an `affinescript`
36-
binary (PATH, `--affinescript`, `AFFINESCRIPT`, or `../affinescript/_build`);
37-
without one the verifier SKIPs, because the invariant is grounded in CI where
38-
the compiler is built.
37+
This is a **claim-path debugger**, not a rubber stamp: a `DIFF` is the tool
38+
locating which face leaves the cube and showing exactly where.
39+
40+
## Grounded result — `greet` (2026-06-18, affinescript @ main)
41+
42+
Run against a freshly-built compiler, the six faces compile to **two** distinct
43+
wasm modules:
44+
45+
| wasm class | faces |
46+
|---|---|
47+
| `56c454be…` | canonical, **jaffa**, **cafe** |
48+
| `2ff63dd1…` | **rattle**, **pseudo**, **lucid** |
49+
50+
The split is **not** a transformer crash — all six compile, type-check, and
51+
print the same string. It is a real, characterised divergence in *lowering
52+
style*: rattle/pseudo/lucid render the trailing call as a **tail expression**
53+
(`fn main() … { println(x) }`), while canonical/jaffa/cafe keep it as a
54+
**statement** (`{ println(x); }`). For `println : … -> ()` both return unit, so
55+
the programs are **observationally identical** — but the emitted wasm is not
56+
byte-identical.
57+
58+
So *"different faces, same cube"* is true **observationally** but **false at the
59+
byte-wasm level** for this corpus: the face transformers do not currently agree
60+
on trailing-statement lowering. Tracked as an affinescript transformer-
61+
consistency item. (lucid additionally emits a `module Greet;` decl and drops
62+
the `-{IO}->` annotation in its preview text; neither changes the wasm class.)
3963

4064
## Adding a program
4165

42-
Add a new sibling directory (e.g. `counter/`) with one file per face you want
43-
to cover, each carrying the right `face:` pragma and encoding the identical
44-
program. Keep to surface features the transformers handle today (see the
45-
"Known transformer gaps" table in `affinescript/examples/faces/README.adoc`);
46-
a face that uses an unsupported construct will show up as a `DIFF`, which is
47-
useful signal, not noise.
66+
Add a sibling directory (e.g. `counter/`) with one file per face, each carrying
67+
the right `face:` pragma and encoding the identical program. Keep to surface
68+
features the transformers handle today (see the "Known transformer gaps" table
69+
in `affinescript/examples/faces/README.adoc`); divergences show up as extra
70+
wasm classes — useful signal, not noise.

profiles/faces.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ same-cube invariant, locating *which* face breaks the cube and *where*.
3838
face source (greet/rattle.affine, …) ← evidence / specification
3939
│ preview-<face> ← transition (transformer T)
4040
41-
canonical text (normalised) ← intermediate target
42-
byte-equality vs canonical.affine ← grounding check
41+
typed-wasm module (compile --face) ← the cube itself
42+
sha256-equality vs canonical wasm ← grounding check
4343
4444
"same cube" holds for this program ← conclusion (Grounded | Ungrounded)
4545
```
@@ -63,21 +63,28 @@ workspace at its corpus (the `tools/invariant-path/` hook), e.g.:
6363
just invariant-path same-cube examples/
6464
```
6565

66-
## What v1 catches
67-
68-
- A face whose `preview-*` lowering no longer normalises to the canonical
69-
cube (the cross-face equality break the per-face snapshots miss).
70-
- A face example that fails to parse after lowering (round-trip break).
71-
- A corpus where one face silently encodes a *different* program (e.g. a
72-
changed string literal or dropped statement).
73-
74-
## What v1 does NOT catch (yet)
75-
76-
- *Semantic* divergence below the canonical-text level — two different
77-
canonical texts that nevertheless typecheck to the same cube. v1
78-
compares normalised canonical text, not ASTs. (AST-level equality is
79-
the planned v2 grounding, once the compiler exposes a stable AST dump.)
80-
- typed-wasm equality. The chain "same canonical ⇒ same wasm" is asserted
81-
by the compiler, not re-checked here.
66+
## What it catches
67+
68+
- A face whose lowering compiles to a **different typed-wasm module** than the
69+
canonical reference (the cross-face equality break the per-face snapshots
70+
miss). Comparing wasm — the cube itself — also avoids text-only
71+
false-positives (e.g. tail-expression vs statement lowering, observationally
72+
identical but textually different).
73+
- A face example that fails to parse or compile (round-trip / build break).
74+
- A corpus where one face silently encodes a *different* program.
75+
76+
When faces split into more than one wasm class, the `preview-*` text diff is
77+
printed per divergence so you can see *where* the lowering parts ways.
78+
79+
## Limits
80+
81+
- **Byte-identical** wasm is stricter than **observational** equivalence: two
82+
faces can print the same thing and return the same value yet emit different
83+
wasm. The grounded `greet` result is exactly this — rattle/pseudo/lucid vs
84+
canonical/jaffa/cafe split into two classes over a trailing-statement
85+
lowering choice. The tool reports the wasm *classes* and leaves "are these
86+
classes observationally equal?" to the reader; it does not execute the
87+
modules to compare runtime output.
88+
- It grounds the corpora it is given; it does not enumerate all programs.
8289
- Effect-handler lowering soundness (tracked in affinescript #555); a
8390
face that exercises `handle` may converge in text yet diverge at runtime.

0 commit comments

Comments
 (0)