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.
|
did-you-mean, whose order is its order; the set answers the membership.
|
||||||
|
|
||||||
** The warning, verbatim
|
** 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))
|
: 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
|
Rendered by ~Loc.entry ~mark:'~' ~label:"warning: "~, which is the
|
||||||
~--warn-memory~ precedent, so flycheck parses it exactly as it parses an
|
~--warn-memory~ precedent, so flycheck parses it exactly as it parses an
|
||||||
error. Nothing raises and the exit status does not move. Unlike
|
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
|
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.
|
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
|
** Pins
|
||||||
- ~test_flan.ml~: the warning's kind, line and column; its message, matched
|
- ~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
|
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
|
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
|
fix is wanted here; the session-end cleanup that item asks for takes the
|
||||||
socket with it.
|
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. *)
|
twenty thousand calls. Filled beside the list. *)
|
||||||
let builtin_set : (string, unit) Hashtbl.t = Hashtbl.create 128
|
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.
|
(* 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
|
[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:
|
(* 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. *)
|
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
|
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" ->
|
| "true" | "false" ->
|
||||||
expect ctx loc ~want (mk loc Types.Bool (Tast.Bool (name = "true")))
|
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
|
(* 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" ->
|
| "context/temp" ->
|
||||||
expect ctx loc ~want
|
expect ctx loc ~want
|
||||||
(mk loc Types.Alloc (Tast.Prim (Tast.Rt "flan_context_temp", [])))
|
(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
|
match lookup ctx name with
|
||||||
| Some b ->
|
| 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
|
(* 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.
|
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. *)
|
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
|
let prim p ty args = expect ctx loc ~want (mk loc ty (Tast.Prim (p, args))) in
|
||||||
match name with
|
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
|
Clojure's rule: a [(defn get ...)] takes the name over, and a call
|
||||||
written in the program that defines it reaches that definition rather
|
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
|
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
|
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
|
for a call in imported package code, which is the same visibility rule a
|
||||||
defn has everywhere else. *)
|
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 ─────────────────────────────────── *)
|
(* ── arithmetic and comparison ─────────────────────────────────── *)
|
||||||
| "+" | "-" | "*" | "/" ->
|
| "+" | "-" | "*" | "/" ->
|
||||||
let p = match name with
|
let p = match name with
|
||||||
@ -8127,6 +8221,13 @@ let () =
|
|||||||
flag, because there is nothing to tune — a program either renamed a
|
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.
|
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
|
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
|
there is a compiler bug rather than news for whoever is compiling. So are
|
||||||
qualified names, for the reason [shadows_builtin]'s [visible] gives —
|
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
|
(Loc.diag ~kind:"check/shadows-builtin" fn.Ast.nloc
|
||||||
(Printf.sprintf
|
(Printf.sprintf
|
||||||
"%s shadows the builtin %s — every call in this program now \
|
"%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)
|
| _ -> None)
|
||||||
decls
|
decls
|
||||||
|
|
||||||
@ -8212,6 +8314,24 @@ let collect env (decls : Ast.decl list) =
|
|||||||
would otherwise be found by LLVM, as [redefinition of function
|
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
|
'@flan.item'], or not at all. A [defn item] and a [defvar item] are two
|
||||||
declarations of one name and are rejected here. *)
|
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
|
let claimed = Hashtbl.create 64 in
|
||||||
List.iter
|
List.iter
|
||||||
(fun (d : Ast.decl) ->
|
(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
|
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
|
(* 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
|
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
|
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
|
[Cimport] has generated the header's declarations, so a macro quasiquoting
|
||||||
[(BeginDrawing)] names [rl/BeginDrawing] like everything else. *)
|
[(BeginDrawing)] names [rl/BeginDrawing] like everything else. *)
|
||||||
let rec import ~seen ~open_ ~loc alias dir =
|
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
|
let dir' = real dir in
|
||||||
(* Checked before [seen], because a cycle's second arrival is also a repeat
|
(* 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. *)
|
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. *)
|
runs and its status is zero. *)
|
||||||
outputs "a defn shadows a builtin, and the package it imports does not"
|
outputs "a defn shadows a builtin, and the package it imports does not"
|
||||||
"programs/shadow-builtin.flan" "7\n4\n99\n999\n4\n";
|
"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
|
(* 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
|
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
|
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"
|
refuses "a runaway instantiation names the chain"
|
||||||
"programs/generic-runaway.flan" "grow at ([2 i32])";
|
"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
|
(* 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
|
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
|
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"
|
check "and the warning says what the name now means"
|
||||||
(d.Loc.dmsg
|
(d.Loc.dmsg
|
||||||
= "get shadows the builtin get — every call in this program now \
|
= "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"
|
check "and it carries no notes, being one sentence about one decision"
|
||||||
(d.Loc.notes = [])
|
(d.Loc.notes = [])
|
||||||
| _ -> check "a shadowing defn is warned about exactly once" false);
|
| _ -> check "a shadowing defn is warned about exactly once" false);
|
||||||
@ -4496,7 +4497,8 @@ let () =
|
|||||||
(d.Loc.kind = "check/shadows-builtin"
|
(d.Loc.kind = "check/shadows-builtin"
|
||||||
&& d.Loc.dmsg
|
&& d.Loc.dmsg
|
||||||
= "+ shadows the builtin + — every call in this program now \
|
= "+ 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);
|
| _ -> check "a shadowed operator warns exactly once" false);
|
||||||
(match checked plus_src with
|
(match checked plus_src with
|
||||||
| p ->
|
| p ->
|
||||||
@ -4525,6 +4527,108 @@ let () =
|
|||||||
check "a call in another file reaches the builtin, at the builtin's arity"
|
check "a call in another file reaches the builtin, at the builtin's arity"
|
||||||
(contains d.Loc.dmsg "len takes 1 argument, given 2"));
|
(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
|
(* 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
|
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
|
mismatch was reported one operand early. FIX.org's accepted fix: blame
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user