Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/instant-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@ jobs:
if: ${{ vars.FARM_DISPATCH_ENABLED == 'true' }}
steps:
- name: Trigger Propagation
# Keep the action out of the job when the optional secret is absent.
# The variable gate above controls policy; this presence gate prevents
# an unconfigured fork from invoking repository-dispatch at all.
if: ${{ env.FARM_DISPATCH_TOKEN != '' }}
env:
FARM_DISPATCH_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN }}
uses: peter-evans/repository-dispatch@v4.0.1
with:
token: ${{ secrets.FARM_DISPATCH_TOKEN }}
token: ${{ env.FARM_DISPATCH_TOKEN }}
repository: hyperpolymath/.git-private-farm
event-type: propagate
client-payload: |-
Expand All @@ -38,4 +44,7 @@ jobs:
"forges": ""
}
- name: Confirm
if: ${{ env.FARM_DISPATCH_TOKEN != '' }}
env:
FARM_DISPATCH_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN }}
run: echo "::notice::Propagation triggered for ${{ github.event.repository.name }}"
3 changes: 2 additions & 1 deletion .github/workflows/publish-jsr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v7.0.1
- uses: denoland/setup-deno@v2.0.5
# v2.0.5 — immutable commit pin (tag: v2.0.5).
- uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed
with:
deno-version: v2.x
- name: Resolve package directory
Expand Down
34 changes: 23 additions & 11 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,18 @@ let parse_file (face : Affinescript.Face.face) path =
Affinescript.Span.pp_short span msg;
`Error (false, "Parse error")

(** Build a loader rooted at the source file's directory. The historical
CLI used the process CWD as [current_dir], so compiling `src/Main.affine`
could not find a sibling `src/Helper.affine` even though the module
loader already supported relative sibling paths (#642). *)
let loader_config_for_file path =
let absolute_path =
if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path
else path
in
let config = Affinescript.Module_loader.default_config () in
{ config with current_dir = Filename.dirname absolute_path }

(** Type-check a file. With [--json], emits a structured diagnostic
report on stderr. *)
let check_file face json path =
Expand All @@ -184,7 +196,7 @@ let check_file face json path =
let resolve_refs = ref [] in
begin try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, span) ->
Expand Down Expand Up @@ -232,7 +244,7 @@ let check_file face json path =
end else begin
try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, _span) ->
Expand Down Expand Up @@ -282,7 +294,7 @@ let eval_file face json path =
let add d = diags := d :: !diags in
begin try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, span) ->
Expand Down Expand Up @@ -325,7 +337,7 @@ let eval_file face json path =
end else begin
try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, _span) ->
Expand Down Expand Up @@ -507,7 +519,7 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
let add d = diags := d :: !diags in
begin try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, span) ->
Expand Down Expand Up @@ -732,7 +744,7 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
end else begin
try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, _span) ->
Expand Down Expand Up @@ -1025,7 +1037,7 @@ let lint_file face json path =
let add d = diags := d :: !diags in
begin try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, span) ->
Expand All @@ -1045,7 +1057,7 @@ let lint_file face json path =
end else begin
try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, _span) ->
Expand Down Expand Up @@ -1084,7 +1096,7 @@ let compile_to_wasm_module face path
: (Affinescript.Wasm.wasm_module, string) Result.t =
try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, _span) ->
Expand Down Expand Up @@ -1148,7 +1160,7 @@ let verify_file face path =
let face = resolve_face face path in
try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, _span) ->
Expand Down Expand Up @@ -1676,7 +1688,7 @@ let preview_cafe_cmd =
let run_pipeline_for_query face path =
try
let prog = parse_with_face face path in
let loader_config = Affinescript.Module_loader.default_config () in
let loader_config = loader_config_for_file path in
let loader = Affinescript.Module_loader.create loader_config in
match Affinescript.Resolve.resolve_program_with_loader prog loader with
| Error (e, _span) ->
Expand Down
54 changes: 33 additions & 21 deletions lib/module_loader.ml
Original file line number Diff line number Diff line change
Expand Up @@ -262,32 +262,45 @@ let clear_cache (loader : t) : unit =
demand. If a referenced module isn't cached, its imports are silently
skipped — the resolver would have reported the error already.

Imports are processed in declaration order; later imports override
earlier ones with the same fn name. Local decls in [prog.prog_decls]
always win over imported ones. *)
Imports are processed in declaration order; later imports replace
earlier imported declarations with the same name. This mirrors the
resolver's name-table order and keeps the two paths deterministic.
Glob/glob collisions are rejected by the resolver before code generation;
this function retains the same last-import policy as a defensive fallback
for callers that flatten an already-loaded program directly. Local decls
in [prog.prog_decls] always win over imported ones. *)
let flatten_imports (loader : t) (prog : program) : program =
(* Local-decl names suppress same-named imports of any kind. *)
let local_names =
let local_name_list =
List.filter_map (function
| TopFn fd -> Some fd.fd_name.name
| TopConst { tc_name; _ } -> Some tc_name.name
| _ -> None
) prog.prog_decls
in
let already_in = Hashtbl.create 32 in
List.iter (fun n -> Hashtbl.add already_in n ()) local_names;
(* A flattened import is either a function or a constant; both share the
same name-collision rule against [already_in]. *)
let imported_decls =
List.concat_map (fun imp ->
let local_names = Hashtbl.create 32 in
List.iter (fun n -> Hashtbl.replace local_names n ()) local_name_list;
(* Keep imported declaration order stable while replacing the declaration
stored for a later duplicate. A plain `already_in` set used to keep the
FIRST declaration here, while resolve.ml kept the LAST one (#743). *)
let imported_by_name = Hashtbl.create 32 in
let imported_order = ref [] in
let add_imported name decl_kind =
if not (Hashtbl.mem local_names name) then begin
if not (Hashtbl.mem imported_by_name name) then
imported_order := name :: !imported_order;
Hashtbl.replace imported_by_name name decl_kind
end
in
List.iter (fun imp ->
let path_strs path =
List.map (fun (id : ident) -> id.name) path
in
let mod_path = match imp with
| ImportSimple (p, _) | ImportList (p, _) | ImportGlob p -> path_strs p
in
match Hashtbl.find_opt loader.loaded mod_path with
| None -> []
| None -> ()
| Some lm ->
let public_decls = List.filter_map (fun decl ->
match decl with
Expand Down Expand Up @@ -334,16 +347,15 @@ let flatten_imports (loader : t) (prog : program) : program =
(bound_name, renamed))
) items
in
List.filter_map (fun (name, decl_kind) ->
if Hashtbl.mem already_in name then None
else begin
Hashtbl.add already_in name ();
match decl_kind with
| `Fn fd -> Some (TopFn fd)
| `Const decl -> Some decl
end
) select
) prog.prog_imports
List.iter (fun (name, decl_kind) -> add_imported name decl_kind) select
) prog.prog_imports;
let imported_decls =
List.rev !imported_order
|> List.filter_map (fun name ->
match Hashtbl.find_opt imported_by_name name with
| Some (`Fn fd) -> Some (TopFn fd)
| Some (`Const decl) -> Some decl
| None -> None)
in
(* #138 follow-up: imported TYPE decls are intentionally NOT inlined here.
An earlier #138 revision carried imported public [TopType]s so the
Expand Down
19 changes: 17 additions & 2 deletions lib/parser.mly
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,11 @@ expr_primary:
stdlib/traits.affine failed to parse. Resolves/typechecks as an
ordinary parameter binding named "self". */
| SELF_KW { ExprVar (mk_ident "self" $startpos $endpos) }
/* `total` is a soft keyword: it modifies `fn` in declaration position,
but is a perfectly ordinary value name in expression/assignment
position. Keep the lexer keyword so `total fn` remains unambiguous,
and recover it here as an identifier (#682). */
| TOTAL { ExprVar (mk_ident "total" $startpos $endpos) }
| name = lower_ident { ExprVar (mk_ident name $startpos $endpos) }
/* Struct literal: `Point #{ x: v, y: w }` (affinescript#215). The `#{`
sigil makes this unambiguous against a bare block and removes the
Expand Down Expand Up @@ -1255,9 +1260,15 @@ block_terminator:
{ ExprBlock inner }

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

pattern_primary:
| UNDERSCORE { PatWildcard (mk_span $startpos $endpos) }
/* `total` is contextual rather than reserved in a binder. The lexer must
keep TOTAL for the `total fn` modifier, so accept it as a pattern
variable here as well (#682). */
| TOTAL { PatVar (mk_ident "total" $startpos $endpos) }
| name = lower_ident { PatVar (mk_ident name $startpos $endpos) }
| n = INT { PatLit (LitInt (n, mk_span $startpos $endpos)) }
| c = CHAR { PatLit (LitChar (c, mk_span $startpos $endpos)) }
Expand Down
64 changes: 47 additions & 17 deletions lib/python_face.ml
Original file line number Diff line number Diff line change
Expand Up @@ -226,20 +226,32 @@ let is_blank_line raw =
(** Transform Python-style AffineScript source text to canonical AffineScript.
The result is valid input for the standard lexer + Menhir parser.

Tail-position detection: a regular statement (non-block-opener) in the
last position of a block — i.e. the next meaningful line's indent is
Tail-position detection: a regular expression in the last position of a
value-producing block — i.e. the next meaningful line's indent is
strictly less than the current line's indent — is emitted WITHOUT a
trailing `;`. This preserves the expression-as-return-value semantics
that AffineScript blocks require (a trailing `;` would make the block
yield unit rather than the expression's value). *)
trailing `;`. Loop bodies are statement blocks, however: their final
assignment/expression still needs a semicolon (#683). *)
type block_kind =
| Value_block
| Statement_block

let transform_source source =
let lines = Array.of_list (String.split_on_char '\n' source) in
let n = Array.length lines in
let out = Buffer.create (String.length source + 256) in
(* Indentation stack: innermost level at head, outermost (0) at tail. *)
let stack = ref [0] in

let top () = match !stack with h :: _ -> h | [] -> 0 in
(* Indentation stack: innermost level at head, outermost (0) at tail.
Each nested indentation also records the kind of the block that opened
it. This matters because `while`/`for` bodies are statement blocks,
even though a function or conditional body may use its final expression
as a return value. *)
let stack = ref [(0, Value_block)] in
let pending_kind = ref None in

let top () = match !stack with (h, _) :: _ -> h | [] -> 0 in

let current_kind () =
match !stack with (_, kind) :: _ -> kind | [] -> Value_block
in

let emit_dedents target =
while top () > target do
Expand Down Expand Up @@ -280,37 +292,55 @@ let transform_source source =
We then continue with `else {` — no leading `}` here because
emit_dedents already supplied it. *)
emit_dedents ind;
pending_kind := Some Value_block;
Buffer.add_string out (with_comment "else {")
end else if is_elif_clause stripped then begin
(* `elif COND:` — same structure as `else:` *)
emit_dedents ind;
pending_kind := Some Value_block;
let cond = apply_keywords (elif_condition stripped) in
Buffer.add_string out (with_comment ("else if " ^ cond ^ " {"))
end else begin
(* Normal line (statement or block-opener) *)
(* Normal line (statement or block-opener). *)
emit_dedents ind;
(* Push a new indent level when we step in *)
if ind > top () then stack := ind :: !stack;
(* Push a new indent level when we step in. The pending opener was
recorded on the preceding line; consume it exactly once. *)
if ind > top () then begin
let kind = Option.value !pending_kind ~default:Value_block in
stack := (ind, kind) :: !stack;
pending_kind := None
end else
(* A same-level or dedented line cannot be the body of the previous
opener. Do not leak its kind into a later block. *)
pending_kind := None;

let indent_str = String.make ind ' ' in

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

let line_text = match transform_import_line stripped with
| Some s -> s (* imports are always top-level statements *)
| None ->
if is_block_opener stripped then
(* Replace trailing `:` with ` {` *)
if is_block_opener stripped then begin
(* Replace trailing `:` with ` {`; remember whether the body is a
statement block for the next indentation level. *)
pending_kind :=
Some (if starts_with stripped "while " || starts_with stripped "for "
then Statement_block else Value_block);
apply_keywords (strip_block_colon stripped) ^ " {"
else if is_tail then
end else if may_drop_tail_semicolon then
(* Tail expression: no `;` — this is the block's return value *)
apply_keywords stripped
else
(* Mid-block statement: terminate with `;` *)
(* Mid-block statement, or the tail of a loop body. *)
apply_keywords stripped ^ ";"
in
Buffer.add_string out (indent_str ^ with_comment line_text)
Expand Down
Loading