Skip to content

test(xmod): INT-01 — prove + regression-lock cross-module wasm linking (Refs #178) - #244

Merged
hyperpolymath merged 1 commit into
mainfrom
int01-178-xmod
May 19, 2026
Merged

test(xmod): INT-01 — prove + regression-lock cross-module wasm linking (Refs #178)#244
hyperpolymath merged 1 commit into
mainfrom
int01-178-xmod

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

INT-01 — cross-module wasm linking: proven + regression-locked

Refs #178 (substrate; owner-gated — a distinct follow-up remains).

Finding (empirical, compiler-as-oracle)

#178: "use A.B inlines at AST level; no real multi-file libraries
shippable."
Falsified for the canonical form:

  • lib/codegen.ml gen_imports already emits real
    (import "Mod" "fn" (func …)) for use Mod::{fn} / use Mod::*; the
    .wasm path feeds the import-shaped program (~loader, not flattened).
  • Compiled CrossCallee.affinecallee.wasm (exports consume) and
    cross_caller_ok.affinecaller.wasm (imports
    CrossCallee.consume); linked both in Deno and main() returned
    42
    via the cross-module call.

What was missing was the guarantee: emission was correct but never
execution-tested (existing xmod tests are structural Tw_interface
only).

This PR locks it in

Layer Where Hermetic?
Structural emission (caller emits the import, callee exports the symbol) test/test_e2e.mlE2E Boundary Verify › INT-01 #178 ✅ in the 270→271 gate
End-to-end link+execute (cross-call = 42) tests/modules/xmod-link/ (run.sh + link.mjs + README) deno; reproducible, out of the hermetic gate by design

Verified: gate 271/271; tests/modules/xmod-link/run.sh
PASS: cross-module call CrossCallee.consume(42) === 42.

Honest scope (ledger truthed)

docs/TECH-DEBT.adoc + docs/ECOSYSTEM.adoc INT-01 updated:

No compiler change — emission was already correct; this proves and
guards it, and tells the truth about the one remaining gap.

🤖 Generated with Claude Code

…g (Refs #178)

#178 said "use A.B inlines at AST level; no real multi-file libraries
shippable". Empirically falsified for the canonical form: `gen_imports`
(lib/codegen.ml) already emits real `(import "Mod" "fn" (func …))` for
`use Mod::{fn}` / `use Mod::*`, and the .wasm caller path feeds the
import-shaped program. What was missing was the *guarantee* — emission
was correct but never execution-tested; the existing cross-module tests
are structural (Tw_interface) only.

This locks the substrate in:

1. Hermetic structural gate test (test/test_e2e.ml, "E2E Boundary
   Verify › INT-01 #178"): compiles CrossCallee.affine +
   cross_caller_ok.affine, asserts the caller emits import
   (CrossCallee . consume) and the callee exports `consume` — i.e. the
   two separately-compiled modules are link-compatible. Pure OCaml on
   the emitted Wasm.wasm_module; no external runtime; gate 270→271.

2. Reproducible execution proof (tests/modules/xmod-link/): run.sh
   compiles both fixtures and deno-links them; link.mjs instantiates
   callee.wasm, passes its `consume` export as the caller's
   `CrossCallee.consume` import, runs `main()`, asserts 42. Verified:
   "PASS: cross-module call CrossCallee.consume(42) === 42". Kept out of
   the hermetic gate by design (needs a wasm engine); documented in the
   directory README.

Honest status (ledger truthed — docs/TECH-DEBT.adoc + ECOSYSTEM.adoc
INT-01): `use Mod::{fn}` / `use Mod::*` cross-module libraries are
*shippable, proven, regression-locked*. The `use Mod;` + qualified value
call `Mod.fn(x)` form is a *distinct resolution gap* (post-#228
qualified-value resolution is unwired — `Resolution error`, before
codegen) — explicitly NOT folded into this emission substrate; tracked
as the remaining INT-01 follow-up.

No compiler change — emission was already correct; this proves and
guards it. Refs #178 (not Closes — qualified-value-call follow-up
remains; owner-gated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 44 issues detected

Severity Count
🔴 Critical 12
🟠 High 21
🟡 Medium 11

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stray AI.a2ml in root -- use 0-AI-MANIFEST.a2ml only",
    "type": "banned",
    "file": "AI.a2ml",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "high"
  },
  {
    "reason": "Superseded by 0-AI-MANIFEST.a2ml",
    "type": "banned",
    "file": "AI.djot",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "high"
  },
  {
    "reason": "Issue in quality.yml",
    "type": "missing_workflow",
    "file": "quality.yml",
    "action": "create",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in security-policy.yml",
    "type": "missing_workflow",
    "file": "security-policy.yml",
    "action": "create",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action hyperpolymath/standards/.github/workflows/governance-reusable.yml@main needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/example/smoke_driver.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/cli.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/lib/compile.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/lib/runner.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath
hyperpolymath merged commit bb647f4 into main May 19, 2026
15 of 16 checks passed
@hyperpolymath
hyperpolymath deleted the int01-178-xmod branch May 19, 2026 16:13
@hyperpolymath

Copy link
Copy Markdown
Owner Author

The single red check (Language / package anti-pattern policy) is pre-existing ReScript baseline rot — this PR adds only .adoc/.ml/.mjs/.sh test-harness + docs files, zero banned-language files, and no .idr proofs (so the idris2-oracle merge guardrail does not apply here). All 15 substantive checks pass. The ReScript finding is the separate #229 workstream. Admin-merging the INT-01 xmod regression-lock (Refs #178).

hyperpolymath added a commit that referenced this pull request May 19, 2026
…(Refs #178)

The INT-01 follow-up recorded after #244: `use Mod;` + qualified *value*
call `Mod.fn(x)` failed with Resolve.UndefinedVariable on the qualifier.

Root cause: `use Mod;` (ImportSimple) flat-imports Mod's public symbols
(import_resolved_symbols) but binds no module namespace; the parser yields
ExprField(ExprVar Mod, fn) (ExprSpan-wrapped), and resolve.ml's ExprField
case drops the field and resolves the base ExprVar Mod -> undefined. So
`Mod.fn` could never resolve even though `fn` was in scope flat.

Fix: a pure, idempotent, total parse-boundary lowering
`Resolve.lower_qualified_value_paths : program -> program` that rewrites
`ExprField(ExprVar m, fld)` -> `ExprVar fld` when `m` is an ImportSimple
qualifier (alias preferred; ImportList/ImportGlob bind no qualifier).
Span-peels the base; genuine record access `r.f` is untouched (`r` is not
an import qualifier). This is the value-expression analogue of #241/ADR-014
(qualified type/effect paths in the grammar). Applied once in
`parse_with_face` so resolve/typecheck/borrow/quantity/codegen all see the
lowered form uniformly; the formatter uses a separate path
(Formatter.format_file) so `fmt` preserves source `Mod.fn`. Embedders that
bypass parse_with_face call the exposed function (as the tests do) —
boundary documented in INT-01.

Verified (oracle): `use CrossCallee; CrossCallee.consume(42)` and
`use CrossCallee as CC; CC.consume(7)` -> Type checking passed;
`use CrossCallee::{consume}; consume(42)` still passes (no regression);
genuine `p.x` preserved. Full gate 275/275 (was 271; +4 hermetic
"E2E Qualified Value #178" regression tests + 2 fixtures).

Scope (honest): the `.`-qualified value path (the recorded gap) is closed.
`Mod::fn(x)` in *expression* position remains a parse error — a DISTINCT
parser gap (`::` is reserved for Type::Variant in expr position), not the
resolver; tracked as a separate follow-up. Refs #178 (not Closes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 19, 2026
…(Refs #178) (#253)

The INT-01 follow-up recorded after #244: `use Mod;` + qualified *value*
call `Mod.fn(x)` failed with Resolve.UndefinedVariable on the qualifier.

Root cause: `use Mod;` (ImportSimple) flat-imports Mod's public symbols
(import_resolved_symbols) but binds no module namespace; the parser yields
ExprField(ExprVar Mod, fn) (ExprSpan-wrapped), and resolve.ml's ExprField
case drops the field and resolves the base ExprVar Mod -> undefined. So
`Mod.fn` could never resolve even though `fn` was in scope flat.

Fix: a pure, idempotent, total parse-boundary lowering
`Resolve.lower_qualified_value_paths : program -> program` that rewrites
`ExprField(ExprVar m, fld)` -> `ExprVar fld` when `m` is an ImportSimple
qualifier (alias preferred; ImportList/ImportGlob bind no qualifier).
Span-peels the base; genuine record access `r.f` is untouched (`r` is not
an import qualifier). This is the value-expression analogue of #241/ADR-014
(qualified type/effect paths in the grammar). Applied once in
`parse_with_face` so resolve/typecheck/borrow/quantity/codegen all see the
lowered form uniformly; the formatter uses a separate path
(Formatter.format_file) so `fmt` preserves source `Mod.fn`. Embedders that
bypass parse_with_face call the exposed function (as the tests do) —
boundary documented in INT-01.

Verified (oracle): `use CrossCallee; CrossCallee.consume(42)` and
`use CrossCallee as CC; CC.consume(7)` -> Type checking passed;
`use CrossCallee::{consume}; consume(42)` still passes (no regression);
genuine `p.x` preserved. Full gate 275/275 (was 271; +4 hermetic
"E2E Qualified Value #178" regression tests + 2 fixtures).

Scope (honest): the `.`-qualified value path (the recorded gap) is closed.
`Mod::fn(x)` in *expression* position remains a parse error — a DISTINCT
parser gap (`::` is reserved for Type::Variant in expr position), not the
resolver; tracked as a separate follow-up. Refs #178 (not Closes).

Co-authored-by: hyperpolymath <hyperpolymath@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 19, 2026
…er bridge (Refs #179) (#267)

The INT-02 loader (`packages/affine-js/loader.js`) was already
implemented + merged (host-agnostic readBytes, multi-namespace
buildImportObject, parseOwnershipSection, Deno/Node/browser detectHost;
mod.js consumes it — SAT-02 gone). What was missing was the *guarantee*,
exactly the INT-01 #244 pattern:

1. loader_test.js (14 unit tests) had NO run task — green only with the
   right --allow-read/--allow-write flags; a fresh run looked "1 failed"
   (a permission error, not a loader bug). Pinned: deno.json
   `tasks.test` -> `deno task test` = 14/14, reproducible.

2. Nothing proved the loader on REAL compiler-emitted wasm (unit tests
   use synthetic bytes; the INT-01 xmod-link harness still hand-rolls
   Deno.readFile + a manual import object — the SAT-02 anti-pattern the
   loader replaces). Added tests/modules/loader-bridge/ (bridge.mjs +
   run.sh + README): compiles the INT-01 fixtures and drives them through
   the *actual* loader API — readBytes loads both; buildImportObject
   wires the genuine `CrossCallee` cross-module namespace; linked
   caller.main() === 42; parseOwnershipSection reads a real Linear-param
   ownership entry from compiler output. Closes INT-01 <-> INT-02.

Finding (documented in the harness README): buildImportObject *spreads*
module members (merge-not-clobber), so a catch-all Proxy wasi stub must
be attached as a whole namespace, not via the spread — mirroring real
usage (host owns wasi; loader owns the affine runtime + cross-module
namespaces).

Ledger truthed: ECOSYSTEM/TECH-DEBT INT-02 -> PROVEN+locked; SAT-02 ->
FIXED. No compiler change (loader is JS); hermetic dune gate unaffected
by construction (docs + deno.json + harness only; dune build clean).
Refs #179 (not Closes — owner-gated; satellite shell downstream).

Co-authored-by: hyperpolymath <hyperpolymath@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jun 1, 2026
…efs #239, #242) (#504)

Closes the smallest, safest tractable gaps that the four merged STEP-2
TS→AffineScript ports — phronesis#19, session-sentinel#25,
tropical-resource-typing#15, nafa-app#23 — flagged for STEP 3 of the
estate-wide campaign (`hyperpolymath/standards#239`, sub-issue `#242`).

## What lands

### `stdlib/Deno.affine` (+6 externs)

| extern | lowers to | gap |
|---|---|---|
| `statIsFile(path) -> Bool` | `Deno.statSync(path).isFile` | 3 |
| `statIsDirectory(path) -> Bool` | `Deno.statSync(path).isDirectory` |
3 |
| `bytesLength(b) -> Int` | `(b).length` | 4 |
| `bytesByteAt(b, i) -> Int` | `(b)[i]` | 4 |
| `bytesAsciiSlice(b, s, e) -> String` |
`String.fromCharCode(...(b).slice(s, e))` | 4 |
| `importMetaUrl() -> String` | `import.meta.url` | 5 |

The `bytes*` accessors give AffineScript first-class read access to the
opaque `Bytes` returned by `readFileBytes` so callers can peek at
file-magic / fixed-width binary headers without round-tripping through
`readTextFile` (which throws on binary). `importMetaUrl` exposes the JS
`__dirname` / `__filename` idiom for "find my own location"; only legal
at module top level, which the Deno-ESM backend's output already is.

### `stdlib/string.affine` (1 char)

`fn ends_with` → `pub fn ends_with`. The function existed but was
private, so the four STEP-2 ports each inlined a `string_sub`-backed
helper instead of importing it. Gap 2.

### `lib/codegen_deno.ml` — wildcard `let _` fix (gap 7)

`StmtLet { sl_pat = PatWildcard _; … }` now lowers to a bare expression
statement instead of `const _ = X;`. Three back-to-back `let _ = side()`
discards in the same scope tripped JS `SyntaxError: Identifier '_' has
already been declared`. The wildcard pattern doesn't bind, so dropping
the binding entirely preserves AffineScript semantics (evaluate for side
effects).

```js
// before
function discard_chain() {
  const _ = side(1);
  const _ = side(2);   // ❌ SyntaxError
  const _ = side(3);
  return 42;
}

// after
function discard_chain() {
  side(1);
  side(2);
  side(3);
  return 42;
}
```

### Tests

`tests/codegen-deno/deno_scripting_part2.{affine,deno.js,harness.mjs}` —
11 assertions across the new lowerings + the wildcard fix. The harness
reaching its discard-chain assertion is itself the test for gap 7: a
syntax-error generated module would have thrown at the dynamic `import`
line before any assert ran.

## Verification

| step | result |
|---|---|
| `dune build bin/main.exe` | ✅ |
| `dune runtest` | ✅ 353/353 |
| `./tools/run_codegen_deno_tests.sh` | ✅ all harnesses (incl. the new
one) |

## What does not land here (deferred deliberately)

- **gap 1** — `Deno.test` extern lowering. The four merged STEP-2 ports
use a panic-on-fail `main()` driver and it works; not unblocking STEP 4.
- **gap 6** — native TOML parser. Heavier work; STEP-2 ports used regex
field-presence checks where TOML mattered.

Gaps 9 (`AFFINESCRIPT_STDLIB` env discovery ladder, shipped #433) and 10
(negative integer literals, verified working in all contexts — match
arms, array literals, equality, ternaries) were audited as
already-resolved.

## Refs

- Closes 6 of the 10 gaps blocking `hyperpolymath/standards#242` (STEP 3
of `#239`).
- Unblocks STEP 4 (`#243`, mid-tier 4-9 file ports) and STEP 5 (`#244`,
idaptik DLC) for any consumer that needs stat predicates, byte
accessors, module-URL inspection, the public `ends_with`, or multiple
`let _` discards in a single scope.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant