Empty fn bodies, the pins for all six items, and the FIX.org entry
This commit is contained in:
parent
6c017c2cf8
commit
fa2b56ba5a
220
FIX.org
220
FIX.org
@ -1598,3 +1598,223 @@ because the code is the same.
|
||||
Not fixed here, deliberately: the fix is to catch ~Closed~ in that poll and
|
||||
read it as the program having ended, which is a claim about what those rows
|
||||
mean and belongs to whoever owns them. Flagged rather than patched.
|
||||
|
||||
* Six dogfooding items off DISCUSS.org, 2026-09-20
|
||||
Each of these is an author note from a session of writing Flan rather than a
|
||||
report from a test. They are small and they are unrelated to each other, which
|
||||
is why they went in one lane: every one of them is a place the language said no
|
||||
for no reason, or did not have a name it should have had.
|
||||
|
||||
** (when test) with no body, and the family it turned out to belong to
|
||||
[lib/parse.ml]'s ~when~ required ~body <> []~ and failed "when is (when test
|
||||
body ...)". The guard is gone and an empty body is the ~Do []~ that ~(do)~
|
||||
already means. A ~(when)~ with no test at all is still refused, because there
|
||||
is nothing to branch on.
|
||||
|
||||
~unless~ is a prelude macro now, not a special form, and carried the same
|
||||
restriction as ~(< (len args) 2)~. It is ~(< (len args) 1)~, and its
|
||||
unknown-name report narrowed with it: ~unless-takes-a-test~ rather than
|
||||
~unless-takes-a-test-and-a-body~, since the body is no longer part of the
|
||||
claim.
|
||||
|
||||
The author then added the third member of the family, and it turned out to be
|
||||
two different questions:
|
||||
|
||||
- =(defn foo [bar i32] ())= — a declared return type of () and no body — was
|
||||
*already legal*, and the refusal for the other case was already the right
|
||||
one: [Check] says "foo returns i32 but has no body" at the declaration. No
|
||||
change; both are pinned now, which they were not.
|
||||
- =(fn [])= was refused by the parser, by the same ~body <> []~ guard ~when~
|
||||
had. Dropped. An fn declares no return type, so "legal exactly when the
|
||||
return type is ()" has to be decided somewhere else, and the position it is
|
||||
written in is the only thing that knows: check_fn now refuses an empty body
|
||||
at a non-unit want — "an fn with no body answers (), and this one is in a
|
||||
position that wants i32". *That refusal is new and it was needed:* without
|
||||
it the empty body fell straight through check_fn's ~List.rev fbody~ match,
|
||||
the fn compiled, and the call read a return value nothing had written. So
|
||||
relaxing the parser here opened a hole that had to be closed in the checker,
|
||||
which is not true of ~when~ or of ~defn~.
|
||||
|
||||
** () as a unit value in expression position — considered and dropped
|
||||
The author, 2026-09-20, closing the question DISCUSS.org left open beside
|
||||
=(rl/with-drawing ())=: making bare ~()~ a unit value in expression position
|
||||
was considered and is not wanted. Empty forms doing the right thing — the
|
||||
three above — covers the need that made it look attractive, and ~()~ stays
|
||||
unspoken-for in value position on purpose, in case the language ever grows
|
||||
lists: "we might want lists at some point."
|
||||
|
||||
So ~()~ remains the type-position spelling of Unit and nothing else, and the
|
||||
guard below is written against that rather than around it.
|
||||
|
||||
** (rl/with-drawing ()) — a body that was not written, spelled the second way
|
||||
[vendor/raylib/modes.flan]'s guards caught zero arguments and not one argument
|
||||
that was itself ~()~, so the latter was spliced into the expansion verbatim and
|
||||
the report came out of the middle of the expanded ~do~ saying ~()~ is not an
|
||||
expression — several forms from anything anyone wrote. All five ~with-*~ macros
|
||||
now treat a lone ~()~ where the body goes as no body, answering the same
|
||||
unknown-name they already answered for the missing one.
|
||||
|
||||
Only a lone ~()~, and only in the body position. ~()~ as a camera or a render
|
||||
target is left to fail on its own: nothing the macro could say about it would
|
||||
be truer than what the compiler says.
|
||||
|
||||
The macros needed a predicate they did not have. ~form-items~ cannot tell ~()~
|
||||
from a symbol — it answers the empty slice for both — so [lib/prelude.ml] grew
|
||||
~form-empty-list?~, which matches ~Form.List~ and asks its length.
|
||||
|
||||
** (comment ...), built in
|
||||
A prelude ~defmacro~ answering ~(do)~ and reading none of its arguments, which
|
||||
is the whole feature: a macro's arguments are raw Form and are never checked as
|
||||
expressions, so what is inside never has to be a program. The pinned test puts
|
||||
an unknown function, a wrong arity, ~(+ 1 "two")~ and a field that does not
|
||||
exist inside one and compiles it.
|
||||
|
||||
The one rule it does obey is the reader's — balanced delimiters, legal tokens —
|
||||
because reading happens before any macro runs. ~#_~ is the other spelling and
|
||||
they are not rivals: ~#_~ is the reader's and discards the one form after it,
|
||||
so it works in argument position; this is a form of its own and takes any
|
||||
number, which is what a parked block wants.
|
||||
|
||||
** inc/dec, ++/--
|
||||
The four the note spells out, in the prelude rather than per project. A word
|
||||
for the pure pair, C's punctuation for the mutating pair, so ~(inc i)~ in an
|
||||
argument and ~(++ i)~ as a statement cannot be confused the way C's ~i++~ and
|
||||
~i+1~ can.
|
||||
|
||||
Generic for free, and verified rather than assumed: the pinned program runs
|
||||
~inc~ over i8, i16, i32, i64, u8, u16, u32, u64, f32, f64 and a dyn, and prints
|
||||
the answers. Nothing in the four macros mentions a type, because ~+~ and ~-~
|
||||
already work at all of them and a macro has no type to get in the way.
|
||||
|
||||
*The accepted tradeoff, documented at the definition:* ~(++ PLACE)~ expands to
|
||||
~(set PLACE (+ PLACE 1))~, so the place is read once and written once and is
|
||||
therefore *evaluated twice*. Free for a variable, a field or a deref. Not free
|
||||
for ~(at arr (next-index))~: ~next-index~ runs twice and the read and the write
|
||||
land on different elements. Not fixable here — macros are non-hygienic by
|
||||
decision, and a macro cannot bind a temporary for a *place* without a reference
|
||||
type the language does not have. rl/with-drawing and rl/with-mode-2d already
|
||||
take the same trade on their arguments.
|
||||
|
||||
The note's four macros have no arity guard, and they needed one: ~(inc)~ would
|
||||
have indexed past the end of its own argument slice and failed inside the
|
||||
compiler rather than saying anything about the program. Each guards on
|
||||
~(!= (len args) 1)~ — both too few and too many — and each is pinned.
|
||||
|
||||
** Type-limit constants
|
||||
[lib/prelude.ml] gained i8/i16/i32/i64 and u8/u16/u32/u64 max and min, and
|
||||
f32/f64 max, min-positive and epsilon. Kebab and the type's own name, following
|
||||
~ns-per-second~: ~i32-max~, not ~INT_MAX~. Each carries its type, so ~i32-max~
|
||||
where a u8 is wanted is a type error rather than a silent 255.
|
||||
|
||||
The u*-min constants are all zero and are all there. A family with a hole in it
|
||||
is worse than four lines that say nothing surprising.
|
||||
|
||||
There is no ~f32-min~, and the absence is the design. A float's least value is
|
||||
the negation of its greatest and needs no constant; what a caller reaching for
|
||||
"min" actually wants is the smallest positive one, which is a different number
|
||||
entirely. Naming either of them ~f32-min~ would put the collision at the worst
|
||||
possible place, so the name says which it is: ~f32-min-positive~, the smallest
|
||||
*normal* value, as Rust's MIN_POSITIVE does.
|
||||
|
||||
*u64-max is written in hex and has to be.* The reader parses a decimal integer
|
||||
through ~Int64.of_string~, and 18446744073709551615 does not fit one;
|
||||
~0xFFFFFFFFFFFFFFFF~ is read as the 64-bit pattern it names, which is what a
|
||||
u64 literal is here — [Check.in_range] accepts any pattern at 64 bits unsigned
|
||||
for exactly this reason. i64-min's decimal *does* fit, being i64's own least
|
||||
value, so it is written the ordinary way.
|
||||
|
||||
*Every value is pinned against an independent derivation, not against itself.*
|
||||
A wrong constant compiles — that is the whole hazard — so
|
||||
[test/programs/limits.flan] does not compare any constant to the way the
|
||||
prelude spells it. The integers are printed, and the expected text in
|
||||
test_acceptance is the decimal spelling written out from the definition of each
|
||||
type; an integer's decimal rendering is exact, so that comparison is the whole
|
||||
value. The floats cannot be pinned that way, because printing one is snprintf
|
||||
"%g" and 3.40282e+38 is equally true of f32-max and of a neighbourhood around
|
||||
it — so each is *derived* by exact power-of-two arithmetic and compared for
|
||||
equality. Every step of those derivations is exact in IEEE-754, and the two
|
||||
that are not powers of two have representable operands and a representable
|
||||
product:
|
||||
|
||||
| constant | derivation | bit pattern |
|
||||
|------------------+-------------------------------+--------------------|
|
||||
| f32-epsilon | 2^-23 | 0x34000000 |
|
||||
| f64-epsilon | 2^-52 | 0x3CB0000000000000 |
|
||||
| f32-min-positive | 2^-126 | 0x00800000 |
|
||||
| f64-min-positive | 2^-1022 | 0x0010000000000000 |
|
||||
| f32-max | (2 - 2^-23) * 2^127 | 0x7F7FFFFF |
|
||||
| f64-max | (2 - 2^-52) * 2^1023 | 0x7FEFFFFFFFFFFFFF |
|
||||
|
||||
and each epsilon additionally against the property its name promises — adding
|
||||
it to 1.0 moves, adding half of it does not — and each max against there being
|
||||
nothing finite above it, since doubling one overflows to an infinity.
|
||||
|
||||
The program runs on *both backends*, and that is not ceremony: materialising a
|
||||
full-width u64 immediate and an f64 bit pattern is a different job in LLVM and
|
||||
in the hand-written x86 backend, and a lowering that truncated one would print
|
||||
a number this row catches and nothing else in the suite does. Both print
|
||||
identical text.
|
||||
|
||||
*No infinity or NaN constant, and none is possible to write down.* The reader
|
||||
has no literal for either. ~(/ 1.0 0.0)~ is the only route to an infinity
|
||||
today, and under the defconst-is-const rule decided the same day it is not one
|
||||
a defconst can take: the folding pass is integers only, so a float division is
|
||||
a computed initialiser and refused by name. So an ~f64-infinity~ defconst is
|
||||
not available without either a reader literal or a second folder, and neither
|
||||
is this lane's. Recorded, not added. The *runtime* test for one is in the
|
||||
prelude already and limits.flan reuses it: an infinity is the value that equals
|
||||
its own double and is not zero.
|
||||
|
||||
** {.row .col} — and the collision the note said was not there
|
||||
DISCUSS.org: "No obvious grammar collision — nothing currently matches a bare
|
||||
.field symbol on its own." *That is false*, and it was worth checking before
|
||||
relying on it. [dmap]'s pair arm takes any pattern in head position, and
|
||||
[destructure]'s first arm accepts any ~Sym~ as a name — a dotted one included.
|
||||
So before this change:
|
||||
|
||||
- =(let [{.x .y} p] ...)= parsed, as "bind a local called ~.x~ to field ~y~",
|
||||
and the program failed later with "unknown name x" pointing at the *use*.
|
||||
Verified against the compiler, not reasoned about.
|
||||
- an odd number of bare fields hit the ~[odd]~ arm and was refused, which is
|
||||
where test_flan's =rejects_check "a field name with no pattern before it"=
|
||||
came from.
|
||||
|
||||
So the dot in head position did have a meaning; it was just never a useful one.
|
||||
The new arm is checked *before* the pair arm and takes both readings away. The
|
||||
~[odd]~ arm survives for the case it was actually written for — a plain name
|
||||
with nothing after it, ~{a}~ — and that test is now two: the old one inverted
|
||||
to an ~accepts~, and a new one on ~{a}~.
|
||||
|
||||
Where it works: ~let~, and nowhere else, which is where ~{name .field}~ works
|
||||
today. Destructuring binds in ~let~ only — a defn parameter, an fn parameter, a
|
||||
dotimes counter and *a match arm's binds* all take a plain name and are refused
|
||||
by [no_pattern] — so "let and match positions" from the brief has only one half
|
||||
to satisfy. The shorthand inherits that rule rather than changing it.
|
||||
|
||||
An unknown field gets the named form's refusal unchanged, because it is the
|
||||
same field access underneath: "Point has no field z" with the declared_note
|
||||
listing the fields there are.
|
||||
|
||||
** Pinned
|
||||
- test_flan.ml: ~(when c)~ parses to if + empty do; ~(when)~ still refused;
|
||||
~(fn [])~ parses with an empty body; ~(fn)~ still refused; a defn returning
|
||||
() with no body accepted and one returning i32 refused; an fn with no body
|
||||
accepted at a ~(Fn [] ())~ want and refused at a ~(Fn [] i32)~ one; the
|
||||
~{.x .y}~ shorthand accepted plain, mixed with a pair, and nested; ~{.z}~
|
||||
refused by field name; ~{.x}~ inverted from a refusal to an ~accepts~; ~{a}~
|
||||
refused.
|
||||
- test/programs/prelude-macros.flan, plain and -O0: ~comment~ with four
|
||||
different kinds of garbage in it; inc/dec over eleven types; ++/-- over a
|
||||
local, a field, an element and a deref; empty ~when~ and ~unless~ bodies.
|
||||
- test/programs/limits.flan, plain, -O0 and --x86: every constant, as above.
|
||||
- test/programs/destructure.flan gained ~shorthand~ and ~shorthand-mixed~
|
||||
rows, so the shorthand is in the program that is the destructuring test.
|
||||
- test_acceptance.ml: the arity guard of every new macro, by the name it
|
||||
answers, plus ~unless~'s narrowed one.
|
||||
|
||||
** Note for the concurrent lanes
|
||||
The diagnostics lane owns check.ml's message strings. This lane added *one* new
|
||||
message at a *new* site — check_fn's empty-body refusal — and rewrote none. The
|
||||
parse.ml edits are structural: a dropped guard in ~when~, a dropped guard in
|
||||
~fn~, a new arm at the top of ~dmap~. Expect a rebase, not a conflict of
|
||||
intent.
|
||||
|
||||
11
lib/check.ml
11
lib/check.ml
@ -2938,6 +2938,17 @@ and check_fn ctx ~want loc (params : string list) body =
|
||||
answer, and it has to be the declared return type. *)
|
||||
let fbody =
|
||||
match List.rev fbody with
|
||||
(* An fn with no body answers unit, the same as a defn whose declared
|
||||
return type is () and whose body is empty. Unlike a defn it declares no
|
||||
return type of its own, so there is nothing here to contradict — but
|
||||
the *position* names one, and a position wanting a value is the case
|
||||
[Check] has to refuse. Without this the empty body would simply fall
|
||||
through and the call would read a return value nothing ever wrote. *)
|
||||
| [] when not (Types.equal ret Types.Unit) ->
|
||||
fail loc
|
||||
"an fn with no body answers (), and this one is in a position that \
|
||||
wants %s — write the value it should answer"
|
||||
(Types.to_string ret)
|
||||
| [] -> fbody
|
||||
| last :: rest ->
|
||||
List.rev (expect fctx last.Tast.loc ~want:(Some ret) last :: rest)
|
||||
|
||||
@ -438,9 +438,16 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
|
||||
(* ── binding and control: never a call ─────────────────────────── *)
|
||||
(* A form that binds a name or alters control flow cannot fall through to
|
||||
Call — it would parse cleanly and mean the wrong thing, silently. *)
|
||||
(* An empty body is allowed here too, and means the same as it does in
|
||||
[when] and in a [defn]: the body is [Do []] and the function answers
|
||||
unit. A [defn] can only have one when its declared return type is (),
|
||||
because there is a type written down to contradict — [Check] refuses
|
||||
"returns i32 but has no body". An [fn] declares nothing, so an empty body
|
||||
is not in conflict with anything: it makes the answer type unit rather
|
||||
than failing to produce a value of some other one. *)
|
||||
| Sym "fn" ->
|
||||
(match args with
|
||||
| { v = Vec ps; _ } :: body when body <> [] ->
|
||||
| { v = Vec ps; _ } :: body ->
|
||||
List.iter no_pattern ps;
|
||||
mk (Ast.Fn (List.map sym ps, body_of body))
|
||||
| _ -> fail f "fn is (fn [param ...] body ...)")
|
||||
|
||||
@ -3484,6 +3484,42 @@ level "1"
|
||||
print_endline "FAIL the report does not say which macro"
|
||||
end);
|
||||
|
||||
(* Every prelude macro's arity guard, said the only way a macro can say
|
||||
anything: a call to a name nothing defines, reported at the call site.
|
||||
The point of pinning these is that without a guard the macro would
|
||||
index past the end of its own argument slice, and the failure would be
|
||||
a bounds trap inside the compiler rather than a message about the
|
||||
program. Each row therefore asserts the *name*, which is the sentence
|
||||
the author actually reads. *)
|
||||
let macro_arity name src needle =
|
||||
match
|
||||
Check.program (Parse.program (Reader.read_all ~file:"<arity>" src))
|
||||
with
|
||||
| _ ->
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s\n it was accepted\n" name
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
if not (contains m needle) then begin
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s\n said: %S\n wanted: %S in it\n"
|
||||
name m needle
|
||||
end
|
||||
in
|
||||
macro_arity "inc with no argument"
|
||||
"(defn main [] i32 (inc))" "inc-takes-one-number";
|
||||
macro_arity "inc with two arguments"
|
||||
"(defn main [] i32 (inc 1 2))" "inc-takes-one-number";
|
||||
macro_arity "dec with no argument"
|
||||
"(defn main [] i32 (dec))" "dec-takes-one-number";
|
||||
macro_arity "++ with no argument"
|
||||
"(defn main [] i32 (++) 0)" "++-takes-one-place";
|
||||
macro_arity "-- with two arguments"
|
||||
"(defn main [] i32 (let [a 1 b 2] (-- a b)) 0)" "---takes-one-place";
|
||||
(* unless keeps a guard, and it is now the narrower one: a body may be
|
||||
missing, a test may not. *)
|
||||
macro_arity "unless with no test at all"
|
||||
"(defn main [] i32 (unless) 0)" "unless-takes-a-test";
|
||||
|
||||
(* The two ways expansion does not terminate, and they are different
|
||||
failures. A ring is a compile-order problem -- each body calls the other
|
||||
while the other is being compiled -- and there is no order, so it is
|
||||
|
||||
@ -314,6 +314,34 @@ let () =
|
||||
| If (_, { e = Do [ _; _ ]; _ }, None) -> ()
|
||||
| _ -> check "when -> if+do" false);
|
||||
|
||||
(* An empty body is the same [Do []] that [(do)] already is, and not a
|
||||
refusal. (when test) is a guard whose consequent has not been written yet
|
||||
-- a state a program passes through while it is being written -- and
|
||||
refusing it bought nothing that the empty [do] does not already allow.
|
||||
[unless] in the prelude took the same change; it is a macro now, so it is
|
||||
asserted in programs/prelude-macros.flan instead of here. *)
|
||||
(match (parse1 "(when c)").e with
|
||||
| If (_, { e = Do []; _ }, None) -> ()
|
||||
| _ -> check "(when test) with no body -> if+(do)" false);
|
||||
|
||||
(* The test is still required, because there is nothing to branch on
|
||||
without one. *)
|
||||
parse_rejects "when with no test at all" "(defn f [] () (when))"
|
||||
~needle:"when is (when test body ...)";
|
||||
|
||||
(* The same rule for a function with nothing in it. A [defn] whose declared
|
||||
return type is () and whose body is empty has always been legal -- there
|
||||
is a unit to answer and no forms needed to reach it -- and [Check] refuses
|
||||
the case where the declaration disagrees, "returns i32 but has no body".
|
||||
An [fn] now parses the same way; it declares no return type, so the
|
||||
position it sits in is what decides, and the two rows below check.ml's
|
||||
arms are in the checker section further down. *)
|
||||
(match (parse1 "(fn [])").e with
|
||||
| Fn ([], []) -> ()
|
||||
| _ -> check "(fn []) parses with an empty body" false);
|
||||
parse_rejects "fn with no parameter vector" "(defn f [] () (fn))"
|
||||
~needle:"fn is (fn [param ...] body ...)";
|
||||
|
||||
(* unless was here, and is not any more: it is a defmacro in the prelude,
|
||||
and the parser has nothing to say about it. What it expands to is the
|
||||
same if-over-(not) this used to assert, and it is asserted where it can
|
||||
@ -2549,6 +2577,28 @@ let () =
|
||||
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [] 2)]))")
|
||||
~needle:"a handler-case clause is (Type [name] body ...)";
|
||||
|
||||
(* ── A function with nothing in it ─────────────────────────────── *)
|
||||
|
||||
(* The empty body, the other half of (when test) with no body: a function
|
||||
that does nothing is a function, and the only question is whether it has
|
||||
a value to answer. A declared () says it does not and the body may be
|
||||
empty; a declared anything else says it does, and an empty body cannot
|
||||
provide one. *)
|
||||
accepts "a defn returning () with no body"
|
||||
"(defn nothing [n i32] ())\n(defn f [] i32 (nothing 1) 0)";
|
||||
rejects_check "a defn returning a value with no body"
|
||||
"(defn nothing [n i32] i32)"
|
||||
~needle:"returns i32 but has no body";
|
||||
|
||||
(* An fn declares no return type, so the position decides instead. Both arms
|
||||
are asserted, because the refusal is the one that would otherwise let a
|
||||
call read a return value nothing wrote. *)
|
||||
accepts "an fn with no body where a (Fn [] ()) is wanted"
|
||||
"(defn call [f (Fn [] ())] () (f))\n(defn f [] i32 (call (fn [])) 0)";
|
||||
rejects_check "an fn with no body where a value is wanted"
|
||||
"(defn call [f (Fn [] i32)] i32 (f))\n(defn f [] i32 (call (fn [])))"
|
||||
~needle:"an fn with no body answers ()";
|
||||
|
||||
(* ── Destructuring ─────────────────────────────────────────────── *)
|
||||
|
||||
(* A pattern is desugared in [Parse] into the bindings and field accesses that
|
||||
@ -2574,6 +2624,35 @@ let () =
|
||||
(pt ^ "(defn mk [] Point (Point {.x 1 .y 2}))\n\
|
||||
(defn f [] i32 (let [{:keys [x y]} (mk)] (+ x y)))");
|
||||
|
||||
(* The shorthand: a lone .field binds a local of the field's own name. It is
|
||||
:keys said in the spelling the rest of the language uses for a field, and
|
||||
it mixes with the pair form in one brace because the two are told apart
|
||||
one item at a time -- a dot in head position is the shorthand, anything
|
||||
else is a pattern expecting its .field next. *)
|
||||
accepts "struct pattern with the .field shorthand"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{.x .y} p] (+ x y)))");
|
||||
accepts "the shorthand mixed with a pair in one brace"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{.x b .y} p] (+ x b)))");
|
||||
accepts "the shorthand inside a nested pattern"
|
||||
(line ^ "(defn f [l Line] i32 (let [{{.x .y} .a} l] (+ x y)))");
|
||||
(* The shorthand names a field, so an unknown one is the same refusal the
|
||||
named form gets -- it is the same field access underneath. *)
|
||||
rejects_check "the shorthand naming a field the struct does not have"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{.z} p] 0))")
|
||||
~needle:"Point has no field z";
|
||||
(* This used to be "{.x} has no .field": a lone dotted symbol was read as a
|
||||
name to bind and the brace then wanted a field after it. An even number
|
||||
of them was worse than a refusal -- {.x .y} parsed as "bind a local
|
||||
called .x to field y" and the program failed later with "unknown name x",
|
||||
several lines from the mistake. Both readings are gone. *)
|
||||
accepts "a lone .field is the shorthand and not a missing pair"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{.x} p] x))");
|
||||
(* And the arm that refusal came from is still there for the case it was
|
||||
written for: a plain name with nothing after it. *)
|
||||
rejects_check "a name with no field after it"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{a} p] 0))")
|
||||
~needle:"has no .field";
|
||||
|
||||
rejects_check "a field the struct does not have"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{:keys [x z]} p] (+ x z)))")
|
||||
~needle:"Point has no field z";
|
||||
@ -2586,9 +2665,6 @@ let () =
|
||||
rejects_check "an empty struct pattern"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{} p] 0))")
|
||||
~needle:"an empty struct pattern {} binds nothing";
|
||||
rejects_check "a field name with no pattern before it"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{.x} p] 0))")
|
||||
~needle:"has no .field";
|
||||
rejects_check "a pattern with no field name after it"
|
||||
(pt ^ "(defn f [p Point] i32 (let [{a b} p] 0))")
|
||||
~needle:"expected .field after a";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user