diff --git a/TODO.org b/TODO.org index a76e8535..72b62a22 100644 --- a/TODO.org +++ b/TODO.org @@ -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 diff --git a/lib/parse.ml b/lib/parse.ml index 48129474..c54c47d3 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -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; _ } ] -> diff --git a/test/test_flan.ml b/test/test_flan.ml index d39a06df..26ef0703 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -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);