A promise the compiler cannot check gets its own refusal, and a session expands its buffer's macros
Two loose ends from NEXT.md. slice-from-ptr's run-time refusal borrowed @flan_slice_error and reported a range and a length the caller never wrote. It has flan_slice_promise_error now: signals BoundsError, walks the handlers, offers the break loop, falls through to a message and a status like the two beside it. The sentence names what was promised and what was passed, and a second line says what is not checked. The condition fields stay (0, n, 0) — the violated condition as a range, and not (0, n, n), which reads as in bounds. And a session now holds the buffer's own defmacros: seeded in Session.create from the same read that produced decls, and added by Session.eval so a defmacro typed at the editor joins the set the way a defn does. Not a re-read of the file, which would put unsaved-versus-saved skew inside expansion. The commit stays below the checker. Macro.program dedupes the ambient set against the forms being parsed, left-wins, because unqualified names can now collide.
This commit is contained in:
parent
af3daaabe2
commit
b9f5b5c44c
104
BUILT.md
104
BUILT.md
@ -5387,3 +5387,107 @@ macros" now reads "no macros of its own", because `rl/with-drawing` is the bindi
|
||||
because nothing headless can. `rl-with-reject.flan` is the arity half: a macro cannot signal while it expands, so
|
||||
the prelude's idiom is to answer a symbol that is not a name and reads as the sentence the caller needs
|
||||
(`with-mode-2d-takes-a-camera-and-a-body`), with the expander's note beneath it naming the macro at the call site.
|
||||
|
||||
## `slice-from-ptr` refuses in its own words, because its promise is the only context there is
|
||||
|
||||
`(slice-from-ptr p n)` shipped with a run-time check that was right and a message that was not. The check is
|
||||
`icmp sge i64 n, 0`, signed, and it has to be signed: `check_slice`'s comparisons are unsigned, and a negative
|
||||
`i32` sign-extended to `i64` is a huge `u64` that passes both of its clauses. What came out of it was
|
||||
|
||||
```
|
||||
slice [0 -2) is out of bounds for length 0
|
||||
```
|
||||
|
||||
— a range and a length the caller never wrote. The arithmetic behind the reuse was defensible: the condition
|
||||
violated is `0 <= n`, which is a reversed range spelled the other way, which is why `@flan_slice_error` accepted
|
||||
it. The sentence was not. It is about a container, and there is no container here.
|
||||
|
||||
**This is the one form in the language where the compiler cannot check the thing that matters.** Everywhere else
|
||||
the length belongs to the compiler — an array has one, a slice carries one, a `Vec` stores one. Here the caller is
|
||||
the only thing that knows how many elements live behind that pointer, and writing `n` *is* the promise. So its
|
||||
refusal is the place that promise has to be spelled out, and it was the one place it was not.
|
||||
|
||||
`flan_slice_promise_error` in `runtime/flan_rt.c` now says it:
|
||||
|
||||
```
|
||||
bounds.flan:34:28: slice-from-ptr was promised -2 elements behind the pointer, and a count of elements is never negative
|
||||
the caller promises the pointer addresses n elements and nothing else can know it, so the sign of n is the whole of
|
||||
what this check can see
|
||||
```
|
||||
|
||||
Two lines, and the second one is deliberate: it says what is *not* checked, so that a caller does not read a trap
|
||||
here as proof that the pointer itself was looked at.
|
||||
|
||||
It is shaped like the two beside it and not like `exit`. It signals `BoundsError` through `flan_bounds_signal`,
|
||||
which walks the handlers and offers the break loop, and only falls through to the message and the status when
|
||||
nothing answered — the same "an index out of range is a condition" rule, so a `flan dev` session survives one.
|
||||
|
||||
**The three condition fields are `(0, n, 0)`**, the violated condition written as a range, which is what those
|
||||
fields can carry. Deliberately not `(0, n, n)`: that reads as a range in bounds, and a handler testing
|
||||
`high <= length` would wave the failure through. One condition type still covers every bad index in the language,
|
||||
so a handler writes one clause and not three.
|
||||
|
||||
On the emit side it is one `signal_block` call and one `declare`; nothing about the lowering of the form changed,
|
||||
and it stays behind `f.md.checks` for the reason the other two do — dropping bounds checks is a release decision,
|
||||
not an optimisation one, so it is on at `-O0` and `-O2` alike. `test/programs/bounds.flan`'s `-2` arm asserts the
|
||||
new sentence at both, and the `Emit.program ~checks:false` assertion names the new symbol alongside the old two.
|
||||
`lib/x86.ml` needs nothing: it lowers `flan_slice_error` for `slice`, and it has no `SliceFromPtr` arm at all.
|
||||
|
||||
## A session expands the buffer's own macros, and a `defmacro` joins it like a `defn`
|
||||
|
||||
`(tenfold 7)` at `C-x C-e` was an unknown name, and `(defn x [] i32 (tenfold 7))` at `C-c C-c` was the same
|
||||
unknown name, in the very file that declares `tenfold`. The cause is one line of `Macro.program`: it collects
|
||||
macros by scanning the forms it is handed, and an evaluation hands it the one form that was sent. The prelude's
|
||||
macros worked because they are ambient, and an imported package's worked because `Session` holds them — the
|
||||
buffer's own were the set nobody held.
|
||||
|
||||
**The design question was what "the buffer's own macros" means to a session, and the answer is the one already
|
||||
written at the top of `lib/session.ml`: the declarations the program was built from, plus every change accepted
|
||||
since.** Applied to macros, that is both halves of the fix and neither is optional:
|
||||
|
||||
- **Seeded in `Session.create`** from the forms `Load.program` was handed. That is the same read that produced
|
||||
`t.decls`, not a second one — so `tenfold` is there from the first evaluation, and no macro can arrive from a
|
||||
version of the file the session was never told about.
|
||||
- **Added in `Session.eval`**, so a `defmacro` typed at the editor joins the set and the *next* evaluation can
|
||||
call it. That is exactly the shape `defn` already has, and a `defmacro` was already an ordinary declaration on
|
||||
this path — it parses to a `Defn` and installs a body like any other. The only thing missing was the session
|
||||
remembering that the name is a macro.
|
||||
|
||||
**The session does not re-read the file, and that was the live alternative.** It has `~origin`, the buffer's own
|
||||
path, and re-reading would pick up macros the session has never been told about. It would also read whatever is
|
||||
*saved*, while the buffer on screen is whatever is *typed* — so an expansion would silently use a body the reader
|
||||
is not looking at. Unsaved-versus-saved skew inside macro expansion is the quiet-wrongness class this codebase
|
||||
refuses everywhere else, and it is worse here than elsewhere because a macro decides what the code *is*.
|
||||
|
||||
The commit point is untouched. The new set is computed into a `ref` at the top and assigned at `t.macros <- ...`
|
||||
with everything else, below the checker — so a form that does not check leaves the session exactly as it was,
|
||||
macros included. `test_session` pins that with a `defmacro` whose body calls an unknown function: the evaluation
|
||||
is refused, and a later call to the name is still an unknown name.
|
||||
|
||||
Ordering is left-wins throughout, which is what `Load.macro_union` already encodes: the forms just sent, then what
|
||||
`Load` just read off disk, then what the session was holding. That is what makes an *edited* macro expand with its
|
||||
new body rather than with the stale copy.
|
||||
|
||||
**One thing had to change below `Session`.** `Macro.program` merged the ambient set with the file's own and said
|
||||
in a comment that an import's names carry a slash, so nothing ambient could collide with a name written here. That
|
||||
stopped being true the moment a session held unqualified names — and it stopped being true on the most ordinary
|
||||
action there is: `C-c C-c` over a `defmacro` the session already knows sends a form declaring a name the ambient
|
||||
set also has. Two forms declaring one name reach `Check.program` as a duplicate declaration, refused with a
|
||||
sentence nobody would connect to editing a macro. The merge dedupes now, on the same left-wins rule and at the one
|
||||
point that joins the two sets.
|
||||
|
||||
`Expand.quasiquote` is applied on the way in, and it is load-bearing rather than tidiness. `Parse.parse_forms`
|
||||
desugars every form before the expander sees it and `Load.qualify_macro` desugars a package's macro on its way
|
||||
out, but `Parse.imported_macros` is read by `Macro.program` directly, past that map. An undesugared body still has
|
||||
its quasiquote in it, which makes a quasiquoted call look like a real one — the false ring the first cycle test
|
||||
walked into.
|
||||
|
||||
The names stay unqualified. A buffer writes its own macro's bare name, so that is the name the session has to
|
||||
answer to; a file inside a package the program also imports ends up holding both, the bare one from here and
|
||||
`alias/name` from `Load`, which is what the two call sites each need.
|
||||
|
||||
Both editor paths work and they are different wraps — `Parse.expr`'s for `C-x C-e` and `Parse.decl`'s for
|
||||
`C-c C-c`. `test/test_session.ml` covers both, plus a `defmacro` the file never had followed by a call to it,
|
||||
plus editing that macro and calling it again; the assertions are on the IR and not on the absence of an
|
||||
exception, because an expression that did not expand raises while one that expanded to the *wrong* thing does
|
||||
not.
|
||||
|
||||
24
NEXT.md
24
NEXT.md
@ -8,7 +8,13 @@ and the author's words are that they feel like extra language features. `any` is
|
||||
tagged union (two words, a pointer and a typeid, no GC). `drop` is `spec-memory.md`'s hook for owning
|
||||
something that is not memory. Their old entries stand; do not schedule either.
|
||||
|
||||
## Queued: `slice-from-ptr`'s run-time refusal names a range the caller never wrote
|
||||
## ~~Queued: `slice-from-ptr`'s run-time refusal names a range the caller never wrote~~ — **done**
|
||||
|
||||
**Built.** `flan_slice_promise_error` is its own function in `runtime/flan_rt.c`, shaped like the two
|
||||
beside it — it signals `BoundsError`, walks the handlers, offers the break loop, and only then falls
|
||||
through to a message and a status. The sentence names the promise and the number that broke it, and a
|
||||
second line says what is *not* checked. See [`BUILT.md`](BUILT.md), "`slice-from-ptr` refuses in its
|
||||
own words". The original entry follows.
|
||||
|
||||
Small, and known rough at the time it shipped. `(slice-from-ptr p n)` with a **computed** negative
|
||||
length is caught at run time — the check is `icmp sge i64 n, 0`, signed, because `check_slice`'s own
|
||||
@ -171,15 +177,15 @@ the wrap `Parse.decl` already had, and the decision it was waiting on came out a
|
||||
expands to a declaration is **refused by name**, in `Parse.expr`'s head dispatch, so a `defn` nested
|
||||
anywhere in the expansion and a `defvar` typed by hand get the same sentence. The spin refusal fires
|
||||
on this path; the ring never can, because a ring is refused while its own package is parsed. What
|
||||
expands is the prelude's macros and the imported packages' — not the file's own, which is a session
|
||||
limit `C-c C-c` shares and which is now pinned rather than fixed.
|
||||
expands is the prelude's macros and the imported packages' — and, since the lane recorded just below,
|
||||
the file's own as well.
|
||||
|
||||
Two things left behind it, neither this lane's. **A session expands no macro the buffer itself
|
||||
declares** — `Macro.program` scans the forms it is handed, and an evaluation hands it the one form
|
||||
that was sent, so `(tenfold 7)` is an unknown name at `C-x C-e` and `(defn x [] i32 (tenfold 7))` is
|
||||
one at `C-c C-c`. Fixing it means the session keeping its file's own `defmacro`s the way it keeps the
|
||||
imports', which is a `Session` change and a decision about what a `defmacro` typed at the REPL does.
|
||||
And **`Dev.eval_expr` catches only `Loc.Error`** around `Session.eval_expr`: the two non-termination
|
||||
Two things left behind it, neither this lane's. ~~**A session expands no macro the buffer itself
|
||||
declares**~~ — **done**, see [`BUILT.md`](BUILT.md), "A session expands the buffer's own macros". The
|
||||
session seeds its file's own `defmacro`s in `Session.create`, from the same read that produced
|
||||
`decls`, and an evaluated `defmacro` joins the set so the next evaluation can call it — the shape
|
||||
`defn` already has. Re-reading the file was the alternative and was refused: it would put
|
||||
unsaved-versus-saved skew inside macro expansion. The commit point stays below the checker. And **`Dev.eval_expr` catches only `Loc.Error`** around `Session.eval_expr`: the two non-termination
|
||||
refusals are that, so they answer as replies, but `C-x C-e` can now reach `Build.macro_module` for
|
||||
the first time, and a macro that fails to *compile* raises `Failure` from the clang driver or
|
||||
`Loc.Errors` from the checker. Neither is caught, and an unhandled exception there is a dead session.
|
||||
|
||||
17
lib/emit.ml
17
lib/emit.ml
@ -1856,10 +1856,15 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
||||
decision, not an optimisation one, so this is on at -O0 and -O2 alike and
|
||||
off only when checks as a whole were asked off.
|
||||
|
||||
It reuses @flan_slice_error rather than growing the runtime a function:
|
||||
the violated condition is 0 <= n, which is the same shape as a reversed
|
||||
slice, so the range reported is [0 n) against a length of 0 and the
|
||||
message reads "slice [0 -5) is out of bounds for length 0". *)
|
||||
It has its own runtime function, @flan_slice_promise_error, and that is
|
||||
the whole of what it needed. It used to borrow @flan_slice_error — the
|
||||
violated condition is 0 <= n, which is a reversed range spelled the other
|
||||
way — and the sentence that came out named a range and a length the
|
||||
caller never wrote. Since this is the one form whose real condition the
|
||||
compiler cannot check, its refusal is the place the caller's promise has
|
||||
to be stated, and it was the one place it was not. The signalled
|
||||
BoundsError is unchanged: same three fields, so a handler writes one
|
||||
clause for every bad index in the language. *)
|
||||
| Tast.SliceFromPtr, [ p; n ] ->
|
||||
let pv = value f p in
|
||||
let nv = value f n in
|
||||
@ -1870,8 +1875,7 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
||||
ins f "%s = icmp sge i64 %s, 0" ok n64;
|
||||
signal_block f e.Tast.loc ~guard:(fun () -> guard f) ok (fun id len ->
|
||||
ins f
|
||||
"call void @flan_slice_error(ptr %s, i64 %d, i64 0, i64 %s, i64 0, \
|
||||
ptr %s)"
|
||||
"call void @flan_slice_promise_error(ptr %s, i64 %d, i64 %s, ptr %s)"
|
||||
id len n64 xfer_param)
|
||||
end;
|
||||
let a = fresh f in
|
||||
@ -2430,6 +2434,7 @@ declare void @flan_transfer_fail(ptr, i64) noreturn cold
|
||||
; it, which is the one path out. The trailing ptr is the transfer channel.
|
||||
declare void @flan_bounds_error(ptr, i64, i64, i64, ptr) cold
|
||||
declare void @flan_slice_error(ptr, i64, i64, i64, i64, ptr) cold
|
||||
declare void @flan_slice_promise_error(ptr, i64, i64, ptr) cold
|
||||
declare ptr @flan_context_allocator()
|
||||
declare ptr @flan_context_temp()
|
||||
declare ptr @flan_heap_allocator()
|
||||
|
||||
20
lib/macro.ml
20
lib/macro.ml
@ -311,13 +311,29 @@ let program (forms : Form.t list) : Form.t list =
|
||||
(* An import's macros, and they go in beside [mine] rather than beside
|
||||
[prelude]: a package macro may call another macro of its own package, so
|
||||
it is exactly as much a candidate for the rounds below as one written
|
||||
here. Their names carry a slash, so nothing they hold can collide with
|
||||
[mine] or with the prelude's. *)
|
||||
here.
|
||||
|
||||
This used to say that an import's names carry a slash, so nothing
|
||||
ambient could collide with a name written here. That stopped being true
|
||||
when a session started holding the buffer's own [defmacro]s — see
|
||||
[Session.eval] — and it stopped being true on the most ordinary action
|
||||
there is: [C-c C-c] over a [defmacro] the session already knows sends a
|
||||
form declaring a name the ambient set also has. Two forms declaring one
|
||||
name reach [Check.program] as a duplicate declaration, refused with a
|
||||
sentence nobody would connect to this.
|
||||
|
||||
So the merge dedupes, and the direction is the one [Load.macro_union]
|
||||
already uses a level up: a name declared in the forms being parsed
|
||||
shadows the ambient copy. That is also what makes an *edited* macro
|
||||
expand with its new body rather than with the session's stale one. *)
|
||||
let imported =
|
||||
List.filter_map
|
||||
(fun f -> Option.map (fun n -> (n, f)) (macro_name f))
|
||||
!Parse.imported_macros
|
||||
in
|
||||
let imported =
|
||||
List.filter (fun (n, _) -> not (List.mem_assoc n mine)) imported
|
||||
in
|
||||
let mine = imported @ mine in
|
||||
let all = prelude @ List.map fst mine in
|
||||
(* The common case by a wide margin, and the reason a build that uses no
|
||||
|
||||
@ -51,8 +51,12 @@ let source = {flan|
|
||||
;; allocator is known.
|
||||
;;
|
||||
;; It is signalled with `error`, from the runtime rather than from Flan:
|
||||
;; flan_bounds_error and flan_slice_error in runtime/flan_rt.c, which every
|
||||
;; bounds check now branches to. **The three fields there are a C struct that
|
||||
;; flan_bounds_error, flan_slice_error and flan_slice_promise_error in
|
||||
;; runtime/flan_rt.c — the last of those is (slice-from-ptr p n), whose
|
||||
;; message is about the caller's promise because there is no container to
|
||||
;; report, and which fills these fields with (0, n, 0): the condition it
|
||||
;; violated, 0 <= n, written as a range. Between them they are where every
|
||||
;; bounds check branches. **The three fields there are a C struct that
|
||||
;; has to agree with this one field for field**, the same hand-kept agreement
|
||||
;; flan_name_id keeps with Check.type_id.
|
||||
;;
|
||||
|
||||
@ -35,10 +35,18 @@ type t = {
|
||||
mutable env : Check.env; (* the same, as the checker sees it *)
|
||||
host : Tast.program; (* what the process was built from *)
|
||||
pkgs : Load.pkg list; (* alias, directory, names owned *)
|
||||
(* The defmacros the imports brought in, qualified. Held rather than
|
||||
re-derived because C-c C-c parses one form with no import in sight, and a
|
||||
macro that works on the first build and not on the reload is worse than
|
||||
one that never existed. *)
|
||||
(* Every [defmacro] this session can expand a call to: the imports', under
|
||||
their aliases, and the buffer's own, under the names the buffer writes.
|
||||
Held rather than re-derived because an evaluation parses one form with no
|
||||
import and no [defmacro] in sight, and a macro that works on the first
|
||||
build and not on the reload is worse than one that never existed.
|
||||
|
||||
It is exactly this record's own rule applied to macros — what the program
|
||||
was built from, plus every change accepted since. The file's own set is
|
||||
seeded in [create] from the forms [Load] was handed, which is the same
|
||||
read that produced [decls]; nothing here ever goes back to disk, so a
|
||||
macro cannot arrive from a version of the file the session was never
|
||||
told about. *)
|
||||
mutable macros : Form.t list;
|
||||
mutable thunks : int; (* expression evaluations so far *)
|
||||
(* Whether the modules this session emits carry DWARF. It belongs to the
|
||||
@ -71,11 +79,40 @@ let rec same_const (a : Tast.expr) (b : Tast.expr) =
|
||||
&& List.for_all2 same_const xs ys
|
||||
| _ -> false
|
||||
|
||||
(* The [defmacro]s among a set of top-level forms, as [t.macros] has to hold
|
||||
them.
|
||||
|
||||
[Expand.quasiquote] is not decoration. [Parse.parse_forms] desugars every
|
||||
form it is handed before the expander sees it, and [Load.qualify_macro]
|
||||
desugars a package's macro on its way out for the same reason — but
|
||||
[Parse.imported_macros] is read by [Macro.program] directly, past that map.
|
||||
An undesugared body still has its quasiquote in it, which makes a
|
||||
quasiquoted call look like a real one: the false ring BUILT.md records the
|
||||
first cycle test walking into.
|
||||
|
||||
Unqualified, and that is the point: a buffer writes its own macro's bare
|
||||
name, so that is the name the session has to answer to. A file inside a
|
||||
package that the program also imports has both — the bare one from here and
|
||||
[alias/name] from [Load] — which is what the two call sites each need. *)
|
||||
let own_macros (forms : Form.t list) : Form.t list =
|
||||
List.filter_map
|
||||
(fun f ->
|
||||
match f.Form.v with
|
||||
| Form.List ({ Form.v = Form.Sym "defmacro"; _ } :: _) ->
|
||||
Some (Expand.quasiquote f)
|
||||
| _ -> None)
|
||||
forms
|
||||
|
||||
let create ?(debug = false) ~file () =
|
||||
let l = Load.program ~file (Reader.read_file file) in
|
||||
let forms = Reader.read_file file in
|
||||
let l = Load.program ~file forms in
|
||||
let p, env = Check.program_with_env l.Load.decls in
|
||||
({ file; decls = l.Load.decls; program = p; env; host = p; pkgs = l.Load.pkgs;
|
||||
macros = l.Load.macros; thunks = 0; debug }, l)
|
||||
(* The file's own first, so that if the file being edited is itself a
|
||||
package the program imports, the bare name wins for a form typed into
|
||||
that buffer. [macro_union] keeps the left. *)
|
||||
macros = Load.macro_union (own_macros forms) l.Load.macros;
|
||||
thunks = 0; debug }, l)
|
||||
|
||||
(* Which package a file being edited belongs to, if any.
|
||||
|
||||
@ -349,8 +386,25 @@ let eval ?(origin = "<eval>") ?pause t src : change =
|
||||
macros included. Nothing between here and there reads [t.macros]:
|
||||
[Load.program] puts the imported set in front of the parse it drives
|
||||
itself, and the checker below is handed declarations that are already
|
||||
parsed. *)
|
||||
macros := Load.macro_union l.Load.macros t.macros;
|
||||
parsed.
|
||||
|
||||
And the forms just sent come in front of both, which is the half that
|
||||
makes a [defmacro] typed at the editor mean anything. A [defmacro] is
|
||||
an ordinary declaration on this path already — it parses to a [Defn]
|
||||
and installs a body like any other — so the only thing missing was the
|
||||
session remembering that the name is a macro. It does now, and the
|
||||
shape that follows is the one [defn] already has: the declaration
|
||||
joins the session, and the *next* evaluation can call it. Not a re-read
|
||||
of the file, which would also pick up macros the session was never told
|
||||
about and would put unsaved-versus-saved skew into expansion.
|
||||
|
||||
Left-wins again, and here it is what lets a macro be edited: the
|
||||
incoming body replaces the one the session was holding under that
|
||||
name. [Macro.program] dedupes the same way on the same rule, because
|
||||
while this parse runs the old copy is still ambient. *)
|
||||
macros :=
|
||||
Load.macro_union (own_macros forms)
|
||||
(Load.macro_union l.Load.macros t.macros);
|
||||
let ds = l.Load.decls in
|
||||
match package_of t origin with
|
||||
| None -> ds
|
||||
|
||||
@ -540,6 +540,51 @@ void flan_slice_error(const uint8_t *loc, int64_t loclen, int64_t lo,
|
||||
flan_slice_fail(loc, loclen, lo, hi, len);
|
||||
}
|
||||
|
||||
/* ── (slice-from-ptr p n), which has its own refusal ───────────────────
|
||||
*
|
||||
* It used to borrow flan_slice_error, and what came out named a range and a
|
||||
* length the caller never wrote: "slice [0 -2) is out of bounds for length 0".
|
||||
* The arithmetic behind that was defensible — the condition violated is
|
||||
* 0 <= n, which is a reversed range spelled the other way — but the sentence
|
||||
* was about a container, and there is no container here.
|
||||
*
|
||||
* This is the one form in the language where the compiler cannot check the
|
||||
* thing that matters. Everywhere else the length is the compiler's: an array
|
||||
* has one, a slice carries one, a Vec stores one. Here the caller is the only
|
||||
* thing that knows how many elements live behind that pointer, and writing n
|
||||
* *is* the promise. So this refusal is where that promise has to be spelled
|
||||
* out, because it is the only context the reader has.
|
||||
*
|
||||
* What is checked is the one half that can be: that the promise is not absurd.
|
||||
* A count of elements is not negative. The message says what is not checked
|
||||
* too, so that a caller does not read a trap here as proof that the pointer
|
||||
* was looked at.
|
||||
*
|
||||
* It signals BoundsError like the other two, with the same three int64s, so a
|
||||
* handler writes one clause and not three. The triple is (0, n, 0): the
|
||||
* violated condition written as a range, which is what those fields can carry.
|
||||
* Deliberately not (0, n, n) — that reads as a range in bounds, and a handler
|
||||
* testing high <= length would wave the failure through. */
|
||||
_Noreturn void flan_slice_promise_fail(const uint8_t *loc, int64_t loclen,
|
||||
int64_t n) {
|
||||
fflush(stdout);
|
||||
fprintf(stderr,
|
||||
"%.*s: slice-from-ptr was promised %lld elements behind the pointer, "
|
||||
"and a count of elements is never negative\n",
|
||||
(int)loclen, (const char *)loc, (long long)n);
|
||||
fprintf(stderr,
|
||||
" the caller promises the pointer addresses n elements and nothing "
|
||||
"else can know it, so the sign of n is the whole of what this check "
|
||||
"can see\n");
|
||||
rt_die();
|
||||
}
|
||||
|
||||
void flan_slice_promise_error(const uint8_t *loc, int64_t loclen, int64_t n,
|
||||
void *xfer) {
|
||||
if (flan_bounds_signal(xfer, 0, n, 0)) return;
|
||||
flan_slice_promise_fail(loc, loclen, n);
|
||||
}
|
||||
|
||||
/* ── Allocators, spec-memory.md ────────────────────────────────────────
|
||||
*
|
||||
* One type-erased procedure plus an opaque data pointer, which is Odin's
|
||||
|
||||
@ -23,6 +23,16 @@
|
||||
;;; against arithmetic the compiler could have done itself.
|
||||
(defconst step-by i64 3)
|
||||
|
||||
;;; Called by nothing here either, and for the same reason: it is the buffer's
|
||||
;;; *own* macro, and what it is for is to be called from an expression typed at
|
||||
;;; the editor. A session used to hold the prelude's macros and its imports'
|
||||
;;; and not these, so (tenfold 7) was an unknown name in the very file that
|
||||
;;; declares it. Nothing in this program names it, so no macro module is built
|
||||
;;; for the build itself -- the first one is paid by the evaluation that calls
|
||||
;;; it.
|
||||
(defmacro tenfold [args]
|
||||
`(* ~(at args 0) 10))
|
||||
|
||||
(defn main [] i32
|
||||
(agent/start "/tmp/flan-printers-fallback.sock")
|
||||
(set big 0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
@ -1201,11 +1201,17 @@ let () =
|
||||
length to compare n against — the caller's number *is* the length — so
|
||||
the only check possible is that it is not negative, and it is a signed
|
||||
one: the two above are unsigned, and a negative i32 sign-extended to
|
||||
i64 passes both of them. The message is @flan_slice_error's, reused
|
||||
rather than growing the runtime a function: the condition violated is
|
||||
0 <= n, which is a reversed range spelled the other way. *)
|
||||
i64 passes both of them.
|
||||
|
||||
Its own sentence, from its own runtime function. It used to borrow
|
||||
@flan_slice_error and say "slice [0 -2) is out of bounds for length
|
||||
0", which named a range and a length the caller never wrote. This is
|
||||
the one form whose real condition the compiler cannot check, so the
|
||||
refusal is where the caller's promise gets stated. The asserted
|
||||
substring stays inside one output line; the second line is the part
|
||||
about what is *not* checked. *)
|
||||
traps "slice-from-ptr with a negative length" "-2"
|
||||
"slice [0 -2) is out of bounds for length 0";
|
||||
"slice-from-ptr was promised -2 elements behind the pointer";
|
||||
(try Sys.remove exe with Sys_error _ -> ())
|
||||
in
|
||||
bounds ();
|
||||
@ -1224,7 +1230,8 @@ let () =
|
||||
end;
|
||||
let off = Emit.program ~checks:false p in
|
||||
if contains off "call void @flan_bounds_error("
|
||||
|| contains off "call void @flan_slice_error(" then begin
|
||||
|| contains off "call void @flan_slice_error("
|
||||
|| contains off "call void @flan_slice_promise_error(" then begin
|
||||
incr failures;
|
||||
print_endline "FAIL --no-bounds-checks: a check survived"
|
||||
end;
|
||||
|
||||
@ -160,6 +160,27 @@ let () =
|
||||
answers a value and one that answers unit. *)
|
||||
value "a prelude macro" "(clamp 9 0 3)" "3";
|
||||
value "a prelude macro for its effect" "(unless false 1 2)" "()";
|
||||
(* And the buffer's own, which is the set nobody held. [Macro.program]
|
||||
collects macros by scanning the forms it is handed and an evaluation
|
||||
hands it one form, so [tenfold] — declared in printers.flan, a few
|
||||
lines above [main] — was an unknown name in its own file while the
|
||||
prelude's and an import's both worked. The session seeds it from the
|
||||
same read that built the program. Over a real socket, because that is
|
||||
the path a person is on; test_session has the in-process halves. *)
|
||||
value "the file's own macro" "(tenfold 7)" "70";
|
||||
(* A macro the file never had, typed at the editor and then called. This
|
||||
is the shape the fix chose: a [defmacro] evaluated into a session
|
||||
*joins* it, exactly as a [defn] does, and the next evaluation can call
|
||||
it. Two round trips, because that is the whole of the claim. *)
|
||||
(let r =
|
||||
request c
|
||||
(Printf.sprintf "(:op \"eval\" :code %s :file \"/tmp/buf.flan\")"
|
||||
(quote "(defmacro thrice [args] `(* ~(at args 0) 3))"))
|
||||
in
|
||||
if status r <> "ok" then
|
||||
fail "evaluating a defmacro over the socket: %s"
|
||||
(Option.value ~default:(status r) (field r "message")));
|
||||
value "a macro defined at the editor" "(thrice 14)" "42";
|
||||
|
||||
(* The one that proves it ran inside the process: the program increments
|
||||
[ticks] every frame, so two evaluations of it must disagree. A copy
|
||||
|
||||
@ -394,15 +394,85 @@ let () =
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "C-u C-x C-e on a macro call: %s" m);
|
||||
|
||||
(* Not the file's own macro, and deliberately not: [Macro.program] collects
|
||||
those by scanning the forms it is handed, and the forms handed to an
|
||||
evaluation are the one thing that was sent. That limit is the session's
|
||||
and not this path's — C-c C-c has always had it too, for the same reason —
|
||||
so it is left where it is rather than half-fixed here. Pinned so that the
|
||||
day it changes, it changes on purpose. *)
|
||||
(* ── And the file's own macro, which used to be pinned as refused ──────
|
||||
[Macro.program] collects macros by scanning the forms it is handed, and
|
||||
the forms handed to an evaluation are the one thing that was sent — so
|
||||
[tenfold], declared in the buffer being edited, was an unknown name at
|
||||
both C-x C-e and C-c C-c while the prelude's and a package's both worked.
|
||||
The session holds it now, seeded in [Session.create] from the same read
|
||||
that produced [decls].
|
||||
|
||||
On the IR, not on the absence of an exception: an expression that did not
|
||||
expand raises, but one that expanded to the wrong thing does not, and the
|
||||
arithmetic is the only witness. [tenfold] multiplies its argument by
|
||||
ten. *)
|
||||
(match Session.eval_expr ~origin:"programs/pkg-macro.flan" tm "(tenfold 7)" with
|
||||
| _ -> fail "the file's own macro expanded in a session — a welcome change, \
|
||||
but BUILT.md says it does not"
|
||||
| c ->
|
||||
if not (has c.Session.ir "7, 10") then
|
||||
fail "the file's own macro through C-x C-e did not expand to its body"
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "the file's own macro through C-x C-e: %s" m);
|
||||
(* The other path, which is a different expander wrap: [Parse.decl]'s. *)
|
||||
(match Session.eval ~origin:"programs/pkg-macro.flan" tm
|
||||
"(defn tenfolded [] i32 (tenfold 7))"
|
||||
with
|
||||
| c ->
|
||||
if not (List.mem "tenfolded" c.Session.fns) then
|
||||
fail "the file's own macro through C-c C-c reported %s"
|
||||
(String.concat " " c.Session.fns);
|
||||
if not (has c.Session.ir "7, 10") then
|
||||
fail "the file's own macro through C-c C-c did not expand to its body"
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "the file's own macro through C-c C-c: %s" m);
|
||||
|
||||
(* A [defmacro] the file never had, typed at the editor. This is the shape
|
||||
the fix chose — a macro evaluated into the session *joins* it, exactly as
|
||||
a [defn] does, and the next evaluation can call it — and it is the only
|
||||
case here that the create-time seed cannot explain. Two evaluations,
|
||||
because that is what the claim is about. *)
|
||||
(match Session.eval ~origin:"programs/pkg-macro.flan" tm
|
||||
"(defmacro thrice [args] `(* ~(at args 0) 3))"
|
||||
with
|
||||
| _ -> ()
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "evaluating a defmacro: %s" m);
|
||||
(match Session.eval_expr ~origin:"programs/pkg-macro.flan" tm "(thrice 5)" with
|
||||
| c ->
|
||||
if not (has c.Session.ir "5, 3") then
|
||||
fail "a defmacro evaluated into the session did not expand to its body"
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "a defmacro evaluated into the session: %s" m);
|
||||
(* And re-evaluating it over the top expands with the *new* body. Left-wins
|
||||
in [Session.eval]'s union and in [Macro.program]'s merge, which is the
|
||||
ordinary editing action and the one that would have reached
|
||||
[Check.program] as a duplicate declaration without the second of those. *)
|
||||
(match Session.eval ~origin:"programs/pkg-macro.flan" tm
|
||||
"(defmacro thrice [args] `(* ~(at args 0) 4))"
|
||||
with
|
||||
| _ -> ()
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "re-evaluating a defmacro: %s" m);
|
||||
(match Session.eval_expr ~origin:"programs/pkg-macro.flan" tm "(thrice 5)" with
|
||||
| c ->
|
||||
if has c.Session.ir "5, 3" then
|
||||
fail "an edited defmacro expanded with its old body"
|
||||
else if not (has c.Session.ir "5, 4") then
|
||||
fail "an edited defmacro did not expand to its new body"
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "an edited defmacro: %s" m);
|
||||
|
||||
(* The robustness lane's property, held for macros too: the commit is below
|
||||
the checker, so a [defmacro] that does not check leaves the session
|
||||
holding nothing of it. The body calls an unknown name, so the form parses
|
||||
and fails at the checker — which is the only interesting place to fail,
|
||||
because a parse failure never reaches the union either. *)
|
||||
(match Session.eval ~origin:"programs/pkg-macro.flan" tm
|
||||
"(defmacro nope [args] (no-such-function args))"
|
||||
with
|
||||
| _ -> fail "a defmacro whose body does not check was accepted"
|
||||
| exception Loc.Error _ -> ());
|
||||
(match Session.eval_expr ~origin:"programs/pkg-macro.flan" tm "(nope 1)" with
|
||||
| _ -> fail "a refused defmacro was left in the session's macro set"
|
||||
| exception Loc.Error _ -> ());
|
||||
|
||||
(* An expression that expands to a declaration. A macro may build one as a
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user