From fd00f725f55bbbefd9a186a2baafdfaa04ded775 Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Wed, 29 Apr 2026 18:15:08 +0200 Subject: [PATCH] step --- bin/main.ml | 14 +------ examples/modules/math.spooky | 3 ++ examples/modules/types.spooky | 4 ++ examples/with_imports.spooky | 9 ++++ lib/module_system.ml | 79 +++++++++++++++++++++++++++++++++++ lib/module_system.mli | 1 + lib/spooky.ml | 15 +++++++ lib/spooky.mli | 4 ++ test/dune | 2 +- test/test_spooky.ml | 27 ++++++++++++ 10 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 examples/modules/math.spooky create mode 100644 examples/modules/types.spooky create mode 100644 examples/with_imports.spooky create mode 100644 lib/module_system.ml create mode 100644 lib/module_system.mli diff --git a/bin/main.ml b/bin/main.ml index 5f1fcac..0dd4af1 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -1,11 +1,5 @@ type generator = Json | C -let read_file path = - let ic = open_in_bin path in - Fun.protect - ~finally:(fun () -> close_in ic) - (fun () -> really_input_string ic (in_channel_length ic)) - let write_file path content = let oc = open_out_bin path in Fun.protect @@ -36,14 +30,8 @@ let () = prerr_endline "Missing required --input argument."; Arg.usage specs usage; exit 2); - let src = - try read_file !input_path - with Sys_error msg -> - prerr_endline ("Input error: " ^ msg); - exit 1 - in let ast = - match Spooky.parse_string src with + match Spooky.parse_file !input_path with | Error msg -> prerr_endline msg; exit 1 diff --git a/examples/modules/math.spooky b/examples/modules/math.spooky new file mode 100644 index 0000000..39a290b --- /dev/null +++ b/examples/modules/math.spooky @@ -0,0 +1,3 @@ +int add(int a, int b) { + return a + b; +} diff --git a/examples/modules/types.spooky b/examples/modules/types.spooky new file mode 100644 index 0000000..9b6c97c --- /dev/null +++ b/examples/modules/types.spooky @@ -0,0 +1,4 @@ +struct Point { + int x; + int y; +}; diff --git a/examples/with_imports.spooky b/examples/with_imports.spooky new file mode 100644 index 0000000..eb09e79 --- /dev/null +++ b/examples/with_imports.spooky @@ -0,0 +1,9 @@ +import "modules/types.spooky"; +import "modules/math.spooky"; + +int main() { + struct Point p; + p.x = 1; + p.y = 2; + return add(p.x, p.y); +} diff --git a/lib/module_system.ml b/lib/module_system.ml new file mode 100644 index 0000000..7932db1 --- /dev/null +++ b/lib/module_system.ml @@ -0,0 +1,79 @@ +module StringSet = Set.Make (String) + +let starts_with ~prefix s = + let lp = String.length prefix in + String.length s >= lp && String.sub s 0 lp = prefix + +let trim = String.trim + +let parse_import_line line = + let t = trim line in + if not (starts_with ~prefix:"import" t) then None + else + let rest = trim (String.sub t 6 (String.length t - 6)) in + if String.length rest < 2 || rest.[0] <> '"' then + Some (Error ("malformed import: " ^ line)) + else + try + let q2 = String.index_from rest 1 '"' in + let path = String.sub rest 1 (q2 - 1) in + let tail = trim (String.sub rest (q2 + 1) (String.length rest - q2 - 1)) in + let valid_tail = + String.equal tail ";" + || String.equal tail "" + || (starts_with ~prefix:";" tail && starts_with ~prefix:"//" (trim (String.sub tail 1 (String.length tail - 1)))) + in + if not valid_tail then Some (Error ("malformed import terminator: " ^ line)) else Some (Ok path) + with Not_found -> Some (Error ("unterminated import string: " ^ line)) + +let read_file path = + let ic = open_in_bin path in + Fun.protect + ~finally:(fun () -> close_in ic) + (fun () -> really_input_string ic (in_channel_length ic)) + +let resolve_path ~from_dir p = if Filename.is_relative p then Filename.concat from_dir p else p + +let load_source_with_imports entry_path = + let rec load_file stack visited path = + if List.mem path stack then + Error ("import cycle detected at: " ^ path) + else if StringSet.mem path visited then Ok (visited, "") + else + let content = + try Ok (read_file path) with Sys_error msg -> Error ("cannot read " ^ path ^ ": " ^ msg) + in + match content with + | Error _ as e -> e + | Ok content -> + let dir = Filename.dirname path in + let lines = String.split_on_char '\n' content in + let rec scan imports body = function + | [] -> Ok (List.rev imports, List.rev body) + | ln :: tl -> + (match parse_import_line ln with + | None -> scan imports (ln :: body) tl + | Some (Ok p) -> scan (p :: imports) body tl + | Some (Error e) -> Error e) + in + (match scan [] [] lines with + | Error _ as e -> e + | Ok (imports, body_lines) -> + let rec load_imports vis acc = function + | [] -> Ok (vis, List.rev acc) + | imp :: tl -> + let resolved = resolve_path ~from_dir:dir imp in + (match load_file (path :: stack) vis resolved with + | Error _ as e -> e + | Ok (vis', src) -> load_imports vis' (src :: acc) tl) + in + match load_imports visited [] imports with + | Error _ as e -> e + | Ok (visited', imported_sources) -> + let self_src = String.concat "\n" body_lines in + let full = String.concat "\n" (imported_sources @ [ self_src ]) in + Ok (StringSet.add path visited', full)) + in + match load_file [] StringSet.empty entry_path with + | Error _ as e -> e + | Ok (_, src) -> Ok src diff --git a/lib/module_system.mli b/lib/module_system.mli new file mode 100644 index 0000000..fa1e9f9 --- /dev/null +++ b/lib/module_system.mli @@ -0,0 +1 @@ +val load_source_with_imports : string -> (string, string) result diff --git a/lib/spooky.ml b/lib/spooky.ml index dede2b7..7b62157 100644 --- a/lib/spooky.ml +++ b/lib/spooky.ml @@ -4,12 +4,19 @@ module Parser = Parser module Typechecker = Typechecker module Generator_json = Generator_json module Generator_c = Generator_c +module Module_system = Module_system type program = Ast.program let parse_string = Parser.parse_string let string_of_program = Ast.string_of_program let type_check = Typechecker.type_check +let load_source_with_imports = Module_system.load_source_with_imports + +let parse_file path = + match load_source_with_imports path with + | Error e -> Error e + | Ok src -> parse_string src let parse_and_type_check src = match parse_string src with @@ -19,5 +26,13 @@ let parse_and_type_check src = | Ok () -> Ok prog | Error e -> Error ("type error: " ^ e)) +let parse_and_type_check_file path = + match parse_file path with + | Error e -> Error e + | Ok prog -> + (match type_check prog with + | Ok () -> Ok prog + | Error e -> Error ("type error: " ^ e)) + let generate_json = Generator_json.generate let generate_c = Generator_c.generate diff --git a/lib/spooky.mli b/lib/spooky.mli index 3e79089..2b3092f 100644 --- a/lib/spooky.mli +++ b/lib/spooky.mli @@ -4,12 +4,16 @@ module Parser = Parser module Typechecker = Typechecker module Generator_json = Generator_json module Generator_c = Generator_c +module Module_system = Module_system type program = Ast.program val parse_string : string -> (program, string) result +val load_source_with_imports : string -> (string, string) result +val parse_file : string -> (program, string) result val string_of_program : program -> string val type_check : program -> (unit, string) result val parse_and_type_check : string -> (program, string) result +val parse_and_type_check_file : string -> (program, string) result val generate_json : program -> string val generate_c : program -> string diff --git a/test/dune b/test/dune index 4e8448c..b1a0ea7 100644 --- a/test/dune +++ b/test/dune @@ -1,3 +1,3 @@ (test (name test_spooky) - (libraries spooky)) + (libraries spooky unix)) diff --git a/test/test_spooky.ml b/test/test_spooky.ml index 37c3639..fdc03d1 100644 --- a/test/test_spooky.ml +++ b/test/test_spooky.ml @@ -46,7 +46,34 @@ let test_invalid_program () = | Ok _ -> failwith "expected type error, but got success" | Error _ -> () +let write_file path content = + let oc = open_out_bin path in + Fun.protect ~finally:(fun () -> close_out oc) (fun () -> output_string oc content) + +let test_imports () = + let base = Filename.concat (Filename.get_temp_dir_name ()) "spooky_import_test" in + let modules_dir = Filename.concat base "modules" in + Unix.mkdir base 0o755; + Unix.mkdir modules_dir 0o755; + let cleanup () = + (try Sys.remove (Filename.concat modules_dir "math.spooky") with _ -> ()); + (try Sys.remove (Filename.concat base "main.spooky") with _ -> ()); + (try Unix.rmdir modules_dir with _ -> ()); + (try Unix.rmdir base with _ -> ()) + in + Fun.protect + ~finally:cleanup + (fun () -> + write_file (Filename.concat modules_dir "math.spooky") + "int add(int a, int b) { return a + b; }\n"; + write_file (Filename.concat base "main.spooky") + "import \"modules/math.spooky\";\nint main() { return add(1, 2); }\n"; + match Spooky.parse_and_type_check_file (Filename.concat base "main.spooky") with + | Ok _ -> () + | Error msg -> failwith ("expected valid import program, got: " ^ msg)) + let () = test_valid_program (); test_invalid_program (); + test_imports (); print_endline "All parser/type-check tests passed."