builtin/ always reaches the compiler's own name
# Conflicts: # FIX.org
This commit is contained in:
commit
8d49d0dddc
180
FIX.org
180
FIX.org
@ -2502,10 +2502,13 @@ program of twenty thousand calls, measured in review. The list stays for the
|
||||
did-you-mean, whose order is its order; the set answers the membership.
|
||||
|
||||
** The warning, verbatim
|
||||
: shadow-builtin.flan:20:7: warning: get shadows the builtin get — every call in this program now reaches your definition
|
||||
: shadow-builtin.flan:20:7: warning: get shadows the builtin get — every call in this program now reaches your definition — the builtin stays reachable as builtin/get
|
||||
: 20 | (defn get [p P] i32 (.x p))
|
||||
: | ~~~
|
||||
|
||||
The clause after the second dash arrived a day later with the escape hatch
|
||||
itself; this entry shipped without it, because there was nothing to name.
|
||||
|
||||
Rendered by ~Loc.entry ~mark:'~' ~label:"warning: "~, which is the
|
||||
~--warn-memory~ precedent, so flycheck parses it exactly as it parses an
|
||||
error. Nothing raises and the exit status does not move. Unlike
|
||||
@ -2587,6 +2590,14 @@ stack-overflows at run time with no diagnostic from the compiler, which has
|
||||
nothing to object to. The warning says the name is taken over; it does not
|
||||
say this. An escape hatch is a language decision and is with the author.
|
||||
|
||||
/Closed the next day./ The author's answer was the qualified spelling — see
|
||||
"builtin/, the reserved qualifier, 2026-09-20" below. ~(defn len [s string]
|
||||
i32 (builtin/+ 1 (builtin/len s)))~ is the wrapper this paragraph said could
|
||||
not be written, and it runs: ~programs/builtin-qualified.flan~ prints 5 for
|
||||
it beside the builtin's own 4. The warning's sentence now carries the escape,
|
||||
so the reader is told what is left at the moment they are told the name was
|
||||
taken over.
|
||||
|
||||
** Pins
|
||||
- ~test_flan.ml~: the warning's kind, line and column; its message, matched
|
||||
whole and not by needle; that it carries no notes; that the source which
|
||||
@ -3380,3 +3391,170 @@ by =Unix._exit 0=, neither of which runs an atexit. The socket sits in
|
||||
above, "The daemon leaves its temp directory behind", still open. No separate
|
||||
fix is wanted here; the session-end cleanup that item asks for takes the
|
||||
socket with it.
|
||||
|
||||
* builtin/, the reserved qualifier, 2026-09-20
|
||||
|
||||
The author's decision, in the author's words:
|
||||
|
||||
#+begin_quote
|
||||
"the full spelling is fine, I like that."
|
||||
#+end_quote
|
||||
|
||||
This closes the dead end disclosed the same day by "Shadowing a builtin,
|
||||
2026-09-20": a defn named after a builtin wins program-wide inside its own
|
||||
file, and before this there was no remaining spelling for the thing it had
|
||||
taken over, so a definition that meant to *wrap* a builtin was unbounded
|
||||
recursion with no diagnostic. ~builtin/len~ is the builtin ~len~, whatever
|
||||
else the file has decided ~len~ means, and it is legal whether or not
|
||||
anything is shadowed — a spelling that only compiled while some other
|
||||
declaration existed would be one nobody could write down in advance.
|
||||
|
||||
** The resolver
|
||||
Two interceptions, both at the very top of the dispatch they sit in, and each
|
||||
strips the prefix and re-enters the same function with one flag set:
|
||||
|
||||
: | _ when not qualified && qualified_builtin name <> None ->
|
||||
: let bare = Option.get (qualified_builtin name) in
|
||||
: if not (Hashtbl.mem builtin_set bare) then not_a_builtin loc bare;
|
||||
: named_call ~qualified:true ctx ~want loc bare args
|
||||
|
||||
~named_call~ gains ~?(qualified = false)~ and the shadowing guard beneath it
|
||||
becomes ~not qualified && shadows_builtin ...~. That flag is the whole of the
|
||||
mechanism: a qualified call has already said which of the two readings it
|
||||
means, so there is nothing left for shadowing to decide, and every arm below
|
||||
sees the *bare* name — which is why ~(builtin/len 1 2)~ is refused with
|
||||
exactly the sentence ~(len 1 2)~ would get. It cannot loop:
|
||||
~builtin/builtin/len~ strips once and is then refused by name, because
|
||||
~builtin/len~ is not in ~builtin_set~.
|
||||
|
||||
~var~ gains the same arm, for the builtins that are names rather than calls —
|
||||
~true~, ~false~, ~nil~, ~None~, ~context/allocator~, ~context/temp~. The last
|
||||
two fall out for free: stripping one prefix off ~builtin/context/allocator~
|
||||
leaves a name the match already has an arm for.
|
||||
|
||||
*The one asymmetry, and it is load-bearing.* ~var~'s qualified path must
|
||||
refuse where the call path falls through. A qualified name that gets past the
|
||||
value arms is in ~builtin_set~ but is call-only, and letting it fall into
|
||||
~lookup~/~globals~/~fns~ would answer ~builtin/len~ with the address of the
|
||||
very definition the qualifier was written to escape — the feature inverted,
|
||||
silently. So a guarded ~| _ when qualified ->~ arm sits above the catch-all:
|
||||
|
||||
: builtin/len is the builtin len, which is a call and not a value — a builtin
|
||||
: has no address to pass. Write (builtin/len ...) at the call, or wrap it in a
|
||||
: defn to pass that
|
||||
|
||||
** Why ~/~ and not ~(builtin get)~
|
||||
The spelling is the package qualifier's, deliberately. A reader who knows
|
||||
that ~rl/draw-fps~ is ~draw-fps~ from the package imported as ~rl~ already
|
||||
knows what ~builtin/len~ is and needs no second syntax. What makes it
|
||||
unambiguous is that ~builtin~ is *reserved* rather than resolved: every
|
||||
qualifier in a finished program comes from ~Load.qualify~, and every
|
||||
~qualify~ takes its alias from an ~import~ form, so refusing that one alias
|
||||
is the whole of the reservation — there is no other door.
|
||||
|
||||
The refusal is at the top of ~Load.import~, before the package is read, which
|
||||
also covers a package importing one under that alias since every import goes
|
||||
through that function:
|
||||
|
||||
: builtin is a reserved qualifier and cannot be an import alias: builtin/name
|
||||
: always means the compiler's builtin, which is how a program reaches a
|
||||
: builtin it has shadowed. Import this package under another alias
|
||||
|
||||
*It is the alias and not the directory.* The task that asked for this said "a
|
||||
package directory named ~builtin~ must be refused at import", and the code
|
||||
says something slightly narrower, because the alias is always written out —
|
||||
~(import rl "vendor:raylib")~, ~parse.ml~'s two-element form — and a
|
||||
directory never becomes a qualifier on its own. A package whose directory is
|
||||
called ~builtin~ imports fine under any other name and collides with nothing;
|
||||
~(import builtin "anything")~ is what is refused. The directory is not named
|
||||
in the message either: it has been resolved to an absolute path by then, and
|
||||
the caret is already under the import form, which carries the path the reader
|
||||
wrote.
|
||||
|
||||
*And from the other side.* ~(defn builtin/len ...)~ reads — ~/~ is an
|
||||
ordinary symbol character — and would land in ~env.fns~ under a name nothing
|
||||
could ever call, since the prefix is stripped before any table is consulted.
|
||||
~Check.collect~ refuses any declaration whose name carries the prefix, over
|
||||
~Ast.declared_name~ so it covers every declaration kind at once:
|
||||
|
||||
: builtin/len cannot be declared: builtin/ is a reserved qualifier, so a name
|
||||
: spelled with it reaches the compiler's builtins and never a declaration —
|
||||
: nothing could call this one
|
||||
|
||||
** The reader, and ~builtin/+~
|
||||
No exception was needed. ~reader.ml~'s ~is_delimiter~ makes ~/~ ordinary and
|
||||
says so in its own comment (~rl/draw-fps~ is one symbol), and the operator
|
||||
characters are ordinary for the same reason ~+~ is a symbol at all. The one
|
||||
path that could have taken ~builtin/+~ apart is the number reader, which
|
||||
takes a token starting with a digit or with ~-~/~+~ followed by a digit, and
|
||||
~builtin/+~ starts with ~b~. So it arrives as one symbol and there is nothing
|
||||
to document as unreadable. ~(builtin/+ 1 2)~ is 3 in a file whose ~+~ answers
|
||||
99, and it lowers to the ~Add~ prim rather than to a ~Call~.
|
||||
|
||||
** The refusals for a qualifier that reaches nothing
|
||||
Its own kind, ~check/unknown-builtin~, and the did-you-mean is over
|
||||
~builtin_names~ *alone* — not over the program's own names. The reader wrote
|
||||
the qualifier, so they were reaching for a compiler name, and offering them a
|
||||
defn called ~lem~ would answer a question they did not ask. Every other
|
||||
did-you-mean in ~check.ml~ keeps the candidates it already had, and the
|
||||
dot-access diagnostic is untouched: nothing anywhere suggests a ~builtin/~
|
||||
spelling for a name that was written bare.
|
||||
|
||||
: nosuch is not a builtin, so builtin/nosuch reaches nothing. The builtin/
|
||||
: qualifier reaches the compiler's own names and nothing else; an ordinary
|
||||
: function is called by the name it was defined under
|
||||
|
||||
: lne is not a builtin, so builtin/lne reaches nothing — did you mean
|
||||
: builtin/len?
|
||||
|
||||
: builtin/ needs a name after it — the qualifier reaches a builtin, as
|
||||
: (builtin/len v)
|
||||
|
||||
** The warning now names the escape
|
||||
: shadow-builtin.flan:20:7: warning: get shadows the builtin get — every call in this program now reaches your definition — the builtin stays reachable as builtin/get
|
||||
|
||||
One sentence still, and the second half is the half the reader wants next:
|
||||
they are being told the name was taken over, and what is left is the thing
|
||||
they are about to go looking for.
|
||||
|
||||
** The arm-scraper, which nearly broke twice
|
||||
~test_flan.ml~ reads ~check.ml~'s source to cross-check the builtin arms
|
||||
against ~Check.builtins~, keying the two regions on the literal prefixes
|
||||
~"and named_call "~ and ~"and var ctx "~. So the optional argument had to go
|
||||
*after* ~ctx~ in ~var~ — ~and var ctx ?(qualified = false) loc ~want name~ —
|
||||
and a leading one would have emptied that region and reported ~true~,
|
||||
~false~, ~nil~, ~None~ and the two ~context/~ names as deleted arms. Both new
|
||||
arms are guarded and carry no string literal in the head, so the scraper skips
|
||||
them exactly as it skips the shadowing guard; nothing in that test changed.
|
||||
|
||||
** Pins
|
||||
- ~test_flan.ml~: ~builtin/len~ checks with nothing shadowed; with a
|
||||
two-parameter ~len~ in the way, the bare call at two arguments checks and
|
||||
the qualified one is refused at *the builtin's* arity ("len takes 1
|
||||
argument, given 2") — the pair is refusable only under one reading each, so
|
||||
it cannot pass under both.
|
||||
- ~test_flan.ml~: the self-referential wrapper ~(defn len [s string] i32
|
||||
(builtin/+ 1 (builtin/len s)))~ checks and its body holds no ~Call ("len",
|
||||
_)~ anywhere, walked with ~Tast.walk~ — the difference between a wrapper
|
||||
and a loop, asserted rather than assumed.
|
||||
- ~test_flan.ml~: ~builtin/+~ with ~+~ shadowed lowers to ~Prim (Add, _)~;
|
||||
~builtin/nil~ and ~builtin/context/allocator~ check; ~builtin/len~ in a
|
||||
value position is refused with the call-and-not-a-value sentence.
|
||||
- ~test_flan.ml~: ~check/unknown-builtin~ as a kind, both of its messages
|
||||
whole (with and without the near miss), and the declaration refusal; plus
|
||||
that a *bare* typo is still answered bare ("did you mean len?") and never
|
||||
with a qualifier.
|
||||
- ~test_flan.ml~, changed: the two shadow-warning messages, matched whole,
|
||||
now carry the escape clause.
|
||||
- ~test_acceptance.ml~: ~programs/builtin-qualified.flan~ outputs
|
||||
~9\n5\n4\n99\n3\n~ — builtin/max unshadowed, the wrapper's 5, builtin/len's
|
||||
4 beside it, the shadowed operator's 99, builtin/+'s 3. It is an ordinary
|
||||
corpus row, so the ~@x86~ sweep compares both backends over it.
|
||||
- ~test_acceptance.ml~: ~programs/import-builtin-alias.flan~ is refused, by
|
||||
the alias sentence and by the clause saying what the qualifier is for.
|
||||
|
||||
** What was run
|
||||
~dune test~ in the lane's worktree, green. Per the batching policy the ~@x86~
|
||||
and ~@sanitize~ sweeps were not run here — the new corpus row is registered
|
||||
for the x86 survey through the existing ~programs/*.flan~ glob and will be
|
||||
compared on the next sweep.
|
||||
|
||||
130
lib/check.ml
130
lib/check.ml
@ -319,6 +319,56 @@ let builtin_names : string list ref = ref []
|
||||
twenty thousand calls. Filled beside the list. *)
|
||||
let builtin_set : (string, unit) Hashtbl.t = Hashtbl.create 128
|
||||
|
||||
(* ── builtin/, the reserved qualifier ──────────────────────────────────
|
||||
[builtin/len] is the builtin [len], whatever else the program has decided
|
||||
[len] means. It is the way out of the dead end shadowing used to leave: a
|
||||
[(defn len ...)] takes the bare name over for its whole file, and before
|
||||
this there was no remaining spelling for the thing it was wrapping, so the
|
||||
wrapper was unbounded recursion instead.
|
||||
|
||||
The spelling is the package qualifier's, deliberately. A reader who knows
|
||||
that [rl/draw-fps] is [draw-fps] from the package imported as [rl] already
|
||||
knows what [builtin/len] is, and needs no second syntax to learn. What
|
||||
makes it work is that [builtin] is reserved rather than resolved: [Load]'s
|
||||
qualifier comes from the alias in an [import] form and from nowhere else,
|
||||
so refusing that one alias ([Load.reserved_alias]) is the whole of keeping
|
||||
this prefix unambiguous.
|
||||
|
||||
It is legal when nothing is shadowed, too. A spelling that only compiles
|
||||
while some other declaration exists is a spelling nobody can write down in
|
||||
advance, and the point of an escape hatch is that it is always there. *)
|
||||
let builtin_prefix = "builtin/"
|
||||
|
||||
let qualified_builtin n =
|
||||
let p = String.length builtin_prefix in
|
||||
if String.length n >= p && String.sub n 0 p = builtin_prefix then
|
||||
Some (String.sub n p (String.length n - p))
|
||||
else None
|
||||
|
||||
(* [builtin/] reached with something that is not a builtin's name. The
|
||||
did-you-mean is over the builtins alone and not over the program's own
|
||||
names: the reader wrote the qualifier, so they were reaching for a
|
||||
compiler name, and offering them a defn called [lem] would be answering a
|
||||
question they did not ask. Every other did-you-mean in this file keeps the
|
||||
candidates it already had. *)
|
||||
let not_a_builtin loc bare =
|
||||
if bare = "" then
|
||||
Loc.failk "check/unknown-builtin" loc
|
||||
"%s needs a name after it — the qualifier reaches a builtin, as \
|
||||
(%slen v)" builtin_prefix builtin_prefix
|
||||
else
|
||||
match nearest !builtin_names bare with
|
||||
| Some m ->
|
||||
Loc.failk "check/unknown-builtin" loc
|
||||
"%s is not a builtin, so %s%s reaches nothing — did you mean %s%s?"
|
||||
bare builtin_prefix bare builtin_prefix m
|
||||
| None ->
|
||||
Loc.failk "check/unknown-builtin" loc
|
||||
"%s is not a builtin, so %s%s reaches nothing. The %s qualifier \
|
||||
reaches the compiler's own names and nothing else; an ordinary \
|
||||
function is called by the name it was defined under"
|
||||
bare builtin_prefix bare builtin_prefix
|
||||
|
||||
(* What a [break] or a [continue] may be talking about, innermost first.
|
||||
|
||||
[Lloop] is a loop it is lexically inside, carrying its label if it was given
|
||||
@ -3093,8 +3143,22 @@ and in_range loc k n =
|
||||
|
||||
(* The arms that are names rather than calls, and the same rule holds for them:
|
||||
each is in [builtins] below, and test_flan reads this match to check it. *)
|
||||
and var ctx loc ~want name =
|
||||
and var ctx ?(qualified = false) loc ~want name =
|
||||
match name with
|
||||
(* The qualifier in a value position: [builtin/nil], [builtin/true],
|
||||
[builtin/context/allocator] — the last one falls out for free, since
|
||||
stripping one prefix off it leaves a name this match has an arm for.
|
||||
|
||||
A qualified name that gets past these arms must be refused and not
|
||||
handed on to the tables below, which is the whole difference between
|
||||
this and the call path. Falling through would look up [len] in
|
||||
[env.fns] and answer with the address of the very definition the reader
|
||||
wrote [builtin/] to get away from — the feature inverted, silently. So
|
||||
the catch-all arm below asks [qualified] before it looks anything up. *)
|
||||
| _ when not qualified && qualified_builtin name <> None ->
|
||||
let bare = Option.get (qualified_builtin name) in
|
||||
if not (Hashtbl.mem builtin_set bare) then not_a_builtin loc bare;
|
||||
var ctx ~qualified:true loc ~want bare
|
||||
| "true" | "false" ->
|
||||
expect ctx loc ~want (mk loc Types.Bool (Tast.Bool (name = "true")))
|
||||
(* The dyn absence value, written down. It arrived with maps — (get m k) on
|
||||
@ -3129,6 +3193,18 @@ and var ctx loc ~want name =
|
||||
| "context/temp" ->
|
||||
expect ctx loc ~want
|
||||
(mk loc Types.Alloc (Tast.Prim (Tast.Rt "flan_context_temp", [])))
|
||||
| _ when qualified ->
|
||||
(* In [builtin_set] — the arm above checked — but not one of the four
|
||||
arms above this one, so it is a builtin that exists only as a call.
|
||||
There is no value to hand back: a builtin is an arm in the compiler,
|
||||
not a function in the program, so it has no address for a [Tast.FnAddr]
|
||||
to carry. Bare [len] in this position says "unknown name"; this says
|
||||
the true thing instead, which is that the name is real and the position
|
||||
is wrong. *)
|
||||
fail loc
|
||||
"%s%s is the builtin %s, which is a call and not a value — a builtin \
|
||||
has no address to pass. Write (%s%s ...) at the call, or wrap it in a \
|
||||
defn to pass that" builtin_prefix name name builtin_prefix name
|
||||
| _ ->
|
||||
match lookup ctx name with
|
||||
| Some b ->
|
||||
@ -5408,10 +5484,27 @@ and vec_at ctx loc (target : Tast.expr) (idx : Ast.expr list) =
|
||||
(* Every arm below is a name an editor can be asked about and no program ever
|
||||
wrote down, so each one needs a line in [builtins] further down this file.
|
||||
A new arm without an entry fails the build — test_flan reads both. *)
|
||||
and named_call ctx ~want loc name args =
|
||||
and named_call ?(qualified = false) ctx ~want loc name args =
|
||||
let prim p ty args = expect ctx loc ~want (mk loc ty (Tast.Prim (p, args))) in
|
||||
match name with
|
||||
(* The user's own definition, first — before every builtin arm below.
|
||||
(* [builtin/len], before anything else including the shadowing guard below.
|
||||
The prefix is stripped and the same dispatch runs again with [qualified],
|
||||
which is the one thing the guard consults: a qualified call has said
|
||||
which of the two it means, so there is nothing left for shadowing to
|
||||
decide. Everything after this point sees the bare name, so an arity or a
|
||||
type refusal on [(builtin/len 1 2)] reads exactly as it does on
|
||||
[(len 1 2)] — which is the point of the spelling, not a loss of detail.
|
||||
|
||||
Recursion rather than a flag threaded through the arms because there is
|
||||
only one thing to skip. It cannot loop: the stripped name has no second
|
||||
[builtin/] on it unless somebody wrote [builtin/builtin/len], which is
|
||||
stripped once and then refused by name. *)
|
||||
| _ when not qualified && qualified_builtin name <> None ->
|
||||
let bare = Option.get (qualified_builtin name) in
|
||||
if not (Hashtbl.mem builtin_set bare) then not_a_builtin loc bare;
|
||||
named_call ~qualified:true ctx ~want loc bare args
|
||||
(* The user's own definition, ahead of every builtin arm below — and behind
|
||||
the qualifier above, which is the one spelling it does not take over.
|
||||
Clojure's rule: a [(defn get ...)] takes the name over, and a call
|
||||
written in the program that defines it reaches that definition rather
|
||||
than the builtin it is named after. The defn site is warned about once
|
||||
@ -5423,7 +5516,8 @@ and named_call ctx ~want loc name args =
|
||||
another's: [shadows_builtin] answers false for a call in the prelude and
|
||||
for a call in imported package code, which is the same visibility rule a
|
||||
defn has everywhere else. *)
|
||||
| _ when shadows_builtin ctx loc name -> ordinary_call ctx ~want loc name args
|
||||
| _ when (not qualified) && shadows_builtin ctx loc name ->
|
||||
ordinary_call ctx ~want loc name args
|
||||
(* ── arithmetic and comparison ─────────────────────────────────── *)
|
||||
| "+" | "-" | "*" | "/" ->
|
||||
let p = match name with
|
||||
@ -8127,6 +8221,13 @@ let () =
|
||||
flag, because there is nothing to tune — a program either renamed a
|
||||
builtin or it did not, and the line is one line and rare.
|
||||
|
||||
The second half of the sentence names the way out. Until [builtin/] there
|
||||
was none: a file that defined [len] had given the builtin [len] up for the
|
||||
whole file, and a defn that meant to *wrap* it was unbounded recursion
|
||||
instead. Saying so here is the cheapest place it can be said — the reader
|
||||
is being told the name was taken over, and the next thing they want to
|
||||
know is what is left.
|
||||
|
||||
The prelude is skipped: its defns are the language's own and a collision
|
||||
there is a compiler bug rather than news for whoever is compiling. So are
|
||||
qualified names, for the reason [shadows_builtin]'s [visible] gives —
|
||||
@ -8143,7 +8244,8 @@ let shadowed_builtins (decls : Ast.decl list) : Loc.diag list =
|
||||
(Loc.diag ~kind:"check/shadows-builtin" fn.Ast.nloc
|
||||
(Printf.sprintf
|
||||
"%s shadows the builtin %s — every call in this program now \
|
||||
reaches your definition" fn.Ast.name fn.Ast.name))
|
||||
reaches your definition — the builtin stays reachable as %s%s"
|
||||
fn.Ast.name fn.Ast.name builtin_prefix fn.Ast.name))
|
||||
| _ -> None)
|
||||
decls
|
||||
|
||||
@ -8212,6 +8314,24 @@ let collect env (decls : Ast.decl list) =
|
||||
would otherwise be found by LLVM, as [redefinition of function
|
||||
'@flan.item'], or not at all. A [defn item] and a [defvar item] are two
|
||||
declarations of one name and are rejected here. *)
|
||||
(* The qualifier is reserved on this side too. [(defn builtin/len ...)]
|
||||
reads — the reader treats [/] as an ordinary symbol character — and would
|
||||
otherwise land in [env.fns] under a name nothing can ever call, because
|
||||
[named_call] strips the prefix before any table is consulted. A
|
||||
declaration that can only ever be dead is refused where it is written
|
||||
rather than left to be discovered. [Load] refuses the same qualifier from
|
||||
the other direction, at an import's alias. *)
|
||||
List.iter
|
||||
(fun (d : Ast.decl) ->
|
||||
match Ast.declared_name d with
|
||||
| Some n when qualified_builtin n <> None ->
|
||||
fail d.Ast.dloc
|
||||
"%s cannot be declared: %s is a reserved qualifier, so a name \
|
||||
spelled with it reaches the compiler's builtins and never a \
|
||||
declaration — nothing could call this one"
|
||||
n builtin_prefix
|
||||
| _ -> ())
|
||||
decls;
|
||||
let claimed = Hashtbl.create 64 in
|
||||
List.iter
|
||||
(fun (d : Ast.decl) ->
|
||||
|
||||
26
lib/load.ml
26
lib/load.ml
@ -144,6 +144,20 @@ let entries dir suffix =
|
||||
|
||||
let qualify alias n = alias ^ "/" ^ n
|
||||
|
||||
(* The one alias this module will not hand out. [builtin/len] is the builtin
|
||||
[len] no matter what a file has defined ([Check]'s [qualified_builtin]),
|
||||
and that only stays true while nothing else can produce the qualifier
|
||||
[builtin]. Every qualifier in a finished program comes from [qualify], and
|
||||
every [qualify] takes its alias from an [import] form, so refusing it here
|
||||
is the whole of the reservation — there is no other door.
|
||||
|
||||
It is the *alias* that is reserved and not the directory. A package whose
|
||||
directory happens to be called [builtin] imports fine under any other
|
||||
name, because the importer chooses the alias and the directory never
|
||||
becomes one: [(import b "vendor:builtin")] declares [b/…] and collides with
|
||||
nothing. *)
|
||||
let reserved_alias = "builtin"
|
||||
|
||||
(* A data type's value is written [Value.Int], which the reader hands over as
|
||||
one symbol with a dot in the middle of it. The half before the dot is a
|
||||
top-level name the package declares; the half after it is a case, and a
|
||||
@ -1042,6 +1056,18 @@ let real dir = try Unix.realpath dir with Unix.Unix_error _ -> dir
|
||||
[Cimport] has generated the header's declarations, so a macro quasiquoting
|
||||
[(BeginDrawing)] names [rl/BeginDrawing] like everything else. *)
|
||||
let rec import ~seen ~open_ ~loc alias dir =
|
||||
(* Before anything is read off the disk, and here rather than at the
|
||||
top-level import list so that a package importing one under this alias is
|
||||
refused too. *)
|
||||
(* The directory is not named in the message on purpose: it has been
|
||||
resolved to an absolute path by here, and the caret is already under the
|
||||
import form, which carries the path the reader wrote. *)
|
||||
if String.equal alias reserved_alias then
|
||||
fail loc
|
||||
"%s is a reserved qualifier and cannot be an import alias: %s/name \
|
||||
always means the compiler's builtin, which is how a program reaches a \
|
||||
builtin it has shadowed. Import this package under another alias"
|
||||
reserved_alias reserved_alias;
|
||||
let dir' = real dir in
|
||||
(* Checked before [seen], because a cycle's second arrival is also a repeat
|
||||
visit and [seen] would otherwise call it a diamond and say nothing. *)
|
||||
|
||||
46
test/programs/builtin-qualified.flan
Normal file
46
test/programs/builtin-qualified.flan
Normal file
@ -0,0 +1,46 @@
|
||||
;;;; builtin/name: the spelling that always reaches the builtin.
|
||||
;;;;
|
||||
;;;; Shadowing a builtin is legal and program-wide within the file that does
|
||||
;;;; it (shadow-builtin.flan is that rule on its own). What it used to cost
|
||||
;;;; was the builtin itself: a (defn len ...) had given up the builtin len for
|
||||
;;;; the whole file, so a definition that meant to *wrap* the builtin was
|
||||
;;;; unbounded recursion instead, and stack-overflowed at run time with
|
||||
;;;; nothing for the compiler to object to.
|
||||
;;;;
|
||||
;;;; builtin/len is the way out, and it is the package qualifier's own
|
||||
;;;; spelling: rl/draw-fps is draw-fps from the package imported as rl, and
|
||||
;;;; builtin/len is len from the compiler. builtin is a reserved qualifier —
|
||||
;;;; an import may not take it as an alias — so the two can never be confused
|
||||
;;;; about which one a name means.
|
||||
;;;;
|
||||
;;;; Five lines, and between them the whole feature. In order:
|
||||
;;;;
|
||||
;;;; - 9: (builtin/max 3 9) with nothing in this program named max. The
|
||||
;;;; qualifier is legal whether or not anything is shadowed; a spelling that
|
||||
;;;; only compiled while some other declaration existed would be a spelling
|
||||
;;;; nobody could write down in advance.
|
||||
;;;; - 5: this file's own len, which is a real wrapper — it is "1 + the
|
||||
;;;; builtin length", and the inner call reaches the builtin rather than
|
||||
;;;; itself. This is the program the earlier lane could not write.
|
||||
;;;; - 4: builtin/len in the same file, beside the bare name that means the
|
||||
;;;; definition. Both spellings, one program, different answers.
|
||||
;;;; - 99: the shadowed operator, unchanged. An operator is a builtin like any
|
||||
;;;; other and shadows like any other.
|
||||
;;;; - 3: builtin/+ — the reader takes builtin/+ as one symbol, because '/'
|
||||
;;;; and '+' are both ordinary symbol characters and the token does not
|
||||
;;;; begin with a digit or a sign, so an operator needs no exception here.
|
||||
|
||||
;;; The wrapper, written the way the dead end said could not be written: the
|
||||
;;; inner builtin/len is the compiler's len, and the outer name is this one.
|
||||
;;; Both additions are qualified too, since + is shadowed below and this
|
||||
;;; function wants the arithmetic and not the 99.
|
||||
(defn len [s string] i32 (builtin/+ 1 (builtin/len s)))
|
||||
|
||||
(defn + [a i32 b i32] i32 99)
|
||||
|
||||
(defn main [] ()
|
||||
(println (builtin/max 3 9))
|
||||
(println (len "abcd"))
|
||||
(println (builtin/len "abcd"))
|
||||
(println (+ 1 2))
|
||||
(println (builtin/+ 1 2)))
|
||||
16
test/programs/import-builtin-alias.flan
Normal file
16
test/programs/import-builtin-alias.flan
Normal file
@ -0,0 +1,16 @@
|
||||
;;;; The reserved qualifier, from the import's side. Refused, never built.
|
||||
;;;;
|
||||
;;;; builtin/len means the compiler's len and must go on meaning it, which is
|
||||
;;;; only true while nothing else can produce the qualifier "builtin". Every
|
||||
;;;; qualifier in a finished program comes from an import's alias, so refusing
|
||||
;;;; that one alias is the whole of the reservation — there is no other door.
|
||||
;;;;
|
||||
;;;; It is the alias that is reserved and not the directory: the package this
|
||||
;;;; names is imported under its own alias by shadow-builtin.flan and is fine
|
||||
;;;; there. The importer chooses the qualifier, so the importer is where the
|
||||
;;;; collision is.
|
||||
|
||||
(import builtin "pkgs/shadowed")
|
||||
|
||||
(defn main [] ()
|
||||
(println (builtin/field {:a 1 :b 4})))
|
||||
@ -2547,6 +2547,20 @@ let () =
|
||||
runs and its status is zero. *)
|
||||
outputs "a defn shadows a builtin, and the package it imports does not"
|
||||
"programs/shadow-builtin.flan" "7\n4\n99\n999\n4\n";
|
||||
(* And the way back out of it. builtin/name is the builtin whatever the
|
||||
file has decided the bare name means, so the two spellings sit side by
|
||||
side in one program and answer differently: 9 is builtin/max with
|
||||
nothing named max in the program at all, 5 is a len that is a real
|
||||
wrapper — 1 + the builtin length — 4 is builtin/len beside it, 99 is
|
||||
the shadowed operator and 3 is builtin/+.
|
||||
|
||||
The 5 is the line that could not be written before this. The same defn
|
||||
without the qualifier is unbounded recursion: the inner call reaches
|
||||
the definition being written, and the program stack-overflows with
|
||||
nothing for the compiler to object to. The two warnings it earns are
|
||||
on stderr and are pinned in test_flan.ml. *)
|
||||
outputs "builtin/name reaches the builtin past a shadow"
|
||||
"programs/builtin-qualified.flan" "9\n5\n4\n99\n3\n";
|
||||
(* A data type crossing the same boundary, which was a refusal by name
|
||||
until vendor/edn needed one. The rename has two halves and the second
|
||||
is the one that is easy to do by accident only: the type's name is a
|
||||
@ -2684,6 +2698,18 @@ let () =
|
||||
refuses "a runaway instantiation names the chain"
|
||||
"programs/generic-runaway.flan" "grow at ([2 i32])";
|
||||
|
||||
(* [builtin] as an import alias. The qualifier has to mean one thing, and
|
||||
an import is the only thing in the language that can produce one, so
|
||||
this is where the reservation is kept. Refused before the package is
|
||||
read, and the same refusal covers a package importing one under that
|
||||
alias, since every import goes through the same function. *)
|
||||
refuses "a package imported under the reserved qualifier"
|
||||
"programs/import-builtin-alias.flan"
|
||||
"builtin is a reserved qualifier and cannot be an import alias";
|
||||
refuses "and the refusal says what the qualifier is for"
|
||||
"programs/import-builtin-alias.flan"
|
||||
"how a program reaches a builtin it has shadowed";
|
||||
|
||||
(* The other half of the map deferral. The operations over a key that is a
|
||||
type variable are deferred to the instantiation — there is no hash and
|
||||
no equality to emit until the key type is concrete — and what makes
|
||||
|
||||
@ -4474,7 +4474,8 @@ let () =
|
||||
check "and the warning says what the name now means"
|
||||
(d.Loc.dmsg
|
||||
= "get shadows the builtin get — every call in this program now \
|
||||
reaches your definition");
|
||||
reaches your definition — the builtin stays reachable as \
|
||||
builtin/get");
|
||||
check "and it carries no notes, being one sentence about one decision"
|
||||
(d.Loc.notes = [])
|
||||
| _ -> check "a shadowing defn is warned about exactly once" false);
|
||||
@ -4496,7 +4497,8 @@ let () =
|
||||
(d.Loc.kind = "check/shadows-builtin"
|
||||
&& d.Loc.dmsg
|
||||
= "+ shadows the builtin + — every call in this program now \
|
||||
reaches your definition")
|
||||
reaches your definition — the builtin stays reachable as \
|
||||
builtin/+")
|
||||
| _ -> check "a shadowed operator warns exactly once" false);
|
||||
(match checked plus_src with
|
||||
| p ->
|
||||
@ -4525,6 +4527,108 @@ let () =
|
||||
check "a call in another file reaches the builtin, at the builtin's arity"
|
||||
(contains d.Loc.dmsg "len takes 1 argument, given 2"));
|
||||
|
||||
(* ── builtin/, the reserved qualifier ──────────────────────────────
|
||||
The escape from the dead end above: [builtin/len] is the builtin [len]
|
||||
whatever the file has decided [len] means. Pinned from both ends —
|
||||
with a shadow in the way and with nothing in the way at all — because a
|
||||
spelling that only worked while some other declaration existed would be
|
||||
one nobody could write down in advance. *)
|
||||
accepts "builtin/ is legal with nothing shadowed"
|
||||
"(defn f [] i32 (builtin/len \"abcd\"))";
|
||||
(* The pair that says the two spellings part company. One declaration list,
|
||||
two calls: the shadow takes two arguments and the builtin takes one, so
|
||||
each call is refusable only under one of the two readings. The bare name
|
||||
at two arguments checks, and the qualified one at two arguments is
|
||||
measured against the builtin's arity. *)
|
||||
accepts "the bare name reaches the shadowing defn"
|
||||
"(defn len [a string b string] i32 999)\n\
|
||||
(defn f [] i32 (len \"a\" \"b\"))";
|
||||
rejects_check "and builtin/ beside it reaches the builtin"
|
||||
"(defn len [a string b string] i32 999)\n\
|
||||
(defn f [] i32 (builtin/len \"a\" \"b\"))"
|
||||
~needle:"len takes 1 argument, given 2";
|
||||
(* The program the earlier lane could not write: a shadowing defn that
|
||||
*wraps* what it shadows. Without the qualifier the inner call reached
|
||||
the definition being written and the program stack-overflowed at run
|
||||
time; what is pinned here is that the body holds no call to [len] at
|
||||
all, which is the difference between a wrapper and a loop. *)
|
||||
(match checked "(defn len [s string] i32 (builtin/+ 1 (builtin/len s)))" with
|
||||
| p ->
|
||||
let recurs = ref false in
|
||||
(match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "len") p.Tast.fns with
|
||||
| Some f ->
|
||||
List.iter
|
||||
(Tast.walk (fun (e : Tast.expr) ->
|
||||
match e.Tast.e with
|
||||
| Tast.Call ("len", _) -> recurs := true
|
||||
| _ -> ()))
|
||||
f.Tast.body;
|
||||
check "a shadowing defn wraps the builtin instead of recurring"
|
||||
(not !recurs)
|
||||
| None -> check "the wrapping defn is checked" false)
|
||||
| exception Loc.Error d ->
|
||||
check "a shadowing defn wraps the builtin instead of recurring" false;
|
||||
print_endline d.Loc.dmsg);
|
||||
(* An operator through the qualifier, which is the case a reader would
|
||||
expect to need an exception and does not: the reader takes [builtin/+]
|
||||
as one symbol, and the call lowers to the [Add] prim even with [+]
|
||||
shadowed two lines above. *)
|
||||
(match checked "(defn + [a i32 b i32] i32 99)\n\
|
||||
(defn f [] i32 (builtin/+ 1 2))" with
|
||||
| p ->
|
||||
(match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "f") p.Tast.fns with
|
||||
| Some { Tast.body = [ { Tast.e = Tast.Prim (Tast.Add, _); _ } ]; _ } -> ()
|
||||
| _ -> check "builtin/+ is the operator and not the shadowing defn" false)
|
||||
| exception _ ->
|
||||
check "builtin/+ is the operator and not the shadowing defn" false);
|
||||
(* The value arms reach the same way, and [builtin/context/allocator] falls
|
||||
out of one strip rather than needing a rule of its own. *)
|
||||
accepts "a builtin written as a name is qualified too"
|
||||
"(defn f [] dyn builtin/nil)";
|
||||
accepts "and so is a builtin whose own name carries a slash"
|
||||
"(defn f [] Allocator builtin/context/allocator)";
|
||||
(* A builtin in a value position has nothing to hand back — it is an arm in
|
||||
the compiler and has no address — and the refusal has to say so rather
|
||||
than falling through to the function table, which holds the very
|
||||
definition the qualifier was written to get away from. *)
|
||||
rejects_check "a call-only builtin is refused as a value, not resolved"
|
||||
"(defn len [s string] i32 1)\n(defn f [] () (println builtin/len))"
|
||||
~needle:"builtin/len is the builtin len, which is a call and not a value";
|
||||
(* The qualifier reaching nothing. Named as not a builtin, and the
|
||||
did-you-mean is over the builtins alone. *)
|
||||
(match diag_of "(defn f [] i32 (builtin/nosuch))" with
|
||||
| Some d ->
|
||||
check "builtin/ with no builtin behind it has its own kind"
|
||||
(d.Loc.kind = "check/unknown-builtin");
|
||||
check "and says what is wrong with it"
|
||||
(d.Loc.dmsg
|
||||
= "nosuch is not a builtin, so builtin/nosuch reaches nothing. The \
|
||||
builtin/ qualifier reaches the compiler's own names and nothing \
|
||||
else; an ordinary function is called by the name it was defined \
|
||||
under")
|
||||
| None -> check "builtin/nosuch is refused" false);
|
||||
(match diag_of "(defn f [] i32 (builtin/lne \"ab\"))" with
|
||||
| Some d ->
|
||||
check "and a near miss is offered in the qualified spelling"
|
||||
(d.Loc.dmsg
|
||||
= "lne is not a builtin, so builtin/lne reaches nothing — did you \
|
||||
mean builtin/len?")
|
||||
| None -> check "builtin/lne is refused" false);
|
||||
(* And the did-you-mean everywhere else is untouched: a bare typo is still
|
||||
answered with a bare name, not with a qualifier nobody reached for. *)
|
||||
rejects_check "an unqualified typo is not answered with builtin/"
|
||||
"(defn f [] i32 (lne \"ab\"))" ~needle:"did you mean len?";
|
||||
(* The reservation from the other side. [(defn builtin/len ...)] reads —
|
||||
'/' is an ordinary symbol character — and would land in the function
|
||||
table under a name nothing can ever call, because the prefix is stripped
|
||||
before any table is consulted. *)
|
||||
(match diag_of "(defn builtin/len [s string] i32 1)" with
|
||||
| Some d ->
|
||||
check "a declaration cannot take the reserved qualifier"
|
||||
(contains d.Loc.dmsg
|
||||
"builtin/len cannot be declared: builtin/ is a reserved qualifier")
|
||||
| None -> check "a declaration under builtin/ is refused" false);
|
||||
|
||||
(* and's last operand is the then arm and the sentinel carrying the previous
|
||||
operand's location is the else arm, so with no expectation in hand the
|
||||
mismatch was reported one operand early. FIX.org's accepted fix: blame
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user