Skip to content

Commit 4b36de2

Browse files
test(faces): prove a Python-face program runs end-to-end (face → wasm → result) (#684)
## What The README's headline is *"faces are the product"* — write a familiar surface, get the affine core underneath, compiled to wasm. That path had **never been exercised as running code**, only parsed (the scan flagged "faces exist but no face-authored program is proven end-to-end to wasm"). This closes that. - `test/e2e/fixtures/python_face_runnable.pyaff` — Python-shaped source (`def`, `if/else`, recursion) with a `# face: python` pragma. - `test/e2e/python_face_e2e.sh` — compiles it **through the Python face** to core-WASM and asserts `main() === 5170` (`fac(5)=120 + sum_to(100)=5050`). Skips loudly without the compiler/node; `AFFINESCRIPT_BIN` overrides the path. ``` Compiled fixtures/python_face_runnable.pyaff -> pf.wasm (WASM) main() = 5170 (expected 5170) PASS: Python-face program ran end-to-end (face → wasm → correct result) ``` ## Two real bugs the faces path surfaced (filed) Recursion is used deliberately, because proving this found: - **#683** — the Python face drops the trailing `;` on the last statement of a `while`/`for` body (tail-position detection doesn't distinguish loop bodies from value bodies), so face-authored **loops** don't yet compile. Recursion's tail line is a value expression, which the face handles correctly. - **#682** — `total` is rejected as a variable name in `let`-binding / assignment position (the `TOTAL` soft-keyword's ident-recovery is incomplete). An honest end-to-end proof of the face→wasm path *today*, a regression lock, with the loop gap tracked in #683. ## Gates `dune build` 0 · face e2e PASS · doc-truthing OK · soundness ledger all-5 OK. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4a6a31f commit 4b36de2

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# face: python
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# End-to-end proof (2026-07-07) that a *face-authored* program compiles
5+
# through the Python face → canonical → typecheck → wasm and RUNS with the
6+
# right answer. This is the README's headline claim ("faces are the product")
7+
# exercised for real, not just parsed.
8+
#
9+
# Recursion, not a loop: the Python face currently drops the trailing `;` on
10+
# the last statement of a while/for body (issue #683), so face-authored loops
11+
# don't yet compile. Recursion's tail line is a value expression, which the
12+
# face handles correctly — so this proves the face→wasm path without waiting
13+
# on #683. (Variable names also avoid the `total` soft-keyword collision,
14+
# issue #682.)
15+
16+
def fac(n: Int) -> Int:
17+
if n == 0:
18+
1
19+
else:
20+
n * fac(n - 1)
21+
22+
def sum_to(n: Int) -> Int:
23+
if n == 0:
24+
0
25+
else:
26+
n + sum_to(n - 1)
27+
28+
def main() -> Int:
29+
# fac(5)=120, sum_to(100)=5050 -> 120 + 5050 = 5170
30+
fac(5) + sum_to(100)

test/e2e/python_face_e2e.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# End-to-end proof that a Python-face-authored program runs: compile
5+
# fixtures/python_face_runnable.pyaff *through the Python face* to core-WASM
6+
# and assert main() === 5170 (fac(5)=120 + sum_to(100)=5050). Exercises the
7+
# README's "faces are the product" claim as running code, not just a parse.
8+
#
9+
# Requires the compiler built (dune build → _build/default/bin/main.exe;
10+
# override with AFFINESCRIPT_BIN) and node on PATH. Skips loudly (exit 0)
11+
# if either is absent.
12+
set -uo pipefail
13+
cd "$(dirname "$0")"
14+
REPO="$(cd ../.. && pwd)"
15+
BIN="${AFFINESCRIPT_BIN:-$REPO/_build/default/bin/main.exe}"
16+
SRC="fixtures/python_face_runnable.pyaff"
17+
EXPECT=5170
18+
19+
[ -x "$BIN" ] || { echo "SKIP: compiler not built ($BIN) — run dune build"; exit 0; }
20+
command -v node >/dev/null 2>&1 || { echo "SKIP: node not on PATH"; exit 0; }
21+
22+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
23+
WASM="$TMP/pf.wasm"
24+
25+
# The `# face: python` pragma on line 1 selects the face; no --face needed.
26+
"$BIN" compile "$SRC" -o "$WASM" || { echo "FAIL: compile through Python face"; exit 1; }
27+
28+
node - "$WASM" "$EXPECT" <<'JS'
29+
import { readFile } from "node:fs/promises";
30+
const [wasm, expect] = [process.argv[2], Number(process.argv[3])];
31+
const { instance } = await WebAssembly.instantiate(
32+
await readFile(wasm), { wasi_snapshot_preview1: { fd_write: () => 0 } });
33+
const got = instance.exports.main();
34+
console.log(`main() = ${got} (expected ${expect})`);
35+
if (got !== expect) { console.error("FAIL: wrong result"); process.exit(1); }
36+
console.log("PASS: Python-face program ran end-to-end (face → wasm → correct result)");
37+
JS

0 commit comments

Comments
 (0)