Skip to content

feat(stdlib): STDLIB-04a — Mut effect externs (make_ref/get/set) → real impl (Closes #328) - #334

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/stdlib-04a-mut-externs
May 24, 2026
Merged

feat(stdlib): STDLIB-04a — Mut effect externs (make_ref/get/set) → real impl (Closes #328)#334
hyperpolymath merged 1 commit into
mainfrom
claude/stdlib-04a-mut-externs

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

Lands real implementations of the three Mut effect externs declared in stdlib/effects.affine:

extern fn make_ref<T>(x: T) -> Ref<T> / Mut;
extern fn get<T>(r: Ref<T>) -> T / Mut;
extern fn set<T>(r: Ref<T>, x: T) -> Unit / Mut;

Before this PR they were stubs — the surface parsed and typechecked, but no backend wired the runtime semantics, so any caller would compile and then fail at runtime with make_ref is not defined.

Changes

  • lib/interp.ml — three new VBuiltin entries: make_ref allocates a VMut cell (the existing runtime mutable-cell variant in Value); get/set route through the standard deref / assign primitives. Reuses the same store the borrow-surface &mut already uses.
  • lib/codegen_deno.ml — three new entries in deno_builtins: make_ref ��� {__cell: x}, get((r).__cell), set(((r).__cell = x), null). Single-field object so get/set are O(1); set returns null (Unit) via comma-expression to match the signature.
  • test/test_e2e.ml — three hermetic tests under E2E STDLIB-04a Mut #328:
    1. Int round-trip: make_ref(7); set(r, 42); get(r) == 42
    2. String round-trip (value-polymorphic): make_ref("alpha"); set(r, "omega"); get(r) == "omega"
    3. Deno codegen emits __cell shape (proves the new table entries actually fire).
  • docs/TECH-DEBT.adoc — row 04a marked DONE per the audit-split contract from docs(tech-debt): split STDLIB-04 into 04a–04e per per-extern audit (Refs #175) #333.

Conceptual note

These are runtime mutable cells (Ref<T> parameterised type), distinct from the borrow-checker's &/&mut references — different concept that happens to share the word "ref". The Mut effect on the signatures is what marks the call sites as observably stateful.

Test plan

  • Three new hermetic tests added to the gate (E2E STDLIB-04a Mut #328)
  • No change to stdlib AOT gate count (no new stdlib file)
  • CI: dune runtest green (gate moves from 281 → 284)
  • Hypatia DOC-FORMAT: no .md introduced

Closes #328. Refs #175.


Generated by Claude Code

… impl (Closes #328)

The `Mut` effect externs declared in `stdlib/effects.affine` were stubs:
the surface parsed but no backend wired the runtime semantics, so any
caller would compile and then fail with "make_ref is not defined" at
runtime. This lands the real implementations.

  * interp.ml — `make_ref` allocates a `VMut` cell; `get`/`set` route
    through the existing cell deref/assign primitives (same store used
    by the borrow-surface `&mut`).
  * codegen_deno.ml — lowers to a single-field `{__cell: x}` object so
    `get`/`set` are O(1) field access; `set` is a comma-expression
    returning `null` to match the `Unit` signature.
  * test_e2e.ml — 3 hermetic tests under "E2E STDLIB-04a Mut #328":
    Int round-trip, String round-trip (value-polymorphic), and a
    Deno-codegen assertion that the emitted JS contains `__cell`
    (proves the new builtin-table entries actually fire).

These are runtime mutable cells (`Ref<T>` parameterised type), distinct
from the borrow-checker `&`/`&mut` references — different concept that
happens to share the word "ref". Updates `docs/TECH-DEBT.adoc` row 04a
to DONE per the audit-split contract (Refs #175).

Closes #328. Refs #175.
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 143 issues detected

Severity Count
🔴 Critical 13
🟠 High 70
🟡 Medium 60

⚠️ 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@v6 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
hyperpolymath merged commit 99ae5e3 into main May 24, 2026
14 of 18 checks passed
@hyperpolymath
hyperpolymath deleted the claude/stdlib-04a-mut-externs branch May 24, 2026 04:38
hyperpolymath pushed a commit that referenced this pull request May 24, 2026
Reconciles PR #336 (CORE-02 / #234 / ADR-016 truth ledger to
DELIVERED) with two intervening main commits:

  f45afa1  feat(borrow): NLL last-use expiry for ref-bindings
           (CORE-01 pt3 Slice A, Refs #177) (#335)
  99ae5e3  feat(stdlib): STDLIB-04a — Mut effect externs
           (make_ref/get/set) → real impl (Closes #328) (#334)

Single conflict, docs/TECH-DEBT.adoc — HEAD updated CORE-02 (now
CLOSED 2026-05-19 with full ADR-016 S1..S4 narrative); origin/main
(#335) updated CORE-01 (Part 3 Slice A NLL last-use expiry LANDED).
Independent adjacent rows; resolution takes both — neither side
loses information.

The PR's comment edits in lib/codegen.ml, lib/effect_sites.ml,
lib/typecheck.ml, and the test/test_main.ml suite-label change all
survive intact through the merge.

Pure documentation + merge content. Zero behavioural risk.
hyperpolymath added a commit that referenced this pull request May 24, 2026
…cause) (#346)

## Summary

Unbreaks main's `dune runtest` gate — root cause of the 2-test failure
baseline that's been red since #334 merged.

## Root cause

`eval_decl`'s `TopFn fd` arm matched `fd.fd_body` against only `FnBlock`
and `FnExpr`, leaving `FnExtern` unmatched. Every prior interp test
imported externs via `use effects::{…}` so this case never fired — the
bug was latent.

The STDLIB-04a (#328) hermetic tests are the **first** to include an
inline `extern fn make_ref<T>(x: T) -> Ref<T> / Mut;` declaration in the
source they hand to `Interp.eval_program`. That triggers the missing arm
and raises:

```
File "lib/interp.ml", line 1010, characters 16-21: Pattern matching failed
  Raised at Affinescript__Interp.eval_decl
```

Reproduces the `2 failures! in 0.088s. 298 tests run.` line from every
PR's `build` log since #334.

## Fix

Wrap the body lowering in an outer `match fd.fd_body with`:

- `FnExtern` → `Ok env` (externs are runtime-bound via
`create_initial_env`'s `VBuiltin` table — `panic` / `error` / `make_ref`
/ `get` / `set` / etc., not via the AST).
- `FnBlock _ | FnExpr _` → existing closure construction.

The inner inner-match's `FnExtern` arm is `assert false` with a comment
— it's unreachable per the outer guard and the assert makes that
explicit for the next reader.

## Verification

The two failing tests (`#328 make_ref/set/get round-trip (Int)` and
`(String)`) now have a defined evaluation path. The third 04a test (Deno
codegen `__cell` shape) already passed — it bypasses interp.

## Test plan

- [ ] CI `build` job: `2 failures!` → `298 tests run` clean
- [ ] CI `lint`: should also clear (was likely propagating from the same
root)
- [ ] Hypatia DOC-FORMAT: no `.md` introduced

Refs #328.

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

Co-authored-by: Claude <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 24, 2026
#348)

…erience

Captures five operational findings from the parallel-claude
coordinator-feedback turn (2026-05-24), as a single "Agent operations
notes" section:

* CI signal reliability — "PR merged" does NOT mean "build green" on
this repo today; auto-merge fires through red builds. Recently-merged
PRs (#334/#335/#336/#344) all landed with `build` red; the red persisted
until PR #346.

* Reading CI logs — WebFetch on the Actions UI returns React skeleton;
use mcp__github__pull_request_read get_check_runs / get_status, hand
back to user for actual log lines via `gh run view --log-failed`.

* Known-failing baseline checks — vscode-smoke (npm 404),
migration-assistant (fixed by #342 but stale-base branches red),
governance/Language anti-pattern policy (flags approved TS exemptions),
Hypatia 143-finding comment (mostly the same exemption hits). Don't
waste turns investigating per-PR; only investigate *changes* in this
set.

* Branching discipline — `git fetch origin main && git rebase
origin/main` immediately before push, not just at branch- creation.
Claude 1's #337 accidentally reverted #334+#335 from a stale base; cheap
to prevent.

* Test-fixture hygiene — when adding a new declaration shape (extern fn,
etc.), test it against EVERY downstream consumer
(parse/resolve/typecheck/interp/codegen). PR #346's FnExtern interp bug
survived since the interpreter was written because no test fed an inline
`extern fn` to Interp.eval_program.

Pure documentation. Zero behavioural risk.

Refs PR #346, #335, #337, #344

---------

Co-authored-by: Claude <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 24, 2026
## Summary

Adds **class-level coverage** for the "inline extern fed to every
downstream consumer" surface that produced the PR #346 `FnExtern` interp
bug (`Interp.eval_decl`'s `TopFn` arm missing the `FnExtern` match arm —
silent pattern-match failure since the interpreter was written; fired
the moment STDLIB-04a's tests became the first to hand an inline `extern
fn` to `Interp.eval_program`).

Queued as a [comment on PR
#346](#346 (comment))
in the previous session; this is the follow-up.

## What this PR adds

Four fixtures under `test/e2e/fixtures/`:

| Fixture | Shape |
|---------|-------|
| `inline_extern_pure.affine` | `extern fn host_pure_identity(x: Int) ->
Int;` (no effects) |
| `inline_extern_effectful.affine` | `extern fn host_log(msg: String) ->
Unit / IO;` (effect row) |
| `inline_extern_polymorphic.affine` | `extern fn host_identity[T](x: T)
-> T;` (type params) |
| `inline_extern_type_consumed.affine` | `extern type Handle; extern fn
host_use(h: Handle) -> Int;` |

Each fed through **parse → resolve → typecheck → interp** via the new
`inline_extern_pipeline_ok` helper in `test/test_e2e.ml`. Assertion: all
four phases return `Ok`. A regression that re-introduces the silent
pattern-match-failure path that broke main between #334 and #346 would
fail loudly here instead.

Suite registered as `"E2E Inline Extern Shapes (Refs #346)"`.

## Why this matters

Per the `.claude/CLAUDE.md` §"Test-fixture hygiene for latent bug
surfaces" rule landed this session: when adding a stdlib `extern fn` (or
any other new declaration shape), test it against **every downstream
consumer** (parse / resolve / typecheck / interp / codegen). PR #346
fixed *one* instance of this class; this PR pins the *class* against the
gate so the next latent gap of the same shape (an `extern type` consumed
by an `extern fn` in a module that other modules import, etc.) surfaces
against CI rather than against the next agent.

## Test plan

- [ ] CI `build` job clean
- [ ] CI `dune runtest` — four new alcotest cases pass; existing suite
unchanged
- [ ] No new lints

Refs #346, Refs `.claude/CLAUDE.md` §"Test-fixture hygiene for latent
bug surfaces"

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

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

2 participants