A struct writes itself where its type is already known, and positionally
This commit is contained in:
commit
92bf091361
135
FIX.org
135
FIX.org
@ -1611,3 +1611,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