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
23 changes: 14 additions & 9 deletions lib/resolve.ml
Original file line number Diff line number Diff line change
Expand Up @@ -669,15 +669,20 @@ let rec resolve_and_typecheck_module
| Ok () -> resolve_decl mod_ctx decl
) (Ok ()) prog.prog_decls in

(* Type-check all declarations. *)
let type_result = List.fold_left (fun acc decl ->
match acc with
| Error e -> Error e
| Ok () -> Typecheck.check_decl type_ctx decl
) (Ok ()) prog.prog_decls in

match type_result with
| Ok () -> Ok (symbols, type_ctx)
(* Type-check via [check_program] (NOT a raw check_decl fold): it runs
the forward-declaration pass that pre-registers every function
signature, so a module with internal forward references — e.g.
collections.affine's `binary_search` calling the later
`binary_search_helper` — type-checks on the import path exactly as
it does standalone. The imports this module resolved above were
written into [type_ctx.name_types]; thread them in as [import_types]
so [check_program]'s fresh context still sees them. (#128 coherence:
imported modules must check the same way top-level programs do.) *)
match
Typecheck.check_program
~import_types:type_ctx.Typecheck.name_types symbols prog
with
| Ok final_ctx -> Ok (symbols, final_ctx)
| Error type_err ->
let msg = Typecheck.show_type_error type_err in
Error (ImportError ("Type checking failed: " ^ msg), Span.dummy)
Expand Down
2 changes: 1 addition & 1 deletion stdlib/collections.affine
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use prelude::{Option, Some, None, filter, map};
// ============================================================================

/// Reverse a list
fn reverse<T>(list: [T]) -> [T] {
pub fn reverse<T>(list: [T]) -> [T] {
let mut result = [];
for x in list {
result = [x] ++ result;
Expand Down
2 changes: 1 addition & 1 deletion stdlib/option.affine
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ fn expect<T>(opt: Option<T>, msg: String) -> T {
}

/// Unwrap or provide default value
fn unwrap_or<T>(opt: Option<T>, default: T) -> T {
pub fn unwrap_or<T>(opt: Option<T>, default: T) -> T {
match opt {
Some(value) => value,
None => default
Expand Down
3 changes: 2 additions & 1 deletion test/dune
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
(deps
(source_tree golden)
(source_tree e2e)
(source_tree ../examples)))
(source_tree ../examples)
(source_tree ../stdlib)))
2 changes: 1 addition & 1 deletion test/test_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ let () =
(* ("Parser", Test_parser.tests); *) (* TODO: Re-enable when test_parser is implemented *)
("Golden", Test_golden.tests);
("Examples", Test_golden.example_tests);
] @ Test_e2e.tests)
] @ Test_e2e.tests @ Test_stdlib_aot.tests)
145 changes: 145 additions & 0 deletions test/test_stdlib_aot.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
(* SPDX-License-Identifier: PMPL-1.0-or-later *)
(* SPDX-FileCopyrightText: 2025 hyperpolymath *)

(** STAGE-A closers (affinescript#128):

- #136: stdlib-wide AOT compile-smoke gate — every [stdlib/*.affine]
driven through resolve -> typecheck -> borrow -> codegen
(Deno-ESM), so the AOT path cannot silently rot again.
- #137: a multi-module integration program that [use]s several
stdlib modules together in one compiled unit, proving cross-module
resolution/typecheck/codegen works as a coherent set. *)

open Affinescript

(* Locate the stdlib directory from the test's runtime cwd
(`_build/default/test`). The `(source_tree ../stdlib)` dep
materialises it; fall back to a couple of plausible roots. *)
let stdlib_dir =
let candidates =
(match Sys.getenv_opt "AFFINESCRIPT_STDLIB" with
| Some d -> [ d ] | None -> [])
@ [ "../stdlib"; "stdlib"; "../../stdlib" ]
in
match
List.find_opt
(fun d -> Sys.file_exists (Filename.concat d "prelude.affine"))
candidates
with
| Some d -> d
| None -> failwith "test_stdlib_aot: cannot locate stdlib/ (no prelude.affine)"

let loader () =
let cfg =
{ (Module_loader.default_config ()) with
Module_loader.stdlib_path = stdlib_dir }
in
Module_loader.create cfg

(** Full AOT pipeline: resolve -> typecheck -> borrow -> Deno-ESM codegen.
Returns [Ok js] (the emitted ES-module source) or [Error stage-msg]. *)
let pipeline_to_deno (prog : Ast.program) : (string, string) result =
let ld = loader () in
match Resolve.resolve_program_with_loader prog ld with
| Error (e, sp) ->
Error (Printf.sprintf "resolve: %s @ %s"
(Resolve.show_resolve_error e) (Span.show sp))
| Ok (rctx, itc) ->
(match
Typecheck.check_program
~import_types:itc.Typecheck.name_types rctx.symbols prog
with
| Error e ->
Error (Printf.sprintf "typecheck: %s" (Typecheck.format_type_error e))
| Ok _ ->
(match Borrow.check_program rctx.symbols prog with
| Error e ->
Error (Printf.sprintf "borrow: %s" (Borrow.format_borrow_error e))
| Ok () ->
let flat = Module_loader.flatten_imports ld prog in
(match Codegen_deno.codegen_deno flat rctx.symbols with
| Error e -> Error (Printf.sprintf "deno-codegen: %s" e)
| Ok js -> Ok js)))

let parse_file_safe path =
try Ok (Parse_driver.parse_file path)
with
| Parse_driver.Parse_error (msg, span) ->
Error (Printf.sprintf "parse: %s @ %s" msg (Span.show span))
| e -> Error (Printf.sprintf "parse: %s" (Printexc.to_string e))

(* ---- #136: stdlib-wide AOT compile-smoke gate -------------------------- *)

let stdlib_files () =
Sys.readdir stdlib_dir
|> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".affine")
|> List.sort compare
|> List.map (fun f -> Filename.concat stdlib_dir f)

let check_one path () =
let result =
match parse_file_safe path with
| Error m -> Error m
| Ok prog -> pipeline_to_deno prog
in
match result with
| Ok js ->
Alcotest.(check bool)
(Printf.sprintf "%s emits a non-empty ES module" (Filename.basename path))
true (String.length js > 0)
| Error m ->
Alcotest.failf "AOT pipeline failed for %s: %s"
(Filename.basename path) m

let aot_smoke_tests =
List.map
(fun path ->
Alcotest.test_case
(Printf.sprintf "AOT %s" (Filename.basename path))
`Quick (check_one path))
(stdlib_files ())

(* ---- #137: multi-module integration ----------------------------------- *)

(* One compiled program that pulls in several stdlib modules together and
actually uses a symbol from each, exercising cross-module
resolution/typecheck/codegen as a coherent set (not file-by-file). *)
let integration_src = {|
use prelude::{ Option, Some, None };
use string::{ split, join };
use option::{ unwrap_or };
use collections::{ reverse };

fn pipeline(csv: String) -> String {
let parts = split(csv, ",");
let flipped = reverse(parts);
join(flipped, "-")
}

fn first_or(xs: [String], dflt: String) -> String {
let head: Option<String> = if len(xs) > 0 { Some(xs[0]) } else { None };
unwrap_or(head, dflt)
}
|}

let test_multi_module_integration () =
match Parse_driver.parse_string ~file:"<integration>" integration_src with
| exception e ->
Alcotest.failf "integration parse raised: %s" (Printexc.to_string e)
| prog ->
(match pipeline_to_deno prog with
| Ok js ->
Alcotest.(check bool)
"multi-module program compiles to a non-empty ES module"
true (String.length js > 0)
| Error m ->
Alcotest.failf "multi-module integration failed: %s" m)

let integration_tests =
[ Alcotest.test_case "string+option+collections together" `Quick
test_multi_module_integration ]

let tests =
[ ("STAGE-A AOT smoke (#136)", aot_smoke_tests);
("STAGE-A multi-module integration (#137)", integration_tests) ]
Loading