Braces are no longer a type: (Map K V) is the only spelling

The author's decision, and it removes the one syntax question generics
had. A return type can no longer be written in braces, so a {...} after
the signature is unambiguously the constraint map and there is no
structural rule to explain.

The reasons for the record: the brace's value meaning and its type
meaning do not correspond the way the bracket's do - [1 2 3] is a value
whose type is [3 i32], but {.x 1} is a value whose type is a name, and a
map value is built by map-new with no braces anywhere - and dropping it
reserves {} in type position for anonymous struct types.

Braces in a type are refused with the surviving spelling named rather
than falling through to "expected a type". Types.to_string and
Cimport's source printer both print (Map K V) now, and Shim refuses the
application spelling where it used to refuse only Ast.Tmap.
This commit is contained in:
Joseph Ferano 2026-09-13 14:58:27 +07:00
parent de93ffc89e
commit 70af1966a2
7 changed files with 75 additions and 48 deletions

View File

@ -392,7 +392,8 @@ let rec ty_source (t : Ast.texpr) =
| Ast.Tslice e -> Printf.sprintf "[%s]" (ty_source e)
| Ast.Tarray (Ast.Lint n, e) -> Printf.sprintf "[%Ld %s]" n (ty_source e)
| Ast.Tarray (Ast.Lname n, e) -> Printf.sprintf "[%s %s]" n (ty_source e)
| Ast.Tmap (k, v) -> Printf.sprintf "{%s %s}" (ty_source k) (ty_source v)
| Ast.Tmap (k, v) ->
Printf.sprintf "(Map %s %s)" (ty_source k) (ty_source v)
| Ast.Tfn (ps, r) ->
Printf.sprintf "(Fn [%s] %s)"
(String.concat " " (List.map ty_source ps)) (ty_source r)

View File

@ -65,8 +65,26 @@ let rec texpr (f : Form.t) : Ast.texpr =
| Vec [ n; elem ] -> mk (Ast.Tarray (len n, texpr elem))
| Vec _ ->
fail f "a type in brackets is [T] for a slice or [n T] for a fixed array"
| Map [ k; v ] -> mk (Ast.Tmap (texpr k, texpr v))
| Map _ -> fail f "a map type is {K V}"
(* Braces are not a type. [{K V}] used to spell [(Map K V)] and the two
resolved to the same thing; the brace spelling is withdrawn, and the
refusal names the surviving one rather than letting the form fall through
to "expected a type".
Two reasons, and the second is the one that decided it. The brace's value
meaning and its type meaning do not correspond the way the bracket's do:
[[1 2 3]] is a value whose type is [[3 i32]], but [{.x 1 .y 0}] is a
value whose type is a *name*, and a map value is built by [map-new] with
no braces anywhere. And dropping it reserves [{}] in type position for
anonymous struct types, [{.x f32 .y f32}], which is a likelier thing to
want than a second spelling of a type that already has one.
It also settles the one syntax question generics had: a defn's constraint
map, [{:where (ordered? $t)}], sits immediately after the return type,
and with braces gone from type position there is nothing for it to be
confused with. *)
| Map _ ->
fail f "a map type is written (Map K V), not in braces — braces in type \
position are not a type"
| List ({ v = Sym "Fn"; _ } :: rest) ->
(match rest with
| [ { v = Vec params; _ }; ret ] ->
@ -99,21 +117,18 @@ let rec fields (f : Form.t) (items : Form.t list) : Ast.field list =
rather than a bare keyword: it leaves room for further keys without new
syntax.
**The disambiguation, since it is the one syntax question the feature had
to settle.** [{K V}] is a legal *return type* spelling for [(Map K V)], so
[(defn f [xs [$t]] {string i32} {:where ...} body)] puts two braces in a
row meaning different things. They are told apart structurally, by the
first form inside: a constraint map leads with a *keyword*, and a map type
leads with a type [{string i32}], [{K V}] and a keyword is not a type
anywhere in the language. So [Map ({v = Kw _} :: _)] in the slot after the
return type is a constraint map and nothing else can be. The return-type
slot itself is never ambiguous: [Parse] takes it unconditionally, before
this is consulted. [{K V}] stays exactly as it was whether it survives is
a separate open question, and this feature does not force it.
**The one syntax question it had, and how it stopped being one.** [{K V}]
used to be a legal *return type* spelling for [(Map K V)], which put two
braces in a row meaning different things [(defn f [xs [$t]] {string i32}
{:where ...} body)]. The brace spelling has since been withdrawn from type
position entirely ([texpr] above), so the slot after the return type can be
nothing but this. A bare [{}] in *expression* position is already refused
([expr] below), so there is nothing for it to be confused with on the other
side either.
A bare [{}] in *expression* position is already refused ([expr] below), so
there is also nothing for a constraint map to be confused with once past
the return type. *)
The leading keyword is still required and still checked, because it is what
tells a constraint map from a struct literal's field list, [{.x 1}], which
is what braces mean in the position a body starts in. *)
let constraints (body : Form.t list) : Ast.pred list * Form.t list =
match body with
| ({ Form.v = Form.Map (({ Form.v = Form.Kw _; _ } :: _ as kvs)); _ } as m)

View File

@ -222,7 +222,11 @@ let rec cty env ~needed ~loc ~what (t : Ast.texpr) : string =
"%s is a fixed array, which C passes as a pointer and Flan as a value — \
declare (Ptr T) and say which"
what
| Ast.Tmap _ -> fail loc "%s is a map, which has no C representation" what
(* Two spellings reach the same type: [Ast.Tmap], which only [Cimport]
builds now, and [(Map K V)], which is what source writes since the brace
spelling was withdrawn from type position. Both are refused here. *)
| Ast.Tmap _ | Ast.Tapp ("Map", _) ->
fail loc "%s is a map, which has no C representation" what
(* A Vec owns its storage, so handing its header to C hands out an owner and
there is no rule for what C would then be allowed to do with it. The
elements cross the way any other run of elements does. *)

View File

@ -133,7 +133,7 @@ let rec to_string = function
| Named n | Enum n -> n
| Slice t -> "[" ^ to_string t ^ "]"
| Array (n, t) -> Printf.sprintf "[%Ld %s]" n (to_string t)
| Map (k, v) -> Printf.sprintf "{%s %s}" (to_string k) (to_string v)
| Map (k, v) -> Printf.sprintf "(Map %s %s)" (to_string k) (to_string v)
| Ptr t -> "(Ptr " ^ to_string t ^ ")"
| Alloc -> "Allocator"
| Vec t -> "(Vec " ^ to_string t ^ ")"

View File

@ -18,13 +18,17 @@
;; [4 f32] fixed array — a value, copies on assignment
;; [f32] slice, ptr+len — a NON-OWNING view, copies the view only
;; (Vec f32) owning growable, ptr+len+cap — MOVE-ONLY, carries allocator
;; {string i32} owning hashmap — move-only, shorthand for (Map string i32)
;; (Map string i32) owning hashmap — move-only
;;
;; Braces are read by position: in a TYPE position {K V} is a map type; in a
;; VALUE position {.field v ...} is a struct or condition literal — a field
;; label is a dot, and the colon is left for keys. There is no map literal yet;
;; a map is built with make-map and an allocator, and when a literal arrives it
;; takes {:key value}, which is why the dot is what struct construction uses.
;; Braces are NOT a type. {K V} used to be a second spelling of (Map K V) and
;; was withdrawn: the brace's value and type meanings do not correspond the way
;; the bracket's do, and {} in type position is wanted for anonymous struct
;; types, {.x f32 .y f32}. In a VALUE position {.field v ...} is a struct or
;; condition literal — a field label is a dot, and the colon is left for keys.
;; There is no map literal yet; a map is built with map-new and an allocator,
;; and when a literal arrives it takes {:key value}, which is why the dot is
;; what struct construction uses. A defn's constraint map, {:where (ordered?
;; $t)}, is the other brace form, and it sits after the return type.
;; (Ptr World) pointer
;; (Fn [f32] bool) function pointer, no captured environment
;; (Option a) union from the stdlib

View File

@ -1844,7 +1844,7 @@ ERR@7 unexpected token: not the kind the caller was reading
"(declare-c takes [xs [4 f32]] \"Takes\")"
"which C passes as a pointer and Flan as a value";
shim_refuses "declare-c: a map"
"(declare-c takes [m {string i32}] \"Takes\")"
"(declare-c takes [m (Map string i32)] \"Takes\")"
"which has no C representation";
shim_refuses "declare-c: a returned string"
"(declare-c name [] string \"Name\")"

View File

@ -412,8 +412,10 @@ let () =
| _ -> check "nested array with named lengths" false);
(match ty "(Ptr Cursor)" with
| Tapp ("Ptr", [ _ ]) -> () | _ -> check "(Ptr T)" false);
(match ty "{string i32}" with
| Tmap (_, _) -> () | _ -> check "{K V} is a map type" false);
(* A map type is an application like (Ptr T) and (Vec T) now that the brace
spelling is gone: [Ast.Tmap] survives only as what [Cimport] builds. *)
(match ty "(Map string i32)" with
| Tapp ("Map", [ _; _ ]) -> () | _ -> check "(Map K V) is a map type" false);
(match ty "(Fn [a a] bool)" with
| Tfn ([ _; _ ], _) -> () | _ -> check "(Fn [T] R)" false);
@ -858,11 +860,11 @@ let () =
back as generics. *)
rejects_check "Vec takes one type" "(defn f [x (Vec i32 i32)] ())"
~needle:"exactly one type";
(* {K V} resolves now — it is the Map type spelling, and the only one, since
a bare map form in expression position is a struct literal's field list.
What is still refused is the arity, for the same reason Vec's is: a
near-miss would otherwise resolve to a type variable and come back as
generics. *)
(* (Map K V) is the map type spelling, and now the only one: the brace form
is withdrawn from type position, so braces there are refused with the
surviving spelling named. What is refused here is the arity, for the same
reason Vec's is: a near-miss would otherwise resolve to a type variable
and come back as generics. *)
rejects_check "Map takes two types" "(defn f [x (Map i32)] ())"
~needle:"exactly two types";
rejects_check "Result is milestone 6" "(defn f [] (Result i32 i32) None)"
@ -2118,21 +2120,22 @@ let () =
| [] -> check "a report has a first line" false)
| None -> check "a report needs a diagnostic" false);
(* ── Generics: the syntax, the predicates, and the two defaults ──
The syntax question the feature had to settle first: [{K V}] is a legal
*return type*, so a defn with a map return type and a constraint map puts
two braces in a row meaning different things. They are told apart
structurally, by the first form inside a constraint map leads with a
keyword and a map type leads with a type so [{K V}] did not have to go
and is still exactly what it was. *)
accepts "a map return type is still a map return type"
"(defn f [] {string i32} (map-new string i32))";
(* ── Generics: the syntax, the predicates, and the two defaults ── *)
(* The one syntax question the feature had, and how it stopped being one.
[{K V}] used to be a legal *return type* spelling for (Map K V), so a
defn with a map return type and a constraint map put two braces in a row
meaning different things. The brace spelling is now withdrawn from type
position entirely, so the slot after the return type can be nothing but
the constraint map, and braces in a type say where the spelling went. *)
accepts "a map return type, written the one way there is"
"(defn f [] (Map string i32) (map-new string i32))";
accepts "a map return type followed by a constraint map"
"(defn f [x $t] {string i32} {:where (copyable? $t)} \
"(defn f [x $t] (Map string i32) {:where (copyable? $t)} \
(do x (map-new string i32)))";
rejects_check "a map return type is not read as a constraint map"
~needle:"is not a type variable of f"
"(defn f [] {string i32} {:where (ordered? $t)} (map-new string i32))";
rejects_check "braces in type position say where the spelling went"
~needle:"written (Map K V)"
"(defn f [] {string i32} (map-new string i32))";
(* The predicates, and each one gating the operator it is for. *)
accepts "ordered? admits <"
@ -2194,9 +2197,9 @@ let () =
they are chosen from the concrete type, which does not exist yet. *)
rejects_check "a map keyed by a type variable that is not hashable?"
~needle:"is not a map key"
"(defn f [m {$t i32}] i32 {:where (copyable? $t)} (len m))";
"(defn f [m (Map $t i32)] i32 {:where (copyable? $t)} (len m))";
accepts "and hashable? is what says it is"
"(defn f [m {$t i32}] i32 {:where (hashable? $t)} (len m))";
"(defn f [m (Map $t i32)] i32 {:where (hashable? $t)} (len m))";
(* ── The acceptance program checks end to end ──────────────────── *)
accepts "calc-me.flan type checks"