A declared name that starts with $ is refused where it is declared

This commit is contained in:
Joseph Ferano 2026-09-25 10:04:23 +07:00
parent bf827dc55b
commit b153f325b8
3 changed files with 70 additions and 26 deletions

View File

@ -598,12 +598,12 @@ back to the call site. Costs about 2µs a call. Rules out putting a =loc= field
on the wire, and rules out structural matching of the expansion against the on the wire, and rules out structural matching of the expansion against the
arguments, which can pick the wrong one of two equal subtrees. arguments, which can pick the wrong one of two equal subtrees.
** NEXT A declared name may carry the $ sigil ** DONE A declared name may carry the $ sigil
Decided 2026-09-25: refuse =$= at the start of any declared name; the refusal says =$= marks a type variable. CLOSED: [2026-09-25]
=(defn $foo [x i32] i32 ...)= is accepted and =($foo 3)= calls it; so is A name that starts with =$= is refused where it is declared — every top-level
=(defstruct $S [a i32])=, whose type can then be written nowhere. The character is form, a struct or union field, an enum member, a data case and a =let= name —
reserved in every type position and in no name. Refusing it in a declared name saying =$= marks a type variable and naming the bare spelling. A parameter was
would close it properly, and that is a decision about the spelling. already refused, as a type in a name slot.
* Checker * Checker

View File

@ -14,6 +14,22 @@ let sym (f : Form.t) =
| Sym s -> s | Sym s -> s
| _ -> fail f "expected a name, found %s" (Form.to_string f) | _ -> fail f "expected a name, found %s" (Form.to_string f)
(* A name something declares. [$] opens a type variable in every type
position, so a declared name that starts with one could be written at its
definition and at a call and nowhere a type goes — [(defstruct $S ...)] is a
type no signature can name. Refused at the declaration, where the fix is. *)
let no_sigil (f : Form.t) =
match f.v with
| Sym s when String.length s > 1 && s.[0] = '$' ->
let bare = String.sub s 1 (String.length s - 1) in
Loc.failk "parse/sigil-in-name" f.loc
"%s cannot be declared: a name does not start with $, which marks a \
type variable, as in [x $t]. Name it %s"
s bare
| _ -> ()
let dname (f : Form.t) = no_sigil f; sym f
(* Names for the temporaries this file mints — the value is bound once and (* Names for the temporaries this file mints — the value is bound once and
everything that needs it reads *that*, so a destructuring pattern over a everything that needs it reads *that*, so a destructuring pattern over a
call calls it once and a short-circuit operand is evaluated once. [~] is a call calls it once and a short-circuit operand is evaluated once. [~] is a
@ -122,7 +138,7 @@ let rec fields (f : Form.t) (items : Form.t list) : Ast.field list =
| [] -> [] | [] -> []
| name :: ty :: rest -> | name :: ty :: rest ->
no_pattern name; no_pattern name;
{ Ast.fname = sym name; fty = texpr ty; floc = name.loc } :: fields f rest { Ast.fname = dname name; fty = texpr ty; floc = name.loc } :: fields f rest
| [ odd ] -> | [ odd ] ->
Loc.fail odd.loc "field %s has no type — these come in name/type pairs" Loc.fail odd.loc "field %s has no type — these come in name/type pairs"
(Form.to_string odd) (Form.to_string odd)
@ -879,7 +895,9 @@ and temp (p : Form.t) (v : Ast.expr) : Ast.expr * Ast.binding =
name is what it always was. *) name is what it always was. *)
and destructure (p : Form.t) (v : Ast.expr) : Ast.binding list = and destructure (p : Form.t) (v : Ast.expr) : Ast.binding list =
match p.v with match p.v with
| Sym name -> [ { Ast.bname = name; bty = None; bval = v; bloc = p.loc } ] | Sym name ->
no_sigil p;
[ { Ast.bname = name; bty = None; bval = v; bloc = p.loc } ]
(* The value goes into a temporary first, so it is evaluated once however (* The value goes into a temporary first, so it is evaluated once however
many names the pattern binds, and so that [(let [{:keys [p]} p] ...)] many names the pattern binds, and so that [(let [{:keys [p]} p] ...)]
reads the old [p] rather than the one it is in the middle of rebinding. *) reads the old [p] rather than the one it is in the middle of rebinding. *)
@ -1290,17 +1308,17 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defalias"; _ } :: args) -> | List ({ v = Sym "defalias"; _ } :: args) ->
(match args with (match args with
| [ n; t ] -> mk (Ast.Defalias (sym n, texpr t)) | [ n; t ] -> mk (Ast.Defalias (dname n, texpr t))
| _ -> fail f "defalias is (defalias Name Type)") | _ -> fail f "defalias is (defalias Name Type)")
| List ({ v = Sym "defstruct"; _ } :: args) -> | List ({ v = Sym "defstruct"; _ } :: args) ->
(match args with (match args with
| [ n; { v = Vec fs; _ } ] -> mk (Ast.Defstruct (sym n, fields f fs)) | [ n; { v = Vec fs; _ } ] -> mk (Ast.Defstruct (dname n, fields f fs))
| _ -> fail f "defstruct is (defstruct Name [field Type ...])") | _ -> fail f "defstruct is (defstruct Name [field Type ...])")
| List ({ v = Sym "defdata"; _ } :: args) -> | List ({ v = Sym "defdata"; _ } :: args) ->
(match args with (match args with
| [ n; { v = Vec vs; _ } ] -> mk (Ast.Defdata (sym n, List.map variant vs)) | [ n; { v = Vec vs; _ } ] -> mk (Ast.Defdata (dname n, List.map variant vs))
| _ -> fail f "defdata is (defdata Name [(Case [field Type ...]) ...])") | _ -> fail f "defdata is (defdata Name [(Case [field Type ...]) ...])")
(* C's union: one storage, as many ways of reading it as there are members. (* C's union: one storage, as many ways of reading it as there are members.
@ -1343,7 +1361,7 @@ let rec decl (f : Form.t) : Ast.decl =
[member Type ...]). This reads as a tagged sum — write \ [member Type ...]). This reads as a tagged sum — write \
(defdata Name [(Case [field Type ...]) ...])") (defdata Name [(Case [field Type ...]) ...])")
ms; ms;
mk (Ast.Defunion (sym n, fields f ms)) mk (Ast.Defunion (dname n, fields f ms))
| _ -> fail f "defunion is (defunion Name [member Type ...])") | _ -> fail f "defunion is (defunion Name [member Type ...])")
(* The slot after the parameters is unconditionally the return type. It used (* The slot after the parameters is unconditionally the return type. It used
@ -1437,7 +1455,7 @@ let rec decl (f : Form.t) : Ast.decl =
(Form.to_string ret) (Form.to_string ret)
in in
let fwhere, body = constraints body in let fwhere, body = constraints body in
mk (Ast.Defn { Ast.name = sym n; params = []; praw = Some (pitems ps); mk (Ast.Defn { Ast.name = dname n; params = []; praw = Some (pitems ps);
ret = Some rty; fwhere; fbody = body_of body; ret = Some rty; fwhere; fbody = body_of body;
nloc = n.loc; fprivate }) nloc = n.loc; fprivate })
| _ -> | _ ->
@ -1463,7 +1481,7 @@ let rec decl (f : Form.t) : Ast.decl =
(match args with (match args with
| [ n; { v = Vec slots; _ } ] -> | [ n; { v = Vec slots; _ } ] ->
mk (Ast.Defclass mk (Ast.Defclass
(sym n, (dname n,
List.map List.map
(fun (s : Form.t) -> (fun (s : Form.t) ->
match s.v with match s.v with
@ -1493,7 +1511,7 @@ let rec decl (f : Form.t) : Ast.decl =
when if generic then body = [] else body <> [] -> when if generic then body = [] else body <> [] ->
mk ((if generic then (fun fn -> Ast.Defgeneric fn) mk ((if generic then (fun fn -> Ast.Defgeneric fn)
else fun fn -> Ast.Defmulti fn) else fun fn -> Ast.Defmulti fn)
{ Ast.name = sym n; params = dyn_params which ps; praw = None; { Ast.name = dname n; params = dyn_params which ps; praw = None;
ret = Some (texpr ret); fwhere = []; fbody = body_of body; ret = Some (texpr ret); fwhere = []; fbody = body_of body;
nloc = n.loc; fprivate = Ast.Exported }) nloc = n.loc; fprivate = Ast.Exported })
| _ -> fail f "%s" usage) | _ -> fail f "%s" usage)
@ -1539,11 +1557,11 @@ let rec decl (f : Form.t) : Ast.decl =
| { v = Str csym; _ } :: rest -> | { v = Str csym; _ } :: rest ->
(match List.rev rest with (match List.rev rest with
| [ n; { v = Form.Vec ps; _ } ] -> | [ n; { v = Form.Vec ps; _ } ] ->
mk (mkd { Ast.name = sym n; params = fields f ps; praw = None; mk (mkd { Ast.name = dname n; params = fields f ps; praw = None;
ret = None; fwhere = []; fbody = []; nloc = n.loc; ret = None; fwhere = []; fbody = []; nloc = n.loc;
fprivate = Ast.Exported } csym) fprivate = Ast.Exported } csym)
| [ n; { v = Form.Vec ps; _ }; r ] -> | [ n; { v = Form.Vec ps; _ }; r ] ->
mk (mkd { Ast.name = sym n; params = fields f ps; praw = None; mk (mkd { Ast.name = dname n; params = fields f ps; praw = None;
ret = Some (texpr r); fwhere = []; fbody = []; ret = Some (texpr r); fwhere = []; fbody = [];
nloc = n.loc; fprivate = Ast.Exported } csym) nloc = n.loc; fprivate = Ast.Exported } csym)
| _ -> fail f "%s" usage) | _ -> fail f "%s" usage)
@ -1566,7 +1584,7 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defenum"; _ } :: args) -> | List ({ v = Sym "defenum"; _ } :: args) ->
(match args with (match args with
| [ n; { v = Form.Vec ms; _ } ] -> | [ n; { v = Form.Vec ms; _ } ] ->
let ename = sym n in let ename = dname n in
(* An enum member is an [i32] at run time. [Shim] lowers the type to (* An enum member is an [i32] at run time. [Shim] lowers the type to
int32_t for C's benefit and [Check] builds every member as a int32_t for C's benefit and [Check] builds every member as a
[Tast.Int (v, I32)] -- but the reader hands this pass an [int64], so [Tast.Int (v, I32)] -- but the reader hands this pass an [int64], so
@ -1613,7 +1631,8 @@ let rec decl (f : Form.t) : Ast.decl =
refusals here and below can be made; neither reaches the AST. *) refusals here and below can be made; neither reaches the AST. *)
let rec members next = function let rec members next = function
| [] -> [] | [] -> []
| { v = Form.Sym m; loc } :: { v = Form.Int k; _ } :: rest -> | ({ v = Form.Sym m; loc } as mf) :: { v = Form.Int k; _ } :: rest ->
no_sigil mf;
(* The [let] is load-bearing rather than tidiness. OCaml leaves the (* The [let] is load-bearing rather than tidiness. OCaml leaves the
evaluation order of [::]'s two operands unspecified and in evaluation order of [::]'s two operands unspecified and in
practice takes the tail first, so an inlined [fits ... k] would practice takes the tail first, so an inlined [fits ... k] would
@ -1626,7 +1645,8 @@ let rec decl (f : Form.t) : Ast.decl =
i32 by the time it is incremented, so the sum cannot overflow. *) i32 by the time it is incremented, so the sum cannot overflow. *)
let k = fits m loc ~explicit:true k in let k = fits m loc ~explicit:true k in
(m, k, true, loc) :: members (Int64.add k 1L) rest (m, k, true, loc) :: members (Int64.add k 1L) rest
| { v = Form.Sym m; loc } :: rest -> | ({ v = Form.Sym m; loc } as mf) :: rest ->
no_sigil mf;
let next = fits m loc ~explicit:false next in let next = fits m loc ~explicit:false next in
(m, next, false, loc) :: members (Int64.add next 1L) rest (m, next, false, loc) :: members (Int64.add next 1L) rest
| bad :: _ -> | bad :: _ ->
@ -1690,11 +1710,11 @@ let rec decl (f : Form.t) : Ast.decl =
(match args with (match args with
| [ n; t ] -> | [ n; t ] ->
let ty, init = defvar3 t in let ty, init = defvar3 t in
mk (Ast.Defvar (sym n, Some ty, init, kind)) mk (Ast.Defvar (dname n, Some ty, init, kind))
| [ n; t; { v = Sym "uninit"; _ } ] -> | [ n; t; { v = Sym "uninit"; _ } ] ->
mk (Ast.Defvar (sym n, Some (texpr t), Ast.Uninit, kind)) mk (Ast.Defvar (dname n, Some (texpr t), Ast.Uninit, kind))
| [ n; t; v ] -> | [ n; t; v ] ->
mk (Ast.Defvar (sym n, Some (texpr t), Ast.Init (expr v), kind)) mk (Ast.Defvar (dname n, Some (texpr t), Ast.Init (expr v), kind))
| _ -> | _ ->
fail f fail f
"%s is (%s name Type value?) or (%s name value) — a third element \ "%s is (%s name Type value?) or (%s name value) — a third element \
@ -1733,8 +1753,8 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defconst"; _ } :: args) -> | List ({ v = Sym "defconst"; _ } :: args) ->
(match args with (match args with
| [ n; v ] -> mk (Ast.Defconst (sym n, None, expr v)) | [ n; v ] -> mk (Ast.Defconst (dname n, None, expr v))
| [ n; t; v ] -> mk (Ast.Defconst (sym n, Some (texpr t), expr v)) | [ n; t; v ] -> mk (Ast.Defconst (dname n, Some (texpr t), expr v))
| _ -> fail f "defconst is (defconst name Type? value)") | _ -> fail f "defconst is (defconst name Type? value)")
(* A macro is an ordinary function, and this is where it becomes one: (* A macro is an ordinary function, and this is where it becomes one:
@ -1764,7 +1784,7 @@ let rec decl (f : Form.t) : Ast.decl =
let sg = Expand.params_of ps in let sg = Expand.params_of ps in
let form_t = { Ast.t = Ast.Tname "Form"; tloc = f.loc } in let form_t = { Ast.t = Ast.Tname "Form"; tloc = f.loc } in
mk (Ast.Defn mk (Ast.Defn
{ Ast.name = sym n; { Ast.name = dname n;
(* A name the reader cannot produce -- [~] opens an unquote, so (* A name the reader cannot produce -- [~] opens an unquote, so
no symbol read out of a source file holds one -- which is no symbol read out of a source file holds one -- which is
what keeps the compiler's own parameter out of the way of what keeps the compiler's own parameter out of the way of
@ -1844,6 +1864,10 @@ and macro_body (sg : Expand.msig) (body : Form.t list) : Ast.expr list =
[ call loc0 (s loc0 "let" :: Form.make (Form.Vec items) loc0 :: body) ] [ call loc0 (s loc0 "let" :: Form.make (Form.Vec items) loc0 :: body) ]
and variant (f : Form.t) : Ast.variant = and variant (f : Form.t) : Ast.variant =
(match f.v with
| Sym _ -> no_sigil f
| List (n :: _) -> no_sigil n
| _ -> ());
match f.v with match f.v with
| Sym n -> { Ast.vname = n; vfields = []; vloc = f.loc } | Sym n -> { Ast.vname = n; vfields = []; vloc = f.loc }
| List [ { v = Sym n; _ }; { v = Vec fs; _ } ] -> | List [ { v = Sym n; _ }; { v = Vec fs; _ } ] ->

View File

@ -6323,6 +6323,26 @@ let () =
(defn main [] () (add2 1 2))" (defn main [] () (add2 1 2))"
[]; [];
(* ── A declared name does not start with $ ─────────────────────── *)
(* $ marks a type variable in every type position, so a name that starts
with one could not be written where a type goes. *)
let sigil what src =
parse_rejects ("a declared name with a $: " ^ what) src
~needle:"a name does not start with $, which marks a type variable"
in
sigil "defn" "(defn $foo [x i32] i32 (+ x 1))";
sigil "defstruct" "(defstruct $S [a i32])";
sigil "a struct field" "(defstruct S [$a i32])";
sigil "defenum" "(defenum $E [A B])";
sigil "an enum member" "(defenum E [A $B])";
sigil "defonce" "(defonce $g i32 0)";
sigil "defconst" "(defconst $k 3)";
sigil "defdata case" "(defdata D [($C [a i32])])";
sigil "defmacro" "(defmacro $m [x] x)";
sigil "a let binding" "(defn f [] i32 (let [$y 1] y))";
parse_rejects "the $ refusal names the bare spelling"
"(defn $foo [x i32] i32 x)" ~needle:"Name it foo";
(* ── The acceptance program checks end to end ──────────────────── *) (* ── The acceptance program checks end to end ──────────────────── *)
accepts "calc-me.flan type checks" accepts "calc-me.flan type checks"
(In_channel.with_open_bin "../calc-me.flan" In_channel.input_all); (In_channel.with_open_bin "../calc-me.flan" In_channel.input_all);