Repository navigation
fix: multi-module programs check, evaluate and compile (found running kyq) #788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -177,13 +177,21 @@ let parse_file (face : Affinescript.Face.face) path = | |
| 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 loader_config_for_file ?prog 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 } | ||
| let mod_path = | ||
| Option.bind prog (fun (p : Affinescript.Ast.program) -> | ||
| Option.map (List.map (fun (id : Affinescript.Ast.ident) -> id.name)) | ||
| p.prog_module) | ||
| in | ||
| { config with | ||
| current_dir = | ||
| Affinescript.Module_loader.module_root_dir | ||
| ~file_dir:(Filename.dirname absolute_path) mod_path } | ||
|
|
||
| (** Type-check a file. With [--json], emits a structured diagnostic | ||
| report on stderr. *) | ||
|
|
@@ -196,7 +204,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 = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, span) -> | ||
|
|
@@ -244,7 +252,7 @@ let check_file face json path = | |
| end else begin | ||
| try | ||
| let prog = parse_with_face face path in | ||
| let loader_config = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, _span) -> | ||
|
|
@@ -294,7 +302,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 = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, span) -> | ||
|
|
@@ -316,7 +324,8 @@ let eval_file face json path = | |
| | Error (err, span) -> | ||
| add (Affinescript.Json_output.of_quantity_error (err, span)) | ||
| | Ok () -> | ||
| (match Affinescript.Interp.eval_program prog with | ||
| (match Affinescript.Interp.eval_program | ||
| (Affinescript.Module_loader.flatten_imports loader prog) with | ||
|
Comment on lines
+327
to
+328
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| | Ok env -> | ||
| (* Auto-call main() if defined — entry point for TEA apps and scripts *) | ||
| (match Affinescript.Value.lookup_env "main" env with | ||
|
|
@@ -337,7 +346,7 @@ let eval_file face json path = | |
| end else begin | ||
| try | ||
| let prog = parse_with_face face path in | ||
| let loader_config = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, _span) -> | ||
|
|
@@ -365,7 +374,8 @@ let eval_file face json path = | |
| (Affinescript.Face.format_quantity_error face err); | ||
| `Error (false, "Quantity error") | ||
| | Ok () -> | ||
| (match Affinescript.Interp.eval_program prog with | ||
| (match Affinescript.Interp.eval_program | ||
| (Affinescript.Module_loader.flatten_imports loader prog) with | ||
| | Ok env -> | ||
| (* Auto-call main() if defined — entry point for TEA apps and scripts *) | ||
| (match Affinescript.Value.lookup_env "main" env with | ||
|
|
@@ -519,7 +529,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 = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, span) -> | ||
|
|
@@ -744,7 +754,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 = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, _span) -> | ||
|
|
@@ -1037,7 +1047,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 = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, span) -> | ||
|
|
@@ -1057,7 +1067,7 @@ let lint_file face json path = | |
| end else begin | ||
| try | ||
| let prog = parse_with_face face path in | ||
| let loader_config = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, _span) -> | ||
|
|
@@ -1096,7 +1106,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 = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, _span) -> | ||
|
|
@@ -1160,7 +1170,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 = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| (match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, _span) -> | ||
|
|
@@ -1688,7 +1698,7 @@ let preview_cafe_cmd = | |
| let run_pipeline_for_query face path = | ||
| try | ||
| let prog = parse_with_face face path in | ||
| let loader_config = loader_config_for_file path in | ||
| let loader_config = loader_config_for_file ~prog path in | ||
| let loader = Affinescript.Module_loader.create loader_config in | ||
| match Affinescript.Resolve.resolve_program_with_loader prog loader with | ||
| | Error (e, _span) -> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1434,6 +1434,15 @@ let enter_fn_scope ctx (params : param list) : codegen_ctx = | |
| call whose head is a known [extern fn] lowers via {!deno_builtins}. | ||
| ============================================================================ *) | ||
|
|
||
| (* Fresh temporaries for `let` initializers lowered to statements. *) | ||
| let let_tmp_counter = ref 0 | ||
|
|
||
| (* Initializer shapes {!gen_assign_tail} can lower to statements. *) | ||
| let rec lowers_to_statements = function | ||
| | ExprSpan (e, _) -> lowers_to_statements e | ||
| | ExprMatch _ | ExprIf _ | ExprBlock _ -> true | ||
| | _ -> false | ||
|
|
||
| (** Render an expression as JavaScript using the names and host in [ctx]. | ||
| Lambdas are synchronous arrows; known qualified constructors refer to | ||
| their emitted bindings. Nested statements can update integer tracking. | ||
|
|
@@ -1587,8 +1596,15 @@ let rec gen_expr ctx (expr : expr) : string = | |
| the enclosing async context: inside a synthesised (async) method, | ||
| `(x) => (await ...)` is a SyntaxError in browsers — Bun's parser | ||
| happens to accept it, which hid the bug. *) | ||
| "((" ^ String.concat ", " ps ^ ") => " | ||
| ^ gen_expr { ctx with in_async = false } elam_body ^ ")" | ||
| let lctx = { ctx with in_async = false } in | ||
| if lowers_to_statements elam_body && expr_contains_return elam_body then | ||
| (* A `return` in the body returns from the lambda: emit a block | ||
| body so it is not swallowed by an IIFE (see {!gen_match}). *) | ||
| "((" ^ String.concat ", " ps ^ ") => { " | ||
| ^ gen_return_tail lctx elam_body ^ " })" | ||
| else | ||
| "((" ^ String.concat ", " ps ^ ") => " | ||
| ^ gen_expr lctx elam_body ^ ")" | ||
| | ExprTry { et_body; et_catch; et_finally } -> | ||
| gen_try ctx et_body et_catch et_finally | ||
| | ExprVariant (ty, ctor) -> | ||
|
|
@@ -1651,6 +1667,21 @@ and gen_pattern ctx (pat : pattern) : string = | |
| | PatOr (p, _) -> gen_pattern ctx p | ||
|
|
||
| and gen_match ctx scrutinee arms = | ||
| (* As an expression the match is an IIFE, so a `return` in an arm would | ||
| leave the IIFE, not the enclosing function, and the function would | ||
| carry on with the returned value as the match's result: silently wrong | ||
| output. `let` initializers and tail/statement position are lowered to | ||
| statements and handled correctly; any other position (an argument, an | ||
| operand) is refused here rather than miscompiled. *) | ||
| if List.exists (fun arm -> | ||
| expr_contains_return arm.ma_body | ||
| || (match arm.ma_guard with | ||
| | Some g -> expr_contains_return g | None -> false)) arms | ||
| then | ||
| failwith | ||
|
Comment on lines
+1677
to
+1681
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| "a `return` inside a match arm is not supported when the match is \ | ||
| used as a sub-expression (an argument or operand); bind the match \ | ||
| to a `let` first"; | ||
| let scrutinee_str = gen_expr ctx scrutinee in | ||
| let scrut_var = "__scrut" in | ||
| let rec gen_arms = function | ||
|
|
@@ -1844,6 +1875,58 @@ and gen_match_stmt ctx scrut arms = | |
| in | ||
| "{ const " ^ sv ^ " = " ^ gen_expr ctx scrut ^ "; " ^ arms_js arms ^ " }" | ||
|
|
||
| (* Statements that evaluate [e] and hand its value to [sink], which turns a | ||
| JS expression into a statement (`x = v;` or `return v;`). A `return` | ||
| anywhere along a tail path stays a real function return, which the IIFE | ||
| expression form cannot express. Used for `let` initializers and for | ||
| function and lambda bodies that contain an early return. *) | ||
| and gen_tail_to ctx (sink : string -> string) e = | ||
| match e with | ||
| | ExprSpan (inner, _) -> gen_tail_to ctx sink inner | ||
| | ExprReturn _ -> gen_stmt_expr ctx e | ||
| | ExprBlock blk -> | ||
| let b = Buffer.create 64 in | ||
| List.iter (fun s -> | ||
| Buffer.add_string b (gen_stmt ctx s); | ||
| Buffer.add_char b ' ') blk.blk_stmts; | ||
| (match blk.blk_expr with | ||
| | Some tail -> Buffer.add_string b (gen_tail_to ctx sink tail) | ||
| | None -> Buffer.add_string b (sink "Unit")); | ||
| "{ " ^ Buffer.contents b ^ " }" | ||
| | ExprIf { ei_cond; ei_then; ei_else } -> | ||
| let elseb = match ei_else with | ||
| | Some el -> gen_tail_to ctx sink el | ||
| | None -> sink "Unit" | ||
| in | ||
| "if (" ^ gen_expr ctx ei_cond ^ ") { " ^ gen_tail_to ctx sink ei_then | ||
| ^ " } else { " ^ elseb ^ " }" | ||
| | ExprMatch { em_scrutinee; em_arms } -> | ||
|
Comment on lines
+1901
to
+1903
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| incr let_tmp_counter; | ||
| let sv = Printf.sprintf "__scrut_%d" !let_tmp_counter in | ||
| let rec arms_js = function | ||
| | [] -> "throw new Error(\"non-exhaustive match\");" | ||
| | arm :: rest -> | ||
| let cond = gen_pattern_test sv arm.ma_pat in | ||
| let binds = gen_pattern_bindings sv arm.ma_pat in | ||
| let guard = match arm.ma_guard with | ||
| | Some g -> " && (" ^ gen_expr ctx g ^ ")" | None -> "" in | ||
| "if (" ^ cond ^ guard ^ ") { " ^ binds | ||
| ^ gen_tail_to ctx sink arm.ma_body ^ " } else " ^ arms_js rest | ||
|
Comment on lines
+1909
to
+1914
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| in | ||
| "{ const " ^ sv ^ " = " ^ gen_expr ctx em_scrutinee ^ "; " | ||
| ^ arms_js em_arms ^ " }" | ||
| | other -> sink (gen_expr ctx other) | ||
|
|
||
| and gen_assign_tail ctx target e = | ||
| gen_tail_to ctx (fun v -> target ^ " = " ^ v ^ ";") e | ||
|
|
||
| (* A function or lambda body's tail: statements when it holds an early | ||
| return, else the plain `return <expr>;`. *) | ||
| and gen_return_tail ctx e = | ||
| if lowers_to_statements e && expr_contains_return e then | ||
| gen_tail_to ctx (fun v -> "return " ^ v ^ ";") e | ||
| else "return " ^ gen_expr ctx e ^ ";" | ||
|
|
||
| and gen_try_stmt ctx body catch finally = | ||
| let b = gen_stmt_seq ctx body in | ||
| let c = match catch with | ||
|
|
@@ -1874,8 +1957,22 @@ and gen_stmt ctx (stmt : stmt) : string = | |
| gen_expr ctx sl_value ^ ";" | ||
| | StmtLet { sl_pat; sl_value; sl_mut; sl_quantity = _; sl_ty = _ } -> | ||
| let kw = if sl_mut then "let" else "const" in | ||
| let js = kw ^ " " ^ gen_pattern ctx sl_pat ^ " = " | ||
| ^ gen_expr ctx sl_value ^ ";" in | ||
| let js = | ||
| if lowers_to_statements sl_value && expr_contains_return sl_value then begin | ||
| (* `let x = match e { A(v) => v, B(err) => return f(err) };` | ||
| As an expression the match is an IIFE, and the arm's `return` | ||
|
Comment on lines
+1960
to
+1963
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| left the IIFE, not the function: the IIFE's result was then | ||
| bound to `x` and the function carried on. Lower the | ||
| initializer to statements that assign a temporary, so the | ||
| `return` is a real function return. *) | ||
| incr let_tmp_counter; | ||
| let tmp = Printf.sprintf "__let_tmp_%d" !let_tmp_counter in | ||
| "let " ^ tmp ^ "; " ^ gen_assign_tail ctx tmp sl_value ^ " " | ||
| ^ kw ^ " " ^ gen_pattern ctx sl_pat ^ " = " ^ tmp ^ ";" | ||
| end else | ||
|
Comment on lines
+1968
to
+1972
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| kw ^ " " ^ gen_pattern ctx sl_pat ^ " = " | ||
| ^ gen_expr ctx sl_value ^ ";" | ||
| in | ||
| (* Track an [Int]-bound name for the rest of this body (#478). A | ||
| block statement's scope is the enclosing function, which | ||
| {!enter_fn_scope} already bounds, so no restore is needed here. *) | ||
|
|
@@ -1961,11 +2058,11 @@ let visibility_is_public = function | |
| let gen_body ctx (fb : fn_body) : unit = | ||
| match fb with | ||
| | FnExpr e -> | ||
| emit_line ctx ("return " ^ gen_expr ctx e ^ ";") | ||
| emit_line ctx (gen_return_tail ctx e) | ||
| | FnBlock block -> | ||
| List.iter (fun s -> emit_line ctx (gen_stmt ctx s)) block.blk_stmts; | ||
| (match block.blk_expr with | ||
| | Some e -> emit_line ctx ("return " ^ gen_expr ctx e ^ ";") | ||
| | Some e -> emit_line ctx (gen_return_tail ctx e) | ||
| | None -> ()) | ||
| | FnExtern -> () | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -555,6 +555,17 @@ and eval_binop (op : binary_op) (left : value) (right : value) : value result = | |
| | (VFloat a, VFloat b) -> binop_float op a b | ||
| | (VString a, VString b) -> binop_string op a b | ||
| | (VBool a, VBool b) -> binop_bool op a b | ||
| (* The checker accepts `++` on arrays, `==`/`!=` at every type and the | ||
| relational operators on Char; without these arms such well-typed | ||
| programs stopped with "Type mismatch in binary operation" at run | ||
| time (kyq's line splitter, `out ++ [cur]` and `ch == '\n'`). *) | ||
| | (VArray a, VArray b) when op = OpConcat -> Ok (VArray (Array.append a b)) | ||
|
Comment on lines
+559
to
+562
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| | (VChar a, VChar b) when op = OpLt -> Ok (VBool (a < b)) | ||
| | (VChar a, VChar b) when op = OpLe -> Ok (VBool (a <= b)) | ||
| | (VChar a, VChar b) when op = OpGt -> Ok (VBool (a > b)) | ||
| | (VChar a, VChar b) when op = OpGe -> Ok (VBool (a >= b)) | ||
| | _ when op = OpEq -> Ok (VBool (Value.value_eq left right)) | ||
| | _ when op = OpNe -> Ok (VBool (not (Value.value_eq left right))) | ||
| | _ -> Error (TypeMismatch "Type mismatch in binary operation") | ||
|
|
||
| (** Apply function to arguments *) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 6187
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 42302
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 38387
Use the document’s package root in the language server.
When the server starts outside the package root, it can report
Kyaml.Typesas missing even though CLI checking finds it. Resolution failure also clears the document’s navigation cache. Derive the loader root from the document path and parsed module declaration, as the CLI does.Script executed:
Repository: hyperpolymath/affinescript
Length of output: 2310
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 42171
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 42083
Web query:
OCaml 4.14 stdlib filename.ml source dirname basename concat dot components githubResult:
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 18121
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 12427
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 582
Web query:
OCaml Cmdliner Arg.file converter source returns original filename parse existing file normalizationResult:
Normalise the source directory before matching module paths.
When
Kyaml/./Parser.affinedeclaresKyaml.Parser, the trailing.prevents the root match. The CLI can then reportKyaml.Typesas missing even when the sibling module exists. Normalise the directory before matching it, while preserving filesystem path semantics.Script executed:
Repository: hyperpolymath/affinescript
Length of output: 8821
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 42066
Script executed:
Repository: hyperpolymath/affinescript
Length of output: 42415
Keep the source directory in the module search path.
When
src/A/Main.affinedeclaresA.Main,use Helper::{seven}no longer findssrc/A/Helper.affine. This breaks check, compile and eval for previously supported imports. Search the source directory as well as the package root, and add a regression test.