diff --git a/TODO.org b/TODO.org index fe34908c..f12d0401 100644 --- a/TODO.org +++ b/TODO.org @@ -524,13 +524,20 @@ both. ** DONE A package cannot mark a name private CLOSED: [2026-09-25] -=defn-= declares a function private to its package; it is a =defn= otherwise. -A use from a file outside the package's directory — a call or the name as a -value, from the importer or from another package — is refused in =Check=, so it -holds under C-c C-c too. Functions only. A single-file package's siblings share -its directory and can call its =defn-=s, and a package macro expanding to a -=defn-= call is refused at the expansion. The edn and json internals are now -=defn-=; =rl/get-color-raw= no longer exists. =docs/BUILT.md= has the placement. +=defn-= declares a function private to its module; it is a =defn= otherwise. +The module is the import boundary: every file of a directory package, or the +one file of a package imported by naming the file. A use from outside — a call +or the name as a value — is refused in =Check=, so it holds under C-c C-c too. +Code the package's own macro wrote counts as inside (SBCL's rule, not +Clojure's); what the importer wrote, passed through or from its own macro, does +not. Functions only. The edn and json internals are now =defn-=; +=rl/get-color-raw= no longer exists. =docs/BUILT.md= has the placement. + +** WAIT A private scoped to one file of a directory package +Deferred until a need appears: Odin's =@(private="file")=, a function visible +to its own file only when the package is a directory. =defn-= covers the +package; a one-file package already gets file scope because the file is the +module. ** CANCELLED A struct version word, so a redefined layout keeps working CLOSED: [2026-09-20] diff --git a/docs/BUILT.md b/docs/BUILT.md index e307e696..3512eb84 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -794,18 +794,29 @@ takes a macro-stamped location — and `Loc.from_macro` sets a name and leaves f the frame the break loop reports is still the line the reader is looking at. `temps` is not reset on this path, unlike `decl`'s — a declaration is a fresh top level, an expression is evaluated into a session that has been handing out temporaries all along. -A function can be private to its package: `defn-`, Clojure's spelling, is a `defn` whose uses outside the package's -directory are refused. It is not done in `load.ml`, although `main`'s refusal is. `Load.refuse_hidden` walks every +A function can be private to its module: `defn-`, Clojure's spelling, is a `defn` whose uses outside the module that +declares it are refused. It is not done in `load.ml`, although `main`'s refusal is. `Load.refuse_hidden` walks every declaration after the rename, the package's own included, and by then the package's calls to its helper read `alias/helper` like anyone else's; and a C-c C-c goes through `Load.program` with one form and no imports in sight. So -the flag rides on `Ast.fn`, `Check` records every `defn-` in `privates`, and `Check.private_ref` compares the directory -of the file the use is written in with the directory of the file the definition is — the same shape as -`shadows_builtin`'s file test. Being in `Check` is what makes it hold in the session too: every evaluation re-checks -the whole declaration list. Only a qualified name is asked about, because an unqualified one is the program's own. -Two consequences follow from the rule being about files. A single-file package shares its directory with its -siblings, and they can call its `defn-`s. And a package macro that expands to a call to one of its `defn-`s is -refused at the expansion, because the expansion is located at the call site in the importer's file; the edn and json -readers' generated code calls `need-int` and its neighbours, which is why those stay `defn`. +the flag rides on `Ast.fn`, `Check` records every `defn-` in `privates`, and `Check.private_ref` compares the file the +use is written in with the file the definition is — the same shape as `shadows_builtin`'s file test. Being in `Check` +is what makes it hold in the session too: every evaluation re-checks the whole declaration list. Only a qualified name +is asked about, because an unqualified one is the program's own. + +The module is what `Load` imports as one unit. For a directory package that is every `.flan` file in the directory, so +the test is the two files' directories. For a package imported by naming a single file it is that file, and its +siblings in the directory are not part of it; the parser cannot tell which kind it is reading, so `Load.file_scoped` +narrows the flag to `Private_to_file` at import, and `Session.eval` does the same for a C-c C-c in such a file. A +file-only private inside a directory package, Odin's `@(private="file")`, is not provided. + +Code a package's own macro wrote counts as inside the package wherever it is expanded, which is SBCL's rule: an +expansion there refers to the package's internal symbols freely. Clojure refuses the same thing. `Expand.unmarshal` +already separates the two kinds of node an expansion holds. A node the macro invented carries the call site's +location, tagged with the macro's name and no separate call site; a form the author wrote and the macro passed +through keeps its own position, with the call recorded in `msite`. So an invented node whose macro has the function's +qualifier is accepted, and an author's form is judged by the file it sits in. The macro name is the outermost one, +because the tag is outermost-wins: a call written by a package macro that was itself expanded out of an importer's +macro is judged as the importer's. ## The link follows the program diff --git a/lib/ast.ml b/lib/ast.ml index bd5bbf68..066a0e28 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -208,6 +208,12 @@ and pattern = makes each instantiation check the concrete type answers yes. *) type pred = { pname : string; pvar : string; ploc : Loc.t } +(* Who may use a function. [defn-] parses as [Private_to_package]; [Load] + narrows it to [Private_to_file] when the package is a single file named + outright, because that file is the whole module and its siblings in the + directory are not part of it. [Check.private_ref] is what enforces both. *) +type privacy = Exported | Private_to_package | Private_to_file + type fn = { name : string; params : field list; @@ -225,9 +231,7 @@ type fn = { fwhere : pred list; fbody : expr list; nloc : Loc.t; - (* Written [defn-]: callable only from the files of the package that - declares it. [Check.private_ref] is the whole of what it means. *) - fprivate : bool; + fprivate : privacy; } type decl = { d : decl_kind; dloc : Loc.t } diff --git a/lib/check.ml b/lib/check.ml index c66b777c..11bae323 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -114,8 +114,9 @@ type env = { function can show it. Kept apart from [fparams] because a foreign [declare] has a location and no parameter vector worth showing. *) fn_locs : (string, Loc.t) Hashtbl.t; - (* Every [defn-], by name, with where it was written. See [private_ref]. *) - privates : (string, Loc.t) Hashtbl.t; + (* Every [defn-], by name, with where it was written and how far it is + visible. See [private_ref]. *) + privates : (string, Loc.t * Ast.privacy) Hashtbl.t; globals : (string, Types.t * bool) Hashtbl.t; (* type, is a constant *) (* Where each global was declared, so a refusal about one can show it. A second table rather than a third field, because every other reader of @@ -9008,32 +9009,61 @@ and ordinary_call ctx ~want loc name args = is not the file the defn was written in, so it reaches the builtin. That is the conservative direction, and C-c C-c — which sends the buffer's own path — is not affected. *) -(* A [defn-] is its package's own. A use of one — a call, or the name taken - as a value — is refused unless it is written in a file of the package that - declares it, and a package is its directory, so the test is the directory of - the two files. Realpath'd, because one side is usually the path the importer - was given on the command line and the other the one [Load] resolved. +(* A [defn-] is its module's own. A use of one — a call, or the name taken + as a value — is refused unless it is written inside the module that + declares it. A directory package is every file in its directory, so the + test there is the two files' directories; a single file imported outright + is that file alone, which [Load] records by narrowing the flag to + [Private_to_file]. Realpath'd, because one side is usually the path the + importer was given on the command line and the other the one [Load] + resolved. + + Code the package's own macro wrote counts as inside, wherever it was + expanded: SBCL's rule, where a macro's expansion refers to its package's + internal symbols freely. What the importer wrote itself does not, even when + it is an argument the macro passed through. [Expand.unmarshal] tells the two + apart already: a node the macro invented carries the call site's location + with the macro's name on it and no separate call site, and a form the author + wrote keeps its own position with the call recorded beside it. The macro's + package is its qualifier, which is the function's when both come from the + same module, because an import qualifies every name a directory declares + under the one alias it is read as. Only a qualified name is asked about. An unqualified one belongs to the - program being built, and nothing outside a program can name it — the REPL, - which evaluates with no file behind it, included. - - A package that is a single file named outright, rather than a directory, - shares its directory with whatever sits beside it, so a sibling file that - imports it can call its [defn-]s. *) + program being built, and nothing outside a program can name it. *) and private_ref ctx loc name = + let qualifier n = + match String.rindex_opt n '/' with + | Some i -> Some (String.sub n 0 i) + | None -> None + in + let written_by_own_macro () = + match loc.Loc.macro, loc.Loc.msite with + | Some m, None -> qualifier m <> None && qualifier m = qualifier name + | _ -> false + in match Hashtbl.find_opt ctx.env.privates name with - | Some at when String.contains name '/' -> - let dir file = - Filename.dirname - (try Unix.realpath file with Unix.Unix_error _ -> file) + | Some (at, scope) when String.contains name '/' -> + let real file = try Unix.realpath file with Unix.Unix_error _ -> file in + let inside = + match scope with + | Ast.Private_to_file -> String.equal (real at.Loc.file) (real loc.Loc.file) + | _ -> + String.equal (Filename.dirname (real at.Loc.file)) + (Filename.dirname (real loc.Loc.file)) in - if not (String.equal (dir at.Loc.file) (dir loc.Loc.file)) then + if not inside && not (written_by_own_macro ()) then begin + let where = + match scope with + | Ast.Private_to_file -> real at.Loc.file + | _ -> "the files in " ^ Filename.dirname (real at.Loc.file) + in Loc.failk "check/private" loc ~notes:[ Loc.note at (Printf.sprintf "%s is declared here" name) ] - "%s is private to its package: it is declared with defn-, and a \ - defn- can be used only from a file in %s" - name (dir at.Loc.file) + "%s is private to its package: it is declared with defn-, so only %s \ + can use it. Declaring it with defn instead makes it usable from here" + name where + end | _ -> () and shadows_builtin ctx loc name = @@ -10472,8 +10502,9 @@ let collect env (decls : Ast.decl list) = in env.tyvars <- []; env.tvpreds <- []; - if fn.Ast.fprivate then - Hashtbl.replace env.privates fn.Ast.name fn.Ast.nloc; + if fn.Ast.fprivate <> Ast.Exported then + Hashtbl.replace env.privates fn.Ast.name + (fn.Ast.nloc, fn.Ast.fprivate); if vars = [] then begin Hashtbl.replace env.fns fn.Ast.name (params, ret); Hashtbl.replace env.fparams fn.Ast.name fn.Ast.params; diff --git a/lib/cimport.ml b/lib/cimport.ml index 8091fe69..0e4823c9 100644 --- a/lib/cimport.ml +++ b/lib/cimport.ml @@ -838,7 +838,7 @@ let of_dump ~env ~taken ~bound_syms ~config (d : dump) : imported = decls := { Ast.d = Ast.DeclareC - ({ Ast.name = flan; params; praw = None; ret; fwhere = []; fbody = []; nloc = f.cloc; fprivate = false }, + ({ Ast.name = flan; params; praw = None; ret; fwhere = []; fbody = []; nloc = f.cloc; fprivate = Ast.Exported }, f.csym); dloc = f.cloc } :: !decls) diff --git a/lib/classes.ml b/lib/classes.ml index a86cfa84..a6b7325b 100644 --- a/lib/classes.ml +++ b/lib/classes.ml @@ -175,7 +175,7 @@ let constructor n slots loc : Ast.decl = Ast.Defn { Ast.name = n; params; praw = None; ret = Some (dyn_at loc); fwhere = []; fbody = [ ex loc (Ast.MapLit (Some n, pairs)) ]; - nloc = loc; fprivate = false }; + nloc = loc; fprivate = Ast.Exported }; dloc = loc } (* The dispatch value a method answers for, as an expression to compare diff --git a/lib/load.ml b/lib/load.ml index c41e8ee5..8fa0b01d 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -26,7 +26,7 @@ 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. And a [defn-] is exported like - any other name but refused at a use outside its package's directory, which + any other name but refused at a use outside its package, which is [Check.private_ref]'s and not this module's: the rename has to qualify the package's own calls to it exactly as it qualifies everything else. @@ -903,6 +903,20 @@ let refuse_hidden hidden 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. *) +(* A [defn-] in a package that is one file named outright is private to that + file: the file is the whole module, and whatever else sits in its directory + is not part of it. The parser cannot know which kind of package it is + reading, so the flag is narrowed here and in [Session.eval], the two places + that do. *) +let file_scoped (ds : Ast.decl list) = + List.map + (fun (d : Ast.decl) -> + match d.Ast.d with + | Ast.Defn ({ Ast.fprivate = Ast.Private_to_package; _ } as fn) -> + { d with Ast.d = Ast.Defn { fn with Ast.fprivate = Ast.Private_to_file } } + | _ -> d) + ds + let owned_names (ds : Ast.decl list) = List.filter_map Ast.declared_name ds @@ -1413,6 +1427,7 @@ let rec import ~seen ~open_ ~loc alias dir = in let owned = List.filter exported (owned_names ds) in let decls = List.map (qualify_decl owned alias) own in + let decls = if one_file then file_scoped decls else decls in let lflags = if one_file then [] else link_flags dir in let csrcs = if one_file then [] else entries dir ".c" in let phidden = diff --git a/lib/parse.ml b/lib/parse.ml index d413937e..43f9aec3 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -1377,7 +1377,9 @@ let rec decl (f : Form.t) : Ast.decl = (* [defn-] is a [defn] in every respect but one: [Check.private_ref] refuses a use of it from outside the package that declares it. *) | List ({ v = Sym ("defn" | "defn-" as head); _ } :: args) -> - let fprivate = String.equal head "defn-" in + let fprivate = + if String.equal head "defn-" then Ast.Private_to_package else Ast.Exported + in (match args with | n :: { v = Vec ps; _ } :: ret :: body -> (* The slot's own failure, because the thing found there is almost @@ -1467,7 +1469,7 @@ let rec decl (f : Form.t) : Ast.decl = else fun fn -> Ast.Defmulti fn) { Ast.name = sym n; params = dyn_params which ps; praw = None; ret = Some (texpr ret); fwhere = []; fbody = body_of body; - nloc = n.loc; fprivate = false }) + nloc = n.loc; fprivate = Ast.Exported }) | _ -> fail f "%s" usage) | List ({ v = Sym "defmethod"; _ } :: args) -> @@ -1481,7 +1483,7 @@ let rec decl (f : Form.t) : Ast.decl = mfn = { Ast.name = gen ^ "@" ^ Ast.dispatch_text k; params = dyn_params "defmethod" ps; praw = None; ret = None; fwhere = []; fbody = body_of body; - nloc = n.loc; fprivate = false } }) + nloc = n.loc; fprivate = Ast.Exported } }) | _ -> fail f "defmethod is (defmethod generic dispatch [param ...] body ...). \ @@ -1513,11 +1515,11 @@ let rec decl (f : Form.t) : Ast.decl = | [ n; { v = Form.Vec ps; _ } ] -> mk (mkd { Ast.name = sym n; params = fields f ps; praw = None; ret = None; fwhere = []; fbody = []; nloc = n.loc; - fprivate = false } csym) + fprivate = Ast.Exported } csym) | [ n; { v = Form.Vec ps; _ }; r ] -> mk (mkd { Ast.name = sym n; params = fields f ps; praw = None; ret = Some (texpr r); fwhere = []; fbody = []; - nloc = n.loc; fprivate = false } csym) + nloc = n.loc; fprivate = Ast.Exported } csym) | _ -> fail f "%s" usage) | _ -> fail f "%s" usage) @@ -1749,7 +1751,7 @@ let rec decl (f : Form.t) : Ast.decl = leave off. *) praw = None; ret = Some form_t; fwhere = []; fbody = macro_body sg body; - nloc = n.loc; fprivate = false }) + nloc = n.loc; fprivate = Ast.Exported }) | _ -> fail f "defmacro is (defmacro name [param ...] body ...)") diff --git a/lib/session.ml b/lib/session.ml index b54592e2..191853ea 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -644,7 +644,8 @@ let eval ?(origin = "") ?pause t src : change = p.Load.owns @ List.filter_map Ast.declared_name ds in - List.map (Load.qualify_decl owns p.Load.alias) ds + let ds = List.map (Load.qualify_decl owns p.Load.alias) ds in + if Load.is_package_file p.Load.dir then Load.file_scoped ds else ds in let loc = match incoming with d :: _ -> d.Ast.dloc | [] -> Loc.unknown diff --git a/test/dune b/test/dune index 0b7edebd..b283a1b6 100644 --- a/test/dune +++ b/test/dune @@ -68,6 +68,7 @@ ; pkg-private*.flan. (glob_files programs/pkgs/secret/*) (glob_files programs/pkgs/nosy/*) + (glob_files programs/loose/*) ; The synthetic C header the importer's table reads. Committed rather than ; reached for on the machine: the raylib case needs raylib installed, at the ; right version, with a variable set, so it skips everywhere and covers @@ -241,7 +242,8 @@ (glob_files programs/pkgs/shadowed/*) (glob_files programs/pkgs/gen/*) (glob_files programs/pkgs/secret/*) - (glob_files programs/pkgs/nosy/*)) + (glob_files programs/pkgs/nosy/*) + (glob_files programs/loose/*)) (action (run ./test_valgrind.exe))) ; The corpus a fourth time, through the hand-written x86-64 backend, compared @@ -303,7 +305,8 @@ (glob_files programs/pkgs/shadowed/*) (glob_files programs/pkgs/gen/*) (glob_files programs/pkgs/secret/*) - (glob_files programs/pkgs/nosy/*)) + (glob_files programs/pkgs/nosy/*) + (glob_files programs/loose/*)) (action (setenv SURVEY_STRICT 1 (setenv SURVEY_QUIET 1 @@ -482,7 +485,8 @@ (glob_files programs/pkgs/shadowed/*) (glob_files programs/pkgs/gen/*) (glob_files programs/pkgs/secret/*) - (glob_files programs/pkgs/nosy/*)) + (glob_files programs/pkgs/nosy/*) + (glob_files programs/loose/*)) (action (setenv SURVEY_STRICT 1 (setenv SURVEY_QUIET 1 diff --git a/test/programs/loose/lib.flan b/test/programs/loose/lib.flan new file mode 100644 index 00000000..26492220 --- /dev/null +++ b/test/programs/loose/lib.flan @@ -0,0 +1,7 @@ +;;;; A package that is one file, imported by naming the file. Its defn- is +;;;; private to this file: the files beside it in the directory are not part +;;;; of it. + +(defn- inner [a i32] i32 (* a 3)) + +(defn outer [a i32] i32 (inner a)) diff --git a/test/programs/loose/peek.flan b/test/programs/loose/peek.flan new file mode 100644 index 00000000..dfd12630 --- /dev/null +++ b/test/programs/loose/peek.flan @@ -0,0 +1,8 @@ +;;;; Sits beside lib.flan and imports it by name. lib's defn- is private to +;;;; lib.flan, so the shared directory does not make it callable here. + +(import lib "lib.flan") + +(defn main [] i32 + (print (lib/inner 5)) + 0) diff --git a/test/programs/loose/use.flan b/test/programs/loose/use.flan new file mode 100644 index 00000000..5dbddf5e --- /dev/null +++ b/test/programs/loose/use.flan @@ -0,0 +1,7 @@ +;;;; Imports lib.flan by name and calls its public function. + +(import lib "lib.flan") + +(defn main [] i32 + (print (lib/outer 5)) (println "") + 0) diff --git a/test/programs/pkg-private-own-macro.flan b/test/programs/pkg-private-own-macro.flan new file mode 100644 index 00000000..fbe43ff5 --- /dev/null +++ b/test/programs/pkg-private-own-macro.flan @@ -0,0 +1,10 @@ +;;;; A call to a package's private function written by a macro of this file, +;;;; not of the package. + +(import secret "pkgs/secret") + +(defmacro sneak [& args] `(do (secret/mix 1 2))) + +(defn main [] i32 + (print (sneak)) + 0) diff --git a/test/programs/pkg-private-passed.flan b/test/programs/pkg-private-passed.flan new file mode 100644 index 00000000..b7cdf5b1 --- /dev/null +++ b/test/programs/pkg-private-passed.flan @@ -0,0 +1,9 @@ +;;;; A call to a package's private function written here and handed to the +;;;; package's own macro. The macro passes it through; the call is still this +;;;; file's. + +(import secret "pkgs/secret") + +(defn main [] i32 + (print (secret/pass (secret/mix 1 2))) + 0) diff --git a/test/programs/pkg-private.flan b/test/programs/pkg-private.flan index f3125bbd..00e84df9 100644 --- a/test/programs/pkg-private.flan +++ b/test/programs/pkg-private.flan @@ -1,5 +1,6 @@ ;;;; A package's private function, used from inside the package: from the -;;;; file that declares it, from the package's other file, and as a value. +;;;; file that declares it, from the package's other file, as a value, and +;;;; through code the package's own macros write here. (import secret "pkgs/secret") @@ -7,4 +8,6 @@ (print (secret/combine 1 2)) (println "") (print (secret/twice 3)) (println "") (print (secret/via-value 4 5)) (println "") + (print (secret/mixed 6 7)) (println "") + (print (secret/mixed-in-do 8 9)) (println "") 0) diff --git a/test/programs/pkgs/secret/secret.flan b/test/programs/pkgs/secret/secret.flan index 9f759136..37d73dd0 100644 --- a/test/programs/pkgs/secret/secret.flan +++ b/test/programs/pkgs/secret/secret.flan @@ -5,3 +5,14 @@ (defn- mix [a i32 b i32] i32 (+ (* a 10) b)) (defn combine [a i32 b i32] i32 (mix a b)) + +;; Code these macros write is the package's own, wherever it is expanded: the +;; call to mix they answer with is accepted in the importer's file, on its own +;; and nested inside a do. +(defmacro mixed [& args] `(mix ~(at args 0) ~(at args 1))) + +(defmacro mixed-in-do [& args] `(do (mix ~(at args 0) ~(at args 1)))) + +;; What the importer writes and this macro only passes through is still the +;; importer's. +(defmacro pass [& args] (at args 0)) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 77924f8c..ace20986 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2796,7 +2796,12 @@ let () = declares it, the package's other file, and as a value handed out from there. The refusals are with the others, further down. *) outputs "a defn- used inside its package" "programs/pkg-private.flan" - "12\n33\n45\n"; + "12\n33\n45\n67\n89\n"; + (* A single-file package's defn- is callable from that file, and the file + beside it that imports it reaches the public function. The refusal for + the same sibling calling the defn- directly is with the others. *) + outputs "a single-file package calls its own defn-" + "programs/loose/use.flan" "15\n"; (* A defn named after a builtin, and the boundary the shadow stops at. The numbers are the whole claim and none of them could be printed by the other reading: 7 is the program's own one-argument (get p), which @@ -3085,6 +3090,20 @@ let () = "programs/pkg-private-nested.flan" "secret/mix is private to its package"; refuses "and the refusal names the package's directory" "programs/pkg-private-call.flan" "pkgs/secret"; + refuses "and names the fix" + "programs/pkg-private-call.flan" "Declaring it with defn instead"; + (* The package's own macro may write a call to its defn- (that half is in + pkg-private.flan); what the importer writes is the importer's, whether + the package's macro passes it through or the importer's macro wrote it. *) + refuses "a defn- call the importer wrote, passed through the package's macro" + "programs/pkg-private-passed.flan" "secret/mix is private to its package"; + refuses "a defn- call written by the importer's own macro" + "programs/pkg-private-own-macro.flan" + "secret/mix is private to its package"; + (* A single file named outright is the whole module, so its defn- is not + visible to the file beside it in the directory. *) + refuses "a single-file package's defn-, from the file beside it" + "programs/loose/peek.flan" "lib/inner is private to its package"; refuses "one directory under two aliases" "programs/pkg-two-aliases.flan" "one directory takes one alias"; (* A ring is refused and the ring is named. The needle is the chain, not diff --git a/test/test_session.ml b/test/test_session.ml index ec59874e..78a3936d 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -615,6 +615,20 @@ let () = | _ -> () | exception Loc.Error { Loc.dmsg = m; _ } -> fail "a defn- in the program's own file: %s" m); + (* And a single-file package: redefined from its own file, it stays private + to that file, so the file beside it that imports it is still refused. *) + let tl, _ = Session.create ~file:"programs/loose/use.flan" () in + (match Session.eval ~origin:"programs/loose/lib.flan" tl + "(defn- inner [a i32] i32 (* a 4))" with + | _ -> () + | exception Loc.Error { Loc.dmsg = m; _ } -> + fail "redefining a single-file package's defn-: %s" m); + (match Session.eval ~origin:"programs/loose/use.flan" tl + "(defn peek [] i32 (lib/inner 1))" with + | _ -> fail "a redefined single-file defn- became callable beside its file" + | exception Loc.Error { Loc.dmsg = m; _ } -> + if not (has m "lib/inner is private to its package") then + fail "a call to a single-file defn- was refused for another reason: %s" m); (* ── C-x C-e expands, which it never used to ──────────────────────── [Parse.expr] did not call the expander at all, so an expression typed at diff --git a/web/index.html b/web/index.html index 0dc03481..791be8c7 100644 --- a/web/index.html +++ b/web/index.html @@ -1528,11 +1528,12 @@ existed.

A function declared with defn- is private to its package. It is a defn in every other respect, and every file in the package's -directory can call it. A use from anywhere else — a call, or the name passed as a -value — is refused at compile time, and the message names the function and the -directory it belongs to. Only functions have a private form. A single-file package -shares its directory with the files beside it, so those files can call its -defn-s too.

+directory can call it. For a package imported by naming a single file, the package is +that file alone, and the other files in its directory cannot call it. A use from +anywhere else — a call, or the name passed as a value — is refused at compile time, +and the message names the function and where it may be used. Code written by one of +the package's own macros counts as the package's, wherever the macro is called. Only +functions have a private form.

A package may carry the C it binds to. Every .c file in the directory is compiled into the build, and a file named link lists extra linker