Merge: a builtin is a name the documentation knows
This commit is contained in:
commit
d757fd23f5
@ -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
|
program per keystroke. It refreshes at the two moments the answer can have
|
||||||
changed: when you connect, and after an evaluation the daemon accepted.
|
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`
|
## 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.
|
;; can have appeared in between, because this editor is the only client.
|
||||||
|
|
||||||
(defvar flan--defs nil
|
(defvar flan--defs nil
|
||||||
"What the running program defines: a list of (NAME KIND SIGNATURE LOC).
|
"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.")
|
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 ()
|
(defun flan--forget-defs ()
|
||||||
"Drop what is known about the program's names."
|
"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"
|
(user-error "flan: %s could be %s; write the one you mean"
|
||||||
identifier (string-join (mapcar #'car hits) " or "))
|
identifier (string-join (mapcar #'car hits) " or "))
|
||||||
(user-error "flan: the running program defines no %s" identifier)))
|
(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) "")
|
((equal (nth 3 d) "")
|
||||||
;; Tast.global and Tast.extern carry no Loc, so there is nothing to go
|
;; 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
|
;; 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))
|
(let* ((loc (nth 3 d))
|
||||||
(parts (and (not (equal loc "")) (flan--parse-loc loc))))
|
(parts (and (not (equal loc "")) (flan--parse-loc loc))))
|
||||||
(cond
|
(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
|
;; 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
|
;; 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.
|
;; 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 (propertize (nth 2 d) 'face 'font-lock-type-face) "\n\n")
|
||||||
(insert (format "Kind %s\n" (nth 1 d)))
|
(insert (format "Kind %s\n" (nth 1 d)))
|
||||||
(flan-doc--where 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 —
|
;; Parameter names are not in the Tast — the checker keeps types —
|
||||||
;; so a signature is types only, and someone reading this buffer
|
;; so a signature is types only, and someone reading this buffer
|
||||||
;; should be told that rather than left to wonder.
|
;; 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"
|
(test-flan--check "and a name the program has not got is refused"
|
||||||
(and raised (string-match-p "no no-such-name" raised))))
|
(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 ──────────────────────────────────────────────────
|
;; ── The watch buffer ──────────────────────────────────────────────────
|
||||||
;;
|
;;
|
||||||
;; test_dev.ml proves the table itself: a program pushes and the daemon reads
|
;; 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
|
if ok then n
|
||||||
else fail loc "%Ld does not fit in %s" n (Types.ikind_name k)
|
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 =
|
and var ctx loc ~want name =
|
||||||
match name with
|
match name with
|
||||||
| "true" | "false" ->
|
| "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 \
|
"a Vec takes exactly one index — (at v i) — and its element is indexed \
|
||||||
separately"
|
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 =
|
and named_call ctx ~want loc name args =
|
||||||
let prim p ty args = expect loc ~want (mk loc ty (Tast.Prim (p, args))) in
|
let prim p ty args = expect loc ~want (mk loc ty (Tast.Prim (p, args))) in
|
||||||
match name with
|
match name with
|
||||||
@ -5145,6 +5150,287 @@ and binary ctx name loc ~want args =
|
|||||||
end
|
end
|
||||||
| _ -> fail loc "%s takes two arguments" name
|
| _ -> 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 ─────────────────────────────────── *)
|
(* ── Declarations: pass 1, collect ─────────────────────────────────── *)
|
||||||
|
|
||||||
(* Constant folding, only over integers and only for defconst — enough for an
|
(* 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
|
connect and again after each install. Putting signatures on the poll would
|
||||||
pay for them every time anyone looked at the output buffer.
|
pay for them every time anyone looked at the output buffer.
|
||||||
|
|
||||||
One entry per name: (name kind signature loc). Four strings, so the editor
|
One entry per name: (name kind signature loc doc). Five strings, so the
|
||||||
reads it with [read] and nothing here needs a new wire type. [loc] is empty
|
editor reads it with [read] and nothing here needs a new wire type. [loc] is
|
||||||
where there is none to give — only [Tast.fn] carries one — and an editor
|
empty where there is none to give — only [Tast.fn] carries one — and an
|
||||||
that finds it empty must say so rather than guess a file.
|
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. *)
|
Parameter *names* are not in the Tast, so a signature shows types only. *)
|
||||||
let signature_of_fn (f : Tast.fn) =
|
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))
|
(String.concat " " (List.map Types.to_string f.Tast.params))
|
||||||
(Types.to_string f.Tast.ret)
|
(Types.to_string f.Tast.ret)
|
||||||
|
|
||||||
let entry ~name ~kind ~sign ~loc =
|
let entry ~name ~kind ~sign ~loc ?(doc = "") () =
|
||||||
Wire.list [ Wire.quote name; Wire.quote kind; Wire.quote sign; Wire.quote loc ]
|
Wire.list
|
||||||
|
[ Wire.quote name; Wire.quote kind; Wire.quote sign; Wire.quote loc;
|
||||||
|
Wire.quote doc ]
|
||||||
|
|
||||||
let defs t =
|
let defs t =
|
||||||
let p = t.session.Session.program in
|
let p = t.session.Session.program in
|
||||||
@ -1025,7 +1034,7 @@ let defs t =
|
|||||||
| None ->
|
| None ->
|
||||||
Some
|
Some
|
||||||
(entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f)
|
(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
|
p.Tast.fns
|
||||||
in
|
in
|
||||||
let globals =
|
let globals =
|
||||||
@ -1035,7 +1044,7 @@ let defs t =
|
|||||||
~kind:(if g.Tast.gconst then "const" else "var")
|
~kind:(if g.Tast.gconst then "const" else "var")
|
||||||
~sign:
|
~sign:
|
||||||
(Printf.sprintf "%s %s" g.Tast.gname (Types.to_string g.Tast.gty))
|
(Printf.sprintf "%s %s" g.Tast.gname (Types.to_string g.Tast.gty))
|
||||||
~loc:"")
|
~loc:"" ())
|
||||||
p.Tast.globals
|
p.Tast.globals
|
||||||
in
|
in
|
||||||
let externs =
|
let externs =
|
||||||
@ -1046,10 +1055,27 @@ let defs t =
|
|||||||
(Printf.sprintf "%s [%s] %s" e.Tast.ename
|
(Printf.sprintf "%s [%s] %s" e.Tast.ename
|
||||||
(String.concat " " (List.map Types.to_string e.Tast.eparams))
|
(String.concat " " (List.map Types.to_string e.Tast.eparams))
|
||||||
(Types.to_string e.Tast.eret))
|
(Types.to_string e.Tast.eret))
|
||||||
~loc:"")
|
~loc:"" ())
|
||||||
p.Tast.externs
|
p.Tast.externs
|
||||||
in
|
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.
|
(* [(:op "layout" :type T)] — a struct's fields and their types.
|
||||||
|
|
||||||
|
|||||||
@ -77,6 +77,10 @@
|
|||||||
(file %{workspace_root}/bin/main.exe)
|
(file %{workspace_root}/bin/main.exe)
|
||||||
; The Emacs client, which test_emacs drives against a real daemon.
|
; The Emacs client, which test_emacs drives against a real daemon.
|
||||||
(glob_files %{workspace_root}/emacs/*.el)
|
(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
|
; The WASI host the wasm32 case runs its module under, when no wasmtime or
|
||||||
; wasmer is installed.
|
; wasmer is installed.
|
||||||
(file wasm-run.mjs)))
|
(file wasm-run.mjs)))
|
||||||
|
|||||||
@ -321,7 +321,13 @@ let () =
|
|||||||
carries what eldoc, completion and find-definition each need: a kind,
|
carries what eldoc, completion and find-definition each need: a kind,
|
||||||
a signature, and where the name is written where that is knowable.
|
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
|
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
|
let r = request c "(:op \"defs\")" in
|
||||||
if status r <> "ok" then fail "defs: %s" (status r);
|
if status r <> "ok" then fail "defs: %s" (status r);
|
||||||
(match Wire.field r "defs" with
|
(match Wire.field r "defs" with
|
||||||
@ -334,26 +340,60 @@ let () =
|
|||||||
({ Form.v = Form.Str n; _ }
|
({ Form.v = Form.Str n; _ }
|
||||||
:: { Form.v = Form.Str kind; _ }
|
:: { Form.v = Form.Str kind; _ }
|
||||||
:: { Form.v = Form.Str sign; _ }
|
:: { Form.v = Form.Str sign; _ }
|
||||||
:: { Form.v = Form.Str loc; _ } :: [])
|
:: { Form.v = Form.Str loc; _ }
|
||||||
when String.equal n name -> Some (kind, sign, loc)
|
:: { Form.v = Form.Str doc; _ } :: [])
|
||||||
|
when String.equal n name -> Some (kind, sign, loc, doc)
|
||||||
| _ -> None)
|
| _ -> None)
|
||||||
entries
|
entries
|
||||||
in
|
in
|
||||||
(match find "step" with
|
(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
|
(* Absolute, because an editor is not in this process's working
|
||||||
directory and cannot resolve a relative one. *)
|
directory and cannot resolve a relative one. *)
|
||||||
if loc.[0] <> '/' then fail "a fn's location is relative: %s" loc
|
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");
|
| None -> fail "defs did not mention step");
|
||||||
(match find "ticks" with
|
(match find "ticks" with
|
||||||
| Some ("var", "ticks i64", "") -> ()
|
| Some ("var", "ticks i64", "", "") -> ()
|
||||||
| Some (k, s, l) -> fail "ticks is described as (%s, %s, %s)" k s l
|
| Some (k, s, l, _) -> fail "ticks is described as (%s, %s, %s)" k s l
|
||||||
| None -> fail "defs did not mention ticks");
|
| None -> fail "defs did not mention ticks");
|
||||||
(match find "agent/wait-raw" with
|
(match find "agent/wait-raw" with
|
||||||
| Some ("extern", _, _) -> ()
|
| Some ("extern", _, _, _) -> ()
|
||||||
| Some (k, _, _) -> fail "an extern is described as %s" k
|
| Some (k, _, _, _) -> fail "an extern is described as %s" k
|
||||||
| None -> fail "defs did not mention an imported extern")
|
| 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");
|
| _ -> fail "defs did not answer with a list");
|
||||||
|
|
||||||
(* [layout]: a struct's fields and their types, out of [Tast.structs],
|
(* [layout]: a struct's fields and their types, out of [Tast.structs],
|
||||||
|
|||||||
@ -2691,6 +2691,87 @@ let () =
|
|||||||
~needle:"is not a map key"
|
~needle:"is not a map key"
|
||||||
"(defn f [k $t] () (let [m (map-new t i32)] (put m k 1) (free m)))";
|
"(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 ──────────────────── *)
|
(* ── The acceptance program checks end to end ──────────────────── *)
|
||||||
accepts "calc-me.flan type checks"
|
accepts "calc-me.flan type checks"
|
||||||
(In_channel.with_open_bin "../calc-me.flan" In_channel.input_all);
|
(In_channel.with_open_bin "../calc-me.flan" In_channel.input_all);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user