diff --git a/TODO.org b/TODO.org index 8e993c1a..b02cf6fc 100644 --- a/TODO.org +++ b/TODO.org @@ -531,11 +531,22 @@ 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 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 3f845741..59007952 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -794,10 +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. -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 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 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/emacs/flan-mode.el b/emacs/flan-mode.el index 2319497c..3a78cbfa 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" @@ -278,7 +278,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 @@ -575,6 +575,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 2efbfd6c..f0e77dfd 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -2576,7 +2576,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 e7e2f887..d150748b 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 f3677360..41708a09 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -215,6 +215,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; @@ -232,6 +238,7 @@ type fn = { fwhere : pred list; fbody : expr list; nloc : Loc.t; + fprivate : privacy; } type decl = { d : decl_kind; dloc : Loc.t } diff --git a/lib/check.ml b/lib/check.ml index f81e1479..64e43869 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -114,6 +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 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 @@ -194,6 +197,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 = []; @@ -3912,6 +3916,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 @@ -9004,11 +9009,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) @@ -9170,6 +9177,63 @@ 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 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. *) +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, 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 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-, 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 = (* 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. *) @@ -10611,6 +10675,9 @@ let collect env (decls : Ast.decl list) = in env.tyvars <- []; env.tvpreds <- []; + 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 d7663662..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 }, + ({ 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 bab0c784..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 }; + 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 cac0d6c6..a3f28a9b 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, 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 @@ -726,9 +727,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 @@ -905,6 +905,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 @@ -1415,6 +1429,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/macro.ml b/lib/macro.ml index b31ed83a..4691b651 100644 --- a/lib/macro.ml +++ b/lib/macro.ml @@ -168,7 +168,7 @@ let reduce (forms : Form.t list) : Form.t list = List.iter (fun f -> match head_name f with - | Some (("defn" | "defmacro"), n) when not (List.mem n !out) -> + | Some (("defn" | "defn-" | "defmacro"), n) when not (List.mem n !out) -> if names_macro !out f then begin out := n :: !out; changed := true end | _ -> ()) forms @@ -188,7 +188,7 @@ let reduce (forms : Form.t list) : Form.t list = List.filter (fun f -> match head_name f with - | Some ("defn", n) -> not (List.mem n !out) + | Some (("defn" | "defn-"), n) -> not (List.mem n !out) | _ -> true) forms diff --git a/lib/parse.ml b/lib/parse.ml index acc61a0f..48129474 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -710,7 +710,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 @@ -1400,7 +1400,12 @@ 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 = + 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 @@ -1434,11 +1439,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 @@ -1489,7 +1495,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 = Ast.Exported }) | _ -> fail f "%s" usage) | List ({ v = Sym "defmethod"; _ } :: args) -> @@ -1503,7 +1509,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 = Ast.Exported } }) | _ -> fail f "defmethod is (defmethod generic dispatch [param ...] body ...). \ @@ -1534,11 +1540,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 = 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 } csym) + nloc = n.loc; fprivate = Ast.Exported } csym) | _ -> fail f "%s" usage) | _ -> fail f "%s" usage) @@ -1770,7 +1777,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 = Ast.Exported }) | _ -> fail f "defmacro is (defmacro name [param ...] body ...)") diff --git a/lib/session.ml b/lib/session.ml index ed0d7860..977097f4 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/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-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-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-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..00e84df9 --- /dev/null +++ b/test/programs/pkg-private.flan @@ -0,0 +1,13 @@ +;;;; A package's private function, used from inside the package: from the +;;;; 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") + +(defn main [] i32 + (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/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..37d73dd0 --- /dev/null +++ b/test/programs/pkgs/secret/secret.flan @@ -0,0 +1,18 @@ +;;;; 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)) + +;; 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 b55ce306..64a1bba7 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2830,6 +2830,16 @@ 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\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 @@ -3105,6 +3115,33 @@ 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 "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 76475774..78a3936d 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -590,6 +590,46 @@ 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); + (* 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 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 aac03f46..791be8c7 100644 --- a/web/index.html +++ b/web/index.html @@ -1526,8 +1526,14 @@ 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. 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 @@ -2382,7 +2388,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.

@@ -2485,7 +2491,7 @@ disagree with the first.