Two struct spellings the parser was never able to decide
A bare {.field v} had its refusal in Parse.expr, before any checking, so a
defn whose return type was the only place the struct's name appeared could
not build one. The refusal moves to Check: Parse builds an Ast.Bare out of
the same struct_fields the named form uses, and check_bare reads the type
name off the expectation and hands that very list to check_struct. ZII, the
unknown-field refusal and the duplicate-field refusal are therefore not
copies of the named form's rules but the named form's rules.
Braces at a dyn want are the dyn map literal and stay exactly that. A
.field-keyed brace was never part of that spelling, and at a dyn want it is
refused by name rather than given a second meaning.
(Cell 1 2) is the other half, and it is character-for-character an ordinary
call, so only the symbol table separates them. It is decided on the last arm
of named_call, after a local of function type, a generic and the function
table -- so a defclass constructor, which is a real defn, resolves above it
and is untouched. Arity is exact: ZII is what the braces do, and a positional
list cannot say which field it left out, so it is not allowed to leave one
out. The refusal names the first field it did not reach and points at the
spelling that does mean "zero the rest".
Both are gone before any backend sees them -- Tast.Make either way -- and the
three acceptance rows print the same lines to say so.
This commit is contained in:
parent
dc39631db7
commit
c1b18cb181
135
FIX.org
135
FIX.org
@ -1598,3 +1598,138 @@ because the code is the same.
|
|||||||
Not fixed here, deliberately: the fix is to catch ~Closed~ in that poll and
|
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
|
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.
|
mean and belongs to whoever owns them. Flagged rather than patched.
|
||||||
|
|
||||||
|
* Two struct spellings, 2026-09-20
|
||||||
|
Both were DISCUSS.org items, both diagnosed there as the same parse/check
|
||||||
|
boundary problem, and both are decided by the author on 2026-09-20.
|
||||||
|
|
||||||
|
** A. A bare ~{.field v}~ takes its type from the position it stands in
|
||||||
|
The note's own diagnosis was right: the refusal ("a bare map is not an
|
||||||
|
expression; write (Type {.field v})") sat in ~Parse.expr~, before any
|
||||||
|
checking, so ~(defn get-mouse-cell [] Cell ... {.row r .col c})~ could not
|
||||||
|
work no matter what the checker knew. It has moved.
|
||||||
|
|
||||||
|
Parse now builds ~Ast.Bare~ — a field list with no name — out of the same
|
||||||
|
~struct_fields~ the named form uses, so the two field lists are parsed by one
|
||||||
|
function and cannot drift. ~Check.check_bare~ reads the type name off the
|
||||||
|
expectation and hands that same list to ~check_struct~. That is the whole
|
||||||
|
feature: ZII for an omitted field, the unknown-field refusal, the
|
||||||
|
duplicate-field refusal, their notes and their error kinds are not "the same
|
||||||
|
as" the named form's, they *are* the named form's, reached by the same call.
|
||||||
|
|
||||||
|
*Accepted* — every position that carries a want:
|
||||||
|
- a defn's return position (the case from the notes),
|
||||||
|
- an argument of a call, the only argument or a later one,
|
||||||
|
- a field of an enclosing literal, at any depth,
|
||||||
|
- a typed place being ~set~, a local or a field of one,
|
||||||
|
- a union want, which reaches ~check_union~ by the same route.
|
||||||
|
|
||||||
|
*Refused, at checking, by name*:
|
||||||
|
- no want at all (a ~let~ binding, a body form that is not the last): "does
|
||||||
|
not say which struct it builds — the fields alone do not name a type",
|
||||||
|
naming both ways out, and saying why a let binding is not one of them (a
|
||||||
|
local takes its type from its value).
|
||||||
|
- a ~dyn~ want: refused, and told that a dyn map's keys are keywords. This is
|
||||||
|
the boundary that mattered most. Braces at a dyn want are the dyn map
|
||||||
|
literal and stay exactly that; a ~.field~-keyed brace was never part of
|
||||||
|
that spelling and is not being quietly given a second meaning now.
|
||||||
|
- a want that is not a struct type at all: "i32 is expected here, which is
|
||||||
|
not a struct type".
|
||||||
|
- a data type's name as the want: inherits the existing message, which names
|
||||||
|
the cases — ~D~ is not specific enough, a value of ~D~ is one of its cases.
|
||||||
|
|
||||||
|
One thing had to move in ~Check~ as well as in ~Parse~. ~(g {.row 2})~ is
|
||||||
|
parsed as a struct literal named ~g~, because the parser's struct-literal arm
|
||||||
|
fires on the *shape* of the single argument and has no table to consult; it
|
||||||
|
used to be refused with "g is a function, not a struct". Now, when the head is
|
||||||
|
a name that would actually resolve to a callee (a defn, a generic, or a local
|
||||||
|
of function type — ~callable~), it is handed back to ~named_call~ as the call
|
||||||
|
it was written as, with the fields rebuilt into the ~Ast.Bare~ node the parser
|
||||||
|
would have made anywhere else. An unknown name keeps the old "unknown struct"
|
||||||
|
report, because that shape is usually a misspelled struct name. ~(name {})~
|
||||||
|
keeps its old refusal untouched: the empty braces are genuinely ambiguous
|
||||||
|
between the zero-field struct literal and the empty dyn map, and that one does
|
||||||
|
have the let-binding fix its message already names.
|
||||||
|
|
||||||
|
** B. ~(Cell 1 2)~ positional, and arity is exact
|
||||||
|
The note's diagnosis again: the parser cannot tell ~(Cell 1 2)~ from any other
|
||||||
|
call, so ~Check~ does it. The decision sits on the last arm of ~named_call~ —
|
||||||
|
after a local of function type, after a generic, after the global function
|
||||||
|
table — where the old "Cell is a type" refusal used to be.
|
||||||
|
|
||||||
|
*No collision is possible.* ~collect~'s ~claimed~ table spans every
|
||||||
|
declaration kind, so one name is one declaration and a ~defstruct Cell~ beside
|
||||||
|
a ~defn Cell~ is "Cell is defined twice" before any of this is reached. A
|
||||||
|
~(defclass point [x y])~ constructor is a real ~defn~ that ~Classes.expand~
|
||||||
|
wrote before checking began, so ~(point 1 2)~ resolves in ~env.fns~ two arms
|
||||||
|
above the struct one and never reaches it. Pinned both ways.
|
||||||
|
|
||||||
|
*** The arity decision: exact, no partial ZII
|
||||||
|
Positional construction gives every field or it is refused. This is not a
|
||||||
|
retreat from ZII — ZII is what the designated form does, and ~(Cell {.row 1})~
|
||||||
|
still zeroes ~.col~, which is where the message points. The reason is that a
|
||||||
|
positional list cannot *say* which field it left out. ~(Cell 1)~ reads as a
|
||||||
|
Cell with one field given, and which field that is depends on a declaration
|
||||||
|
order the author is free to change later; a trailing field silently zeroed
|
||||||
|
there is the field-reorder hazard at its very worst, arriving as a wrong value
|
||||||
|
rather than as an error. So a short list is a refusal that names the first
|
||||||
|
field it did not reach:
|
||||||
|
|
||||||
|
: Cell has 2 fields and 1 was given positionally — .col has no value.
|
||||||
|
: Positional construction gives every field, in declaration order; to give
|
||||||
|
: some of them and zero the rest, a struct value is written
|
||||||
|
: (Cell {.field value ...})
|
||||||
|
|
||||||
|
with ~declared_note~ pointing at the declaration. A long list points at the
|
||||||
|
first extra argument and shows both spellings. Odin's positional literal takes
|
||||||
|
the same line, and the repo's ZII philosophy is not against this: ZII is about
|
||||||
|
what an *omitted* field means, and this refusal is about a spelling that
|
||||||
|
cannot express omission at all.
|
||||||
|
|
||||||
|
*** The field-reorder hazard, accepted
|
||||||
|
The author's words, on B's remaining cost: "B can be fixed with refactorings
|
||||||
|
later on when we decide to add it." Reordering a ~defstruct~'s fields silently
|
||||||
|
changes what every positional call site builds, and nothing in the compiler
|
||||||
|
catches it when the types happen to line up. Accepted as the price, with
|
||||||
|
refactoring tooling named as the eventual answer rather than a compiler rule.
|
||||||
|
|
||||||
|
*** Argument type errors
|
||||||
|
Each argument is checked against its own field's type by ~map2_lr~ and
|
||||||
|
~~want~~, exactly as a call's arguments are checked against its parameters —
|
||||||
|
so the mismatch is reported at the argument, in the words a call's argument
|
||||||
|
already gets ("expected f32, found string"). What is added is a *note*,
|
||||||
|
"this is Cell's field .col", plus ~declared_note~, because a positional call
|
||||||
|
site is the one place the source does not show the field name. The note is
|
||||||
|
attached only to a diagnostic raised at that argument's own location, and it
|
||||||
|
only ever names the position — which is true whatever went wrong there — so a
|
||||||
|
nested failure inside the argument cannot be miscaptioned by it. Naming the
|
||||||
|
field *in the message* would need an error-context helper ~loc.ml~ does not
|
||||||
|
have; not built here, since the note carries the same information and adding
|
||||||
|
the helper touches a file the Elm-messages lane is in.
|
||||||
|
|
||||||
|
** Backends
|
||||||
|
Zero edits, and nothing to edit. Both features are gone by the time ~Check~
|
||||||
|
finishes: a bare literal becomes the ~Tast.Make~ the named form already built,
|
||||||
|
and a positional call becomes that same node with its arguments put into
|
||||||
|
declaration order. ~Emit~ and ~X86~ have no case for either. The three
|
||||||
|
acceptance rows for ~test/programs/struct-ergonomics.flan~ — default, ~-O0~,
|
||||||
|
~--x86~ — print the same fifteen lines, which is what says so out loud.
|
||||||
|
|
||||||
|
** One existing test's needle had to change, and one did not
|
||||||
|
~test_flan.ml~'s two "bare struct-shaped braces still refuse" rows refused at
|
||||||
|
*parse* and now refuse at *checking*, for a different and better reason; their
|
||||||
|
needles and the comment above them were rewritten together. The row for
|
||||||
|
~(Cursor {:src s})~ did not have to move: that form is now a Cursor built from
|
||||||
|
too few arguments, and the new refusal still contains "a struct value is
|
||||||
|
written (Cursor {.field value ...})", which is what its needle asks for. Its
|
||||||
|
comment was corrected anyway, since the *reason* it refuses changed.
|
||||||
|
|
||||||
|
** What was run
|
||||||
|
~dune test --root .~ in the lane's worktree: exit 0, no FAIL lines. The first
|
||||||
|
run exited 1 with ~Fatal error: exception Flan.Wire.Closed~ and no FAIL line
|
||||||
|
anywhere — the known ~test_dev.ml~ ~trap_park~ race recorded under the classes
|
||||||
|
lane above, not this lane's; the rerun was clean. The survey program was run
|
||||||
|
by hand at all three settings and its output diffed across them before the
|
||||||
|
acceptance rows were written. Not added to ~test_sanitize.ml~: the program
|
||||||
|
allocates one small dyn map and nothing else, so there is nothing for ASan to
|
||||||
|
find that ~dyn-map.flan~ does not already exercise.
|
||||||
|
|||||||
@ -66,6 +66,14 @@ and expr_kind =
|
|||||||
| Call of expr * expr list
|
| Call of expr * expr list
|
||||||
| Match of expr * arm list
|
| Match of expr * arm list
|
||||||
| Struct of string * (string * expr) list (* (Cursor {.src s}) *)
|
| Struct of string * (string * expr) list (* (Cursor {.src s}) *)
|
||||||
|
(* {.src s .pos 0} with no type written in front of it. The fields alone do
|
||||||
|
not name a type, so this node carries no name and is only checkable where
|
||||||
|
the checker already has an expectation to read one off — a defn's return
|
||||||
|
position, a typed argument, a field of an enclosing literal, a typed
|
||||||
|
place. [Check] refuses it everywhere else. The parser cannot make this
|
||||||
|
decision: it has no symbol table and no expectation, which is why the
|
||||||
|
refusal that used to live in [Parse.expr] moved. *)
|
||||||
|
| Bare of (string * expr) list (* {.src s} *)
|
||||||
(* {:a 1 :b s} — a dyn map literal. Braces whose first form is not a
|
(* {:a 1 :b s} — a dyn map literal. Braces whose first form is not a
|
||||||
[.field] symbol are this; the struct spelling keeps the dot. Keys are
|
[.field] symbol are this; the struct spelling keeps the dot. Keys are
|
||||||
ordinary expressions, keywords being the common case. *)
|
ordinary expressions, keywords being the common case. *)
|
||||||
@ -357,6 +365,7 @@ let map_children f (e : expr) : expr =
|
|||||||
| Call (fn, args) -> Call (ex fn, List.map ex args)
|
| Call (fn, args) -> Call (ex fn, List.map ex args)
|
||||||
| Match (s, arms) -> Match (ex s, List.map arm arms)
|
| Match (s, arms) -> Match (ex s, List.map arm arms)
|
||||||
| Struct (n, fs) -> Struct (n, List.map (fun (n, v) -> (n, ex v)) fs)
|
| Struct (n, fs) -> Struct (n, List.map (fun (n, v) -> (n, ex v)) fs)
|
||||||
|
| Bare fs -> Bare (List.map (fun (n, v) -> (n, ex v)) fs)
|
||||||
| MapLit (tag, kvs) -> MapLit (tag, List.map (fun (k, v) -> (ex k, ex v)) kvs)
|
| MapLit (tag, kvs) -> MapLit (tag, List.map (fun (k, v) -> (ex k, ex v)) kvs)
|
||||||
| Arr es -> Arr (List.map ex es)
|
| Arr es -> Arr (List.map ex es)
|
||||||
| Fn (ps, es) -> Fn (ps, List.map ex es)
|
| Fn (ps, es) -> Fn (ps, List.map ex es)
|
||||||
|
|||||||
167
lib/check.ml
167
lib/check.ml
@ -2548,6 +2548,7 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
|
|||||||
let fty = (List.nth s.Tast.fields i).Tast.fty in
|
let fty = (List.nth s.Tast.fields i).Tast.fty in
|
||||||
expect ctx loc ~want (mk loc fty (Tast.Field (target, i))))
|
expect ctx loc ~want (mk loc fty (Tast.Field (target, i))))
|
||||||
| Ast.Struct (name, kvs) -> check_struct ctx ~want loc name kvs
|
| Ast.Struct (name, kvs) -> check_struct ctx ~want loc name kvs
|
||||||
|
| Ast.Bare kvs -> check_bare ctx ~want loc kvs
|
||||||
(* A bracket literal where a dyn is wanted is the runtime's own vec, built
|
(* A bracket literal where a dyn is wanted is the runtime's own vec, built
|
||||||
where it stands — the same lowering the map literal gets, and what makes
|
where it stands — the same lowering the map literal gets, and what makes
|
||||||
{:xs [1 2]} mean what it reads as. Everywhere else brackets stay the
|
{:xs [1 2]} mean what it reads as. Everywhere else brackets stay the
|
||||||
@ -3746,6 +3747,138 @@ and check_if ctx ?(tail = false) ?want loc c t e =
|
|||||||
in
|
in
|
||||||
mk loc ty (Tast.If (c, t, e))
|
mk loc ty (Tast.If (c, t, e))
|
||||||
|
|
||||||
|
(* Whether a name would reach a callee if it were called — a global function, a
|
||||||
|
generic, or a local holding a function value. The three sources [named_call]
|
||||||
|
itself consults, in its own order; builtins are deliberately not among them,
|
||||||
|
so that [(println {.f v})] still reports what it reports today. *)
|
||||||
|
and callable ctx name =
|
||||||
|
Hashtbl.mem ctx.env.fns name
|
||||||
|
|| Hashtbl.mem ctx.env.gsigs name
|
||||||
|
|| (match lookup ctx name with
|
||||||
|
| Some b -> (match b.bty with Types.Fn _ -> true | _ -> false)
|
||||||
|
| None -> false)
|
||||||
|
|
||||||
|
(* [(Cell 1 2)] — a struct built from its fields in declaration order.
|
||||||
|
|
||||||
|
The parser cannot make this one either, and for a sharper reason than the
|
||||||
|
bare literal above: [(Cell 1 2)] is character-for-character an ordinary call
|
||||||
|
and only the symbol table tells the two apart. So it is decided here, on the
|
||||||
|
last arm of [named_call], which is to say *after* a local of function type,
|
||||||
|
after a generic and after the global function table. Nothing can be shadowed
|
||||||
|
into a struct constructor by accident, because a name is one declaration:
|
||||||
|
[collect]'s [claimed] table spans every declaration kind, so a [defstruct
|
||||||
|
Cell] and a [defn Cell] cannot both exist. A [(defclass point [x y])]
|
||||||
|
constructor is a real [defn] that [Classes.expand] wrote before checking
|
||||||
|
began, so [(point 1 2)] resolves in [env.fns] two arms above this one and
|
||||||
|
never reaches here.
|
||||||
|
|
||||||
|
ARITY IS EXACT, and that is the decision worth writing down. ZII is not
|
||||||
|
withdrawn — it is what the designated form does, and [(Cell {.row 1})]
|
||||||
|
still zeroes [.col]. What positional construction cannot do is *say* which
|
||||||
|
field was left out: [(Cell 1)] reads as a Cell with one field given, and
|
||||||
|
which one depends on a declaration order that the author is free to change
|
||||||
|
later. A trailing field silently zeroed there is the field-reorder hazard
|
||||||
|
at its worst, so a short argument list is a refusal that names the first
|
||||||
|
field it did not reach, and points at the spelling that does mean "zero the
|
||||||
|
rest". Odin's positional literal takes the same line. *)
|
||||||
|
and positional_struct ctx ~want loc name args =
|
||||||
|
let s = Hashtbl.find ctx.env.structs name in
|
||||||
|
let fields = s.Tast.fields in
|
||||||
|
let n = List.length fields in
|
||||||
|
let given = List.length args in
|
||||||
|
let note = declared_note ctx.env name in
|
||||||
|
if given < n then begin
|
||||||
|
let missing = List.nth fields given in
|
||||||
|
Loc.failk "check/positional-too-few" loc ~notes:note
|
||||||
|
"%s has %d field%s and %d %s given positionally — .%s has no value. \
|
||||||
|
Positional construction gives every field, in declaration order; to \
|
||||||
|
give some of them and zero the rest, a struct value is written (%s \
|
||||||
|
{.field value ...})"
|
||||||
|
name n (if n = 1 then "" else "s") given
|
||||||
|
(if given = 1 then "was" else "were") missing.Tast.fname name
|
||||||
|
end;
|
||||||
|
if given > n then begin
|
||||||
|
let extra = List.nth args n in
|
||||||
|
Loc.failk "check/positional-too-many" extra.Ast.loc ~notes:note
|
||||||
|
"%s has %d field%s, and this is argument %d — a struct value is written \
|
||||||
|
(%s {.field value ...}) or (%s %s)"
|
||||||
|
name n (if n = 1 then "" else "s") (n + 1) name name
|
||||||
|
(String.concat " " (List.map (fun (f : Tast.field) -> f.Tast.fname) fields))
|
||||||
|
end;
|
||||||
|
(* Left to right, each against its own field's type, exactly as the argument
|
||||||
|
list of a call is checked against its parameters — same [map2_lr], same
|
||||||
|
[~want], so an untyped literal takes the field's type and a nested bare
|
||||||
|
literal (feature A above) gets an expectation here as well. The mismatch
|
||||||
|
is reported at the argument, by [expect], in the words a call's argument
|
||||||
|
already gets; what is added is a note saying which field that argument
|
||||||
|
was, because at a positional call site the field name is the one thing
|
||||||
|
the source does not show. The note is attached only to a failure raised
|
||||||
|
at this argument's own location, and it says nothing about the failure —
|
||||||
|
it names the position, which is true whatever went wrong there. *)
|
||||||
|
let fields =
|
||||||
|
map2_lr
|
||||||
|
(fun (f : Tast.field) (a : Ast.expr) ->
|
||||||
|
try check ctx ~want:f.Tast.fty a with
|
||||||
|
| Loc.Error d when d.Loc.dloc = a.Ast.loc ->
|
||||||
|
Loc.raise_diag
|
||||||
|
{ d with
|
||||||
|
Loc.notes =
|
||||||
|
d.Loc.notes
|
||||||
|
@ [ Loc.note a.Ast.loc
|
||||||
|
(Printf.sprintf "this is %s's field .%s" name
|
||||||
|
f.Tast.fname) ]
|
||||||
|
@ note })
|
||||||
|
fields args
|
||||||
|
in
|
||||||
|
expect ctx loc ~want (mk loc (Types.Named name) (Tast.Make (name, fields)))
|
||||||
|
|
||||||
|
(* [{.f v}] with no type written in front of it, checked against whatever type
|
||||||
|
the position it stands in expects.
|
||||||
|
|
||||||
|
The whole of the feature is the one line that hands [kvs] to [check_struct]
|
||||||
|
with the expected type's name: from there a bare literal and a named one are
|
||||||
|
the same literal, checked by the same code. Duplicate fields, unknown
|
||||||
|
fields, their notes and their error kinds, and ZII zero-fill for the fields
|
||||||
|
left out are therefore not "the same rules" as the named form's — they are
|
||||||
|
the named form's, reached through the same call.
|
||||||
|
|
||||||
|
A named type is the only expectation that says anything. [Dyn] deliberately
|
||||||
|
does not: braces at a dyn want are the dyn map literal ({:key value}, and
|
||||||
|
[Ast.MapLit]) and always have been, and a [.field]-keyed brace was never
|
||||||
|
part of that spelling. So a dyn want is refused here rather than quietly
|
||||||
|
given a second meaning, and the message points at the map spelling because
|
||||||
|
that is what someone at a dyn want almost certainly wanted.
|
||||||
|
|
||||||
|
With no expectation at all there is nothing to infer from and the refusal
|
||||||
|
names both ways out: write the type, or move the literal somewhere a type
|
||||||
|
is known. *)
|
||||||
|
and check_bare ctx ~want loc kvs =
|
||||||
|
let written =
|
||||||
|
"{" ^ String.concat " " (List.map (fun (k, _) -> "." ^ k) kvs) ^ " ...}"
|
||||||
|
in
|
||||||
|
match want with
|
||||||
|
| Some (Types.Named n) -> check_struct ctx ~want loc n kvs
|
||||||
|
| Some Types.Dyn ->
|
||||||
|
Loc.failk "check/bare-struct-dyn" loc
|
||||||
|
"a dyn is expected here, and %s is a struct field list, not a dyn map — \
|
||||||
|
a dyn map's keys are keywords, as {:%s value ...}"
|
||||||
|
written
|
||||||
|
(match kvs with (k, _) :: _ -> k | [] -> "key")
|
||||||
|
| Some other ->
|
||||||
|
Loc.failk "check/bare-struct-want" loc
|
||||||
|
"%s is a struct field list and %s is expected here, which is not a \
|
||||||
|
struct type"
|
||||||
|
written (Types.to_string other)
|
||||||
|
| None ->
|
||||||
|
Loc.failk "check/bare-struct-untyped" loc
|
||||||
|
"%s does not say which struct it builds — the fields alone do not name \
|
||||||
|
a type. Write it, as (Type %s), or put the literal where a type is \
|
||||||
|
already known: a function's return position, an argument of a call, a \
|
||||||
|
field of another literal, or a typed place being set. A let binding is \
|
||||||
|
none of those — a local takes its type from its value, so there is \
|
||||||
|
nothing there to read one off"
|
||||||
|
written written
|
||||||
|
|
||||||
(* A record-shaped literal: one form for both, because [(Name {.f v})] is the
|
(* A record-shaped literal: one form for both, because [(Name {.f v})] is the
|
||||||
same syntax whether [Name] is a struct or a data type case, and the two differ
|
same syntax whether [Name] is a struct or a data type case, and the two differ
|
||||||
only in what is built at the end. Deciding here rather than in the parser is
|
only in what is built at the end. Deciding here rather than in the parser is
|
||||||
@ -3782,16 +3915,26 @@ and check_struct ctx ~want loc name kvs =
|
|||||||
size — a [.field]-first map is never a dyn map argument, only ever
|
size — a [.field]-first map is never a dyn map argument, only ever
|
||||||
this struct-literal shape, whatever [name] is. [(name {:a 1})] is
|
this struct-literal shape, whatever [name] is. [(name {:a 1})] is
|
||||||
the one that keeps [name] an ordinary call, because a
|
the one that keeps [name] an ordinary call, because a
|
||||||
keyword-keyed map is never mistaken for a struct literal. So both
|
keyword-keyed map is never mistaken for a struct literal.
|
||||||
shapes below get the helpful message, not just the empty one: if
|
|
||||||
[name] is a known function, "unknown struct" is the wrong report
|
The [.field]-keyed blind spot is no longer a blind spot, and that
|
||||||
either way, and the two shapes need different advice, because only
|
is the argument half of the bare-literal feature. [(g {.row 2})] is
|
||||||
one of them has a fix. An empty map can be bound to a variable and
|
a call to [g] whose one argument is a bare struct literal — the
|
||||||
passed as an ordinary dyn map argument, [(let [m {}] (name m))]. A
|
parameter is the expectation that names its type — and the only
|
||||||
[.field]-keyed one cannot — [expr] itself refuses a bare
|
reason it arrives here wearing a struct literal's clothes is that
|
||||||
[{.field v}] outside a struct-literal position ("a bare map is not
|
the parser had to guess and guessed by shape. So it is handed back
|
||||||
an expression"), so there is no let-binding that rescues it. The
|
to [named_call] as the call it was written as, with the fields
|
||||||
syntax was never a map's; a dyn map's keys are keywords. *)
|
rebuilt into the [Ast.Bare] node the parser would have made had the
|
||||||
|
braces stood anywhere else. Only for a name that is actually
|
||||||
|
callable: an unknown name keeps the "unknown struct" report below,
|
||||||
|
because a misspelled struct name is what that shape usually is.
|
||||||
|
|
||||||
|
The empty braces keep their old refusal, because they are still
|
||||||
|
genuinely ambiguous — [{}] is the zero-field struct literal AND the
|
||||||
|
empty dyn map, with nothing in the shape to separate them — and
|
||||||
|
that one does have the let-binding fix the message names. *)
|
||||||
|
else if kvs <> [] && callable ctx name then
|
||||||
|
named_call ctx ~want loc name [ { Ast.e = Ast.Bare kvs; loc } ]
|
||||||
else if Hashtbl.mem ctx.env.fns name then
|
else if Hashtbl.mem ctx.env.fns name then
|
||||||
(match kvs with
|
(match kvs with
|
||||||
| [] ->
|
| [] ->
|
||||||
@ -6381,9 +6524,7 @@ and named_call ctx ~want loc name args =
|
|||||||
or %s.%s on its own when it has no fields"
|
or %s.%s on its own when it has no fields"
|
||||||
name dname dname c.Tast.vname dname c.Tast.vname
|
name dname dname c.Tast.vname dname c.Tast.vname
|
||||||
else if Hashtbl.mem ctx.env.structs name then
|
else if Hashtbl.mem ctx.env.structs name then
|
||||||
fail loc
|
positional_struct ctx ~want loc name args
|
||||||
"%s is a type — a struct value is written (%s {.field value ...})"
|
|
||||||
name name
|
|
||||||
else if String.contains name '/' then
|
else if String.contains name '/' then
|
||||||
unimplemented loc
|
unimplemented loc
|
||||||
(Printf.sprintf "the call %s into an imported package" name) 4
|
(Printf.sprintf "the call %s into an imported package" name) 4
|
||||||
|
|||||||
10
lib/load.ml
10
lib/load.ml
@ -265,6 +265,11 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr =
|
|||||||
[ ("s", { v with Ast.e = Ast.Str (qualify_name owned alias bound s) }) ])
|
[ ("s", { v with Ast.e = Ast.Str (qualify_name owned alias bound s) }) ])
|
||||||
| Ast.Struct (n, kvs) ->
|
| Ast.Struct (n, kvs) ->
|
||||||
Ast.Struct (name n, List.map (fun (k, v) -> (k, go v)) kvs)
|
Ast.Struct (name n, List.map (fun (k, v) -> (k, go v)) kvs)
|
||||||
|
(* A bare field list has no type name of its own to qualify — that is what
|
||||||
|
makes it bare — so only its values are walked. The type it turns out to
|
||||||
|
be comes from the expectation at the point of use, and whatever wrote
|
||||||
|
that expectation was qualified where it stands. *)
|
||||||
|
| Ast.Bare kvs -> Ast.Bare (List.map (fun (k, v) -> (k, go v)) kvs)
|
||||||
(* A dyn map literal has no name of its own to qualify; its keys and
|
(* A dyn map literal has no name of its own to qualify; its keys and
|
||||||
values are ordinary expressions and are walked like anything else. *)
|
values are ordinary expressions and are walked like anything else. *)
|
||||||
| Ast.MapLit (tag, kvs) ->
|
| Ast.MapLit (tag, kvs) ->
|
||||||
@ -731,6 +736,11 @@ let rec expr_uses acc (e : Ast.expr) =
|
|||||||
| Ast.Struct (n, kvs) ->
|
| Ast.Struct (n, kvs) ->
|
||||||
acc := (n, e.Ast.loc) :: !acc;
|
acc := (n, e.Ast.loc) :: !acc;
|
||||||
List.iter (fun (_, v) -> go v) kvs
|
List.iter (fun (_, v) -> go v) kvs
|
||||||
|
(* No name to record: a bare field list mentions no type, so it makes no
|
||||||
|
type reachable on its own. The expectation that gives it one is written
|
||||||
|
somewhere that this walk already reaches — a signature, an annotation, or
|
||||||
|
an enclosing literal's own name. *)
|
||||||
|
| Ast.Bare kvs -> List.iter (fun (_, v) -> go v) kvs
|
||||||
| Ast.MapLit (_, kvs) -> List.iter (fun (k, v) -> go k; go v) kvs
|
| Ast.MapLit (_, kvs) -> List.iter (fun (k, v) -> go k; go v) kvs
|
||||||
| Ast.Arr items -> gos items
|
| Ast.Arr items -> gos items
|
||||||
| Ast.ArrayOf t -> texpr_uses acc t
|
| Ast.ArrayOf t -> texpr_uses acc t
|
||||||
|
|||||||
19
lib/parse.ml
19
lib/parse.ml
@ -313,13 +313,20 @@ let rec expr (f : Form.t) : Ast.expr =
|
|||||||
they are a slice or array type. Position disambiguates, as with {}. *)
|
they are a slice or array type. Position disambiguates, as with {}. *)
|
||||||
| Vec items -> mk (Ast.Arr (List.map expr items))
|
| Vec items -> mk (Ast.Arr (List.map expr items))
|
||||||
(* Braces in value position are two literals told apart by their first form.
|
(* Braces in value position are two literals told apart by their first form.
|
||||||
A [.field] symbol says struct, and a bare struct field list still needs
|
A [.field] symbol says struct; anything else, the empty braces included,
|
||||||
its type written — (Type {.field v}) — because the fields alone do not
|
is a dyn map literal: {:a 1 :b s}, keys and values alternating, each an
|
||||||
name one. Anything else, the empty braces included, is a dyn map literal:
|
ordinary expression.
|
||||||
{:a 1 :b s}, keys and values alternating, each an ordinary expression. *)
|
|
||||||
| Map ({ v = Sym s; _ } :: _)
|
A struct field list with no type in front of it is [Ast.Bare]. The fields
|
||||||
|
alone do not name a type, but the *position* often does — a defn's return
|
||||||
|
type, a typed parameter, a field of an enclosing literal — and only the
|
||||||
|
checker can see that. So this parses and [Check] decides: it reads the
|
||||||
|
type off the expectation where there is one, and refuses by name where
|
||||||
|
there is not. The refusal used to stand right here, which is why
|
||||||
|
[(defn f [] Cell {.row r .col c})] could not work at all. *)
|
||||||
|
| Map (({ v = Sym s; _ } :: _) as kvs)
|
||||||
when String.length s > 1 && s.[0] = '.' ->
|
when String.length s > 1 && s.[0] = '.' ->
|
||||||
fail f "a bare map is not an expression; write (Type {.field v})"
|
mk (Ast.Bare (struct_fields f kvs))
|
||||||
| Map items -> mk (Ast.MapLit (None, map_pairs f items))
|
| Map items -> mk (Ast.MapLit (None, map_pairs f items))
|
||||||
| List [] -> fail f "() is not an expression"
|
| List [] -> fail f "() is not an expression"
|
||||||
| List (head :: args) -> form f mk head args
|
| List (head :: args) -> form f mk head args
|
||||||
|
|||||||
70
test/programs/struct-ergonomics.flan
Normal file
70
test/programs/struct-ergonomics.flan
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
;;;; Two spellings a struct value gained, both decided by the checker.
|
||||||
|
;;;;
|
||||||
|
;;;; A bare {.field v} has no type written in front of it. The fields alone do
|
||||||
|
;;;; not name one, but the position usually does: a defn's return type, a
|
||||||
|
;;;; parameter, a field of an enclosing literal, a place being set. The parser
|
||||||
|
;;;; cannot see any of that -- it has no symbol table and no expectation -- so
|
||||||
|
;;;; it builds the field list and the checker reads the type off the want and
|
||||||
|
;;;; hands the very same list to the named form's own code. Everything below
|
||||||
|
;;;; that follows -- ZII for an omitted field, the unknown-field and
|
||||||
|
;;;; duplicate-field refusals -- is therefore not a copy of the named form's
|
||||||
|
;;;; rules, it is the named form's rules.
|
||||||
|
;;;;
|
||||||
|
;;;; (Cell 1 2) is the other half, and it is a call until the checker looks the
|
||||||
|
;;;; head up: a struct name where a function name would be. Arity is exact.
|
||||||
|
;;;; ZII is still available and is what the braces do; what a positional list
|
||||||
|
;;;; cannot do is SAY which field it left out, so it is not allowed to leave
|
||||||
|
;;;; one out.
|
||||||
|
|
||||||
|
(defstruct Cell [row i32 col i32])
|
||||||
|
(defstruct Grid [a Cell b Cell])
|
||||||
|
|
||||||
|
;; The case from the notes: a defn whose return type is the only place the
|
||||||
|
;; type is written. This is what used to fail at parse, before any checking.
|
||||||
|
(defn origin [] Cell {.row 0 .col 0})
|
||||||
|
|
||||||
|
;; An omitted field is zeroed, exactly as (Cell {.row 3}) zeroes .col.
|
||||||
|
(defn just-row [r i32] Cell {.row r})
|
||||||
|
|
||||||
|
;; A parameter is an expectation too -- as the only argument and as a later
|
||||||
|
;; one, which are two different paths into the checker and both arrive here.
|
||||||
|
(defn sum [c Cell] i32 (+ (.row c) (.col c)))
|
||||||
|
(defn offset-sum [n i32 c Cell] i32 (+ n (sum c)))
|
||||||
|
|
||||||
|
;; A field of an enclosing literal. The inner braces are bare and the outer
|
||||||
|
;; ones are not, so both readings stand side by side in one form.
|
||||||
|
(defn grid [] Grid (Grid {.a {.row 1 .col 2} .b (Cell 3 4)}))
|
||||||
|
|
||||||
|
;; Positional, in the position that names the type anyway.
|
||||||
|
(defn diag [n i32] Cell (Cell n n))
|
||||||
|
|
||||||
|
;; Nested positional, and positional feeding a bare literal's field.
|
||||||
|
(defn pair [] Grid (Grid {.a (Cell 5 6) .b (Cell 7 8)}))
|
||||||
|
|
||||||
|
(defn main [] ()
|
||||||
|
(println (sum (origin)))
|
||||||
|
(println (sum (just-row 9)))
|
||||||
|
(println (sum {.row 10 .col 20}))
|
||||||
|
(println (offset-sum 1 {.row 2}))
|
||||||
|
(let [g (grid)]
|
||||||
|
(println (.row (.a g)))
|
||||||
|
(println (.col (.a g)))
|
||||||
|
(println (.row (.b g)))
|
||||||
|
(println (.col (.b g))))
|
||||||
|
(println (sum (diag 11)))
|
||||||
|
(let [p (pair)]
|
||||||
|
(println (sum (.a p)))
|
||||||
|
(println (sum (.b p))))
|
||||||
|
;; A bare literal set into a typed place: the place's type is the want.
|
||||||
|
(let [c (Cell 0 0)]
|
||||||
|
(set c {.row 7 .col 8})
|
||||||
|
(println (sum c))
|
||||||
|
;; And into a field, whose type is the want one level down.
|
||||||
|
(let [g (Grid {.a c .b c})]
|
||||||
|
(set (.b g) {.row 100})
|
||||||
|
(println (sum (.b g)))))
|
||||||
|
;; The dyn map literal is untouched by any of this: keyword keys, and a
|
||||||
|
;; .field-keyed brace was never part of that spelling.
|
||||||
|
(let [m {:a 1 :b 2}]
|
||||||
|
(println (len m))
|
||||||
|
(println (get m :b))))
|
||||||
@ -3650,6 +3650,25 @@ level "1"
|
|||||||
"programs/dyn-struct.flan" dyn_struct_out;
|
"programs/dyn-struct.flan" dyn_struct_out;
|
||||||
outputs ~x86:true "dyn: a struct with dyn fields, under collection, --x86"
|
outputs ~x86:true "dyn: a struct with dyn fields, under collection, --x86"
|
||||||
"programs/dyn-struct.flan" dyn_struct_out;
|
"programs/dyn-struct.flan" dyn_struct_out;
|
||||||
|
(* Two struct spellings the checker decides: a bare {.field v} typed by
|
||||||
|
the position it stands in, and (Cell 1 2) positional. Three rows
|
||||||
|
because that is the house rule, not because the backends could differ
|
||||||
|
— neither feature reaches a backend at all. Both are gone by the time
|
||||||
|
[Check] is finished: a bare literal becomes the [Tast.Make] the named
|
||||||
|
form already built, and a positional call becomes the same node with
|
||||||
|
its arguments put in declaration order. [Emit] and [X86] have no case
|
||||||
|
for either and needed no edit, which is what identical output across
|
||||||
|
the three rows says out loud. *)
|
||||||
|
let struct_erg_out =
|
||||||
|
"0\n9\n30\n3\n1\n2\n3\n4\n22\n11\n15\n15\n100\n2\n2\n"
|
||||||
|
in
|
||||||
|
outputs "structs: a bare literal and a positional constructor"
|
||||||
|
"programs/struct-ergonomics.flan" struct_erg_out;
|
||||||
|
outputs ~opt:"-O0" "structs: a bare literal and a positional constructor, -O0"
|
||||||
|
"programs/struct-ergonomics.flan" struct_erg_out;
|
||||||
|
outputs ~x86:true "structs: a bare literal and a positional constructor, --x86"
|
||||||
|
"programs/struct-ergonomics.flan" struct_erg_out;
|
||||||
|
|
||||||
let dyn_global_out = "0 start\n2 done\n" in
|
let dyn_global_out = "0 start\n2 done\n" in
|
||||||
outputs "dyn: a global" "programs/dyn-global.flan" dyn_global_out;
|
outputs "dyn: a global" "programs/dyn-global.flan" dyn_global_out;
|
||||||
outputs ~opt:"-O0" "dyn: a global, -O0"
|
outputs ~opt:"-O0" "dyn: a global, -O0"
|
||||||
|
|||||||
@ -1318,18 +1318,22 @@ let () =
|
|||||||
"(defn main [] i32 (let [m {:a 1 :b}] (len m)))"
|
"(defn main [] i32 (let [m {:a 1 :b}] (len m)))"
|
||||||
~needle:"odd number of forms";
|
~needle:"odd number of forms";
|
||||||
(* The struct spelling is untouched on both of its sides: bare braces
|
(* The struct spelling is untouched on both of its sides: bare braces
|
||||||
opening on a .field are still a struct literal that wants its type
|
opening on a .field are still a struct field list and not a map, and
|
||||||
written, and (Type {.field v}) still builds one. It is untouched at
|
(Type {.field v}) still builds one. What changed is *where* the two
|
||||||
the head of a defn body too, single-form or not — [constraints]
|
rows below are refused — at checking now, not at parsing, because a
|
||||||
(parse.ml) leaves a [.field]-first map alone precisely so this
|
.field-keyed brace with a type expected of it is a struct literal and
|
||||||
dedicated message, not "a constraint map is keyword/value pairs",
|
only the checker can see the expectation. Neither position has one: a
|
||||||
is what a program gets there. *)
|
let binding takes its type from its value, and a form that is not a
|
||||||
rejects_check "bare struct-shaped braces still refuse"
|
body's last is not the return value. It is untouched at the head of a
|
||||||
|
defn body too, single-form or not — [constraints] (parse.ml) leaves a
|
||||||
|
[.field]-first map alone precisely so this dedicated message, not "a
|
||||||
|
constraint map is keyword/value pairs", is what a program gets there. *)
|
||||||
|
rejects_check "bare struct-shaped braces with nothing to infer from"
|
||||||
"(defn main [] i32 (let [m {.x 1}] 0))"
|
"(defn main [] i32 (let [m {.x 1}] 0))"
|
||||||
~needle:"write (Type {.field v})";
|
~needle:"does not say which struct it builds";
|
||||||
rejects_check "bare struct-shaped braces at a defn body's head, too"
|
rejects_check "bare struct-shaped braces at a defn body's head, too"
|
||||||
"(defn main [] i32 {.x 1} 0)"
|
"(defn main [] i32 {.x 1} 0)"
|
||||||
~needle:"write (Type {.field v})";
|
~needle:"does not say which struct it builds";
|
||||||
accepts "a struct literal still builds"
|
accepts "a struct literal still builds"
|
||||||
"(defstruct P [x i32])\n\
|
"(defstruct P [x i32])\n\
|
||||||
(defn main [] i32 (let [p (P {.x 1})] (.x p)))";
|
(defn main [] i32 (let [p (P {.x 1})] (.x p)))";
|
||||||
@ -1637,11 +1641,113 @@ let () =
|
|||||||
(* {:src s} is a dyn map literal now, not a struct field list with the
|
(* {:src s} is a dyn map literal now, not a struct field list with the
|
||||||
wrong punctuation — the colon is wanted for keys, and this is one. What
|
wrong punctuation — the colon is wanted for keys, and this is one. What
|
||||||
used to be caught as a mispunctuated struct is caught one level up
|
used to be caught as a mispunctuated struct is caught one level up
|
||||||
instead: (Cursor {:src s}) is a call whose head names a struct type, and
|
instead: (Cursor {:src s}) is a struct type applied to one argument, and
|
||||||
that refusal still points at the struct spelling. *)
|
since positional construction landed that is a Cursor built from too few
|
||||||
|
arguments. The refusal still points at the struct spelling, which is why
|
||||||
|
the needle below did not have to move. *)
|
||||||
rejects_check "a struct type called with a colon-keyed map"
|
rejects_check "a struct type called with a colon-keyed map"
|
||||||
(cursor ^ "(defn f [s [u8]] Cursor (Cursor {:src s}))")
|
(cursor ^ "(defn f [s [u8]] Cursor (Cursor {:src s}))")
|
||||||
~needle:"a struct value is written (Cursor {.field value ...})";
|
~needle:"a struct value is written (Cursor {.field value ...})";
|
||||||
|
(* ── A bare {.field v}, typed by its position ──────────────────── *)
|
||||||
|
(* The five positions that carry an expectation, and the three that do
|
||||||
|
not. Everything the named form checks — unknown field, duplicate
|
||||||
|
field, ZII for the ones left out — is checked here by *being* the
|
||||||
|
named form: [check_bare] reads the type name off the want and hands
|
||||||
|
the very same field list to [check_struct]. The two rows below the
|
||||||
|
accepts are what says so, since they are the named form's own kinds
|
||||||
|
and messages arriving at a literal with no name on it. *)
|
||||||
|
let cell = "(defstruct Cell [row i32 col i32]) " in
|
||||||
|
accepts "bare literal in a defn's return position"
|
||||||
|
(cell ^ "(defn f [] Cell {.row 1 .col 2})");
|
||||||
|
accepts "bare literal with a field omitted is ZII, as the named form is"
|
||||||
|
(cell ^ "(defn f [] Cell {.row 1})");
|
||||||
|
accepts "bare literal as the only argument of a call"
|
||||||
|
(cell ^ "(defn g [c Cell] i32 (.row c)) (defn f [] i32 (g {.row 1}))");
|
||||||
|
accepts "bare literal as a later argument of a call"
|
||||||
|
(cell ^ "(defn g [n i32 c Cell] i32 (+ n (.row c))) \
|
||||||
|
(defn f [] i32 (g 1 {.row 2}))");
|
||||||
|
accepts "bare literal as a field of another literal"
|
||||||
|
(cell ^ "(defstruct Grid [a Cell b Cell]) \
|
||||||
|
(defn f [] Grid (Grid {.a {.row 1} .b {.col 2}}))");
|
||||||
|
accepts "bare literal set into a typed place"
|
||||||
|
(cell ^ "(defn f [] i32 (let [c (Cell 0 0)] (set c {.row 7}) (.row c)))");
|
||||||
|
accepts "bare literal at a union want"
|
||||||
|
"(defunion U [i i32 f f32]) (defn f [] U {.i 5})";
|
||||||
|
rejects_check "bare literal with an unknown field"
|
||||||
|
(cell ^ "(defn f [] Cell {.nope 1})") ~needle:"Cell has no field nope";
|
||||||
|
rejects_check "bare literal with a field given twice"
|
||||||
|
(cell ^ "(defn f [] Cell {.row 1 .row 2})") ~needle:"given twice";
|
||||||
|
(* The three no-reading positions. The dyn one is the boundary that
|
||||||
|
matters most: braces at a dyn want are the dyn map literal and stay
|
||||||
|
it, so a .field-keyed brace is refused there rather than quietly
|
||||||
|
given a second meaning — and told which punctuation a map uses. *)
|
||||||
|
rejects_check "bare literal with no expectation to read"
|
||||||
|
(cell ^ "(defn f [] i32 (let [c {.row 1}] (.row c)))")
|
||||||
|
~needle:"does not say which struct it builds";
|
||||||
|
rejects_check "bare literal at a dyn want is not a dyn map"
|
||||||
|
(cell ^ "(defn f [] dyn {.row 1})")
|
||||||
|
~needle:"a dyn map's keys are keywords, as {:row value ...}";
|
||||||
|
rejects_check "bare literal at a want that is not a struct type"
|
||||||
|
(cell ^ "(defn f [] i32 {.row 1})")
|
||||||
|
~needle:"i32 is expected here, which is not a struct type";
|
||||||
|
(* A data type's name is an expectation, but not a specific enough one:
|
||||||
|
the want says D and a value of D is one of its cases. The existing
|
||||||
|
message for that already names them, so the bare path inherits it. *)
|
||||||
|
rejects_check "bare literal at a data type want names the cases"
|
||||||
|
"(defdata D [(A [x i32]) (B [y i32])]) (defn f [] D {.x 1})"
|
||||||
|
~needle:"write (D.A {.field value ...})";
|
||||||
|
(* Patterns are a separate parser ([dmap], not [expr]), so nothing here
|
||||||
|
reaches them: the struct pattern is still {name .field} and a bare
|
||||||
|
{.field v} is still not a pattern of any kind. Both rows answer the
|
||||||
|
same as they did before this feature — checked against the tip by
|
||||||
|
hand, not only asserted here. *)
|
||||||
|
accepts "a struct pattern still destructures in a let"
|
||||||
|
(cell ^ "(defn f [c Cell] i32 (let [{r .row} c] r))");
|
||||||
|
parse_rejects "braces are still not a pattern in a match arm"
|
||||||
|
(cell ^ "(defn f [c Cell] i32 (match c {r .row} r))")
|
||||||
|
~needle:"expected a pattern, found {r .row}";
|
||||||
|
(* The dyn map literal is untouched on every side of this. *)
|
||||||
|
accepts "a keyword-keyed literal is still a dyn map at a dyn want"
|
||||||
|
"(defn f [] dyn {:a 1 :b [2 3]})";
|
||||||
|
accepts "the empty braces are still an empty dyn map"
|
||||||
|
"(defn main [] i32 (let [m {}] (len m)))";
|
||||||
|
|
||||||
|
(* ── (Cell 1 2), positional ────────────────────────────────────── *)
|
||||||
|
accepts "positional struct construction"
|
||||||
|
(cell ^ "(defn f [] Cell (Cell 1 2))");
|
||||||
|
accepts "a zero-field struct called with no arguments"
|
||||||
|
"(defstruct E []) (defn f [] E (E))";
|
||||||
|
(* Exact arity, and the refusal names the first field it did not reach.
|
||||||
|
ZII is not withdrawn — it is what the designated form does, and the
|
||||||
|
message says so — but a positional list cannot say *which* field it
|
||||||
|
left out, so it is not allowed to leave one out. *)
|
||||||
|
rejects_check "positional with too few arguments names the missing field"
|
||||||
|
(cell ^ "(defn f [] Cell (Cell 1))")
|
||||||
|
~needle:".col has no value";
|
||||||
|
rejects_check "positional with too few also offers the designated form"
|
||||||
|
(cell ^ "(defn f [] Cell (Cell 1))")
|
||||||
|
~needle:"a struct value is written (Cell {.field value ...})";
|
||||||
|
rejects_check "positional with too many points at the extra argument"
|
||||||
|
(cell ^ "(defn f [] Cell (Cell 1 2 3))")
|
||||||
|
~needle:"Cell has 2 fields, and this is argument 3";
|
||||||
|
(* The mismatch is reported at the argument, in the words a call's
|
||||||
|
argument already gets; the note is what names the field, because a
|
||||||
|
positional call site is the one place the source does not show it. *)
|
||||||
|
rejects_check "positional argument of the wrong type"
|
||||||
|
"(defstruct Cell [row i32 col f32]) \
|
||||||
|
(defn f [] Cell (Cell 1 \"x\"))"
|
||||||
|
~needle:"expected f32, found string";
|
||||||
|
(* A struct type and a function cannot share a name — [collect]'s
|
||||||
|
[claimed] table spans every declaration kind — so the head of a call
|
||||||
|
resolves to exactly one of them and this refusal is what proves it. *)
|
||||||
|
rejects_check "a struct name and a function name cannot collide"
|
||||||
|
(cell ^ "(defn Cell [] i32 1)") ~needle:"Cell is defined twice";
|
||||||
|
(* Mixed spellings are not a thing: the struct-literal arm in [Parse]
|
||||||
|
takes the braces only as the *whole* argument list. *)
|
||||||
|
parse_rejects "a struct literal followed by more arguments"
|
||||||
|
(cell ^ "(defn f [] Cell (Cell {.row 1} 2))")
|
||||||
|
~needle:"a struct literal is (Cell {.field value ...})";
|
||||||
|
|
||||||
accepts "field through a pointer auto-derefs"
|
accepts "field through a pointer auto-derefs"
|
||||||
(cursor ^ "(defn f [c (Ptr Cursor)] i32 (.pos c))");
|
(cursor ^ "(defn f [c (Ptr Cursor)] i32 (.pos c))");
|
||||||
accepts "set through a pointer"
|
accepts "set through a pointer"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user