A macro may call its package, and may read a file at the call site's path
Two things a type provider needs and neither of which a macro could do. A package macro could not call its own package's functions. Load already renamed the body so that (next c) reads (edn/next c) — the intent was written down — and the module was then compiled from the prelude and the defmacros alone, so the call arrived at the checker as "the call edn/next into an imported package". The declarations now travel beside the macros in Parse.imported_decls, trimmed in Macro.compile to what the macro bodies actually reach. raylib's five with-* are pure quasiquote, so nothing of raylib is reachable and its module is the one it always was — which matters, because raylib's declarations are declares against a library a macro module has no linker argument for. And a macro had no way to resolve a path. (embed "assets/x.edn") resolves against the directory of the source file the form is written in; a macro knows the path it was handed and not what it is relative to, because a Form carries no location. So the compiler pokes the call site's directory into two C symbols before every expansion and (macro-slurp "...") joins the two. C data and not a Flan global: the module is emitted with hidden visibility and only the flan.macro.* thunks stay exported. None rather than a condition, which is why this is not slurp: a condition signalled inside an expansion goes through the module's own copy of the runtime, and that is the failure Build.macro_module's hidden note measured.
This commit is contained in:
parent
4a8a78caac
commit
32ff350001
10
lib/load.ml
10
lib/load.ml
@ -926,12 +926,17 @@ let rec import ~seen ~open_ ~loc alias dir =
|
||||
let nested_macros =
|
||||
List.fold_left (fun acc r -> macro_union acc r.macros) [] nested
|
||||
in
|
||||
(* Beside the macros, what those macros may call — see the note over
|
||||
[Parse.imported_decls]. The same set the package's own files are checked
|
||||
against, so a macro of a package this one imports is compiled against
|
||||
exactly what its author could see. *)
|
||||
let nested_decls = List.concat_map (fun r -> r.decls) nested in
|
||||
(* The package's own files, parsed with what it imported in front of them
|
||||
and nothing else. A parent's macros are deliberately not here: this
|
||||
package did not import that parent, and a name it never asked for is not
|
||||
one it should be able to call. *)
|
||||
let ds =
|
||||
Parse.with_imported nested_macros
|
||||
Parse.with_imported ~decls:nested_decls nested_macros
|
||||
(fun () -> List.concat_map (fun (_, forms) -> Parse.program forms) sources)
|
||||
in
|
||||
(* [main] is the importer's, always. A package that called its own would
|
||||
@ -1280,7 +1285,8 @@ let program ?(parse = Parse.program) ~file (forms : Form.t list) : t =
|
||||
(imports_of forms)
|
||||
in
|
||||
let decls =
|
||||
Parse.with_imported (macro_union imported.macros !Parse.imported_macros)
|
||||
Parse.with_imported ~decls:(imported.decls @ !Parse.imported_decls)
|
||||
(macro_union imported.macros !Parse.imported_macros)
|
||||
(fun () -> parse forms)
|
||||
in
|
||||
let t =
|
||||
|
||||
151
lib/macro.ml
151
lib/macro.ml
@ -82,11 +82,23 @@ let self =
|
||||
| Some s when s <> "" -> s
|
||||
| _ -> Build.stamp_of Sys.executable_name)
|
||||
|
||||
let key (extra : Form.t list) =
|
||||
(* [support] is in the key for the same reason the prelude's text is: it is
|
||||
compiled into the module, so a package whose functions changed while its
|
||||
macros did not is a stale [.so] that the extras alone would not notice.
|
||||
[Marshal] and not a printer, because [Ast] has no printer and one written
|
||||
for a cache key would be a second rendering of the tree to keep in step with
|
||||
the first. The declarations are plain data — variants, strings, floats and
|
||||
locations, no closures and no abstract blocks — so the image is structural,
|
||||
and it moves when a location does. That direction is the safe one: a
|
||||
cosmetic edit above a package's functions costs a rebuild of the module, and
|
||||
nothing costs a stale one. *)
|
||||
let key ?(support = []) (extra : Form.t list) =
|
||||
Digest.to_hex
|
||||
(Digest.string
|
||||
(Lazy.force self ^ "\000" ^ Prelude.source ^ "\000"
|
||||
^ String.concat "\000" (List.map Form.to_string extra)))
|
||||
^ String.concat "\000" (List.map Form.to_string extra)
|
||||
^ "\000"
|
||||
^ (if support = [] then "" else Marshal.to_string support [])))
|
||||
|
||||
(* True while a macro module is being built. [Build.macro_module] goes through
|
||||
[Check.program], which parses the prelude, which calls back into
|
||||
@ -163,9 +175,88 @@ let reduce (forms : Form.t list) : Form.t list =
|
||||
| _ -> true)
|
||||
forms
|
||||
|
||||
(* ── What a package's macro may call ────────────────────────────────
|
||||
The header above says a macro body may call prelude functions and other
|
||||
macros and nothing else, and for a macro written in a *package* that was the
|
||||
machinery missing a piece rather than a rule. [Load.qualify_macro] renames
|
||||
the body so a call to the package's own [next] reads [edn/next] — it says
|
||||
the intent plainly — and the module was then compiled without anything of
|
||||
that name in it, so the call arrived at the checker as "the call edn/next
|
||||
into an imported package".
|
||||
|
||||
So [Parse.imported_decls] carries the package's declarations beside its
|
||||
macros, already qualified, and they go into the module. Trimmed to what the
|
||||
macros actually reach, for two reasons that are both about programs whose
|
||||
macros want none of this: raylib's five [with-*] are pure quasiquote, so
|
||||
nothing of raylib is reachable and the module is the one it always was — and
|
||||
raylib's declarations are [declare]s against a library this link has no
|
||||
argument for, so a module that took the whole package would fail to link
|
||||
for every program that draws anything.
|
||||
|
||||
Reachability over names and not over [Reach]'s checked program, because the
|
||||
trim has to happen *before* [Check]: an [Ast.Declare] that survived into the
|
||||
module would be emitted whether or not the checker was ever asked about it.
|
||||
A type is reached the same way a function is — [Load.uses] walks signatures
|
||||
and bodies alike — which is what keeps [edn/Cursor] in when [edn/next] is. *)
|
||||
|
||||
let support (roots : string list) (ds : Ast.decl list) : Ast.decl list =
|
||||
if ds = [] then []
|
||||
else begin
|
||||
let want = Hashtbl.create 64 in
|
||||
List.iter (fun n -> Hashtbl.replace want n ()) roots;
|
||||
(* Fixpoint over the declarations, since a kept one names more. Bounded by
|
||||
their number: the set only grows and a pass that adds nothing stops. *)
|
||||
let changed = ref true in
|
||||
while !changed do
|
||||
changed := false;
|
||||
List.iter
|
||||
(fun (d : Ast.decl) ->
|
||||
match Ast.declared_name d with
|
||||
| Some n when Hashtbl.mem want n ->
|
||||
List.iter
|
||||
(fun (u, _) ->
|
||||
if not (Hashtbl.mem want u) then begin
|
||||
Hashtbl.replace want u ();
|
||||
changed := true
|
||||
end)
|
||||
(Load.uses [ d ])
|
||||
| _ -> ())
|
||||
ds
|
||||
done;
|
||||
List.filter
|
||||
(fun (d : Ast.decl) ->
|
||||
match Ast.declared_name d with
|
||||
| Some n -> Hashtbl.mem want n
|
||||
| None -> false)
|
||||
ds
|
||||
end
|
||||
|
||||
(* The names a macro's text mentions, which is the root set above. Every symbol
|
||||
in the body, because a macro body reaches a package's names as calls, as
|
||||
types in a [let]'s initialiser and as data-type cases — and over-rooting only
|
||||
ever keeps a declaration that would have compiled anyway. *)
|
||||
let roots_of (extra : Form.t list) =
|
||||
List.fold_left (fun acc f -> Load.form_syms f acc) [] extra
|
||||
|
||||
let compile (names : string list) (extra : Form.t list) : loaded =
|
||||
(* The macros themselves are in [imported_decls] too — a [defmacro] is an
|
||||
[Ast.Defn] by the time [Parse] is finished with it, and [Load] qualifies
|
||||
and carries it like any other declaration. They arrive here a second time
|
||||
in [extra], which is where their *current* text is, so the copy in the
|
||||
support set is dropped rather than reaching the checker as a name defined
|
||||
twice. Current matters: a session that has just re-evaluated a macro holds
|
||||
the new body in [macros] and the old one in [decls]. *)
|
||||
let support =
|
||||
List.filter
|
||||
(fun (d : Ast.decl) ->
|
||||
match Ast.declared_name d with
|
||||
| Some n -> not (List.mem n names)
|
||||
| None -> true)
|
||||
(support (roots_of extra) !Parse.imported_decls)
|
||||
in
|
||||
let out =
|
||||
Filename.concat (Build.cachedir ()) ("flan-macros-" ^ key extra ^ ".so")
|
||||
Filename.concat (Build.cachedir ())
|
||||
("flan-macros-" ^ key ~support extra ^ ".so")
|
||||
in
|
||||
if not (Sys.file_exists out) then begin
|
||||
building := true;
|
||||
@ -177,8 +268,13 @@ let compile (names : string list) (extra : Form.t list) : loaded =
|
||||
(fun () ->
|
||||
(* [Check.program] prepends the prelude itself — reduced, for the one
|
||||
build that cannot have all of it — so only the file's own defmacros
|
||||
go in here. *)
|
||||
let p = Check.program (Parse.program extra) in
|
||||
and the package declarations they reach go in here.
|
||||
|
||||
The support comes first: it holds the types a macro's signature
|
||||
names, and a declaration order that mentioned [edn/Cursor] before
|
||||
declaring it would be refused for a reason that is this line's and
|
||||
not the author's. *)
|
||||
let p = Check.program (support @ Parse.program extra) in
|
||||
(* Written beside the final name and renamed, so a second process
|
||||
reading the cache never sees a half-written object. *)
|
||||
let tmp = out ^ "." ^ string_of_int (Unix.getpid ()) in
|
||||
@ -189,6 +285,45 @@ let compile (names : string list) (extra : Form.t list) : loaded =
|
||||
{ handle;
|
||||
fns = List.map (fun n -> (n, Dynload.dl_sym handle ("flan.macro." ^ n))) names }
|
||||
|
||||
(* ── Where the call site is ────────────────────────────────────────
|
||||
The one thing a macro cannot find out for itself and the one it needs to
|
||||
read a data file: a Form carries no location — deliberately, see
|
||||
[Expand.unmarshal] — so a macro handed [(defedn T "assets/x.edn")] knows the
|
||||
path and not what it is relative to. [(embed "assets/x.edn")] resolves
|
||||
against the directory of the source file the form is written in, and a macro
|
||||
reading a file has to resolve it the same way or a package's data would
|
||||
depend on where flan was invoked from.
|
||||
|
||||
So it is poked in before the call, into the two C symbols the module's own
|
||||
[flan_rt.c] declares for it. C data and not a Flan global because
|
||||
[Build.macro_module] emits with hidden visibility and only the
|
||||
[flan.macro.*] thunks stay exported — the same comment's other half is that
|
||||
the C goes on resolving the way it always did, which is what makes these two
|
||||
findable.
|
||||
|
||||
Set per call rather than once per module: one expansion walks the prelude's
|
||||
forms, the file's own and a package's, and a macro called from a package's
|
||||
source resolves against *that* file's directory. [Filename.dirname] is
|
||||
[embed_path]'s own move, and an empty answer — a bare filename with no
|
||||
directory in it — leaves the length at zero, which the runtime reads as "no
|
||||
better idea than the process's own directory". *)
|
||||
|
||||
let dir_of (l : loaded) (loc : Loc.t) =
|
||||
let dir = Filename.dirname loc.Loc.file in
|
||||
let dir = if String.equal dir "." then "" else dir in
|
||||
match Dynload.dl_sym l.handle "flan_macro_dir" with
|
||||
| exception _ -> ()
|
||||
| buf ->
|
||||
let n = Dynload.dl_sym l.handle "flan_macro_dir_n" in
|
||||
(* 4096 is FLAN_PATH_MAX, and a path at or over it is left unset rather
|
||||
than truncated: half a directory is a path that resolves to the wrong
|
||||
file, where none at all resolves to none. *)
|
||||
if String.length dir > 0 && String.length dir < 4096 then begin
|
||||
Dynload.poke_bytes buf 0 dir;
|
||||
Dynload.poke_i64 n 0 (Int64.of_int (String.length dir))
|
||||
end
|
||||
else Dynload.poke_i64 n 0 0L
|
||||
|
||||
(* ── The walk ──────────────────────────────────────────────────────
|
||||
Bottom up: a macro's arguments are expanded before it is called, so nothing
|
||||
a macro is handed contains a call to another macro. Then what it answers is
|
||||
@ -212,6 +347,7 @@ let rec expand_form (l : loaded) (f : Form.t) : Form.t =
|
||||
every form it produced knows where it came from and an error on one of
|
||||
them can say so. *)
|
||||
let from = Loc.from_macro n loc in
|
||||
dir_of l loc;
|
||||
settle l n loc (Expand.call ~loc:from (List.assoc n l.fns) args) fuel
|
||||
| Form.List xs -> Form.make (Form.List (List.map (expand_form l) xs)) loc
|
||||
| Form.Vec xs -> Form.make (Form.Vec (List.map (expand_form l) xs)) loc
|
||||
@ -230,6 +366,7 @@ and settle l first loc (f : Form.t) left =
|
||||
else begin
|
||||
let args = List.map (expand_form l) args in
|
||||
let from = Loc.from_macro m loc in
|
||||
dir_of l loc;
|
||||
settle l first loc (Expand.call ~loc:from (List.assoc m l.fns) args)
|
||||
(left - 1)
|
||||
end
|
||||
@ -416,6 +553,10 @@ let expand_step (f : Form.t) : Form.t * string option =
|
||||
match f.Form.v with
|
||||
| Form.List ({ Form.v = Form.Sym n; _ } :: args)
|
||||
when List.mem_assoc n l.fns ->
|
||||
(* [C-c C-m] over a type provider reads the data file, which is the
|
||||
whole of what makes the live loop live: edit the .edn, expand
|
||||
again, see the struct that file now implies. *)
|
||||
dir_of l f.Form.loc;
|
||||
( Expand.call ~loc:(Loc.from_macro n f.Form.loc) (List.assoc n l.fns)
|
||||
args,
|
||||
Some n )
|
||||
|
||||
34
lib/parse.ml
34
lib/parse.ml
@ -1188,10 +1188,40 @@ let expander : (Form.t list -> Form.t list) ref = ref (fun fs -> fs)
|
||||
whole of an evaluation. Empty is the ordinary case and costs nothing. *)
|
||||
let imported_macros : Form.t list ref = ref []
|
||||
|
||||
let with_imported (ms : Form.t list) (f : unit -> 'a) : 'a =
|
||||
(* What those macros are allowed to *call*, and it is the same list the
|
||||
importing program gets: the package's declarations, qualified under the
|
||||
alias, as [Load] already built them.
|
||||
|
||||
A macro module is compiled from the prelude plus the [defmacro]s, so until
|
||||
now the header's rule held without anything enforcing it — a macro body
|
||||
could call prelude functions and other macros and nothing else. A package
|
||||
macro that called one of its own package's functions was renamed to
|
||||
[alias/fn] by [Load.rename_form], reached the checker with nothing of that
|
||||
name declared, and was refused as a call into an imported package.
|
||||
|
||||
That refusal was the machinery missing a piece rather than a rule. The
|
||||
rename says the intent plainly: what a package's macro answers with, and
|
||||
what its body calls, is spelled the way the importer spells it. So the
|
||||
declarations travel beside the macros and go into the module with them.
|
||||
[Macro.compile] prunes them to what the macros actually reach, so a package
|
||||
whose macros are pure quasiquote — raylib's five [with-*] — pays nothing and
|
||||
links nothing new.
|
||||
|
||||
An [Ast.decl list] and not forms, because [Load] has already done the
|
||||
qualifying over the Ast and a second renamer over [Form] would be that work
|
||||
written twice, in the file where the two copies could disagree silently. *)
|
||||
let imported_decls : Ast.decl list ref = ref []
|
||||
|
||||
let with_imported ?(decls = []) (ms : Form.t list) (f : unit -> 'a) : 'a =
|
||||
let saved = !imported_macros in
|
||||
let saved_decls = !imported_decls in
|
||||
imported_macros := ms;
|
||||
Fun.protect ~finally:(fun () -> imported_macros := saved) f
|
||||
imported_decls := decls;
|
||||
Fun.protect
|
||||
~finally:(fun () ->
|
||||
imported_macros := saved;
|
||||
imported_decls := saved_decls)
|
||||
f
|
||||
|
||||
(* Two entry points and not one function with a flag, and the reason is the
|
||||
daemon. [Loc.Errors] is a second exception, and the handlers in the session
|
||||
|
||||
@ -1794,6 +1794,41 @@ let source = {flan|
|
||||
(let [n (i64 0)]
|
||||
(if (= (file-stat-raw path (addr n)) 1) (Some n) None)))
|
||||
|
||||
;; ── Reading a file while a macro runs ─────────────────────────────────
|
||||
;;
|
||||
;; The one thing a macro needed that it could not write for itself. A macro is
|
||||
;; compiled and dlopened into the compiler, so `slurp` was always callable from
|
||||
;; one; what was missing is that a macro has no idea where its call site is,
|
||||
;; and so no way to resolve a path the way `(embed "assets/x.edn")` resolves
|
||||
;; one — relative to the directory of the source file the form is written in.
|
||||
;;
|
||||
;; This is that rule, and it is the *same* rule: the compiler pokes the call
|
||||
;; site's directory into the runtime before every expansion (runtime/flan_rt.c,
|
||||
;; "Reading a file while a macro runs", and lib/macro.ml's expand_form), and a
|
||||
;; relative path is joined to it. An absolute path is taken as written.
|
||||
;;
|
||||
;; **None rather than a condition**, which is the whole reason this is not
|
||||
;; `slurp`. A condition signalled inside an expansion is signalled *in the
|
||||
;; compiler*, through the macro module's own copy of the runtime, and that is
|
||||
;; the failure `Build.macro_module`'s hidden-visibility note measured: it takes
|
||||
;; the process down instead of parking it. Absence arriving as an answer is
|
||||
;; what lets a type provider say "there is no file at that path" as a refusal
|
||||
;; with a location, which is the sentence its author wanted anyway.
|
||||
;;
|
||||
;; **Outside a macro it is still a read**, with the path relative to the
|
||||
;; process rather than to any source file — nothing else knows better, and
|
||||
;; every program links this runtime. It is not a file API and `slurp` is; this
|
||||
;; exists so a macro can look at data at compile time.
|
||||
(declare macro-slurp-raw [path string out-len (Ptr i64)] (Ptr u8)
|
||||
"flan_macro_slurp")
|
||||
|
||||
(defn macro-slurp [path string] (Option [u8])
|
||||
(let [n (i64 0)
|
||||
p (macro-slurp-raw path (addr n))]
|
||||
(if (< n 0)
|
||||
None
|
||||
(Some (slice-from-ptr p (i32 n))))))
|
||||
|
||||
;; ── Form: what a macro takes and what it answers ──────────────────────
|
||||
;;
|
||||
;; The reader's output, mirrored on the Flan side, because a macro is a
|
||||
|
||||
@ -122,6 +122,32 @@ let create ?(debug = false) ?(x86 = false) ~file () =
|
||||
macros = Load.macro_union (own_macros forms) l.Load.macros;
|
||||
thunks = 0; debug; x86 }, l)
|
||||
|
||||
(* What a macro may call, for the same reason [macros] is held: an evaluation
|
||||
parses one form with no import in sight, and a package macro whose body
|
||||
calls its own package's functions has to find them. [Load.program] hands
|
||||
this to [Parse.with_imported] from the import it just read; a session has to
|
||||
answer it from what it already holds.
|
||||
|
||||
Filtered out of [decls] by ownership rather than kept as a second list,
|
||||
because [decls] is the one thing every redefinition already maintains and a
|
||||
parallel copy would be a second thing to remember to update. A package's
|
||||
names are qualified in there — that is what "post-Load: flat, one namespace"
|
||||
means — so the prefix is the whole test.
|
||||
|
||||
The buffer's own declarations are deliberately not here. A macro module is
|
||||
built from the prelude with no part of the file in it (see [Macro.reduce]'s
|
||||
header, and the cycle it is about), and a session's [decls] is the file. *)
|
||||
let package_decls t =
|
||||
List.filter
|
||||
(fun (d : Ast.decl) ->
|
||||
match Ast.declared_name d with
|
||||
| Some n ->
|
||||
List.exists
|
||||
(fun (p : Load.pkg) -> String.starts_with ~prefix:(p.Load.alias ^ "/") n)
|
||||
t.pkgs
|
||||
| None -> false)
|
||||
t.decls
|
||||
|
||||
(* Which package a file being edited belongs to, if any.
|
||||
|
||||
A form typed into vendor/agent/agent.flan declares [poll], but the running
|
||||
@ -480,7 +506,7 @@ let restore t h =
|
||||
|
||||
let eval ?(origin = "<eval>") ?pause t src : change =
|
||||
let forms = Reader.read_all ~file:origin src in
|
||||
Parse.with_imported t.macros @@ fun () ->
|
||||
Parse.with_imported ~decls:(package_decls t) t.macros @@ fun () ->
|
||||
(* Through [Load] like any other source, so an evaluated (import ...) means
|
||||
what it means in a file. Its expansion is what gets spliced, which is also
|
||||
why the accumulated list is the post-Load one: re-evaluating a file that
|
||||
@ -1296,7 +1322,7 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
|
||||
untouched: a cold macro module costs its ~300ms before that clock starts,
|
||||
and the non-termination refusals raise [Loc.Error] out of this call, which
|
||||
the daemon already answers as an error rather than a silence. *)
|
||||
let parsed = Parse.with_imported t.macros (fun () -> Parse.expr form) in
|
||||
let parsed = Parse.with_imported ~decls:(package_decls t) t.macros (fun () -> Parse.expr form) in
|
||||
(* Wrapped before the checker, so the call is checked like any other and a
|
||||
prelude that stopped offering [pause] would be an ordinary unknown name
|
||||
rather than a thunk that silently did not stop. The [Do] takes the
|
||||
@ -1440,7 +1466,7 @@ let macroexpand ?(origin = "<eval>") ~(all : bool) t (src : string) : expansion
|
||||
let before = Expand.quasiquote form in
|
||||
(* And the session's macros in front of it, as [eval] and [eval_expr] both
|
||||
put them: [Macro.program] reads [Parse.imported_macros] directly. *)
|
||||
Parse.with_imported t.macros @@ fun () ->
|
||||
Parse.with_imported ~decls:(package_decls t) t.macros @@ fun () ->
|
||||
let after, name =
|
||||
if all then Macro.expand_all before else Macro.expand_step before
|
||||
in
|
||||
|
||||
@ -2957,6 +2957,88 @@ const uint8_t *flan_getenv(const uint8_t *name, int64_t n, int64_t *len) {
|
||||
return (const uint8_t *)v;
|
||||
}
|
||||
|
||||
/* ── Reading a file while a macro runs ─────────────────────────────────
|
||||
*
|
||||
* A macro is compiled and dlopened into the compiler, so it is ordinary native
|
||||
* code and could always have called `slurp`. What it could not do is resolve a
|
||||
* path the way the rest of the language resolves one. (embed "assets/x.edn")
|
||||
* is relative to the directory of the *source file the form is written in* —
|
||||
* lib/check.ml's embed_path, and Odin's rule before it — because anything else
|
||||
* makes a package's assets depend on where flan happened to be invoked from.
|
||||
* A macro has no idea where its call site is: a Form carries no location, on
|
||||
* purpose (see the prelude's Form, and Expand.unmarshal).
|
||||
*
|
||||
* So the compiler tells it, here. lib/macro.ml dlsym's the two symbols below
|
||||
* and pokes the call site's directory into them before every expansion; a
|
||||
* macro-time read joins that to a relative path and opens the result. The
|
||||
* channel is C data and not a Flan global because Build.macro_module emits the
|
||||
* module with hidden visibility — only the flan.macro.* thunks stay exported —
|
||||
* and "the C goes on resolving the way it always did" is the other half of
|
||||
* that same comment.
|
||||
*
|
||||
* It is empty in a process that is not expanding anything, which is every
|
||||
* process but the compiler: the module links this file, so a *program* holding
|
||||
* these symbols simply has a relative path mean what it means to the shell.
|
||||
*
|
||||
* `slurp` is deliberately not what the prelude wraps around this. slurp
|
||||
* signals a FileError, and a condition raised inside an expansion is raised in
|
||||
* the compiler, through the macro module's own copy of the runtime — which is
|
||||
* the failure Build.macro_module's ~hidden comment measured. Absence answers
|
||||
* here as a length of -1, on getenv's pattern, so a data file that is not
|
||||
* there becomes something the macro can refuse *about* rather than a trap. */
|
||||
|
||||
char flan_macro_dir[FLAN_PATH_MAX] = { 0 };
|
||||
int64_t flan_macro_dir_n = 0;
|
||||
|
||||
/* The bytes are the caller's to read and nobody's to free: an expansion is
|
||||
* bounded by the size of the program being compiled, which is exactly the
|
||||
* budget lib/dynload.ml's `owned` note already spends on a macro's own
|
||||
* allocations. Leaking is the same decision as there, for the same reason —
|
||||
* the returned slice is read after the call returns, and there is no `drop`. */
|
||||
const uint8_t *flan_macro_slurp(const uint8_t *path, int64_t n, int64_t *len) {
|
||||
static const char empty[1] = { 0 };
|
||||
char rel[FLAN_PATH_MAX];
|
||||
char full[FLAN_PATH_MAX];
|
||||
FILE *f;
|
||||
long size;
|
||||
uint8_t *buf;
|
||||
size_t got;
|
||||
*len = -1;
|
||||
if (!flan_path_cstr(path, n, rel)) return (const uint8_t *)empty;
|
||||
/* An absolute path is taken as written, and a relative one is joined to the
|
||||
* call site's directory — embed_path's two cases, in the same order. A dir
|
||||
* that was never poked leaves a relative path relative to the process, which
|
||||
* is the only thing it can mean when nothing knows better. */
|
||||
if (rel[0] == '/' || flan_macro_dir_n <= 0) {
|
||||
memcpy(full, rel, (size_t)n + 1);
|
||||
} else {
|
||||
if (flan_macro_dir_n + 1 + n >= FLAN_PATH_MAX) return (const uint8_t *)empty;
|
||||
memcpy(full, flan_macro_dir, (size_t)flan_macro_dir_n);
|
||||
full[flan_macro_dir_n] = '/';
|
||||
memcpy(full + flan_macro_dir_n + 1, rel, (size_t)n + 1);
|
||||
}
|
||||
f = fopen(full, "rb");
|
||||
if (!f) return (const uint8_t *)empty;
|
||||
/* A directory opens on Linux and fails at the read, which is the trap
|
||||
* check.ml's read_embed_file records: guarding only the open turns
|
||||
* (macro-slurp "somedir") into a crash rather than an answer. Both ends are
|
||||
* guarded here and both answer absent. */
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return (const uint8_t *)empty; }
|
||||
size = ftell(f);
|
||||
if (size < 0 || fseek(f, 0, SEEK_SET) != 0) {
|
||||
fclose(f);
|
||||
return (const uint8_t *)empty;
|
||||
}
|
||||
buf = (uint8_t *)malloc((size_t)size + 1);
|
||||
if (!buf) { fclose(f); return (const uint8_t *)empty; }
|
||||
got = fread(buf, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
if (got != (size_t)size) { free(buf); return (const uint8_t *)empty; }
|
||||
buf[size] = 0;
|
||||
*len = (int64_t)size;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ── The rest of the file surface ──────────────────────────────────────
|
||||
*
|
||||
* Four more POSIX-shaped calls under the same rules as flan_file_size,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user