A package may import a package, and may be one file
Three limitations, and the same program wanted all three gone. An imported package's own imports were refused by name. They are resolved now, and the qualification flattens to the inner alias: raylib imported by a package that is itself imported is still rl/..., never sand/rl/.... That is forced, not chosen — a directory reached along two routes has to arrive under one set of names or the checker sees every declaration twice — and it is what lets the dedupe work. A directory is keyed by its real path and read once, which also ends a cycle: a package that imports itself meets its own entry and contributes nothing the second time, and since the namespace is flat, mutually dependent packages simply work. The same directory under two different aliases is refused, because both cannot be true at once. main is not exported. A package carrying one would collide with the importer's the moment anything imported it, so a program could never be a package; and main is a root, so an imported one keeps everything it calls reachable — for a raylib front-end, the whole library, on the target that cannot link it. Writing sand/main is refused at the line that wrote it rather than left to the checker, which would only say the name is unknown. That is true and useless: the name is missing on purpose and the message should say which purpose. A package that calls its own main is refused too — it would silently get the importer's. And a package may be a single .flan file named outright. sand.flan shares the repository root with three other loose programs, so naming its directory would import all four; moving it into a directory of its own would be arranging the tree around a limitation. A file carries no .c and no link file — those belong to a directory, and a package that needs them has one.
This commit is contained in:
parent
0e88954664
commit
d59c72af60
346
lib/load.ml
346
lib/load.ml
@ -15,9 +15,19 @@
|
||||
downstream knows a package existed; the checker sees one flat list of
|
||||
declarations with names that happen to contain a slash.
|
||||
|
||||
That is not a module system yet. There is no visibility, no cycle
|
||||
detection, and a package cannot import another one — milestone 4 needs one
|
||||
package, imported once, and the rest can wait for a use that exercises it.
|
||||
A package may import a package. The qualification flattens to the *inner*
|
||||
alias — raylib imported by a package that is itself imported is still
|
||||
[rl/...] — because a directory reached along two routes has to arrive under
|
||||
one set of names or the checker sees every declaration twice. Two importers
|
||||
of one directory load it once, keyed by its real path; the same directory
|
||||
under two different aliases is refused, and so is a cycle.
|
||||
|
||||
Visibility is one rule so far: [main] is not exported. A package carrying
|
||||
one would collide with the importer's, and worse, would keep everything it
|
||||
calls reachable (see [Reach]) — which for a raylib front-end is the whole
|
||||
library, on the target that cannot link it. Package-private markers for
|
||||
anything else are still missing, which is why [rl/get-color-raw] is
|
||||
callable.
|
||||
|
||||
A package may also carry the C it binds to. Every [.c] file in the
|
||||
directory is compiled into the build, and a file named [link] lists extra
|
||||
@ -67,16 +77,27 @@ let rec find_collection dir name =
|
||||
let parent = Filename.dirname dir in
|
||||
if String.equal parent dir then None else find_collection parent name
|
||||
|
||||
(* A package is a directory, or a single [.flan] file named outright. The file
|
||||
form is for the program that is also a library: sand.flan sits beside three
|
||||
other loose .flan files, so naming its directory would import all four, and
|
||||
moving it into one of its own would be arranging the tree around a
|
||||
limitation. A file carries no [.c] and no [link] — those belong to a
|
||||
directory, and a package that needs them has one. *)
|
||||
let is_package_file path =
|
||||
Filename.check_suffix path ".flan" && Sys.file_exists path
|
||||
&& not (Sys.is_directory path)
|
||||
|
||||
let resolve_dir ~file loc path =
|
||||
let here =
|
||||
let d = Filename.dirname file in
|
||||
if Filename.is_relative d then Filename.concat (Sys.getcwd ()) d else d
|
||||
in
|
||||
let ok d = (Sys.file_exists d && Sys.is_directory d) || is_package_file d in
|
||||
match split_path path with
|
||||
| None, rel ->
|
||||
let d = Filename.concat here rel in
|
||||
if Sys.file_exists d && Sys.is_directory d then d
|
||||
else fail loc "no package directory at %s" d
|
||||
if ok d then d else fail loc "no package at %s — wanted a directory or a \
|
||||
.flan file" d
|
||||
| Some collection, rel ->
|
||||
(match find_collection here collection with
|
||||
| None ->
|
||||
@ -85,8 +106,7 @@ let resolve_dir ~file loc path =
|
||||
there is none" collection collection here
|
||||
| Some root ->
|
||||
let d = Filename.concat root rel in
|
||||
if Sys.file_exists d && Sys.is_directory d then d
|
||||
else fail loc "the package %s is not at %s" path d)
|
||||
if ok d then d else fail loc "the package %s is not at %s" path d)
|
||||
|
||||
let entries dir suffix =
|
||||
Sys.readdir dir
|
||||
@ -254,58 +274,290 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
|
||||
ret = Option.map (rename_texpr owned alias) fn.Ast.ret;
|
||||
fbody = List.map (rename_expr owned alias bound) fn.Ast.fbody }
|
||||
| Ast.Package _ -> Ast.Package alias
|
||||
| Ast.Import _ ->
|
||||
fail loc "an imported package may not import another one yet (milestone 4)"
|
||||
(* A package's own imports were resolved before this ran and are not in
|
||||
the list it is given, so one arriving here is a bug in [import] rather
|
||||
than anything a user wrote. *)
|
||||
| Ast.Import (a, _) ->
|
||||
fail loc "internal: the import of %s was not resolved before qualifying" a
|
||||
| Ast.Defunion (n, _) ->
|
||||
fail loc "%s is a union, and an imported union is not implemented yet \
|
||||
(milestone 4)" n
|
||||
in
|
||||
{ d with Ast.d = k }
|
||||
|
||||
(* ── Visibility ────────────────────────────────────────────────────── *)
|
||||
|
||||
(* The one rule so far: [main] is not a name a package offers.
|
||||
|
||||
There is a single top-level namespace and an import is a rename into it
|
||||
(check.ml), so a package carrying a [main] would collide with the importer's
|
||||
the moment anything imported it — a program could never be a package. And
|
||||
the collision is the smaller half. [main] is a *root*: [Reach] starts there,
|
||||
so an imported one keeps everything it calls alive. A raylib front-end
|
||||
imported for its simulation would drag the whole library back in, on the
|
||||
target that cannot link it, which is the thing the reachable-link change
|
||||
exists to prevent.
|
||||
|
||||
So the package's [main] is dropped rather than qualified, and [alias/main]
|
||||
is not a name. Everything else is still exported; package-private markers
|
||||
are a separate gap (NEXT.md, Packages — [rl/get-color-raw] should not be
|
||||
callable either). *)
|
||||
let exported n = not (String.equal n "main")
|
||||
|
||||
(* Where a name is *used*, which is what a refusal has to point at. A rename
|
||||
does not need this — it rebuilds the tree and the failure is a mismatch
|
||||
later — but "you cannot see that name" has to name the line that tried.
|
||||
|
||||
Only top-level name positions are collected: a field, a keyword, an enum
|
||||
member and a restart name are none of them, exactly as in the rename above.
|
||||
Local bindings are not tracked, because the names this guards are ones no
|
||||
local can be called: a [let] named [sim/main] does not parse. *)
|
||||
let rec texpr_uses acc (t : Ast.texpr) =
|
||||
match t.Ast.t with
|
||||
| Ast.Tname n -> acc := (n, t.Ast.tloc) :: !acc
|
||||
| Ast.Tslice e -> texpr_uses acc e
|
||||
| Ast.Tarray (l, e) ->
|
||||
(match l with Ast.Lname n -> acc := (n, t.Ast.tloc) :: !acc | Ast.Lint _ -> ());
|
||||
texpr_uses acc e
|
||||
| Ast.Tmap (k, v) -> texpr_uses acc k; texpr_uses acc v
|
||||
| Ast.Tapp (_, args) -> List.iter (texpr_uses acc) args
|
||||
| Ast.Tfn (ps, r) -> List.iter (texpr_uses acc) ps; texpr_uses acc r
|
||||
|
||||
let rec expr_uses acc (e : Ast.expr) =
|
||||
let go = expr_uses acc in
|
||||
let gos = List.iter go in
|
||||
match e.Ast.e with
|
||||
| Ast.Int _ | Ast.Float _ | Ast.Byte _ | Ast.Str _ | Ast.Kw _ | Ast.Quote _
|
||||
| Ast.InvokeRestart _ -> ()
|
||||
| Ast.Var n -> acc := (n, e.Ast.loc) :: !acc
|
||||
| Ast.Do body -> gos body
|
||||
| Ast.Let (bs, body) ->
|
||||
List.iter
|
||||
(fun (b : Ast.binding) ->
|
||||
Option.iter (texpr_uses acc) b.Ast.bty; go b.Ast.bval)
|
||||
bs;
|
||||
gos body
|
||||
| Ast.If (c, t, e') -> go c; go t; Option.iter go e'
|
||||
| Ast.While (c, body) -> go c; gos body
|
||||
| Ast.Return v -> Option.iter go v
|
||||
| Ast.Set (p, v) -> place_uses acc e.Ast.loc p; go v
|
||||
| Ast.Field (t, _) -> go t
|
||||
| Ast.Call (h, args) -> go h; gos args
|
||||
| Ast.Match (sc, arms) ->
|
||||
go sc; List.iter (fun (a : Ast.arm) -> gos a.Ast.body) arms
|
||||
| Ast.Struct (n, kvs) ->
|
||||
acc := (n, e.Ast.loc) :: !acc;
|
||||
List.iter (fun (_, v) -> go v) kvs
|
||||
| Ast.Arr items -> gos items
|
||||
| Ast.Fn (_, body) -> gos body
|
||||
| Ast.Dotimes (_, n, body) -> go n; gos body
|
||||
| Ast.Defer body -> gos body
|
||||
| Ast.Unwrap (_, v) -> go v
|
||||
| Ast.Signal (_, c) -> go c
|
||||
| Ast.RestartCase (body, clauses) ->
|
||||
go body;
|
||||
List.iter (fun (c : Ast.rclause) -> gos c.Ast.rbody) clauses
|
||||
| Ast.HandlerBind (clauses, body) ->
|
||||
List.iter
|
||||
(fun (c : Ast.hclause) -> texpr_uses acc c.Ast.hty; gos c.Ast.hbody)
|
||||
clauses;
|
||||
gos body
|
||||
|
||||
(* A place carries no location of its own, so it borrows the [set] form's. *)
|
||||
and place_uses acc loc (p : Ast.place) =
|
||||
match p with
|
||||
| Ast.Pvar n -> acc := (n, loc) :: !acc
|
||||
| Ast.Pfield (t, _) -> expr_uses acc t
|
||||
| Ast.Pindex (t, idx) -> expr_uses acc t; List.iter (expr_uses acc) idx
|
||||
| Ast.Pderef t -> expr_uses acc t
|
||||
|
||||
let decl_uses acc (d : Ast.decl) =
|
||||
let field (f : Ast.field) = texpr_uses acc f.Ast.fty in
|
||||
let fn (f : Ast.fn) =
|
||||
List.iter field f.Ast.params;
|
||||
Option.iter (texpr_uses acc) f.Ast.ret;
|
||||
List.iter (expr_uses acc) f.Ast.fbody
|
||||
in
|
||||
match d.Ast.d with
|
||||
| Ast.Package _ | Ast.Import _ | Ast.Defenum _ -> ()
|
||||
| Ast.Defalias (_, t) -> texpr_uses acc t
|
||||
| Ast.Defstruct (_, fs) -> List.iter field fs
|
||||
| Ast.Defunion (_, vs) ->
|
||||
List.iter (fun (v : Ast.variant) -> List.iter field v.Ast.vfields) vs
|
||||
| Ast.Defn f -> fn f
|
||||
| Ast.Declare (f, _) ->
|
||||
List.iter field f.Ast.params;
|
||||
Option.iter (texpr_uses acc) f.Ast.ret
|
||||
| Ast.Defvar (_, t, init) ->
|
||||
Option.iter (texpr_uses acc) t;
|
||||
(match init with Ast.Init v -> expr_uses acc v | _ -> ())
|
||||
| Ast.Defconst (_, t, v) ->
|
||||
Option.iter (texpr_uses acc) t; expr_uses acc v
|
||||
|
||||
let uses (ds : Ast.decl list) =
|
||||
let acc = ref [] in
|
||||
List.iter (decl_uses acc) ds;
|
||||
List.rev !acc
|
||||
|
||||
(* The refusal, by name and at the line that tried. [hidden] maps a name that
|
||||
cannot be seen to the reason it cannot. *)
|
||||
let refuse_hidden hidden ds =
|
||||
if hidden <> [] then
|
||||
List.iter
|
||||
(fun (n, loc) ->
|
||||
match List.assoc_opt n hidden with
|
||||
| None -> ()
|
||||
| Some why -> fail loc "%s" why)
|
||||
(uses ds)
|
||||
|
||||
(* Every top-level name the package declares — types and values alike, since a
|
||||
use site is rewritten by name and the two never collide in one namespace. *)
|
||||
let owned_names (ds : Ast.decl list) =
|
||||
List.filter_map Ast.declared_name ds
|
||||
|
||||
let import ~loc alias dir =
|
||||
let files = entries dir ".flan" in
|
||||
if files = [] then fail loc "the package at %s has no .flan file" dir;
|
||||
let ds = List.concat_map (fun f -> Parse.program (Reader.read_file f)) files in
|
||||
let owned = owned_names ds in
|
||||
let decls = List.map (qualify_decl owned alias) ds in
|
||||
let lflags =
|
||||
let path = Filename.concat dir "link" in
|
||||
if not (Sys.file_exists path) then []
|
||||
else begin
|
||||
let ch = open_in path in
|
||||
let rec go acc =
|
||||
match input_line ch with
|
||||
| line ->
|
||||
let line = String.trim line in
|
||||
go (if line = "" || line.[0] = '#' then acc else line :: acc)
|
||||
| exception End_of_file -> List.rev acc
|
||||
in
|
||||
let r = go [] in
|
||||
close_in ch; r
|
||||
end
|
||||
in
|
||||
let csrcs = entries dir ".c" in
|
||||
{ decls; csrcs; lflags;
|
||||
pkgs = [ { alias; dir; owns = owned; pcsrcs = csrcs; plflags = lflags } ] }
|
||||
(* The [link] file: extra linker arguments, one per line, blank lines and
|
||||
comments ignored. *)
|
||||
let link_flags dir =
|
||||
let path = Filename.concat dir "link" in
|
||||
if not (Sys.file_exists path) then []
|
||||
else begin
|
||||
let ch = open_in path in
|
||||
let rec go acc =
|
||||
match input_line ch with
|
||||
| line ->
|
||||
let line = String.trim line in
|
||||
go (if line = "" || line.[0] = '#' then acc else line :: acc)
|
||||
| exception End_of_file -> List.rev acc
|
||||
in
|
||||
let r = go [] in
|
||||
close_in ch; r
|
||||
end
|
||||
|
||||
let real dir = try Unix.realpath dir with Unix.Unix_error _ -> dir
|
||||
|
||||
(* One package, and whatever it imports.
|
||||
|
||||
A package may import a package. The qualification flattens to the *inner*
|
||||
alias: if sand/ imports vendor:raylib as [rl], the names are [rl/...] in the
|
||||
finished program and not [sand/rl/...]. That is forced rather than chosen —
|
||||
a directory imported along two routes has to arrive with one set of names,
|
||||
or the checker sees every declaration twice — and it is what makes the
|
||||
dedupe below coherent.
|
||||
|
||||
[seen] is that dedupe, keyed by the real path, so raylib imported by the
|
||||
program and again by a package it imports is loaded once. It is also what
|
||||
terminates a cycle, and there is nothing else to do about one: a directory
|
||||
is entered before it is read, so a package that imports itself — directly or
|
||||
round a ring — meets its own entry and contributes nothing the second time.
|
||||
The namespace is flat, so mutually dependent packages then simply work. *)
|
||||
let rec import ~seen ~loc alias dir =
|
||||
let dir' = real dir in
|
||||
match Hashtbl.find_opt seen dir' with
|
||||
| Some previous when String.equal previous alias ->
|
||||
(* Already in, under the same name. Importing it again is a no-op, which
|
||||
is what lets two packages both depend on a third. *)
|
||||
{ decls = []; csrcs = []; lflags = []; pkgs = [] }
|
||||
| Some previous ->
|
||||
fail loc
|
||||
"%s is imported as %s here and as %s elsewhere; one directory is one set \
|
||||
of names, so the two cannot both be true" dir alias previous
|
||||
| None ->
|
||||
Hashtbl.replace seen dir' alias;
|
||||
let one_file = is_package_file dir in
|
||||
let files = if one_file then [ dir ] else entries dir ".flan" in
|
||||
if files = [] then fail loc "the package at %s has no .flan file" dir;
|
||||
let ds =
|
||||
List.concat_map (fun f -> Parse.program (Reader.read_file f)) files
|
||||
in
|
||||
(* [main] is the importer's, always. A package that called its own would
|
||||
get the importer's instead — silently, since the name still resolves —
|
||||
so it is refused here rather than left to mean something else. *)
|
||||
refuse_hidden
|
||||
[ ("main",
|
||||
Printf.sprintf
|
||||
"the package %s calls main, and main belongs to the program that \
|
||||
imports it, not to a package" dir) ]
|
||||
ds;
|
||||
(* What this package imports, resolved first and relative to itself. Its
|
||||
declarations come back already qualified under their own aliases, so the
|
||||
rename below leaves them alone: they are not in [owned]. *)
|
||||
let nested =
|
||||
List.filter_map
|
||||
(fun (d : Ast.decl) ->
|
||||
match d.Ast.d with
|
||||
| Ast.Import (a, path) ->
|
||||
(* Relative to the package itself: for a directory that is the
|
||||
directory, for a single file the one it sits in. *)
|
||||
let file = if one_file then dir else Filename.concat dir "." in
|
||||
let sub = resolve_dir ~file d.Ast.dloc path in
|
||||
Some (import ~seen ~loc:d.Ast.dloc a sub)
|
||||
| _ -> None)
|
||||
ds
|
||||
in
|
||||
let own =
|
||||
List.filter (fun (d : Ast.decl) ->
|
||||
match d.Ast.d with
|
||||
| Ast.Import _ -> false
|
||||
| _ -> (match Ast.declared_name d with
|
||||
| Some n -> exported n
|
||||
| None -> true))
|
||||
ds
|
||||
in
|
||||
let owned = List.filter exported (owned_names ds) in
|
||||
let decls = List.map (qualify_decl owned alias) own in
|
||||
let lflags = if one_file then [] else link_flags dir in
|
||||
let csrcs = if one_file then [] else entries dir ".c" in
|
||||
let here =
|
||||
{ decls; csrcs; lflags;
|
||||
pkgs = [ { alias; dir; owns = owned; pcsrcs = csrcs; plflags = lflags } ] }
|
||||
in
|
||||
List.fold_left
|
||||
(fun acc p ->
|
||||
{ decls = acc.decls @ p.decls;
|
||||
csrcs = acc.csrcs @ p.csrcs;
|
||||
lflags = acc.lflags @ p.lflags;
|
||||
pkgs = acc.pkgs @ p.pkgs })
|
||||
here nested
|
||||
|
||||
(* What an import did *not* bring: the names an importer might reasonably write
|
||||
and that are not there, each with the reason it is not. *)
|
||||
let hidden_of (t : t) =
|
||||
List.filter_map
|
||||
(fun (p : pkg) ->
|
||||
let ds =
|
||||
List.concat_map (fun f -> Parse.program (Reader.read_file f))
|
||||
(if is_package_file p.dir then [ p.dir ] else entries p.dir ".flan")
|
||||
in
|
||||
if List.exists (fun d -> Ast.declared_name d = Some "main") ds then
|
||||
Some (qualify p.alias "main",
|
||||
Printf.sprintf
|
||||
"%s is not a name: %s declares a main, and a main is an entry \
|
||||
point rather than something a package offers"
|
||||
(qualify p.alias "main") p.dir)
|
||||
else None)
|
||||
t.pkgs
|
||||
|
||||
(* ── The one entry point ───────────────────────────────────────────── *)
|
||||
|
||||
let program ~file (decls : Ast.decl list) : t =
|
||||
List.fold_left
|
||||
(fun acc (d : Ast.decl) ->
|
||||
match d.Ast.d with
|
||||
| Ast.Import (alias, path) ->
|
||||
let dir = resolve_dir ~file d.Ast.dloc path in
|
||||
let p = import ~loc:d.Ast.dloc alias dir in
|
||||
{ decls = acc.decls @ p.decls;
|
||||
csrcs = acc.csrcs @ p.csrcs;
|
||||
lflags = acc.lflags @ p.lflags;
|
||||
pkgs = acc.pkgs @ p.pkgs }
|
||||
| _ -> { acc with decls = acc.decls @ [ d ] })
|
||||
{ decls = []; csrcs = []; lflags = []; pkgs = [] }
|
||||
decls
|
||||
let seen = Hashtbl.create 8 in
|
||||
let t =
|
||||
List.fold_left
|
||||
(fun acc (d : Ast.decl) ->
|
||||
match d.Ast.d with
|
||||
| Ast.Import (alias, path) ->
|
||||
let dir = resolve_dir ~file d.Ast.dloc path in
|
||||
let p = import ~seen ~loc:d.Ast.dloc alias dir in
|
||||
{ decls = acc.decls @ p.decls;
|
||||
csrcs = acc.csrcs @ p.csrcs;
|
||||
lflags = acc.lflags @ p.lflags;
|
||||
pkgs = acc.pkgs @ p.pkgs }
|
||||
| _ -> { acc with decls = acc.decls @ [ d ] })
|
||||
{ decls = []; csrcs = []; lflags = []; pkgs = [] }
|
||||
decls
|
||||
in
|
||||
(* Said here rather than left to the checker: [sim/main] would otherwise be
|
||||
"unknown name", which is true and unhelpful — the name is missing on
|
||||
purpose and the message should say which purpose. *)
|
||||
refuse_hidden (hidden_of t) t.decls;
|
||||
t
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user