Skip to content

docs(adr): ADR-016 — effect-threaded async-boundary detection (#234 S1) - #270

Merged
hyperpolymath merged 1 commit into
mainfrom
adr016-234-effect-table
May 19, 2026
Merged

docs(adr): ADR-016 — effect-threaded async-boundary detection (#234 S1)#270
hyperpolymath merged 1 commit into
mainfrom
adr016-234-effect-table

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

#234 slice S1 — ADR-016: effect-threaded async-boundary detection

The CPS transform's async-boundary detector is a hardcoded set async_primitives=["http_request_thenable"] — brittle (every new async primitive needs a codegen edit) and blind to user-defined Async functions. Generalising it ("boundary = any call whose effect row ⊇ Async") touches the typecheck→codegen interface (shared infra) → a one-way-door fork, escalated via AskUserQuestion (2026-05-19).

Owner chose the side-table mechanism (over AST annotation / codegen-local re-derivation).

A one-way shared-infra change gets its decision record before implementation. Slice S1: ADR only, no code (mirrors ADR-015):

dune test --force 278/278 (no code; zero regression).

Refs #234. Not Closes — staged campaign; owner closes per ISSUE-CLOSURE.

🤖 Generated with Claude Code

#234: the CPS transform's async-boundary detector is a hardcoded set
`async_primitives=["http_request_thenable"]` — brittle and blind to
user-defined `Async` fns. The generalisation ("boundary = any call
whose effect row ⊇ Async") touches the typecheck→codegen interface
(shared infra) → one-way-door fork, escalated (AskUserQuestion
2026-05-19). Owner chose the **side-table** mechanism over AST
annotation / codegen-local re-derivation.

This is slice S1 — the decision record only, no code (the discipline
for a one-way shared-infra change, mirroring ADR-015):
- ADR-016 in docs/specs/SETTLED-DECISIONS.adoc + the META.a2ml
  `[[adr]]` block (format-matched to ADR-012/014/015). Side-table
  keyed by a deterministic shared call-site numbering
  (`lib/effect_sites.ml`, called identically by typecheck & codegen so
  keys can't drift — no AST shape change, robust to future rebuilds);
  structural recogniser retained as the sound table-miss fallback.
  Staged S1..S4 (S2 build+return table unused/gate-neutral; S3
  pipeline+codegen switch + user-Async e2e; S4 retire hardcoded set).
- TECH-DEBT CORE-02 truthed (#225 line CLOSED; #234 ADR-016 accepted).

Gate: `dune test --force` 278/278 (no code; zero regression).

Refs #234. Not Closes — staged campaign; owner closes per ISSUE-CLOSURE.
@hyperpolymath
hyperpolymath merged commit 8a7c358 into main May 19, 2026
12 of 13 checks passed
@hyperpolymath
hyperpolymath deleted the adr016-234-effect-table branch May 19, 2026 18:22
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 47 issues detected

Severity Count
🔴 Critical 12
🟠 High 21
🟡 Medium 14

⚠️ 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": "Action actions/checkout@v4 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action denoland/setup-deno@v2 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "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"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

hyperpolymath added a commit that referenced this pull request May 19, 2026
…#245 #229) (#272)

ReScript %%raw injects arbitrary untyped host source; AffineScript has
only typed extern fn / extern type. 14 estate %%raw (ESC-01 #245) had no
clean target. ADR-018 (accepted): no raw escape by design — typed extern
is the SOLE FFI surface; every %%raw ports to a typed extern, host impl
to the embedder shim; no untyped extern-raw will be added (an
arbitrary-source hole defeats affine/effect tracking — the ADR-012
contortion). No compiler change (extern already exists). ADR-017 = the
block-module disposition (#262); this is 018 because main merged
ADR-016 = effect-threaded async-boundary (#234/#270).

SETTLED-DECISIONS + META.a2ml [[adr]] ADR-018 + RESCRIPT-ELIMINATION
cross-linked. Docs-only; gate unaffected by construction. Refs #245 #229
(not Closes — per-repo %%raw->extern port execution remains).

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
 #229)

AffineScript is strictly one-module-per-file (ADR-011 file=module;
grammar parser.mly:130-134 — `module A { }` parse-errors). #229 ports
carry ReScript block modules; single-per-file is mechanical (canonical
map), multi-block-per-file had no clean target (ESC-04 #262).

ADR-017 (accepted): one module per file — split, do not nest. Each
`module X { body }` -> its own X.affine with a `module X;` header; N
block-modules -> N files. Grammar NOT extended (ADR-011/ADR-012). No
compiler change — settles the porting doctrine + #229 canonical-map
structural rule. The split-file Resolve.UndefinedModule is cross-module
graph coherence (INT-02), tracked in RESCRIPT-ELIMINATION Tier-4, NOT
conflated here.

ADR-017 is sequential between ADR-016 (effect-threaded async-boundary,
#234/#270) and ADR-018 (no-raw-escape, #245/#272); inserted in order in
SETTLED-DECISIONS + META.a2ml + RESCRIPT-ELIMINATION ESC-04 row.
Reconstructed cleanly on current main (the original branch's ADR-016
->017 renumber history is collapsed to one in-order commit; supersedes
the prior #271 head). Docs-only; gate unaffected. Refs #262 #229.

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

AffineScript is strictly one-module-per-file (ADR-011 file=module;
grammar parser.mly:130-134 — `module A { }` parse-errors). #229 ports
carry ReScript block modules; single-per-file is mechanical (canonical
map), multi-block-per-file had no clean target (ESC-04 #262).

ADR-017 (accepted): one module per file — split, do not nest. Each
`module X { body }` -> its own X.affine with a `module X;` header; N
block-modules -> N files. Grammar NOT extended (ADR-011/ADR-012). No
compiler change — settles the porting doctrine + #229 canonical-map
structural rule. The split-file Resolve.UndefinedModule is cross-module
graph coherence (INT-02), tracked in RESCRIPT-ELIMINATION Tier-4, NOT
conflated here.

ADR-017 is sequential between ADR-016 (effect-threaded async-boundary,
#234/#270) and ADR-018 (no-raw-escape, #245/#272); inserted in order in
SETTLED-DECISIONS + META.a2ml + RESCRIPT-ELIMINATION ESC-04 row.
Reconstructed cleanly on current main (the original branch's ADR-016
->017 renumber history is collapsed to one in-order commit; supersedes
the prior #271 head). Docs-only; gate unaffected. Refs #262 #229.

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
…DR-016 complete) (#278)

Final ADR-016 slice. The async boundary is now decided *purely* from
the effect side-table; the structural name list is gone.

- codegen.ml: deleted `async_primitives = ["http_request_thenable"]`.
  `is_async_prim_call e = Effect_sites.is_async_call e` (no name
  disjunct). `mentions_async_prim` (PR3a single-boundary guard) is now
  `Effect_sites.exists_call Effect_sites.is_async_call e` — effect-
  driven over the SAME shared traversal the numbering uses, so it
  cannot miss a call shape the detector counts. The ADR's "table-miss
  fallback" is the oracle being empty / count-mismatched (⇒ no
  transform = exact pre-#234 behaviour), NOT a name list.
- effect_sites.ml: factored the call traversal into shared
  `visit_expr`/`visit_block`/`visit_program`; `fold_calls` (program)
  and the new `exists_call` (expr) both use it — single source, no
  drift between numbering and sub-expression scans. (Dead legacy body
  removed.)
- typecheck.ml: **root-cause fix for the cross-module gap S4 exposed.**
  `populate_call_effects` only scanned the local unit's `prog_decls`;
  imported async primitives (`http_request_thenable` — declared
  `/{Net,Async}` in stdlib Http, resolved as a wasm import, NOT in
  `prog_decls`) got EPure ⇒ once the structural mask was removed the
  CPS transform stopped firing for them. Now falls back to the
  callee's resolved scheme in `ctx.name_types` (populated by resolve,
  incl. imports), unioning the arrow-spine effect components (declared
  row ⊆ that; superset is sound for "row ⊇ Async").

Verification: full `tools/run_codegen_wasm_tests.sh` green —
http_cps_base/capture/chain + http_response_reader (imported
`http_request_thenable`, now via scheme-eff) AND
effect_async_boundary (user `/{Async}` fn) all pass with NO structural
list. `dune test --force` 290/290. Zero regression.

#234 fully delivered: S1 ADR-016 (#270), S2a numbering (#275), S2b
table (#276), S3 codegen switch (#277), S4 (this).

Closes #234.
hyperpolymath added a commit that referenced this pull request May 24, 2026
## Summary

The effect-threaded async-boundary work (issue #234, ADR-016) shipped
end-to-end on 2026-05-19 via PRs #270 / #275 / #276 / #277 / #278, but
the ledger entries and several in-source comments were still describing
intermediate states (`"S1 done"`, `"Built, not yet consumed"`, `"falls
back to the structural recogniser"`). This PR brings them into agreement
with the realised end state. **No behavioural change** — pure
documentation truthing of work already merged.

### Ledger refresh

- `docs/TECH-DEBT.adoc` — **CORE-02** and **CONV-02** marked `*CLOSED
2026-05-19*` with the full S1..S4 / PR cross-reference and the
steady-state miss path.
- `docs/specs/SETTLED-DECISIONS.adoc` +
`.machine_readable/6a2/META.a2ml` ADR-016 — staging block records
per-slice PR numbers and `DONE` status; the *Fallback / safety*
paragraph rewritten so the steady-state miss path is "no transform" (S4
retired the structural recogniser).
- `docs/ECOSYSTEM.adoc` + `docs/specs/async-on-wasm-cps.adoc` — #234
marked DELIVERED with the PR list, removing the "follow-up, still
tracked" framing.

### In-source comment refresh

- `lib/codegen.ml` — `is_async_prim_call` docstring + the
`bind_consumer` call-site comment rewritten to describe the realised
behaviour (single-sourced from `Effect_sites.is_async_call`; table-miss
= no transform).
- `lib/effect_sites.ml` — async-oracle header no longer claims "falls
back to the structural recogniser".
- `lib/typecheck.ml` — `call_effects` field doc +
`populate_call_effects` comment no longer say "Built, not yet consumed".
- `test/test_main.ml` — Alcotest suite label `"Effect-sites (#234 S2a)"`
→ `"Effect-sites (#234, ADR-016)"`.

### Why zero behaviour risk

Code-side edits touch only `(* … *)` / `(** … *)` comment blocks
(stripped by the OCaml lexer) and one Alcotest suite-label string. No
identifier, no expression, no top-level binding is changed.

## Test plan

- [ ] CI: `dune build` clean
- [ ] CI: `dune test --force` green (gate baseline holds — the S2a
`Effect_sites` test suite + the S3
`tests/codegen/effect_async_boundary.affine` e2e are the in-tree
witnesses of the delivered behaviour)
- [ ] No new lints flagged

> Local `dune test` was not run in this session because the
remote-execution env's network policy blocks the non-GitHub opam hosts
(`erratique.ch`, `gitlab.inria.fr`, `ocaml.janestreet.com`) that
`menhir`, `cmdliner`, `fmt`, `sexplib0`, … fetch from, so the switch
could not be brought up. The actual S1..S4 behaviour was CI-verified at
merge time on each of #270/#275/#276/#277/#278.

https://claude.ai/code/session_01HZ3i2wX5R5rbY8Ycmug4Ao

---
_Generated by [Claude
Code](https://claude.ai/code/session_01HZ3i2wX5R5rbY8Ycmug4Ao)_

Co-authored-by: Claude <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