The compiler's own names answer C-c C-v, and M-. says where they are

`arena-new` is a builtin, so it is in no program's symbol table and `defs`
never mentioned it — which made the editor answer "the running program
defines no arena-new" about a name that works. The fix is not a better
refusal: it is that the seventy-eight names the checker answers without
being told are now in the reply, under a kind of their own.

check.ml carries the table, beside the arms it describes, because a table
in another file drifts from them with nothing said. test_flan reads both
the arms and the table and fails on either having a name the other does
not, in both directions — there is no reflecting over a match, so it reads
the source.

Each entry is a signature in `signature_of_fn`'s shape and one line. The
arms that do not have one shape say what is true instead of pretending:
`?` for an argument that may be left out, `|` for the types an arm really
takes, and the checker's own predicate names for the type-directed ones.
The three user-allocator names carry a bare name and no bracket list,
because they are refused wherever they are written.

`defs` grows a fifth string for the prose, and builtins are appended last
so a completion table does not bury the names being worked on. A defn has
no docstring to put there and does not get one here: the Tast keeps none,
and that is a different piece of work.

The editor reads the kind, not a special case. `flan-doc--where` and the
xref backend both answer before their empty-location branch, because a
global's missing location and a builtin's absent one are different facts
and only one of them is about the daemon.
This commit is contained in:
Joseph Ferano 2026-09-19 03:12:38 +07:00
parent 668f0d6686
commit 38a04f7a47
8 changed files with 552 additions and 22 deletions

View File

@ -640,6 +640,12 @@ Completion, eldoc and `M-.` all read one cached answer rather than asking the
program per keystroke. It refreshes at the two moments the answer can have
changed: when you connect, and after an evaluation the daemon accepted.
The answer covers the compiler's builtins as well as the program's own names,
so `C-c C-v` on `arena-new` or `map-next!` gives you its signature and a line
about what it does. They are marked `builtin`, and they come after the
program's names in a completion list. `M-.` on one refuses rather than jumping:
it is written in the compiler, so there is no file to open.
---
## Every lowering of one function — `C-c C-l`

View File

@ -1395,8 +1395,15 @@ Interactively, the whole buffer: the point of asking is to be rid of them."
;; can have appeared in between, because this editor is the only client.
(defvar flan--defs nil
"What the running program defines: a list of (NAME KIND SIGNATURE LOC).
LOC is the empty string where the daemon has none to give.")
"What the running program defines: a list of (NAME KIND SIGNATURE LOC DOC).
LOC is the empty string where the daemon has none to give. DOC is a line of
prose, and today only a builtin has one.
The list is not only the program's own names: the daemon appends every
compiler builtin with a KIND of \"builtin\", after the program's, so that
`arena-new' is a name this end knows about rather than one it reports as
undefined. Nothing here special-cases them a builtin is an entry like any
other, and KIND is what tells it apart where that matters.")
(defun flan--forget-defs ()
"Drop what is known about the program's names."
@ -1503,6 +1510,13 @@ Nothing is offered when nothing is known — an empty table would look like
(user-error "flan: %s could be %s; write the one you mean"
identifier (string-join (mapcar #'car hits) " or "))
(user-error "flan: the running program defines no %s" identifier)))
;; Ahead of the empty-location branch: `arena-new' has no location for a
;; different reason than `ticks' does, and M-. on it should say which.
;; There is nowhere to jump either way, so this refuses too — but with
;; the answer to "where is it" rather than with a shrug about the daemon.
((equal (nth 1 d) "builtin")
(user-error "flan: %s is a builtin, written in the compiler rather than \
in this program; C-c C-v describes it" (car d)))
((equal (nth 3 d) "")
;; Tast.global and Tast.extern carry no Loc, so there is nothing to go
;; to. Guessing by searching for "(defvar ticks" would find the wrong
@ -1573,6 +1587,13 @@ Nothing is offered when nothing is known — an empty table would look like
(let* ((loc (nth 3 d))
(parts (and (not (equal loc "")) (flan--parse-loc loc))))
(cond
;; Before the empty-location branch, because a builtin's empty LOC means
;; something that branch would get wrong. A global was written down and
;; the Tast dropped where; a builtin was never written down at all, and
;; "no location is reported for it" would read as a gap in the daemon
;; rather than as the answer. The answer is the compiler.
((equal (nth 1 d) "builtin")
(insert "Defined in the compiler, so there is no file to visit\n"))
;; Said, not omitted. A missing line reads as "it has no home"; the
;; truth is that Tast.global and Tast.extern carry no Loc, which is a
;; fact about the compiler and worth saying in the same words M-. uses.
@ -1620,6 +1641,16 @@ same words `M-.' does: picking one would be a guess about which you meant."
(insert (propertize (nth 2 d) 'face 'font-lock-type-face) "\n\n")
(insert (format "Kind %s\n" (nth 1 d)))
(flan-doc--where d)
;; The prose last rather than under the signature, so the four facts
;; stay the block they have always been and a name with nothing to say
;; about itself looks exactly as it did before. Filled, because the
;; daemon sends one line and one line in a narrow window is two.
(when (and (nth 4 d) (not (equal (nth 4 d) "")))
(insert "\n")
(let ((start (point)))
(insert (nth 4 d) "\n")
(let ((fill-column (min fill-column 76)))
(fill-region start (point)))))
;; Parameter names are not in the Tast — the checker keeps types —
;; so a signature is types only, and someone reading this buffer
;; should be told that rather than left to wonder.

View File

@ -762,6 +762,62 @@ is written instead — the real `message' call the real command makes."
(test-flan--check "and a name the program has not got is refused"
(and raised (string-match-p "no no-such-name" raised))))
;; A builtin, which is the case this buffer used to refuse outright: the name
;; is fine and the program's symbol table has never heard of it, so C-c C-v
;; answered "the running program defines no arena-new" about a name that
;; works. `arena-new' by name because it is the one that was reported.
(flan-doc "arena-new")
(with-current-buffer flan-doc-buffer
(let ((text (buffer-string)))
(test-flan--check "C-c C-v on a builtin answers"
(string-match-p "\\`arena-new" text))
(test-flan--check "with its signature"
(string-match-p "arena-new \\[i64\\] Allocator" text))
(test-flan--check "and says it is a builtin"
(string-match-p "Kind +builtin" text))
;; The honest answer to "where", and not the one an empty location would
;; otherwise have produced: a global's location is missing, a builtin's
;; never existed.
(test-flan--check "and that it lives in the compiler, not in a file"
(string-match-p "Defined +in the compiler" text))
(test-flan--check "and carries a line about what it does"
(string-match-p "capacity is explicit" text))))
;; The other half of the table: a name written as a name rather than as a
;; call, which `var' answers and which reads here as a builtin too.
(flan-doc "context/allocator")
(with-current-buffer flan-doc-buffer
(test-flan--check "and so does a builtin that is a name, not a call"
(string-match-p "Kind +builtin" (buffer-string))))
;; M-. has nowhere to jump either way, but the reason is the point: the
;; refusal names the compiler instead of reporting a gap in the daemon.
(let ((raised nil))
(condition-case err (xref-backend-definitions 'flan "arena-new")
(user-error (setq raised (error-message-string err))))
(test-flan--check "M-. on a builtin says it is in the compiler"
(and raised (string-match-p "arena-new" raised)
(string-match-p "compiler" raised))))
(test-flan--check "eldoc has a builtin's signature too"
(let ((said nil))
(with-temp-buffer
(insert "arena-new")
(flan-eldoc-function
(lambda (s &rest _) (setq said s))))
(and said (string-match-p "arena-new \\[i64\\]" said)
;; The kind rides along, as it does for a global,
;; so the echo area says which of the two this is.
(string-match-p "builtin" said))))
(let* ((capf (with-temp-buffer
(insert "arena")
(flan-completion-at-point)))
(table (nth 2 capf)))
(test-flan--check "completion offers builtins"
(member "arena-new" (all-completions "arena-" table)))
;; They are appended after the program's own names rather than merged in,
;; so a table built from this order does not bury what is being worked on.
(test-flan--check "and still offers the program's names first"
(< (seq-position table "step")
(seq-position table "arena-new"))))
;; ── The watch buffer ──────────────────────────────────────────────────
;;
;; test_dev.ml proves the table itself: a program pushes and the daemon reads

View File

@ -1837,6 +1837,8 @@ and in_range loc k n =
if ok then n
else fail loc "%Ld does not fit in %s" n (Types.ikind_name k)
(* 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 =
match name with
| "true" | "false" ->
@ -3370,6 +3372,9 @@ and vec_at ctx loc (target : Tast.expr) (idx : Ast.expr list) =
"a Vec takes exactly one index — (at v i) — and its element is indexed \
separately"
(* 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 =
let prim p ty args = expect loc ~want (mk loc ty (Tast.Prim (p, args))) in
match name with
@ -5145,6 +5150,287 @@ and binary ctx name loc ~want args =
end
| _ -> fail loc "%s takes two arguments" name
(* ── The builtins, said out loud ───────────────────────────────────────
A name, a signature and one line, for every name [named_call] and [var]
answer without the program having written it. The editor's C-c C-v used to
say "the running program defines no arena-new", which was true and useless:
a builtin is in the compiler, so it is in no program's symbol table and
[Dev.defs] had nothing to hand over. This is what it hands over instead.
It lives here rather than in dev.ml because it describes the arms above it,
and a table in another file drifts from them silently. [test_flan]'s
[builtin_table] reads this file and the arms and fails on either one having
a name the other does not, so the drift is a failing build rather than a
name that answers nothing.
The signature follows [Dev.signature_of_fn]'s shape [name [params] ret]
so eldoc reads a builtin the way it reads a defn. Where an arm does not
have one shape the signature says what is true rather than inventing one:
[?] marks an argument that may be left out, [|] separates the types an arm
really does accept, and the predicate names are the compiler's own
([numeric?], [ordered?], [equal?] check.ml's [where] evaluator), because
a generic's author already writes them. Three names have no shape at all
and carry a bare name instead of a bracket list; their line says why.
One line each, because this is read in an echo area and a help buffer. The
lines are the arms' own reasons, cut down where an arm argues for a
decision above itself, the sentence a person needs at the call site is the
conclusion, not the argument. *)
let builtins : (string * string * string) list =
[ (* arithmetic and comparison *)
("+", "+ [numeric? ...] numeric?",
"Sum, folded left over two or more operands that share one numeric \
type nothing widens implicitly.");
("-", "- [numeric? ...] numeric?",
"Difference, folded left: (- a b c) is ((a - b) - c).");
("*", "* [numeric? ...] numeric?",
"Product, folded left over two or more operands of one numeric type.");
("/", "/ [numeric? ...] numeric?",
"Quotient, folded left. Integer division truncates toward zero.");
("%", "% [numeric? numeric?] numeric?",
"Remainder, and it stays at two operands: (% a b c) would mean \
(% (% a b) c), which is a thing nobody writes on purpose.");
("=", "= [equal? equal?] bool",
"Equality. It admits one type < does not — a handle, where \"the same \
entity\" is the question the type exists to answer.");
("!=", "!= [equal? equal?] bool",
"Inequality, over everything = accepts.");
("<", "< [ordered? ordered?] bool",
"Less than. Machine numbers only: ordering a handle would order a \
free-list slot index, which means nothing.");
("<=", "<= [ordered? ordered?] bool", "Less than or equal.");
(">", "> [ordered? ordered?] bool", "Greater than.");
(">=", ">= [ordered? ordered?] bool", "Greater than or equal.");
("not", "not [bool] bool",
"Negates a bool. Nothing else in this language is a truth value.");
("bit-and", "bit-and [int ...] int",
"Bitwise and, folded left. Integers only, and every operand has the \
same width.");
("bit-or", "bit-or [int ...] int", "Bitwise or, folded left over integers.");
("bit-xor", "bit-xor [int ...] int",
"Bitwise exclusive or, folded left over integers.");
("<<", "<< [int int] int",
"Left shift. The count has the shifted value's own type, and a literal \
count at or past its width is refused LLVM calls that poison.");
(">>", ">> [int int] int",
"Right shift, by a count of the value's own type; a literal count at or \
past the width is refused, as it is for <<.");
("min", "min [ordered? ...] ordered?",
"The smallest of two or more operands, each of them evaluated exactly \
once however many there are.");
("max", "max [ordered? ...] ordered?",
"The largest of two or more operands, each evaluated exactly once.");
("zeroed", "zeroed [] T",
"The all-bytes-zero value of whatever it is being stored into, so it \
only means anything where a type is expected of it.");
("destructure~nth", "destructure~nth [[n T] i32 i32 i32] T",
"Written by the compiler for a destructuring let, and unspellable: the \
reader makes ~ a delimiter, so no source symbol can name this.");
(* allocators, spec-memory.md *)
("make-allocator", "make-allocator",
"A user-written allocator, not implemented and refused wherever it is \
written. Use (arena-new n), which is the parameterised allocator that \
does exist.");
("allocator-from", "allocator-from",
"A user-written allocator, not implemented — see make-allocator.");
("allocator", "allocator",
"A user-written allocator, not implemented — see make-allocator.");
("heap-allocator", "heap-allocator [] Allocator",
"The process heap as an Allocator: it releases one block at a time, so \
can-free? is true of it.");
("arena-new", "arena-new [i64] Allocator",
"A new arena of exactly this many bytes. The capacity is explicit and \
the backing store never grows, which is what makes \"exhausted\" a \
state a test can reach on purpose.");
("arena-destroy", "arena-destroy [Allocator] ()",
"Hands the arena's pages back to the system, which free-all \
deliberately does not.");
("free-all", "free-all [Allocator] ()",
"Releases everything the allocator holds and bumps its epoch, keeping \
the capacity. It traps rather than quietly doing nothing when there is \
no region to release.");
("can-free?", "can-free? [Allocator] bool",
"Whether this allocator can release a single block, read off its \
capability set rather than asked of a query procedure.");
("can-free-all?", "can-free-all? [Allocator] bool",
"Whether this allocator can release everything it holds at once.");
("alloc-epoch", "alloc-epoch [Allocator] i64",
"The counter free-all bumps. A container records it and traps if it \
moved; this is the same number, readable, so a program can say what it \
saw.");
("alloc-id", "alloc-id [Allocator] i64",
"The allocator's identity — its address — which is what a condition's \
:allocator field carries, so a handler holding several regions can \
tell which one ran out.");
("alloc-budget", "alloc-budget [Allocator] i64",
"The ceiling on live bytes, 0 for none. A handler that answers \
StorageExhausted with retry is the one that raises it.");
("set-alloc-budget", "set-alloc-budget [Allocator i64] ()",
"Sets the ceiling on live bytes; 0 for none. It is also how a program \
exhausts an allocator on purpose.");
("alloc-live-blocks", "alloc-live-blocks [Allocator] i64",
"How many blocks are still live — \"did you forget to free\", answered \
at the tier that can answer it.");
("with-allocator", "with-allocator [Allocator body ...] T",
"Runs the body with this allocator in the context, and answers the \
body's last expression. It releases nothing: not at the end of the \
body, not anywhere.");
(* (Vec T) *)
("vec-new", "vec-new [T? Allocator?] (Vec T)",
"An empty Vec. A let has no type annotation, so the element type is \
written at the call (vec-new i32) wherever the context does not \
say it; an allocator may be named the same way.");
("push", "push [(Vec T) T] ()",
"Appends one element, growing the Vec through its allocator. Unit and \
not an error code: a failed allocation signals StorageExhausted.");
("reserve", "reserve [(Vec T)|(Map K V) i32] ()",
"Makes room for n more. For a map the number is entries rather than \
slots the block is sized so that n still sits under the load \
factor.");
("as-slice", "as-slice [(Vec T) i32? i32?] [T]",
"A non-owning view of the whole Vec, or of the half-open range \
[lo hi). It carries no allocator, and a push, a put or a reserve may \
invalidate it.");
("free", "free [(Vec T)|(Map K V)] ()",
"Releases the container's block. It does not recurse into elements that \
own storage such a container is refused here, and releasing its \
region with free-all is the answer.");
("clone", "clone [(Vec T)|(Map K V) Allocator?] (Vec T)|(Map K V)",
"A deep, independent copy, from the current allocator or one named. \
Refused for a container whose elements own storage: a bytewise copy \
would alias the original's blocks under a name promising otherwise.");
(* (Map K V) *)
("map-new", "map-new [K? V? Allocator?] (Map K V)",
"An empty map. The key and value types are written at the call — \
(map-new string i32) wherever the context does not say them.");
("put", "put [(Map K V) K V] ()",
"Inserts or replaces. Unit rather than an error code, and \
(set (get m k) v) is not map syntax.");
("get", "get [(Map K V) K] (Option V)",
"The value at the key, or None. Nothing signals here — a lookup that \
finds nothing is an answer and the value comes back as a copy of \
its bytes.");
("map-remove!", "map-remove! [(Map K V) K] (Option V)",
"Removes the entry and answers the value it held, or None if there was \
none.");
("map-next!", "map-next! [(Map K V) (Ptr i64) (Ptr K) (Ptr V)] bool",
"Walks the map one entry per call through a cursor the caller owns, and \
is the whole of map iteration: (while (map-next! m (addr cur) (addr k) \
(addr v)) ...).");
("has-key?", "has-key? [(Map K V) K] bool",
"Whether the key is present, copying no value — the form a condition \
wants, where get would hand back an Option to match on.");
(* assets, embedded at compile time *)
("embed", "embed [\"path\" string?] [u8]",
"The file's bytes, read at compile time and baked in as a constant; \
(embed \"p\" string) reads it as a string instead. The path is \
relative to the file the form is written in, and the slice points into \
read-only data.");
("embed-dir", "embed-dir [\"path\"] [n EmbedFile]",
"Every file in the directory, read at compile time, as a fixed array of \
EmbedFile. It does not descend.");
(* files *)
("slurp", "slurp [string Allocator?] (Vec u8)",
"Reads a whole file. No Result and no out-parameter: a failure to read \
signals FileError under retry and use-value, and a failure to allocate \
signals StorageExhausted.");
("barf", "barf [string [u8]] ()",
"Writes a whole file. On the web target it signals FileError every \
time, with the path there is no conditional compilation, so the \
program decides rather than the build.");
("delete-file", "delete-file [string] ()",
"Removes the file, or signals FileError with retry and use-value. It \
answers () and not a bool, because the failure is the condition.");
("make-directory", "make-directory [string] ()",
"Creates the directory, or signals FileError. () for the reason \
delete-file answers one.");
("rename-file", "rename-file [string string] ()",
"Renames the first path to the second, or signals FileError. A \
use-value names a different source for the same destination, which is \
the direction a handler can act on.");
(* containers *)
("len", "len [[n T]|[T]|string|(Vec T)|(Map K V)] i32",
"How many elements. One question and one word across an array, a slice, \
a string, a Vec and a Map.");
("at", "at [collection i32 ...] T",
"The element at an index, bounds-checked — and for a Vec with the \
allocator's epoch checked first. It is also a place, so \
(set (at v i) x) goes through the same check.");
("slice", "slice [[n T]|[T] i32 i32] [T]",
"The half-open range [lo hi) as a non-owning view. A bound may sit one \
past the end; a literal pair that runs backwards is refused here.");
("slice-from-ptr", "slice-from-ptr [(Ptr T) i32] [T]",
"Puts a length on a pointer that came back from C. The caller promises \
it addresses that many initialised T and that they outlive the result; \
the compiler checks none of it.");
("addr", "addr [place] (Ptr T)",
"The address of a place — a name, (.field x), (at a i) or (deref p) — \
and not of an arbitrary expression.");
("deref", "deref [(Ptr T)] T",
"The value behind a pointer, and a place, so (set (deref p) x) writes \
through it.");
(* Option *)
("Some", "Some [T] (Option T)",
"Wraps a value as a present Option. None is the other half, and is \
written as a name rather than as a call.");
(* the host primitives *)
("bytes", "bytes [string] [u8]",
"A string seen as a byte slice. It costs nothing — both are a ptr and a \
length at run time and it decodes nothing. A literal's bytes are \
constant data, so the slice looks writable and is not.");
("string", "string [[u8]] string",
"A byte slice seen as a string, and free at run time. It does not check \
UTF-8, because `string` does not claim UTF-8 valid-utf8? is an \
ordinary function you call when you care.");
("bytes->f64", "bytes->f64 [[u8]] f64", "Parses a float out of the bytes.");
("bytes->i64", "bytes->i64 [[u8]] i64",
"Parses an integer out of the bytes.");
("f64->bytes", "f64->bytes [f64] [u8]",
"The number's text, in a frame slot belonging to this call site — so \
two of them can be held at once, and neither survives its frame. Copy \
the bytes to keep one.");
("i64->bytes", "i64->bytes [i64] [u8]",
"The number's text, in a frame slot belonging to this call site; it \
does not survive the frame.");
("write-stdout", "write-stdout [[u8]] ()",
"Writes the bytes to standard output exactly as given: no newline and \
no formatting.");
("print", "print [T] ()",
"The structural printer, selected for the argument's concrete type. A \
string prints raw at the top level and quoted inside a structure, and \
a Ptr or a Handle prints its address rather than being followed. \
Printing is a read, so it does not consume the value.");
("println", "println [T] ()", "print, with a newline after it.");
("exit", "exit [i32] never",
"Ends the process with this status. It has no value, so nothing written \
after it runs.");
("argv", "argv [] [string]", "The command line, as a slice of strings.");
(* the five that are names rather than calls — [var]'s arms. Their
signature is the [name type] shape [Dev.defs] gives a global, because
that is what they are at the site: a value, not a call. *)
("true", "true bool", "The true boolean literal.");
("false", "false bool", "The false boolean literal.");
("None", "None (Option T)",
"The absent Option. It takes its type from its context — a return type \
or an annotated binding because nothing about the word says what it \
is an Option of.");
("context/allocator", "context/allocator Allocator",
"The allocator in effect here: what with-allocator rebinds, and what an \
allocating operation uses when none is named at the site.");
("context/temp", "context/temp Allocator",
"The scratch allocator the calling convention carries beside \
context/allocator.")
]
(* ── Declarations: pass 1, collect ─────────────────────────────────── *)
(* Constant folding, only over integers and only for defconst — enough for an

View File

@ -999,10 +999,17 @@ let describe t =
connect and again after each install. Putting signatures on the poll would
pay for them every time anyone looked at the output buffer.
One entry per name: (name kind signature loc). Four strings, so the editor
reads it with [read] and nothing here needs a new wire type. [loc] is empty
where there is none to give only [Tast.fn] carries one and an editor
that finds it empty must say so rather than guess a file.
One entry per name: (name kind signature loc doc). Five strings, so the
editor reads it with [read] and nothing here needs a new wire type. [loc] is
empty where there is none to give only [Tast.fn] carries one and an
editor that finds it empty must say so rather than guess a file.
[doc] carries a line of prose, and today only a builtin has one. A [defn]
does not, and that is not an oversight this op can fix: the Tast keeps no
docstring, so the field would be empty for every program name until the
parser and the Tast both learn to hold one. It is a fifth string rather than
a sixth op because it is the same question what is this name asked at
the same two moments.
Parameter *names* are not in the Tast, so a signature shows types only. *)
let signature_of_fn (f : Tast.fn) =
@ -1010,8 +1017,10 @@ let signature_of_fn (f : Tast.fn) =
(String.concat " " (List.map Types.to_string f.Tast.params))
(Types.to_string f.Tast.ret)
let entry ~name ~kind ~sign ~loc =
Wire.list [ Wire.quote name; Wire.quote kind; Wire.quote sign; Wire.quote loc ]
let entry ~name ~kind ~sign ~loc ?(doc = "") () =
Wire.list
[ Wire.quote name; Wire.quote kind; Wire.quote sign; Wire.quote loc;
Wire.quote doc ]
let defs t =
let p = t.session.Session.program in
@ -1025,7 +1034,7 @@ let defs t =
| None ->
Some
(entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f)
~loc:(Loc.to_string f.Tast.floc)))
~loc:(Loc.to_string f.Tast.floc) ()))
p.Tast.fns
in
let globals =
@ -1035,7 +1044,7 @@ let defs t =
~kind:(if g.Tast.gconst then "const" else "var")
~sign:
(Printf.sprintf "%s %s" g.Tast.gname (Types.to_string g.Tast.gty))
~loc:"")
~loc:"" ())
p.Tast.globals
in
let externs =
@ -1046,10 +1055,27 @@ let defs t =
(Printf.sprintf "%s [%s] %s" e.Tast.ename
(String.concat " " (List.map Types.to_string e.Tast.eparams))
(Types.to_string e.Tast.eret))
~loc:"")
~loc:"" ())
p.Tast.externs
in
ok [ ":defs " ^ Wire.list (fns @ globals @ externs) ]
(* Last, so a program's own names sort ahead of them in every list an editor
builds out of this a completion table above all, where 78 compiler names
interleaved with a handful of a program's own would bury the ones being
worked on. The kind is what separates them, and it is on every entry
already, so an editor that wants only the program's names filters rather
than asking a second op.
[loc] is empty here as it is for a global, but for a different reason, and
the editor has to say the difference: a global was written somewhere and
the Tast dropped the location, while a builtin was never written down at
all. So the honest sentence names the compiler, and the kind is what an
editor keys it on. *)
let builtins =
List.map
(fun (name, sign, doc) -> entry ~name ~kind:"builtin" ~sign ~loc:"" ~doc ())
Check.builtins
in
ok [ ":defs " ^ Wire.list (fns @ globals @ externs @ builtins) ]
(* [(:op "layout" :type T)] — a struct's fields and their types.

View File

@ -77,6 +77,10 @@
(file %{workspace_root}/bin/main.exe)
; The Emacs client, which test_emacs drives against a real daemon.
(glob_files %{workspace_root}/emacs/*.el)
; The checker's own source, which test_flan reads: there is no reflecting
; over a match, so the only way to assert that Check.builtins still lists
; every builtin arm is to read the arms.
(file %{workspace_root}/lib/check.ml)
; The WASI host the wasm32 case runs its module under, when no wasmtime or
; wasmer is installed.
(file wasm-run.mjs)))

View File

@ -321,7 +321,13 @@ let () =
carries what eldoc, completion and find-definition each need: a kind,
a signature, and where the name is written where that is knowable.
An empty location is the honest answer for a global Tast.global has
no Loc and an editor is expected to refuse rather than guess. *)
no Loc and an editor is expected to refuse rather than guess.
A fifth string carries a line of prose, and the compiler's builtins
ride the same op under a kind of their own: [arena-new] is a name an
editor is asked about and no program's symbol table holds, so without
them here C-c C-v answers "the running program defines no arena-new"
about a name that is perfectly good. *)
let r = request c "(:op \"defs\")" in
if status r <> "ok" then fail "defs: %s" (status r);
(match Wire.field r "defs" with
@ -334,26 +340,60 @@ let () =
({ Form.v = Form.Str n; _ }
:: { Form.v = Form.Str kind; _ }
:: { Form.v = Form.Str sign; _ }
:: { Form.v = Form.Str loc; _ } :: [])
when String.equal n name -> Some (kind, sign, loc)
:: { Form.v = Form.Str loc; _ }
:: { Form.v = Form.Str doc; _ } :: [])
when String.equal n name -> Some (kind, sign, loc, doc)
| _ -> None)
entries
in
(match find "step" with
| Some ("fn", "step [] i64", loc) when String.length loc > 0 ->
| Some ("fn", "step [] i64", loc, "") when String.length loc > 0 ->
(* Absolute, because an editor is not in this process's working
directory and cannot resolve a relative one. *)
if loc.[0] <> '/' then fail "a fn's location is relative: %s" loc
| Some (k, s, l) -> fail "step is described as (%s, %s, %s)" k s l
| Some (k, s, l, _) -> fail "step is described as (%s, %s, %s)" k s l
| None -> fail "defs did not mention step");
(match find "ticks" with
| Some ("var", "ticks i64", "") -> ()
| Some (k, s, l) -> fail "ticks is described as (%s, %s, %s)" k s l
| Some ("var", "ticks i64", "", "") -> ()
| Some (k, s, l, _) -> fail "ticks is described as (%s, %s, %s)" k s l
| None -> fail "defs did not mention ticks");
(match find "agent/wait-raw" with
| Some ("extern", _, _) -> ()
| Some (k, _, _) -> fail "an extern is described as %s" k
| None -> fail "defs did not mention an imported extern")
| Some ("extern", _, _, _) -> ()
| Some (k, _, _, _) -> fail "an extern is described as %s" k
| None -> fail "defs did not mention an imported extern");
(* The builtin the author hit. It is here with a signature, a line of
prose and no location and the empty location has to be read
against the kind, because a global's empty one means the Tast
dropped it while this one means there was never a file. *)
(match find "arena-new" with
| Some ("builtin", sign, "", doc) ->
if sign = "" then fail "arena-new has no signature";
if doc = "" then fail "arena-new has no description"
| Some (k, _, l, _) ->
fail "arena-new is described as a %s at %S" k l
| None -> fail "defs did not mention arena-new");
(* And a name in value position, which is [var]'s half of the same
table: it is a builtin too, and reads as one. *)
(match find "context/allocator" with
| Some ("builtin", _, "", doc) when doc <> "" -> ()
| Some (k, _, _, _) -> fail "context/allocator is described as a %s" k
| None -> fail "defs did not mention context/allocator");
(* A program's own names come first, so a completion table built out
of this order does not bury them under the compiler's. *)
let kind_of (e : Form.t) =
match e.Form.v with
| Form.List (_ :: { Form.v = Form.Str k; _ } :: _) -> k
| _ -> ""
in
let rec no_program_after_builtin seen = function
| [] -> ()
| e :: rest ->
let k = kind_of e in
if seen && k <> "builtin" then
fail "a %s entry comes after a builtin in defs" k;
no_program_after_builtin (seen || k = "builtin") rest
in
no_program_after_builtin false entries
| _ -> fail "defs did not answer with a list");
(* [layout]: a struct's fields and their types, out of [Tast.structs],

View File

@ -2691,6 +2691,87 @@ let () =
~needle:"is not a map key"
"(defn f [k $t] () (let [m (map-new t i32)] (put m k 1) (free m)))";
(* ── The builtin table against the arms it describes ──────────────
[Check.builtins] is what the editor's C-c C-v and M-. read for a name no
program wrote [arena-new] and the seventy-seven others. A table like
that is worth less than nothing once it is stale: a builtin added without
an entry answers nothing, and an entry for an arm that was deleted
describes a name that no longer exists, which is worse because it reads
as authoritative.
There is no way to reflect over an OCaml match, so this reads the source
instead. The two regions are [named_call]'s arms and [var]'s, each from
its own [and] down to the first catch-all at the same indentation, and
the names are the string literals in the arm heads. It is a regex over
one file and costs nothing, which is why it is in the default run rather
than behind an alias. *)
let arm_names () =
let src =
In_channel.with_open_bin "../lib/check.ml" In_channel.input_all
in
let lines = String.split_on_char '\n' src in
let starts_with p s =
String.length s >= String.length p && String.sub s 0 (String.length p) = p
in
let quoted line =
let out = ref [] and i = ref 0 and n = String.length line in
while !i < n do
if line.[!i] = '"' then begin
let j = ref (!i + 1) in
while !j < n && line.[!j] <> '"' do incr j done;
if !j < n then out := String.sub line (!i + 1) (!j - !i - 1) :: !out;
i := !j + 1
end else incr i
done;
List.rev !out
in
let region head =
let rec drop = function
| [] -> []
| l :: rest -> if starts_with head l then rest else drop rest
in
let rec take = function
| [] -> []
| l :: rest ->
if starts_with " | _" l then []
else if starts_with " | \"" l then quoted l @ take rest
else take rest
in
take (drop lines)
in
region "and named_call " @ region "and var ctx "
in
let arms = arm_names () in
let table = List.map (fun (n, _, _) -> n) Check.builtins in
check "every builtin arm is described" (arms <> [] && List.length arms > 60);
List.iter
(fun n ->
if not (List.mem n table) then begin
incr failures;
Printf.printf
"FAIL the builtin %s has an arm in check.ml and no entry in \
Check.builtins\n" n
end)
arms;
List.iter
(fun n ->
if not (List.mem n arms) then begin
incr failures;
Printf.printf
"FAIL Check.builtins describes %s, which is no longer an arm\n" n
end)
table;
(* A signature and a line, for every one of them: an entry that is present
and empty answers the question no better than a missing one. *)
List.iter
(fun (n, sign, doc) ->
if sign = "" || doc = "" then begin
incr failures;
Printf.printf "FAIL the builtin %s has no %s\n" n
(if sign = "" then "signature" else "description")
end)
Check.builtins;
(* ── The acceptance program checks end to end ──────────────────── *)
accepts "calc-me.flan type checks"
(In_channel.with_open_bin "../calc-me.flan" In_channel.input_all);