Skip to content

fix(interp): wire missing string_length builtin (Refs #332, #329) - #362

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/stdlib-04e-string-length-interp-binding
May 25, 2026
Merged

fix(interp): wire missing string_length builtin (Refs #332, #329)#362
hyperpolymath merged 1 commit into
mainfrom
claude/stdlib-04e-string-length-interp-binding

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

string_length : String -> Int / Pure is declared as a primitive in three places that AffineScript programs traverse during the compile pipeline:

Layer File State
Surface decl stdlib/effects.affine ✓ declared
Resolver lib/resolve.ml ✓ recognised
Typechecker lib/typecheck.ml ✓ typed String -> Int / EPure
Codegen (Deno) lib/codegen_deno.ml (s).length
Interpreter lib/interp.ml ❌ MISSING VBuiltin entry

Programs calling string_length compile cleanly and fail at run with Unbound variable: string_length.

What this breaks (without the fix)

Two E2E test suites currently red on main:

  • E2E STDLIB-04e Pure #332 / "string_length(\"hello\") == 5" — direct miss.
  • E2E STDLIB-04b error #329 / error_diverges_string_call_site — uses string_length(k) > 0 as a guard, so the missing binding masked the RuntimeError "empty key" assertion as an Unbound variable error.

Fix

One-block addition to create_initial_env's builtin table, right after int_to_string / float_to_string. Matches the resolver/typechecker contract and the codegen-Deno semantics. 14 lines including the explanatory comment.

("string_length", VBuiltin ("string_length", fun args ->
  match args with
  | [VString s] -> Ok (VInt (String.length s))
  | _ -> Error (TypeMismatch "string_length expects String")
));

How this was found

affinescript#361's build job was red after that PR's bench/dune + fixture-exemption fixes cleared the other two baselines. The two failing E2E tests both pointed at the same root cause — surface-decl/resolver/typechecker all knew string_length, but the interp didn't.

Test plan

  • dune runtest passes the STDLIB-04e suite (4 cases) and STDLIB-04b suite
  • No regression in Lexer / Parser / Typechecker / Resolver / other E2E suites (this is purely additive in the builtin table)

Refs #332 (STDLIB-04e), Refs #329 (STDLIB-04b). Companion to #361 — landing this first will turn #361's build job green.

🤖 Generated with Claude Code

`string_length : String -> Int / Pure` is declared as a primitive in three
places that AffineScript programs traverse during the compile pipeline:

  * stdlib/effects.affine    — the surface declaration
  * lib/resolve.ml           — recognises the name during resolution
  * lib/typecheck.ml         — types it as String -> Int / EPure

…but the runtime `VBuiltin` entry in lib/interp.ml's
`create_initial_env` was missing. Programs calling `string_length`
compiled cleanly and failed at run with:

  Unbound variable: string_length

This broke two E2E test suites on main:

  - E2E STDLIB-04e Pure #332 / "string_length(\"hello\") == 5"
  - E2E STDLIB-04b error #329 / divergence assertion
    (`lookup` used `string_length(k) > 0` as a guard — the missing
     binding produced the wrong eval_error variant, so the test
     `RuntimeError "empty key"` assertion saw an Unbound variable
     instead.)

Wire the builtin as `String.length` of an OCaml `VString`. Matches the
codegen-Deno path (`(s).length`) and the resolve/typecheck layers'
existing assumptions about the surface.

Found via affinescript#361's build-job CI red after the bench/dune +
fixture-exemption fixes cleared the other two baselines; this stdlib
bug was latent under those failures.

Refs #332 (STDLIB-04e pure externs), Refs #329 (STDLIB-04b throws extern).

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

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 112 issues detected

Severity Count
🔴 Critical 15
🟠 High 47
🟡 Medium 50

⚠️ 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": "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"
  },
  {
    "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 26a73a0 into main May 25, 2026
16 of 20 checks passed
@hyperpolymath
hyperpolymath deleted the claude/stdlib-04e-string-length-interp-binding branch May 25, 2026 22:41
hyperpolymath added a commit that referenced this pull request May 25, 2026
…binding

The interp binding for string_length landed in #362 (already on main),
but the wasm-backend codegen still does not know it — `codegen.ml`'s
`ExprApp` dispatch fell through to the "Function or variable not found"
error path. The codegen-test fixtures env_at.affine / arg_at.affine /
env_count_and_at.affine all call `string_length(env_at(0))` etc., so
the dune-runtest codegen-fixture loop fails at code-generation time
even after the resolver / typechecker (#332) and interp (#362) layers
were wired.

AS string layout is `[len: i32][bytes...]` at the pointer the arg
evaluates to — reading the length is one `i32.load` at offset 0. Same
shape as the existing tuple-index / record-field load patterns. Three
lines of effective code in the handler.

Closes the gap for ADR-015 S5 (env_at/arg_at) too: their codegen
emits a length-prefixed AS string, and the fixture tests then need
string_length to read that length back. Both pieces (env_at/arg_at
wiring + string_length codegen lowering) are required for the
fixtures to compile end-to-end.

Refs #332 (STDLIB-04e originating ticket), Refs #361 (queue-clearance
incident), follow-on to #362 (interp side) and #364's env_at/arg_at
wiring earlier in this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 25, 2026
…r_at_via_get (ADR-015 S5, #180) (#364)

## Summary

4th baseline-rot iteration after #361 / #362 / the `.hypatia-ignore`
format fix. The codegen-test fixtures
`tests/codegen/{env_at,arg_at,env_count_and_at}.affine` all reference
`env_at` / `arg_at`, but those surface names were **never wired** in
`resolve.ml` or `typecheck.ml` — `dune runtest` hits
`Resolve.UndefinedVariable` before codegen even fires.

The wasm IR + helper was added in commit `58f08b9` (ADR-015 S5) but the
surface plumbing was left out. This PR completes the surface.

## What's wired

| Layer | File | Change |
|---|---|---|
| Resolver | `lib/resolve.ml` | `def "env_at"; def "arg_at"` |
| Typechecker | `lib/typecheck.ml` | `Int -> String / Time` for both
(matches the effect row of `env_count` / `arg_count`) |
| Codegen — dispatch | `lib/codegen.ml` | New `ExprApp` handler right
after `env_count`/`arg_count`. Allocates 8 fresh locals (n / scratch /
count / bufsize / ptrvec / src / dst / result), looks up matching
sizes_get + get WASI func indices, calls
`Wasi_runtime.gen_str_at_via_get` (already present, just unreferenced).
|
| Codegen — WASI imports | `lib/codegen.ml` `optional_wasi` table | Four
new rows: `("env_at", "environ_sizes_get")`, `("env_at",
"environ_get")`, `("arg_at", "args_sizes_get")`, `("arg_at",
"args_get")`. |
| Codegen — dedup | `lib/codegen.ml` `optional_wasi` table | New dedup
pass keyed by WASI import name (keep first occurrence). Required because
`env_count + env_at` both want `environ_sizes_get`; without dedup the
wasm carries two imports under the same name and instantiation fails. |

## Regression coverage

`tests/codegen/env_count_and_at.affine` is the dedup regression — its
docstring already documented the invariant. The `dune runtest`
invocation in CI compiles all three fixtures end-to-end.

## What this does NOT cover

`Interp.eval_program` does not gain `env_at` / `arg_at`. Same posture as
`env_count` / `arg_count` (also absent from interp). These are WASI-only
surfaces; interp would need a synthetic environ table to simulate them.
Out of scope for the baseline-rot fix.

## How this was found

`#361`'s `build` job remained red after the bench/dune +
`.hypatia-ignore` + `string_length` fixes cleared the other reds. The
failure was `Resolution error: UndefinedVariable arg_at` at
`tests/codegen/arg_at.affine:7`. Same root-cause class as
`string_length` (#362): a stdlib surface declared in test fixtures but
never wired in resolve+typecheck.

## Test plan

- [ ] `dune build` succeeds (no OCaml compile error from the codegen
dispatch or dedup pass).
- [ ] `dune runtest` clears the codegen-test compile loop (env_at,
arg_at, env_count_and_at all produce valid wasm).
- [ ] No regression in the wider STDLIB-04* E2E suites (env_at / arg_at
don't appear in them, but `string_length` does — make sure the existing
#362 wiring still works).

Refs #180 (ADR-015 parent), Refs #339 (the IR-only commit), Closes-Refs
#361 (originating queue-clearance incident).

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

---------

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