Skip to content

Commit ff03732

Browse files
Merge branch 'main' into claude/fix-borrow-ml-comment-1304
2 parents 09dc53d + 6b09ed6 commit ff03732

4 files changed

Lines changed: 255 additions & 63 deletions

File tree

.claude/CLAUDE.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,117 @@ Rule: An issue is closed only by explicit merge of a PR with "Closes #N" OR by e
140140
Action (Hypatia): When asked to close an issue, confirm via a reply on the issue thread before doing so.
141141
Action (gitbot): Never use GitHub's "close issue" API directly; only close via PR merge with "Closes #N" keywords.
142142

143+
## Agent operations notes
144+
145+
Practical guidance for agents (Claude / other) operating in this repo,
146+
captured from parallel-bot session experience. Read once; saves turns.
147+
148+
### CI signal reliability
149+
150+
**"PR merged" does NOT mean "build green".** Auto-merge on this repo
151+
currently fires even when `build` or `lint` is failing — multiple
152+
recently-merged PRs (#334, #335, #336, #344) landed with `build` red,
153+
and the red persisted until PR #346's `FnExtern` interp fix. If you
154+
inherit a session reasoning about a recently-merged PR, do not assume
155+
its CI was green; check `mcp__github__pull_request_read` with method
156+
`get_check_runs` for the actual statuses, and check whether `main`
157+
itself is currently red before treating a build failure on your own
158+
PR as something *you* introduced.
159+
160+
### Reading CI logs
161+
162+
`WebFetch` against the GitHub Actions UI returns the React skeleton,
163+
not the log content. The fast paths for an agent are:
164+
165+
* `mcp__github__pull_request_read` with method `get_check_runs`
166+
per-job status (queued / in_progress / success / failure) with
167+
`details_url`. Sufficient for "did the build pass".
168+
* `mcp__github__pull_request_read` with method `get_status`
169+
combined commit status.
170+
* For actual log lines on a failed run, hand back to the user with
171+
`gh run view --log-failed <run-id>`; do not loop trying to scrape
172+
the UI.
173+
174+
### Known-failing baseline checks
175+
176+
These checks currently fail on *every* PR for repo-wide reasons, not
177+
because of any individual PR's changes. Do not waste turns
178+
investigating them on a per-PR basis:
179+
180+
* `vscode-smoke` — npm 404 on `@hyperpolymath/affine-vscode` (the
181+
in-editor harness depends on a not-yet-published npm package).
182+
* `migration-assistant` — was fixed by #342, but any branch created
183+
from a base older than #342 will still see it red until rebased.
184+
* `governance / Language / package anti-pattern policy` — flags the
185+
approved TypeScript exemptions (`affinescript-deno-test/*.ts`,
186+
`editors/vscode/test/*.js`, etc., all documented in this file's
187+
exemptions tables); the check has no allowlist for them.
188+
* The Hypatia security-scan bot comment — 143 findings; the bulk are
189+
the same TypeScript exemption hits + pre-existing root files. A
190+
real new finding will show as a *delta* in the count; otherwise
191+
ignore.
192+
193+
If a check from this list *changes status* on a PR (e.g.
194+
`vscode-smoke` suddenly passes, or Hypatia surfaces a new class of
195+
finding), that's signal worth investigating.
196+
197+
### Branching discipline with concurrent merges
198+
199+
When multiple agents are spawned in parallel, branch-creation time
200+
can lag `main` by hours and a stale base will silently revert other
201+
agents' work at merge time. **Before pushing any branch, run:**
202+
203+
```
204+
git fetch origin main
205+
git rebase origin/main
206+
```
207+
208+
Not just at branch-creation; immediately before push, after any
209+
in-session work. This guards against parallel-merge drift. Claude 1's
210+
STDLIB-04c branch (#337) accidentally reverted #334 and #335 because
211+
its base was stale; force-rebased to fix. Cheap to prevent, expensive
212+
to clean up.
213+
214+
### Post-squash-merge branch divergence
215+
216+
When a PR is squash-merged, the squashed commit on `main` gets a
217+
*new* SHA, distinct from any of the source-branch commits. If you
218+
then reset your local branch to `main` (or simply re-resolve it),
219+
`git status` reports N "ahead of origin/branch" — but those N
220+
commits are just the main-side commits the obsolete remote
221+
branch-tip never saw, not unpushed work.
222+
223+
Recognising the situation:
224+
225+
* The branch was already merged (PR closed, `merged: true`).
226+
* The local working tree matches `main`.
227+
* The remote branch still points at the *pre-merge* tip
228+
(`origin/<branch>` is an old SHA, not the squashed one).
229+
* `git log origin/<branch>..HEAD` lists commits that look like
230+
other people's work.
231+
232+
Safe fix — pick the one matching intent:
233+
234+
```
235+
git push origin --delete <branch> # done with the branch
236+
git push --force-with-lease origin <branch> # align the remote to main
237+
```
238+
239+
`--force-with-lease` is safe here because nothing on the remote
240+
branch is unmerged work; force-push without `--lease` only matters
241+
if someone else pushed concurrently, which is irrelevant for an
242+
already-merged branch you're cleaning up.
243+
244+
### Test-fixture hygiene for latent bug surfaces
245+
246+
When you add a stdlib `extern fn` (or any other new declaration
247+
shape), add a test that feeds it to *every* downstream consumer
248+
(parse, resolve, typecheck, interp, every codegen target that
249+
shouldn't reject it). The PR #346 `FnExtern` interp bug had survived
250+
since the interpreter was written because no test had ever fed an
251+
inline `extern fn` to `Interp.eval_program` — STDLIB-04a's tests
252+
were the first, and only then did the missing match arm fire.
253+
254+
Treat "first user of an existing-but-untested declaration shape" as a
255+
class-level surface, not a single test case.
256+

lib/codegen.ml

Lines changed: 26 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@ open Ast
1111
open Wasm
1212

1313
(** Ownership kind for typed-wasm schema annotations.
14-
Maps AffineScript ownership qualifiers to typed-wasm Level 7/10 verification. *)
15-
type ownership_kind =
16-
| Unrestricted (** Plain value, no ownership constraint (Wasm i32/f64 etc.) *)
17-
| Linear (** TyOwn / own — consumed exactly once (typed-wasm Level 10 linearity) *)
18-
| SharedBorrow (** TyRef / ref — read-only aliasing safety (typed-wasm Level 7) *)
19-
| ExclBorrow (** TyMut / mut — exclusive mutable aliasing safety (typed-wasm Level 7) *)
14+
15+
Re-exported from [Tw_ownership_section] (the dedicated home,
16+
extracted 2026-05-24 per A3 of TYPED-WASM-ROADMAP.adoc). The
17+
type equation preserves constructor accessibility so
18+
[Codegen.Linear] etc. continue to resolve and [open Codegen]
19+
in [Tw_verify] / [Tw_interface] / [Test_e2e] is unaffected. *)
20+
type ownership_kind = Tw_ownership_section.ownership_kind =
21+
| Unrestricted
22+
| Linear
23+
| SharedBorrow
24+
| ExclBorrow
2025

2126
(** Code generation context *)
2227
type context = {
@@ -111,19 +116,9 @@ let create_context () : context = {
111116
wasi_func_indices = [];
112117
}
113118

114-
(** Extract ownership kind from a parameter declaration.
115-
Checks p_ownership first; falls back to the shape of p_ty. *)
116-
let ownership_kind_of_param (p : param) : ownership_kind =
117-
match p.p_ownership with
118-
| Some Own -> Linear
119-
| Some Ref -> SharedBorrow
120-
| Some Mut -> ExclBorrow
121-
| None ->
122-
match p.p_ty with
123-
| TyOwn _ -> Linear
124-
| TyRef _ -> SharedBorrow
125-
| TyMut _ -> ExclBorrow
126-
| _ -> Unrestricted
119+
(** Extract ownership kind from a parameter declaration. Re-exported
120+
from [Tw_ownership_section] (A3 refactor, 2026-05-24). *)
121+
let ownership_kind_of_param = Tw_ownership_section.ownership_kind_of_param
127122

128123
(** If [ty] names a known struct (through any number of own/ref/mut wrappers),
129124
return that struct's name. Lets us recover a struct's field layout from
@@ -136,45 +131,21 @@ let rec struct_name_of_ty (ty : type_expr) : string option =
136131
| TyOwn inner | TyRef inner | TyMut inner -> struct_name_of_ty inner
137132
| _ -> None
138133

139-
(** Extract ownership kind from an optional return type expression *)
140-
let ownership_kind_of_ret (ret : type_expr option) : ownership_kind =
141-
match ret with
142-
| Some (TyOwn _) -> Linear
143-
| Some (TyRef _) -> SharedBorrow
144-
| Some (TyMut _) -> ExclBorrow
145-
| _ -> Unrestricted
134+
(** Extract ownership kind from an optional return type expression.
135+
Re-exported from [Tw_ownership_section]. *)
136+
let ownership_kind_of_ret = Tw_ownership_section.ownership_kind_of_ret
146137

147-
(** Encode an ownership_kind as a single byte (0–3) *)
148-
let ownership_kind_byte = function
149-
| Unrestricted -> 0 | Linear -> 1 | SharedBorrow -> 2 | ExclBorrow -> 3
138+
(** Encode an ownership_kind as a single byte (0–3).
139+
Re-exported from [Tw_ownership_section]. *)
140+
let ownership_kind_byte = Tw_ownership_section.ownership_kind_byte
150141

151142
(** Build the payload for the [affinescript.ownership] Wasm custom section.
152-
Encoding (all little-endian):
153-
u32 entry_count
154-
per entry:
155-
u32 func_index
156-
u8 param_count
157-
u8* param_kind (one per param, see kind encoding above)
158-
u8 return_kind *)
159-
let build_ownership_section (annots : (int * ownership_kind list * ownership_kind) list) : bytes =
160-
if annots = [] then Bytes.empty
161-
else
162-
let buf = Buffer.create 64 in
163-
let write_u32_le n =
164-
Buffer.add_char buf (Char.chr (n land 0xff));
165-
Buffer.add_char buf (Char.chr ((n lsr 8) land 0xff));
166-
Buffer.add_char buf (Char.chr ((n lsr 16) land 0xff));
167-
Buffer.add_char buf (Char.chr ((n lsr 24) land 0xff))
168-
in
169-
let write_u8 n = Buffer.add_char buf (Char.chr (n land 0xff)) in
170-
write_u32_le (List.length annots);
171-
List.iter (fun (func_idx, param_kinds, ret_kind) ->
172-
write_u32_le func_idx;
173-
write_u8 (List.length param_kinds);
174-
List.iter (fun k -> write_u8 (ownership_kind_byte k)) param_kinds;
175-
write_u8 (ownership_kind_byte ret_kind)
176-
) annots;
177-
Buffer.to_bytes buf
143+
Re-exported from [Tw_ownership_section.build_section] (A3 refactor,
144+
2026-05-24). The dedicated module is the home for the on-wire format;
145+
this alias preserves the [Codegen.build_ownership_section] public
146+
API surface that downstream callers (lib/tw_verify.ml,
147+
lib/tw_interface.ml, test/test_e2e.ml) rely on. *)
148+
let build_ownership_section = Tw_ownership_section.build_section
178149

179150
(** Map AffineScript type to WASM value type *)
180151
let type_to_wasm (ty : type_expr) : value_type result =

lib/interp.ml

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,14 +1027,25 @@ let create_initial_env () : env =
10271027
let eval_decl (env : env) (decl : top_level) : env result =
10281028
match decl with
10291029
| TopFn fd ->
1030-
let closure = VClosure {
1031-
cl_params = fd.fd_params;
1032-
cl_body = (match fd.fd_body with
1033-
| FnBlock blk -> ExprBlock blk
1034-
| FnExpr e -> e);
1035-
cl_env = env;
1036-
} in
1037-
Ok (extend_env fd.fd_name.name closure env)
1030+
(match fd.fd_body with
1031+
| FnExtern ->
1032+
(* Externs have no AST body to evaluate. Their runtime binding is
1033+
provided by [create_initial_env]'s builtin table (panic, error,
1034+
make_ref, …). Skip here so an inline `extern fn` declaration in
1035+
a test source (e.g. the STDLIB-04a Mut round-trip tests) doesn't
1036+
blow up the [FnBlock|FnExpr] match below.
1037+
Refs #328 root-cause for the interp pattern-match-failure. *)
1038+
Ok env
1039+
| FnBlock _ | FnExpr _ ->
1040+
let closure = VClosure {
1041+
cl_params = fd.fd_params;
1042+
cl_body = (match fd.fd_body with
1043+
| FnBlock blk -> ExprBlock blk
1044+
| FnExpr e -> e
1045+
| FnExtern -> assert false (* unreachable: outer match guards *));
1046+
cl_env = env;
1047+
} in
1048+
Ok (extend_env fd.fd_name.name closure env))
10381049

10391050
| TopConst tc ->
10401051
let* v = eval env tc.tc_value in

lib/tw_ownership_section.ml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
(* SPDX-License-Identifier: MPL-2.0 *)
2+
(* SPDX-FileCopyrightText: 2026 hyperpolymath *)
3+
(*
4+
* tw_ownership_section.ml — the dedicated home for the
5+
* `affinescript.ownership` Wasm custom section: kind encoding,
6+
* extraction from the AST, and binary serialisation.
7+
*
8+
* Extracted from lib/codegen.ml at 2026-05-24 per Tranche A3 of
9+
* docs/specs/TYPED-WASM-ROADMAP.adoc. Behaviour-preserving move.
10+
*
11+
* The format itself is specified in
12+
* docs/specs/TYPED-WASM-INTERFACE.adoc (v1, current) and
13+
* docs/specs/TYPED-WASM-ROADMAP.adoc §"Tranche B" (v2, the
14+
* ratified ADR-020 widening; landed via [Tw_verify] parse-support
15+
* first, with the producer-emit flip deferred to the coordinated
16+
* landing window per ADR-021 — see TYPED-WASM-COORDINATION-LEDGER.adoc
17+
* Q-001).
18+
*
19+
* Backwards compatibility: [Codegen] re-exports the same names so
20+
* `open Codegen` continues to expose [ownership_kind] and friends —
21+
* downstream callers ([Tw_verify], [Tw_interface], [Test_e2e]) need
22+
* no change.
23+
*)
24+
25+
open Ast
26+
27+
(** Ownership kind for typed-wasm schema annotations.
28+
Maps AffineScript ownership qualifiers to typed-wasm Level 7/10
29+
verification. *)
30+
type ownership_kind =
31+
| Unrestricted (** Plain value, no ownership constraint (Wasm i32/f64 etc.) *)
32+
| Linear (** TyOwn / own — consumed exactly once (typed-wasm Level 10) *)
33+
| SharedBorrow (** TyRef / ref — read-only aliasing safety (typed-wasm Level 7) *)
34+
| ExclBorrow (** TyMut / mut — exclusive mutable aliasing safety (typed-wasm Level 7) *)
35+
36+
(** Extract ownership kind from a parameter declaration.
37+
Checks [p.p_ownership] first; falls back to the shape of [p.p_ty]. *)
38+
let ownership_kind_of_param (p : param) : ownership_kind =
39+
match p.p_ownership with
40+
| Some Own -> Linear
41+
| Some Ref -> SharedBorrow
42+
| Some Mut -> ExclBorrow
43+
| None ->
44+
match p.p_ty with
45+
| TyOwn _ -> Linear
46+
| TyRef _ -> SharedBorrow
47+
| TyMut _ -> ExclBorrow
48+
| _ -> Unrestricted
49+
50+
(** Extract ownership kind from an optional return type expression *)
51+
let ownership_kind_of_ret (ret : type_expr option) : ownership_kind =
52+
match ret with
53+
| Some (TyOwn _) -> Linear
54+
| Some (TyRef _) -> SharedBorrow
55+
| Some (TyMut _) -> ExclBorrow
56+
| _ -> Unrestricted
57+
58+
(** Encode an [ownership_kind] as a single byte (0..3). *)
59+
let ownership_kind_byte = function
60+
| Unrestricted -> 0
61+
| Linear -> 1
62+
| SharedBorrow -> 2
63+
| ExclBorrow -> 3
64+
65+
(** Build the payload for the [affinescript.ownership] Wasm custom section.
66+
67+
v1 encoding (current emit; LE):
68+
u32 entry_count
69+
per entry:
70+
u32 func_index
71+
u8 param_count
72+
u8* param_kind (one per param, see kind encoding above)
73+
u8 return_kind
74+
75+
Returns [Bytes.empty] when there are no annotations so the
76+
caller can omit the section entirely. *)
77+
let build_section
78+
(annots : (int * ownership_kind list * ownership_kind) list) : bytes =
79+
if annots = [] then Bytes.empty
80+
else
81+
let buf = Buffer.create 64 in
82+
let write_u32_le n =
83+
Buffer.add_char buf (Char.chr (n land 0xff));
84+
Buffer.add_char buf (Char.chr ((n lsr 8) land 0xff));
85+
Buffer.add_char buf (Char.chr ((n lsr 16) land 0xff));
86+
Buffer.add_char buf (Char.chr ((n lsr 24) land 0xff))
87+
in
88+
let write_u8 n = Buffer.add_char buf (Char.chr (n land 0xff)) in
89+
write_u32_le (List.length annots);
90+
List.iter (fun (func_idx, param_kinds, ret_kind) ->
91+
write_u32_le func_idx;
92+
write_u8 (List.length param_kinds);
93+
List.iter (fun k -> write_u8 (ownership_kind_byte k)) param_kinds;
94+
write_u8 (ownership_kind_byte ret_kind)
95+
) annots;
96+
Buffer.to_bytes buf

0 commit comments

Comments
 (0)