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
arguments, which can pick the wrong one of two equal subtrees.
** NEXT 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.
=(defn $foo [x i32] i32 ...)= is accepted and =($foo 3)= calls it; so is
=(defstruct $S [a i32])=, whose type can then be written nowhere. The character is
reserved in every type position and in no name. Refusing it in a declared name
would close it properly, and that is a decision about the spelling.
** DONE A declared name may carry the $ sigil
CLOSED: [2026-09-25]
A name that starts with =$= is refused where it is declared — every top-level
form, a struct or union field, an enum member, a data case and a =let= name —
saying =$= marks a type variable and naming the bare spelling. A parameter was
already refused, as a type in a name slot.
* Checker

View File

@ -14,6 +14,22 @@ let sym (f : Form.t) =
| Sym s -> s
| _ -> 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
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
@ -122,7 +138,7 @@ let rec fields (f : Form.t) (items : Form.t list) : Ast.field list =
| [] -> []
| name :: ty :: rest ->
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 ] ->
Loc.fail odd.loc "field %s has no type — these come in name/type pairs"
(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. *)
and destructure (p : Form.t) (v : Ast.expr) : Ast.binding list =
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
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. *)
@ -1290,17 +1308,17 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defalias"; _ } :: args) ->
(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)")
| List ({ v = Sym "defstruct"; _ } :: args) ->
(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 ...])")
| List ({ v = Sym "defdata"; _ } :: args) ->
(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 ...]) ...])")
(* 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 \
(defdata Name [(Case [field Type ...]) ...])")
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 ...])")
(* 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)
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;
nloc = n.loc; fprivate })
| _ ->
@ -1463,7 +1481,7 @@ let rec decl (f : Form.t) : Ast.decl =
(match args with
| [ n; { v = Vec slots; _ } ] ->
mk (Ast.Defclass
(sym n,
(dname n,
List.map
(fun (s : Form.t) ->
match s.v with
@ -1493,7 +1511,7 @@ let rec decl (f : Form.t) : Ast.decl =
when if generic then body = [] else body <> [] ->
mk ((if generic then (fun fn -> Ast.Defgeneric 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;
nloc = n.loc; fprivate = Ast.Exported })
| _ -> fail f "%s" usage)
@ -1539,11 +1557,11 @@ let rec decl (f : Form.t) : Ast.decl =
| { v = Str csym; _ } :: rest ->
(match List.rev rest with
| [ 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;
fprivate = Ast.Exported } csym)
| [ 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 = [];
nloc = n.loc; fprivate = Ast.Exported } csym)
| _ -> fail f "%s" usage)
@ -1566,7 +1584,7 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defenum"; _ } :: args) ->
(match args with
| [ 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
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
@ -1613,7 +1631,8 @@ let rec decl (f : Form.t) : Ast.decl =
refusals here and below can be made; neither reaches the AST. *)
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
evaluation order of [::]'s two operands unspecified and in
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. *)
let k = fits m loc ~explicit:true k in
(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
(m, next, false, loc) :: members (Int64.add next 1L) rest
| bad :: _ ->
@ -1690,11 +1710,11 @@ let rec decl (f : Form.t) : Ast.decl =
(match args with
| [ n; t ] ->
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"; _ } ] ->
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 ] ->
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
"%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) ->
(match args with
| [ n; v ] -> mk (Ast.Defconst (sym n, None, expr v))
| [ n; t; v ] -> mk (Ast.Defconst (sym n, Some (texpr t), expr v))
| [ n; v ] -> mk (Ast.Defconst (dname n, None, expr v))
| [ n; t; v ] -> mk (Ast.Defconst (dname n, Some (texpr t), expr v))
| _ -> fail f "defconst is (defconst name Type? value)")
(* 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 form_t = { Ast.t = Ast.Tname "Form"; tloc = f.loc } in
mk (Ast.Defn
{ Ast.name = sym n;
{ Ast.name = dname n;
(* A name the reader cannot produce -- [~] opens an unquote, so
no symbol read out of a source file holds one -- which is
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) ]
and variant (f : Form.t) : Ast.variant =
(match f.v with
| Sym _ -> no_sigil f
| List (n :: _) -> no_sigil n
| _ -> ());
match f.v with
| Sym n -> { Ast.vname = n; vfields = []; vloc = f.loc }
| List [ { v = Sym n; _ }; { v = Vec fs; _ } ] ->

View File

@ -6323,6 +6323,26 @@ let () =
(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 ──────────────────── *)
accepts "calc-me.flan type checks"
(In_channel.with_open_bin "../calc-me.flan" In_channel.input_all);