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