diff --git a/TODO.org b/TODO.org index fcf9257b..6af68f51 100644 --- a/TODO.org +++ b/TODO.org @@ -522,11 +522,15 @@ routes must arrive under one set of names or the checker sees every declaration twice. The price is that the same directory under two aliases is refused, naming both. -** TODO A package cannot mark a name private -=rl/get-color-raw= is callable from outside its package. The refusal machinery -takes a second rule in one line; the blocker is that there is no way for a package -to *say* a name is private, and adding one is a parser change the author has to -choose a spelling for. Every lane has skipped it for that reason. +** 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. ** 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 6dfdd95c..62b3239c 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -794,10 +794,18 @@ 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. -Still missing: a package-private marker for anything other than `main`, which is why `rl/get-color-raw` is callable. -The gap is surface syntax and not `load.ml` — `exported` is one predicate and the refusal machinery that points at the -line which tried already exists, so a second rule is a line. What does not exist is any way for a package to *mark* a -name private, and inventing one is a reader and parser change. +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 +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 link follows the program diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 578c4c70..30bbc3ff 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -119,7 +119,7 @@ ;; patched only when somebody notices a gap is the list that is always a ;; release behind the parser. (defconst flan--definers - '("defn" "defmacro" "def" "defonce" "defconst" "defstruct" "defdata" "defunion" + '("defn" "defn-" "defmacro" "def" "defonce" "defconst" "defstruct" "defdata" "defunion" "defenum" "defalias" ;; The object and dispatch heads (lib/parse.ml:1277-1334). "defclass" "defgeneric" "defmulti" "defmethod" @@ -277,7 +277,7 @@ below — and not again here.") ;; it implements, which is the name in the same place, so a file of several ;; methods shows that name several times — the honest answer, and better ;; than listing none of them as it did. - `(("Functions" ,(concat "^(def\\(?:n\\|generic\\|multi\\|method\\)\\s-+" + `(("Functions" ,(concat "^(def\\(?:n-?\\|generic\\|multi\\|method\\)\\s-+" flan--name-re) 1) ;; Its own heading rather than a second `Functions' entry: a macro runs at @@ -574,6 +574,7 @@ For `syntax-propertize-function'." ;; the parameters and the body is optional; a count would have to know ;; whether one is there, and `:defn' does not care. ("defn" . :defn) + ("defn-" . :defn) ;; `(declare-c NAME [params] RET "CSymbol")'. The name is the one special ;; argument; everything after it is written down the page in one column. ("declare" . 1) diff --git a/emacs/flan.el b/emacs/flan.el index b6e6cd18..8dd61cf1 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -2529,7 +2529,7 @@ daemon reads that as stopping on entry instead." ;; and `package' is a single named exception rather than the edge of a subtler ;; rule that was never quite true. (defconst flan--declaration-heads - '("defmacro" "defn" "def" "defonce" "defconst" + '("defmacro" "defn" "defn-" "def" "defonce" "defconst" "defstruct" "defdata" "defunion" "defenum" "defalias" ;; The object and dispatch heads (lib/parse.ml:1277-1334), which are ;; declarations in exactly the way `defn' is: each introduces a top-level diff --git a/emacs/test-flan-mode.el b/emacs/test-flan-mode.el index 4727dacd..99e51e97 100644 --- a/emacs/test-flan-mode.el +++ b/emacs/test-flan-mode.el @@ -388,6 +388,12 @@ "and the name it introduces") ("(defonce seed i64 1)" "defonce" font-lock-keyword-face "defonce") + ;; A private defn: the head as a whole, not `defn' and a + ;; stray `-', and the name after it. + ("(defn- mix [a i32] i32 a)" "defn-" font-lock-keyword-face + "defn-'s head") + ("(defn- mix [a i32] i32 a)" "mix" + font-lock-function-name-face "and the private function's name") ("(defclass point [x y])" "defclass" font-lock-keyword-face "defclass") ("(defgeneric area [self] dyn)" "defgeneric" diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 76e8c8fc..3a6ae6ce 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -730,7 +730,7 @@ already rely on it — so nothing here is a stand-in for the real thing." ;; back. Read off `Parse.decl', so the check is the derivation. (with-temp-buffer (flan-mode) - (dolist (head '("defmacro" "defn" "def" "defonce" "defconst" "defstruct" + (dolist (head '("defmacro" "defn" "defn-" "def" "defonce" "defconst" "defstruct" "defdata" "defunion" "defenum" "defalias" "defclass" "defgeneric" "defmulti" "defmethod" "import" "declare" "declare-c")) diff --git a/lib/ast.ml b/lib/ast.ml index c49e9e36..bd5bbf68 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -225,6 +225,9 @@ 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; } type decl = { d : decl_kind; dloc : Loc.t } diff --git a/lib/check.ml b/lib/check.ml index 1383ce91..c66b777c 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -114,6 +114,8 @@ 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; 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 @@ -194,6 +196,7 @@ let new_env () = { fns = Hashtbl.create 32; fparams = Hashtbl.create 32; fn_locs = Hashtbl.create 32; + privates = Hashtbl.create 8; globals = Hashtbl.create 16; global_locs = Hashtbl.create 16; lifted = []; @@ -3863,6 +3866,7 @@ and var ctx ?(qualified = false) loc ~want name = a question this language does not ask. *) (match Hashtbl.find_opt ctx.env.fns name with | Some (params, ret) -> + private_ref ctx loc name; (* A foreign function is in [fns] too, and its emitted signature is C's: no transfer channel, and an aggregate flattened by the shim. Nothing could call the resulting pointer correctly, so it @@ -8836,11 +8840,13 @@ and ordinary_call ctx ~want loc name args = | Some b -> call_value ctx ~want loc (mk loc b.bty (Tast.Local b.slot)) args | None -> assert false) | _ when Hashtbl.mem ctx.env.gsigs name -> + private_ref ctx loc name; let vars, params, ret = Hashtbl.find ctx.env.gsigs name in generic_call ctx ~want loc name vars params ret args | _ -> match Hashtbl.find_opt ctx.env.fns name with | Some (params, ret) -> + private_ref ctx loc name; if List.length args <> List.length params then fail loc "%s takes %d argument%s, given %d" name (List.length params) @@ -9002,6 +9008,34 @@ 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. + + 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. *) +and private_ref ctx loc name = + 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) + in + if not (String.equal (dir at.Loc.file) (dir loc.Loc.file)) then + 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) + | _ -> () + and shadows_builtin ctx loc name = (* Where the definition was written, if this name has one. A generic is in [generics] and nowhere near [fn_locs], so both tables are asked. *) @@ -10438,6 +10472,8 @@ 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 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 d7663662..8091fe69 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 }, + ({ Ast.name = flan; params; praw = None; ret; fwhere = []; fbody = []; nloc = f.cloc; fprivate = false }, f.csym); dloc = f.cloc } :: !decls) diff --git a/lib/classes.ml b/lib/classes.ml index bab0c784..a86cfa84 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 }; + nloc = loc; fprivate = false }; 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 96629722..c41e8ee5 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -22,12 +22,13 @@ 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 + Visibility is two rules. [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. + 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 + 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. 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 @@ -725,9 +726,8 @@ let imports_of (forms : Form.t list) = 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 (TODO.org, "A package cannot mark a name private" — - [rl/get-color-raw] should not be callable either). *) + is not a name. Everything else is exported, a [defn-] included — see + [Check.private_ref] for where that one is refused. *) let exported n = not (String.equal n "main") (* Where a name is *used*, which is what a refusal has to point at. A rename diff --git a/lib/parse.ml b/lib/parse.ml index 456c8a8b..d413937e 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -684,7 +684,7 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr = a head, which is the same property that makes a quasiquoted macro call output rather than a dependency. Building a declaration as a value is what a macro is for. *) - | Sym ("defmacro" | "defn" | "def" | "defonce" | "defconst" | "defstruct" + | Sym ("defmacro" | "defn" | "defn-" | "def" | "defonce" | "defconst" | "defstruct" | "defdata" | "defunion" | "defclass" | "defgeneric" | "defmulti" | "defmethod" | "defenum" | "defalias" | "import" as name) -> fail f @@ -1374,7 +1374,10 @@ let rec decl (f : Form.t) : Ast.decl = rule's and not this file's, and [Session.compatible] is where it is felt -- a redefinition that changes a signature is refused there, and this is a way for a signature to change with nothing redefined. *) - | List ({ v = Sym "defn"; _ } :: args) -> + (* [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 (match args with | n :: { v = Vec ps; _ } :: ret :: body -> (* The slot's own failure, because the thing found there is almost @@ -1408,11 +1411,12 @@ let rec decl (f : Form.t) : Ast.decl = let fwhere, body = constraints body in mk (Ast.Defn { Ast.name = sym n; params = []; praw = Some (pitems ps); ret = Some rty; fwhere; fbody = body_of body; - nloc = n.loc }) + nloc = n.loc; fprivate }) | _ -> fail f - "defn is (defn name [param Type ...] ReturnType body ...). The return \ - type is not optional; a function that returns nothing writes ()") + "%s is (%s name [param Type ...] ReturnType body ...). The return \ + type is not optional; a function that returns nothing writes ()" + head head) (* ── The dyn side's classes and generic functions ────────────────── Four forms, all of them shorthand: nothing below [Classes.expand] knows @@ -1463,7 +1467,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 }) + nloc = n.loc; fprivate = false }) | _ -> fail f "%s" usage) | List ({ v = Sym "defmethod"; _ } :: args) -> @@ -1477,7 +1481,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 } }) + nloc = n.loc; fprivate = false } }) | _ -> fail f "defmethod is (defmethod generic dispatch [param ...] body ...). \ @@ -1508,11 +1512,12 @@ let rec decl (f : Form.t) : Ast.decl = (match List.rev rest with | [ n; { v = Form.Vec ps; _ } ] -> mk (mkd { Ast.name = sym n; params = fields f ps; praw = None; - ret = None; fwhere = []; fbody = []; nloc = n.loc } csym) + ret = None; fwhere = []; fbody = []; nloc = n.loc; + fprivate = false } 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 } csym) + nloc = n.loc; fprivate = false } csym) | _ -> fail f "%s" usage) | _ -> fail f "%s" usage) @@ -1744,7 +1749,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 }) + nloc = n.loc; fprivate = false }) | _ -> fail f "defmacro is (defmacro name [param ...] body ...)") diff --git a/test/dune b/test/dune index f00bff7b..0b7edebd 100644 --- a/test/dune +++ b/test/dune @@ -64,6 +64,10 @@ ; pkg-generic-reject.flan import: the body has to be present where the copy ; is made, so the directory comes whole like every other package. (glob_files programs/pkgs/gen/*) + ; The package with a defn-, and the package that reaches into it: + ; pkg-private*.flan. + (glob_files programs/pkgs/secret/*) + (glob_files programs/pkgs/nosy/*) ; 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 @@ -235,7 +239,9 @@ (glob_files programs/pkgs/macspin/*) ; And the package shadow-builtin.flan imports. (glob_files programs/pkgs/shadowed/*) - (glob_files programs/pkgs/gen/*)) + (glob_files programs/pkgs/gen/*) + (glob_files programs/pkgs/secret/*) + (glob_files programs/pkgs/nosy/*)) (action (run ./test_valgrind.exe))) ; The corpus a fourth time, through the hand-written x86-64 backend, compared @@ -295,7 +301,9 @@ (glob_files programs/pkgs/macspin/*) ; And the package shadow-builtin.flan imports. (glob_files programs/pkgs/shadowed/*) - (glob_files programs/pkgs/gen/*)) + (glob_files programs/pkgs/gen/*) + (glob_files programs/pkgs/secret/*) + (glob_files programs/pkgs/nosy/*)) (action (setenv SURVEY_STRICT 1 (setenv SURVEY_QUIET 1 @@ -472,7 +480,9 @@ (glob_files programs/pkgs/macspin/*) ; And the package shadow-builtin.flan imports. (glob_files programs/pkgs/shadowed/*) - (glob_files programs/pkgs/gen/*)) + (glob_files programs/pkgs/gen/*) + (glob_files programs/pkgs/secret/*) + (glob_files programs/pkgs/nosy/*)) (action (setenv SURVEY_STRICT 1 (setenv SURVEY_QUIET 1 diff --git a/test/programs/pkg-private-call.flan b/test/programs/pkg-private-call.flan new file mode 100644 index 00000000..cd91891e --- /dev/null +++ b/test/programs/pkg-private-call.flan @@ -0,0 +1,7 @@ +;;;; A call to a package's private function, from outside the package. + +(import secret "pkgs/secret") + +(defn main [] i32 + (print (secret/mix 1 2)) + 0) diff --git a/test/programs/pkg-private-nested.flan b/test/programs/pkg-private-nested.flan new file mode 100644 index 00000000..b8800109 --- /dev/null +++ b/test/programs/pkg-private-nested.flan @@ -0,0 +1,8 @@ +;;;; A package that calls another package's private function. The refusal is +;;;; at nosy's call, in nosy's file. + +(import nosy "pkgs/nosy") + +(defn main [] i32 + (print (nosy/peek 1)) + 0) diff --git a/test/programs/pkg-private-value.flan b/test/programs/pkg-private-value.flan new file mode 100644 index 00000000..fc1960f5 --- /dev/null +++ b/test/programs/pkg-private-value.flan @@ -0,0 +1,7 @@ +;;;; A package's private function, taken as a value from outside the package. + +(import secret "pkgs/secret") + +(defn main [] i32 + (print (secret/apply2 secret/mix 1 2)) + 0) diff --git a/test/programs/pkg-private.flan b/test/programs/pkg-private.flan new file mode 100644 index 00000000..f3125bbd --- /dev/null +++ b/test/programs/pkg-private.flan @@ -0,0 +1,10 @@ +;;;; 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. + +(import secret "pkgs/secret") + +(defn main [] i32 + (print (secret/combine 1 2)) (println "") + (print (secret/twice 3)) (println "") + (print (secret/via-value 4 5)) (println "") + 0) diff --git a/test/programs/pkgs/nosy/nosy.flan b/test/programs/pkgs/nosy/nosy.flan new file mode 100644 index 00000000..ec7336af --- /dev/null +++ b/test/programs/pkgs/nosy/nosy.flan @@ -0,0 +1,6 @@ +;;;; A package that imports secret and reaches for its private function under +;;;; the qualified name it arrives as. + +(import secret "../secret") + +(defn peek [a i32] i32 (secret/mix a 1)) diff --git a/test/programs/pkgs/secret/more.flan b/test/programs/pkgs/secret/more.flan new file mode 100644 index 00000000..05645116 --- /dev/null +++ b/test/programs/pkgs/secret/more.flan @@ -0,0 +1,8 @@ +;;;; The package's second file. It is in the same directory, so mix is its +;;;; own to call. + +(defn twice [a i32] i32 (mix a a)) + +(defn apply2 [f (Fn [i32 i32] i32) a i32 b i32] i32 (f a b)) + +(defn via-value [a i32 b i32] i32 (apply2 mix a b)) diff --git a/test/programs/pkgs/secret/secret.flan b/test/programs/pkgs/secret/secret.flan new file mode 100644 index 00000000..9f759136 --- /dev/null +++ b/test/programs/pkgs/secret/secret.flan @@ -0,0 +1,7 @@ +;;;; A package with a private function. mix is declared with defn-, so only +;;;; the files in this directory may use it: combine calls it here, and the +;;;; package's other file calls it and hands it out as a value. + +(defn- mix [a i32 b i32] i32 (+ (* a 10) b)) + +(defn combine [a i32 b i32] i32 (mix a b)) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 58a8cf4e..01a40f3c 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2778,6 +2778,11 @@ let () = shape/Box and not area/shape/Box. *) outputs "a diamond, with a type crossing it" "programs/pkg-diamond.flan" "3\n6\n20\n"; + (* A [defn-] is callable from anywhere in its own package: the file that + 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"; (* 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 @@ -3053,6 +3058,19 @@ let () = if sand_checks then refuses "a package's main is not visible" "programs/pkg-hidden-main.flan" "sand/main is not a name"; + (* [defn-]: a package's own function, refused at every use from outside + its directory — a call, the name taken as a value, and a call from a + second package that imports it, where the name arrives qualified under + the alias that package chose. The working half is + [outputs "a defn- used inside its package"]. *) + refuses "a defn- called from outside its package" + "programs/pkg-private-call.flan" "secret/mix is private to its package"; + refuses "a defn- taken as a value from outside its package" + "programs/pkg-private-value.flan" "secret/mix is private to its package"; + refuses "a defn- called from another package" + "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 "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 76475774..ec59874e 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -590,6 +590,32 @@ let () = (fun d -> try Unix.rmdir d with Unix.Unix_error _ -> ()) [ pkg; tmp ]; + (* ── A defn- through the dev loop ────────────────────────────────── + C-c C-c on a [defn-] in its package's own file redefines it under the + qualified name and the session keeps it private: the package's callers + still reach the new body, and a form evaluated in the importer's buffer + is still refused. A [defn-] in the program's own file is the program's + and is callable from the rest of it. *) + let tp, _ = Session.create ~file:"programs/pkg-private.flan" () in + (match Session.eval ~origin:"programs/pkgs/secret/secret.flan" tp + "(defn- mix [a i32 b i32] i32 (+ (* a 100) b))" with + | c -> + if not (has c.Session.ir "mul i32") then + fail "a defn- redefined in its package's file installed no new body" + | exception Loc.Error { Loc.dmsg = m; _ } -> + fail "redefining a defn- in its package's file: %s" m); + (match Session.eval ~origin:"programs/pkg-private.flan" tp + "(defn peek [] i32 (secret/mix 1 2))" with + | _ -> fail "a redefined defn- became callable from outside its package" + | exception Loc.Error { Loc.dmsg = m; _ } -> + if not (has m "secret/mix is private to its package") then + fail "a call to a redefined defn- was refused for another reason: %s" m); + (match Session.eval ~origin:"programs/pkg-private.flan" tp + "(defn- helper [] i32 7)\n(defn use-helper [] i32 (helper))" with + | _ -> () + | exception Loc.Error { Loc.dmsg = m; _ } -> + fail "a defn- in the program's own file: %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 the REPL saw no macros — not a package's and not the prelude's, which is diff --git a/vendor/edn/edn.flan b/vendor/edn/edn.flan index 7c524245..e7cb7058 100644 --- a/vendor/edn/edn.flan +++ b/vendor/edn/edn.flan @@ -228,18 +228,18 @@ ;; A comma is whitespace in EDN, which is the rule most hand-written readers ;; get wrong: {:a 1, :b 2} is one map and the comma is not a token. -(defn ws? [b u8] bool +(defn- ws? [b u8] bool (or (space? b) (= b \,))) ;; Everything that ends an unquoted token. Note `;` is here: `[1;c` has the ;; comment start immediately after the 1, with no space, and a scanner that ;; only stopped on whitespace and brackets would read "1;c" as one number. -(defn delim? [b u8] bool +(defn- delim? [b u8] bool (or (ws? b) (= b \() (= b \)) (= b \[) (= b \]) (= b \{) (= b \}) (= b \") (= b \;))) -(defn alpha? [b u8] bool +(defn- alpha? [b u8] bool (or (and (>= b \a) (<= b \z)) (and (>= b \A) (<= b \Z)))) @@ -247,7 +247,7 @@ ;; "anything that is not a delimiter": without it every stray byte becomes a ;; one-character symbol, and `@` or a backtick — a Clojure reader macro, not ;; EDN — reads as a name instead of being reported at the byte it is on. -(defn sym-start? [b u8] bool +(defn- sym-start? [b u8] bool (or (alpha? b) (= b \.) (= b \*) (= b \+) (= b \!) (= b \-) (= b \_) (= b \?) (= b \$) (= b \%) (= b \&) (= b \=) (= b \<) (= b \>) @@ -259,26 +259,26 @@ ;; yet, so edn/scan-atom and edn/push-open are as callable as edn/next is. ;; Nothing below is part of the API and none of it will keep its shape. -(defn at-end? [c (Ptr Cursor)] bool +(defn- at-end? [c (Ptr Cursor)] bool (>= (.pos c) (length (.src c)))) ;; An empty slice of src, positioned at p. Used for the tokens that have no ;; text of their own — eof, error, and every delimiter. It is still a slice of ;; the input rather than a slice of nothing, so `text` has one meaning for all ;; token kinds. -(defn empty-at [c (Ptr Cursor) p i32] [u8] +(defn- empty-at [c (Ptr Cursor) p i32] [u8] (slice (.src c) p p)) -(defn token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token +(defn- token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token (Token {.kind kind .text (slice (.src c) lo hi) .pos p})) -(defn error-token [c (Ptr Cursor)] Token +(defn- error-token [c (Ptr Cursor)] Token (Token {.kind tok-error .text (empty-at c (.err-pos c)) .pos (.err-pos c)})) ;; Whitespace, commas, and `;` comments, which run to the newline or to the end ;; of input — a comment on the last line of a file with no trailing newline is ;; the case that decides whether the loop tests the length before the byte. -(defn skip-trivia [c (Ptr Cursor)] () +(defn- skip-trivia [c (Ptr Cursor)] () (while (not (at-end? c)) (let [b (at (.src c) (.pos c))] (cond @@ -313,7 +313,7 @@ (set (.depth c) (+ (.depth c) 1)) true) -(defn pop-close [c (Ptr Cursor) closer i32 p i32] bool +(defn- pop-close [c (Ptr Cursor) closer i32 p i32] bool (when (or (= (.depth c) 0) (!= (at (.open c) (- (.depth c) 1)) closer)) (fail c err-unbalanced p) @@ -325,7 +325,7 @@ ;; A token starting with a digit, or with a sign or a dot followed by one. ;; `-` alone is a symbol in EDN and stays one here. -(defn number-start? [c (Ptr Cursor) i i32] bool +(defn- number-start? [c (Ptr Cursor) i i32] bool (let [s (.src c)] (when (>= i (length s)) (return false)) @@ -335,7 +335,7 @@ (< (+ i 1) (length s)) (digit? (at s (+ i 1)))))) -(defn read-number [c (Ptr Cursor) lo i32] Token +(defn- read-number [c (Ptr Cursor) lo i32] Token (let [hi (scan-atom c lo)] (set (.pos c) hi) (let [text (slice (.src c) lo hi)] @@ -362,7 +362,7 @@ ;; A backslash anywhere inside is the refusal, reported at the backslash ;; rather than at the start of the string, because the backslash is what has ;; to be removed. -(defn read-string [c (Ptr Cursor) lo i32] Token +(defn- read-string [c (Ptr Cursor) lo i32] Token (let [i (+ lo 1) s (.src c)] (while (< i (length s)) @@ -536,7 +536,7 @@ (Some (bytes=? (.text t) (bytes-view "true"))) None)) -(defn text=? [t Token s string] bool +(defn- text=? [t Token s string] bool (bytes=? (.text t) (bytes-view s))) ;; A keyword whose name is s. The leading colon is not part of `text`, so this diff --git a/vendor/edn/provide.flan b/vendor/edn/provide.flan index d2814461..6196ea6e 100644 --- a/vendor/edn/provide.flan +++ b/vendor/edn/provide.flan @@ -57,13 +57,13 @@ ;; ── Small string work, for the refusals and the names ─────────────── -(defn joined [a string b string] string +(defn- joined [a string b string] string (let [v (vec-new u8)] (append (addr v) (bytes-view a)) (append (addr v) (bytes-view b)) (string (slice v)))) -(defn joined3 [a string b string c string] string +(defn- joined3 [a string b string c string] string (joined a (joined b c))) ;; Copied out, and not `(string (i64->bytes n))`. The prelude's note over @@ -71,7 +71,7 @@ ;; in the runtime, so two of its results cannot be held at once — and `where` ;; below holds a line and a column at the same time, which read as the same ;; number until this copied. -(defn i64->string [n i64] string +(defn- i64->string [n i64] string (let [v (vec-new u8)] (append-i64 (addr v) n) (string (slice v)))) @@ -80,7 +80,7 @@ ;; buffer costs nothing to produce. A person reading a refusal wants a line and ;; a column, so the newlines before the offset are counted here — once per ;; refusal, which is as often as this is ever called. -(defn where [src [u8] pos i32] string +(defn- where [src [u8] pos i32] string (let [line (i64 1) col (i64 1) i (i32 0)] @@ -110,13 +110,13 @@ reader Form bad string]) -(defn derived-bad [msg string] Derived +(defn- derived-bad [msg string] Derived (Derived {.ty `i64 .decls (form-nil) .reader `0 .bad msg})) -(defn ok-derived [ty Form decls [Form] reader Form] Derived +(defn- ok-derived [ty Form decls [Form] reader Form] Derived (Derived {.ty ty .decls decls .reader reader .bad ""})) -(defn bad? [d Derived] bool +(defn- bad? [d Derived] bool (> (length (bytes-view (.bad d))) 0)) ;; ── The scalars a generated reader calls ──────────────────────────── @@ -212,7 +212,7 @@ ;; One value, from the cursor's current position, consumed. `name` is what a ;; struct here would be called; `src` is the whole buffer, for the positions a ;; refusal names. -(defn derive [c (Ptr Cursor) name string src [u8]] Derived +(defn- derive [c (Ptr Cursor) name string src [u8]] Derived (let [t (next c)] (when (not (ok? c)) (return (derived-bad @@ -242,7 +242,7 @@ ;; decides; every one after it is compared against that decision and both ;; positions are named when they disagree, because "heterogeneous" without ;; saying where sends someone to read the whole file. -(defn derive-vec [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived +(defn- derive-vec [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \]) (return (derived-bad (joined3 "the empty vector at " (where src at-pos) @@ -286,12 +286,12 @@ ;; a signature, and the bare call in the body gets it from `want`. It is also ;; the more readable expansion: the reader says `(cells-new a)` where it would ;; otherwise carry a type nobody wrote. -(defn with-decl [decls [Form] d Form] [Form] +(defn- with-decl [decls [Form] d Form] [Form] (form-append decls (form-cons d (form-nil)))) ;; A set becomes `(Map T bool)`, so its elements are map keys. `derive-key` is ;; where that constraint is enforced and said. -(defn derive-set [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived +(defn- derive-set [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \}) (return (derived-bad (joined3 "the empty set at " (where src at-pos) @@ -326,7 +326,7 @@ ;; fixed array, which is one where a Vec is not; anything else is refused here ;; rather than at the `(Map ...)` the caller would build out of it, because a ;; map-key refusal names a type nobody wrote. -(defn derive-key [c (Ptr Cursor) name string src [u8]] Derived +(defn- derive-key [c (Ptr Cursor) name string src [u8]] Derived (when (at-byte? c \[) (return (derive-array c name src))) (let [d (derive c name src)] @@ -342,7 +342,7 @@ ;; of the set has to be the same length as well as the same shape — which falls ;; out of the type comparison the caller already makes, since the length is in ;; the type it compares. -(defn derive-array [c (Ptr Cursor) name string src [u8]] Derived +(defn- derive-array [c (Ptr Cursor) name string src [u8]] Derived (let [open (next c)] (when (at-byte? c \]) (return (derived-bad @@ -379,7 +379,7 @@ (expect c tok-vec-close) arr))))))) -(defn disagreement [what string src [u8] at-pos i32 n i64 +(defn- disagreement [what string src [u8] at-pos i32 n i64 first Form second Form] string (joined3 (joined3 "the " what " at ") (where src at-pos) @@ -400,7 +400,7 @@ ;; differently is the two arms a hand-written reader had no reason to have — a ;; key that is not a field of the struct, and a field the file did not have. ;; Both signal SchemaDrift. See the note over that type. -(defn derive-map [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived +(defn- derive-map [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \}) (return (derived-bad (joined3 "the empty map at " (where src at-pos) @@ -485,7 +485,7 @@ ;; ── Comparing and rendering a type form ───────────────────────────── -(defn same-type? [a Form b Form] bool +(defn- same-type? [a Form b Form] bool (bytes=? (bytes-view (render a)) (bytes-view (render b)))) ;; A type form as text, for the refusals. Only the shapes this file builds — a @@ -499,7 +499,7 @@ (Form.Vec xs) (joined3 "[" (render-items xs) "]") _ "?")) -(defn render-items [xs [Form]] string +(defn- render-items [xs [Form]] string (let [out ""] (dotimes [i (length xs)] (set out (if (= i 0) @@ -510,7 +510,7 @@ ;; What check.ml takes as a map key, narrowed to what this file can produce. ;; A float is deliberately absent and the checker says why: NaN is not equal to ;; itself, so there is no equality for a map to hash. -(defn key-type? [t Form] bool +(defn- key-type? [t Form] bool (let [s (bytes-view (render t))] (or (bytes=? s (bytes-view "i64")) (or (bytes=? s (bytes-view "bool")) @@ -543,7 +543,7 @@ _ (refuse "defedn's first argument is the name of the struct to declare, written as a name")) _ (refuse "defedn's second argument is the path to the data file, written as a string literal — the file is read while this is being compiled, so there is nothing here to compute a path from")))) -(defn provide [name string path string src [u8]] Form +(defn- provide [name string path string src [u8]] Form (let [cur (cursor src) d (derive (addr cur) name src)] (if (bad? d) @@ -580,5 +580,5 @@ ;; already put the report on the `defedn` the author wrote. The name is a ;; gensym, so two refusals in one file are two reports rather than a name ;; defined twice. -(defn refuse [msg string] Form +(defn- refuse [msg string] Form `(defn ~(gensym) [] () (compile-error ~(Form.Str {.s msg})))) diff --git a/vendor/json/json.flan b/vendor/json/json.flan index 707d66f9..57f2b4bb 100644 --- a/vendor/json/json.flan +++ b/vendor/json/json.flan @@ -281,21 +281,21 @@ ;; start against the byte before it — `1//x` — and a scanner that stopped only ;; on whitespace and brackets would read `1//x` as one atom and then report a ;; bad number instead of a comment. -(defn delim? [b u8] bool +(defn- delim? [b u8] bool (or (space? b) (= b \() (= b \)) (= b \[) (= b \]) (= b \{) (= b \}) (= b \") (= b \') (= b \,) (= b \:) (= b \/))) -(defn alpha? [b u8] bool +(defn- alpha? [b u8] bool (or (and (>= b \a) (<= b \z)) (and (>= b \A) (<= b \Z)))) -(defn hex? [b u8] bool +(defn- hex? [b u8] bool (or (digit? b) (and (>= b \a) (<= b \f)) (and (>= b \A) (<= b \F)))) -(defn hex-val [b u8] i32 +(defn- hex-val [b u8] i32 (cond (digit? b) (- (i32 b) (i32 \0)) (and (>= b \a) (<= b \f)) (+ 10 (- (i32 b) (i32 \a))) @@ -307,24 +307,24 @@ ;; yet, so json/scan-atom and json/push-open are as callable as json/next is. ;; Nothing below is part of the API and none of it will keep its shape. -(defn at-end? [c (Ptr Cursor)] bool +(defn- at-end? [c (Ptr Cursor)] bool (>= (.pos c) (length (.src c)))) ;; An empty slice of src, positioned at p. Used for the tokens that have no ;; text of their own — eof, error, and every delimiter — so that `text` has one ;; meaning for every token kind and not two. -(defn empty-at [c (Ptr Cursor) p i32] [u8] +(defn- empty-at [c (Ptr Cursor) p i32] [u8] (slice (.src c) p p)) -(defn token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token +(defn- token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token (Token {.kind kind .text (slice (.src c) lo hi) .pos p})) -(defn error-token [c (Ptr Cursor)] Token +(defn- error-token [c (Ptr Cursor)] Token (Token {.kind tok-error .text (empty-at c (.err-pos c)) .pos (.err-pos c)})) ;; Whitespace only. There is no comment case here and there is deliberately no ;; comment case anywhere: a `/` reaches the dispatch and is refused by name. -(defn skip-trivia [c (Ptr Cursor)] () +(defn- skip-trivia [c (Ptr Cursor)] () (while (and (not (at-end? c)) (space? (at (.src c) (.pos c)))) (set (.pos c) (+ (.pos c) 1)))) @@ -344,7 +344,7 @@ (set (.depth c) (+ (.depth c) 1)) true) -(defn pop-close [c (Ptr Cursor) closer i32 p i32] bool +(defn- pop-close [c (Ptr Cursor) closer i32 p i32] bool (when (or (= (.depth c) 0) (!= (at (.open c) (- (.depth c) 1)) closer)) (fail c err-unbalanced p) @@ -358,7 +358,7 @@ ;; are here so that they reach the scanner and get the refusal that names them, ;; instead of falling through to "unexpected byte" — which would be true and ;; would not tell anyone that the fix is to write 0.5. -(defn number-start? [b u8] bool +(defn- number-start? [b u8] bool (or (digit? b) (= b \-) (= b \+) (= b \.))) ;; JSON's number grammar, written out rather than left to parse-i64: -?(0 | @@ -371,7 +371,7 @@ ;; A number with neither a fraction nor an exponent is tok-int and anything ;; else is tok-float, so 1e3 is a float even though its value is whole. That is ;; the grammar's own split and not a guess about the caller's field. -(defn read-number [c (Ptr Cursor) lo i32] Token +(defn- read-number [c (Ptr Cursor) lo i32] Token (let [s (.src c) i lo float? false] @@ -447,7 +447,7 @@ ;; Four hex digits starting at i, as a code point, or -1. Used twice — for an ;; escape and for the low half of a surrogate pair — which is the whole reason ;; it is a function. -(defn hex4 [c (Ptr Cursor) i i32] i32 +(defn- hex4 [c (Ptr Cursor) i i32] i32 (let [s (.src c)] (when (> (+ i 4) (length s)) (return -1)) @@ -459,8 +459,8 @@ (set v (+ (* v 16) (hex-val b))))) v))) -(defn high-surrogate? [r i32] bool (and (>= r 0xd800) (<= r 0xdbff))) -(defn low-surrogate? [r i32] bool (and (>= r 0xdc00) (<= r 0xdfff))) +(defn- high-surrogate? [r i32] bool (and (>= r 0xd800) (<= r 0xdbff))) +(defn- low-surrogate? [r i32] bool (and (>= r 0xdc00) (<= r 0xdfff))) ;; The whole reason this is not three lines. `text` is the interior, between ;; the quotes and RAW; `pos` is the opening quote, so an editor underlines the @@ -472,7 +472,7 @@ ;; token rather than at the backslash. Surrogate PAIRING is checked here too, ;; and not only escape syntax, so that string-of's encode-rune can never be ;; handed a code point the prelude refuses. -(defn read-string [c (Ptr Cursor) lo i32] Token +(defn- read-string [c (Ptr Cursor) lo i32] Token (let [s (.src c) i (+ lo 1)] (while (< i (length s)) diff --git a/vendor/json/provide.flan b/vendor/json/provide.flan index f5a75c6c..cc5fddef 100644 --- a/vendor/json/provide.flan +++ b/vendor/json/provide.flan @@ -42,20 +42,20 @@ ;; ── Small string work ─────────────────────────────────────────────── -(defn joined [a string b string] string +(defn- joined [a string b string] string (let [v (vec-new u8)] (append (addr v) (bytes-view a)) (append (addr v) (bytes-view b)) (string (slice v)))) -(defn joined3 [a string b string c string] string +(defn- joined3 [a string b string c string] string (joined a (joined b c))) ;; Copied out, and not `(string (i64->bytes n))`: the prelude's note over ;; append-i64 is the reason — i64->bytes renders into one shared static buffer ;; in the runtime, so two of its results cannot be held at once, and `where` ;; holds a line and a column at the same time. -(defn i64->string [n i64] string +(defn- i64->string [n i64] string (let [v (vec-new u8)] (append-i64 (addr v) n) (string (slice v)))) @@ -63,7 +63,7 @@ ;; The tokenizer answers byte offsets. A person reading a refusal wants a line ;; and a column, so the newlines before the offset are counted here — once per ;; refusal, which is as often as this is ever called. -(defn where [src [u8] pos i32] string +(defn- where [src [u8] pos i32] string (let [line (i64 1) col (i64 1) i (i32 0)] @@ -87,16 +87,16 @@ reader Form bad string]) -(defn derived-bad [msg string] Derived +(defn- derived-bad [msg string] Derived (Derived {.ty `i64 .decls (form-nil) .reader `0 .bad msg})) -(defn ok-derived [ty Form decls [Form] reader Form] Derived +(defn- ok-derived [ty Form decls [Form] reader Form] Derived (Derived {.ty ty .decls decls .reader reader .bad ""})) -(defn bad? [d Derived] bool +(defn- bad? [d Derived] bool (> (length (bytes-view (.bad d))) 0)) -(defn with-decl [decls [Form] d Form] [Form] +(defn- with-decl [decls [Form] d Form] [Form] (form-append decls (form-cons d (form-nil)))) ;; ── The scalars a generated reader calls ──────────────────────────── @@ -173,7 +173,7 @@ ;; ── Deriving ──────────────────────────────────────────────────────── -(defn derive [c (Ptr Cursor) name string src [u8]] Derived +(defn- derive [c (Ptr Cursor) name string src [u8]] Derived (let [t (next c)] (when (not (ok? c)) (return (derived-bad @@ -202,7 +202,7 @@ ;; every one after it is compared against that, and both positions are named ;; when they disagree — "heterogeneous" on its own sends someone to read the ;; whole file. -(defn derive-array [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived +(defn- derive-array [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \]) (return (derived-bad (joined3 "the empty array at " (where src at-pos) @@ -248,7 +248,7 @@ ;; ── An object, which is a struct ──────────────────────────────────── -(defn derive-object [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived +(defn- derive-object [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \}) (return (derived-bad (joined3 "the empty object at " (where src at-pos) @@ -349,7 +349,7 @@ (defn key=? [t Token s string] bool (bytes=? (.text t) (bytes-view s))) -(defn has-escape? [s [u8]] bool +(defn- has-escape? [s [u8]] bool (dotimes [i (length s)] (when (= (at s i) \\) (return true))) @@ -358,7 +358,7 @@ ;; What a field name may be made of. Deliberately narrower than what the reader ;; would accept: this is the set a *person* would recognise as a name, and a ;; member called "a b" or "x.y" has no field it could become. -(defn name-like? [s [u8]] bool +(defn- name-like? [s [u8]] bool (when (= (length s) 0) (return false)) (dotimes [i (length s)] @@ -372,14 +372,14 @@ ;; A copy of a token's raw text as a string. The Vec header is dropped here on ;; purpose: this runs inside the compiler, where an expansion is bounded by the ;; size of the program being compiled. -(defn copy-of [s [u8]] string +(defn- copy-of [s [u8]] string (let [b (vec-new u8)] (append (addr b) s) (string (slice b)))) ;; ── Comparing and rendering a type form ───────────────────────────── -(defn same-type? [a Form b Form] bool +(defn- same-type? [a Form b Form] bool (bytes=? (bytes-view (render a)) (bytes-view (render b)))) ;; A type form as text, for the refusals. Only the shapes this file builds — a @@ -392,7 +392,7 @@ (Form.Vec xs) (joined3 "[" (render-items xs) "]") _ "?")) -(defn render-items [xs [Form]] string +(defn- render-items [xs [Form]] string (let [out ""] (dotimes [i (length xs)] (set out (if (= i 0) @@ -424,7 +424,7 @@ _ (refuse "defjson's first argument is the name of the struct to declare, written as a name")) _ (refuse "defjson's second argument is the path to the data file, written as a string literal — the file is read while this is being compiled, so there is nothing here to compute a path from")))) -(defn provide [name string path string src [u8]] Form +(defn- provide [name string path string src [u8]] Form (let [cur (cursor src) d (derive (addr cur) name src)] (if (bad? d) @@ -460,5 +460,5 @@ ;; calls: the checker walks it, the arm fires, and `Loc.from_macro` has already ;; put the report on the `defjson` the author wrote. The name is a gensym, so ;; two refusals in one file are two reports rather than a name defined twice. -(defn refuse [msg string] Form +(defn- refuse [msg string] Form `(defn ~(gensym) [] () (compile-error ~(Form.Str {.s msg})))) diff --git a/web/index.html b/web/index.html index ea157cfe..ad24e795 100644 --- a/web/index.html +++ b/web/index.html @@ -1523,8 +1523,13 @@ existed.
refused at the line that wrote it. -Visibility is that one rule and no more: there is no package-private marker for
-anything other than main yet.
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.
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
@@ -2379,7 +2384,7 @@ refused inside a loop or a branch (a let is fine — it has the fun
extent); find-restart and compute-restarts are blocked on a
Restart type rather than on effort; a restart with parameters cannot be
taken from the break loop, which aims at a frame by position and has nothing to fill them
-with; there is no package-private marker other than main not being exported;
+with;
and there are no threads in the language. The class facility plan.org describes is
built — see dyn — and what is not built of it is the named-slot
constructor spelling and the user-written migration hook.