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:
parent
668f0d6686
commit
38a04f7a47
@ -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`
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
286
lib/check.ml
286
lib/check.ml
@ -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
|
||||
|
||||
46
lib/dev.ml
46
lib/dev.ml
@ -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.
|
||||
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user