Skip to content

Commit 3bed17c

Browse files
fix remaining parser, face, module, and CI issues
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
1 parent 856f7a2 commit 3bed17c

14 files changed

Lines changed: 317 additions & 68 deletions

File tree

‎.github/workflows/instant-sync.yml‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@ jobs:
2525
if: ${{ vars.FARM_DISPATCH_ENABLED == 'true' }}
2626
steps:
2727
- name: Trigger Propagation
28+
# Keep the action out of the job when the optional secret is absent.
29+
# The variable gate above controls policy; this presence gate prevents
30+
# an unconfigured fork from invoking repository-dispatch at all.
31+
if: ${{ env.FARM_DISPATCH_TOKEN != '' }}
32+
env:
33+
FARM_DISPATCH_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN }}
2834
uses: peter-evans/repository-dispatch@v4.0.1
2935
with:
30-
token: ${{ secrets.FARM_DISPATCH_TOKEN }}
36+
token: ${{ env.FARM_DISPATCH_TOKEN }}
3137
repository: hyperpolymath/.git-private-farm
3238
event-type: propagate
3339
client-payload: |-
@@ -38,4 +44,7 @@ jobs:
3844
"forges": ""
3945
}
4046
- name: Confirm
47+
if: ${{ env.FARM_DISPATCH_TOKEN != '' }}
48+
env:
49+
FARM_DISPATCH_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN }}
4150
run: echo "::notice::Propagation triggered for ${{ github.event.repository.name }}"

‎.github/workflows/publish-jsr.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ jobs:
4343
timeout-minutes: 10
4444
steps:
4545
- uses: actions/checkout@v7.0.1
46-
- uses: denoland/setup-deno@v2.0.5
46+
# v2.0.5 — immutable commit pin (tag: v2.0.5).
47+
- uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed
4748
with:
4849
deno-version: v2.x
4950
- name: Resolve package directory

‎bin/main.ml‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ let parse_file (face : Affinescript.Face.face) path =
173173
Affinescript.Span.pp_short span msg;
174174
`Error (false, "Parse error")
175175

176+
(** Build a loader rooted at the source file's directory. The historical
177+
CLI used the process CWD as [current_dir], so compiling `src/Main.affine`
178+
could not find a sibling `src/Helper.affine` even though the module
179+
loader already supported relative sibling paths (#642). *)
180+
let loader_config_for_file path =
181+
let absolute_path =
182+
if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path
183+
else path
184+
in
185+
let config = Affinescript.Module_loader.default_config () in
186+
{ config with current_dir = Filename.dirname absolute_path }
187+
176188
(** Type-check a file. With [--json], emits a structured diagnostic
177189
report on stderr. *)
178190
let check_file face json path =
@@ -184,7 +196,7 @@ let check_file face json path =
184196
let resolve_refs = ref [] in
185197
begin try
186198
let prog = parse_with_face face path in
187-
let loader_config = Affinescript.Module_loader.default_config () in
199+
let loader_config = loader_config_for_file path in
188200
let loader = Affinescript.Module_loader.create loader_config in
189201
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
190202
| Error (e, span) ->
@@ -232,7 +244,7 @@ let check_file face json path =
232244
end else begin
233245
try
234246
let prog = parse_with_face face path in
235-
let loader_config = Affinescript.Module_loader.default_config () in
247+
let loader_config = loader_config_for_file path in
236248
let loader = Affinescript.Module_loader.create loader_config in
237249
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
238250
| Error (e, _span) ->
@@ -282,7 +294,7 @@ let eval_file face json path =
282294
let add d = diags := d :: !diags in
283295
begin try
284296
let prog = parse_with_face face path in
285-
let loader_config = Affinescript.Module_loader.default_config () in
297+
let loader_config = loader_config_for_file path in
286298
let loader = Affinescript.Module_loader.create loader_config in
287299
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
288300
| Error (e, span) ->
@@ -325,7 +337,7 @@ let eval_file face json path =
325337
end else begin
326338
try
327339
let prog = parse_with_face face path in
328-
let loader_config = Affinescript.Module_loader.default_config () in
340+
let loader_config = loader_config_for_file path in
329341
let loader = Affinescript.Module_loader.create loader_config in
330342
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
331343
| Error (e, _span) ->
@@ -507,7 +519,7 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
507519
let add d = diags := d :: !diags in
508520
begin try
509521
let prog = parse_with_face face path in
510-
let loader_config = Affinescript.Module_loader.default_config () in
522+
let loader_config = loader_config_for_file path in
511523
let loader = Affinescript.Module_loader.create loader_config in
512524
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
513525
| Error (e, span) ->
@@ -732,7 +744,7 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
732744
end else begin
733745
try
734746
let prog = parse_with_face face path in
735-
let loader_config = Affinescript.Module_loader.default_config () in
747+
let loader_config = loader_config_for_file path in
736748
let loader = Affinescript.Module_loader.create loader_config in
737749
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
738750
| Error (e, _span) ->
@@ -1025,7 +1037,7 @@ let lint_file face json path =
10251037
let add d = diags := d :: !diags in
10261038
begin try
10271039
let prog = parse_with_face face path in
1028-
let loader_config = Affinescript.Module_loader.default_config () in
1040+
let loader_config = loader_config_for_file path in
10291041
let loader = Affinescript.Module_loader.create loader_config in
10301042
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
10311043
| Error (e, span) ->
@@ -1045,7 +1057,7 @@ let lint_file face json path =
10451057
end else begin
10461058
try
10471059
let prog = parse_with_face face path in
1048-
let loader_config = Affinescript.Module_loader.default_config () in
1060+
let loader_config = loader_config_for_file path in
10491061
let loader = Affinescript.Module_loader.create loader_config in
10501062
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
10511063
| Error (e, _span) ->
@@ -1084,7 +1096,7 @@ let compile_to_wasm_module face path
10841096
: (Affinescript.Wasm.wasm_module, string) Result.t =
10851097
try
10861098
let prog = parse_with_face face path in
1087-
let loader_config = Affinescript.Module_loader.default_config () in
1099+
let loader_config = loader_config_for_file path in
10881100
let loader = Affinescript.Module_loader.create loader_config in
10891101
match Affinescript.Resolve.resolve_program_with_loader prog loader with
10901102
| Error (e, _span) ->
@@ -1148,7 +1160,7 @@ let verify_file face path =
11481160
let face = resolve_face face path in
11491161
try
11501162
let prog = parse_with_face face path in
1151-
let loader_config = Affinescript.Module_loader.default_config () in
1163+
let loader_config = loader_config_for_file path in
11521164
let loader = Affinescript.Module_loader.create loader_config in
11531165
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
11541166
| Error (e, _span) ->
@@ -1676,7 +1688,7 @@ let preview_cafe_cmd =
16761688
let run_pipeline_for_query face path =
16771689
try
16781690
let prog = parse_with_face face path in
1679-
let loader_config = Affinescript.Module_loader.default_config () in
1691+
let loader_config = loader_config_for_file path in
16801692
let loader = Affinescript.Module_loader.create loader_config in
16811693
match Affinescript.Resolve.resolve_program_with_loader prog loader with
16821694
| Error (e, _span) ->

‎lib/module_loader.ml‎

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -262,32 +262,45 @@ let clear_cache (loader : t) : unit =
262262
demand. If a referenced module isn't cached, its imports are silently
263263
skipped — the resolver would have reported the error already.
264264
265-
Imports are processed in declaration order; later imports override
266-
earlier ones with the same fn name. Local decls in [prog.prog_decls]
267-
always win over imported ones. *)
265+
Imports are processed in declaration order; later imports replace
266+
earlier imported declarations with the same name. This mirrors the
267+
resolver's name-table order and keeps the two paths deterministic.
268+
Glob/glob collisions are rejected by the resolver before code generation;
269+
this function retains the same last-import policy as a defensive fallback
270+
for callers that flatten an already-loaded program directly. Local decls
271+
in [prog.prog_decls] always win over imported ones. *)
268272
let flatten_imports (loader : t) (prog : program) : program =
269273
(* Local-decl names suppress same-named imports of any kind. *)
270-
let local_names =
274+
let local_name_list =
271275
List.filter_map (function
272276
| TopFn fd -> Some fd.fd_name.name
273277
| TopConst { tc_name; _ } -> Some tc_name.name
274278
| _ -> None
275279
) prog.prog_decls
276280
in
277-
let already_in = Hashtbl.create 32 in
278-
List.iter (fun n -> Hashtbl.add already_in n ()) local_names;
279-
(* A flattened import is either a function or a constant; both share the
280-
same name-collision rule against [already_in]. *)
281-
let imported_decls =
282-
List.concat_map (fun imp ->
281+
let local_names = Hashtbl.create 32 in
282+
List.iter (fun n -> Hashtbl.replace local_names n ()) local_name_list;
283+
(* Keep imported declaration order stable while replacing the declaration
284+
stored for a later duplicate. A plain `already_in` set used to keep the
285+
FIRST declaration here, while resolve.ml kept the LAST one (#743). *)
286+
let imported_by_name = Hashtbl.create 32 in
287+
let imported_order = ref [] in
288+
let add_imported name decl_kind =
289+
if not (Hashtbl.mem local_names name) then begin
290+
if not (Hashtbl.mem imported_by_name name) then
291+
imported_order := name :: !imported_order;
292+
Hashtbl.replace imported_by_name name decl_kind
293+
end
294+
in
295+
List.iter (fun imp ->
283296
let path_strs path =
284297
List.map (fun (id : ident) -> id.name) path
285298
in
286299
let mod_path = match imp with
287300
| ImportSimple (p, _) | ImportList (p, _) | ImportGlob p -> path_strs p
288301
in
289302
match Hashtbl.find_opt loader.loaded mod_path with
290-
| None -> []
303+
| None -> ()
291304
| Some lm ->
292305
let public_decls = List.filter_map (fun decl ->
293306
match decl with
@@ -334,16 +347,15 @@ let flatten_imports (loader : t) (prog : program) : program =
334347
(bound_name, renamed))
335348
) items
336349
in
337-
List.filter_map (fun (name, decl_kind) ->
338-
if Hashtbl.mem already_in name then None
339-
else begin
340-
Hashtbl.add already_in name ();
341-
match decl_kind with
342-
| `Fn fd -> Some (TopFn fd)
343-
| `Const decl -> Some decl
344-
end
345-
) select
346-
) prog.prog_imports
350+
List.iter (fun (name, decl_kind) -> add_imported name decl_kind) select
351+
) prog.prog_imports;
352+
let imported_decls =
353+
List.rev !imported_order
354+
|> List.filter_map (fun name ->
355+
match Hashtbl.find_opt imported_by_name name with
356+
| Some (`Fn fd) -> Some (TopFn fd)
357+
| Some (`Const decl) -> Some decl
358+
| None -> None)
347359
in
348360
(* #138 follow-up: imported TYPE decls are intentionally NOT inlined here.
349361
An earlier #138 revision carried imported public [TopType]s so the

‎lib/parser.mly‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,11 @@ expr_primary:
951951
stdlib/traits.affine failed to parse. Resolves/typechecks as an
952952
ordinary parameter binding named "self". */
953953
| SELF_KW { ExprVar (mk_ident "self" $startpos $endpos) }
954+
/* `total` is a soft keyword: it modifies `fn` in declaration position,
955+
but is a perfectly ordinary value name in expression/assignment
956+
position. Keep the lexer keyword so `total fn` remains unambiguous,
957+
and recover it here as an identifier (#682). */
958+
| TOTAL { ExprVar (mk_ident "total" $startpos $endpos) }
954959
| name = lower_ident { ExprVar (mk_ident name $startpos $endpos) }
955960
/* Struct literal: `Point #{ x: v, y: w }` (affinescript#215). The `#{`
956961
sigil makes this unambiguous against a bare block and removes the
@@ -1255,9 +1260,15 @@ block_terminator:
12551260
{ ExprBlock inner }
12561261

12571262
block:
1258-
| LBRACE stmts = list(stmt) RBRACE
1263+
/* Keep the empty block explicit. Using `list(stmt)` for this alternative
1264+
made `Pat => {}` depend on Menhir's conflict choice between the inner
1265+
and outer closing braces (#644). A block with statements remains
1266+
non-empty in the two statement-bearing alternatives below. */
1267+
| LBRACE RBRACE
1268+
{ { blk_stmts = []; blk_expr = None } }
1269+
| LBRACE stmts = nonempty_list(stmt) RBRACE
12591270
{ { blk_stmts = stmts; blk_expr = None } }
1260-
| LBRACE stmts = list(stmt) final = block_terminator RBRACE
1271+
| LBRACE stmts = nonempty_list(stmt) final = block_terminator RBRACE
12611272
{ { blk_stmts = stmts; blk_expr = Some final } }
12621273
| LBRACE stmts = stmt_list_nonempty_trailing_expr RBRACE
12631274
{ { blk_stmts = fst stmts; blk_expr = Some (snd stmts) } }
@@ -1300,6 +1311,10 @@ pattern_or:
13001311

13011312
pattern_primary:
13021313
| UNDERSCORE { PatWildcard (mk_span $startpos $endpos) }
1314+
/* `total` is contextual rather than reserved in a binder. The lexer must
1315+
keep TOTAL for the `total fn` modifier, so accept it as a pattern
1316+
variable here as well (#682). */
1317+
| TOTAL { PatVar (mk_ident "total" $startpos $endpos) }
13031318
| name = lower_ident { PatVar (mk_ident name $startpos $endpos) }
13041319
| n = INT { PatLit (LitInt (n, mk_span $startpos $endpos)) }
13051320
| c = CHAR { PatLit (LitChar (c, mk_span $startpos $endpos)) }

‎lib/python_face.ml‎

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -226,20 +226,32 @@ let is_blank_line raw =
226226
(** Transform Python-style AffineScript source text to canonical AffineScript.
227227
The result is valid input for the standard lexer + Menhir parser.
228228
229-
Tail-position detection: a regular statement (non-block-opener) in the
230-
last position of a block — i.e. the next meaningful line's indent is
229+
Tail-position detection: a regular expression in the last position of a
230+
value-producing block — i.e. the next meaningful line's indent is
231231
strictly less than the current line's indent — is emitted WITHOUT a
232-
trailing `;`. This preserves the expression-as-return-value semantics
233-
that AffineScript blocks require (a trailing `;` would make the block
234-
yield unit rather than the expression's value). *)
232+
trailing `;`. Loop bodies are statement blocks, however: their final
233+
assignment/expression still needs a semicolon (#683). *)
234+
type block_kind =
235+
| Value_block
236+
| Statement_block
237+
235238
let transform_source source =
236239
let lines = Array.of_list (String.split_on_char '\n' source) in
237240
let n = Array.length lines in
238241
let out = Buffer.create (String.length source + 256) in
239-
(* Indentation stack: innermost level at head, outermost (0) at tail. *)
240-
let stack = ref [0] in
241-
242-
let top () = match !stack with h :: _ -> h | [] -> 0 in
242+
(* Indentation stack: innermost level at head, outermost (0) at tail.
243+
Each nested indentation also records the kind of the block that opened
244+
it. This matters because `while`/`for` bodies are statement blocks,
245+
even though a function or conditional body may use its final expression
246+
as a return value. *)
247+
let stack = ref [(0, Value_block)] in
248+
let pending_kind = ref None in
249+
250+
let top () = match !stack with (h, _) :: _ -> h | [] -> 0 in
251+
252+
let current_kind () =
253+
match !stack with (_, kind) :: _ -> kind | [] -> Value_block
254+
in
243255

244256
let emit_dedents target =
245257
while top () > target do
@@ -280,37 +292,55 @@ let transform_source source =
280292
We then continue with `else {` — no leading `}` here because
281293
emit_dedents already supplied it. *)
282294
emit_dedents ind;
295+
pending_kind := Some Value_block;
283296
Buffer.add_string out (with_comment "else {")
284297
end else if is_elif_clause stripped then begin
285298
(* `elif COND:` — same structure as `else:` *)
286299
emit_dedents ind;
300+
pending_kind := Some Value_block;
287301
let cond = apply_keywords (elif_condition stripped) in
288302
Buffer.add_string out (with_comment ("else if " ^ cond ^ " {"))
289303
end else begin
290-
(* Normal line (statement or block-opener) *)
304+
(* Normal line (statement or block-opener). *)
291305
emit_dedents ind;
292-
(* Push a new indent level when we step in *)
293-
if ind > top () then stack := ind :: !stack;
306+
(* Push a new indent level when we step in. The pending opener was
307+
recorded on the preceding line; consume it exactly once. *)
308+
if ind > top () then begin
309+
let kind = Option.value !pending_kind ~default:Value_block in
310+
stack := (ind, kind) :: !stack;
311+
pending_kind := None
312+
end else
313+
(* A same-level or dedented line cannot be the body of the previous
314+
opener. Do not leak its kind into a later block. *)
315+
pending_kind := None;
294316

295317
let indent_str = String.make ind ' ' in
296318

297319
(* Tail-position check: the next meaningful line is less indented (or
298320
EOF), meaning this is the last expression in its block. Omit `;`
299-
so the block's value is this expression, not unit. *)
321+
only for value-producing blocks; the final statement in a loop body
322+
must remain terminated. *)
300323
let next_ind = next_meaningful_indent i in
301324
let is_tail = next_ind < ind in (* -1 (EOF) satisfies this for ind > 0 *)
325+
let may_drop_tail_semicolon =
326+
is_tail && current_kind () = Value_block
327+
in
302328

303329
let line_text = match transform_import_line stripped with
304330
| Some s -> s (* imports are always top-level statements *)
305331
| None ->
306-
if is_block_opener stripped then
307-
(* Replace trailing `:` with ` {` *)
332+
if is_block_opener stripped then begin
333+
(* Replace trailing `:` with ` {`; remember whether the body is a
334+
statement block for the next indentation level. *)
335+
pending_kind :=
336+
Some (if starts_with stripped "while " || starts_with stripped "for "
337+
then Statement_block else Value_block);
308338
apply_keywords (strip_block_colon stripped) ^ " {"
309-
else if is_tail then
339+
end else if may_drop_tail_semicolon then
310340
(* Tail expression: no `;` — this is the block's return value *)
311341
apply_keywords stripped
312342
else
313-
(* Mid-block statement: terminate with `;` *)
343+
(* Mid-block statement, or the tail of a loop body. *)
314344
apply_keywords stripped ^ ";"
315345
in
316346
Buffer.add_string out (indent_str ^ with_comment line_text)

0 commit comments

Comments
 (0)