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.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.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.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) -> | Ast.Tfn (ps, r) ->
Printf.sprintf "(Fn [%s] %s)" Printf.sprintf "(Fn [%s] %s)"
(String.concat " " (List.map ty_source ps)) (ty_source r) (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 [ n; elem ] -> mk (Ast.Tarray (len n, texpr elem))
| Vec _ -> | Vec _ ->
fail f "a type in brackets is [T] for a slice or [n T] for a fixed array" 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)) (* Braces are not a type. [{K V}] used to spell [(Map K V)] and the two
| Map _ -> fail f "a map type is {K V}" 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) -> | List ({ v = Sym "Fn"; _ } :: rest) ->
(match rest with (match rest with
| [ { v = Vec params; _ }; ret ] -> | [ { 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 rather than a bare keyword: it leaves room for further keys without new
syntax. syntax.
**The disambiguation, since it is the one syntax question the feature had **The one syntax question it had, and how it stopped being one.** [{K V}]
to settle.** [{K V}] is a legal *return type* spelling for [(Map K V)], so used to be a legal *return type* spelling for [(Map K V)], which put two
[(defn f [xs [$t]] {string i32} {:where ...} body)] puts two braces in a braces in a row meaning different things [(defn f [xs [$t]] {string i32}
row meaning different things. They are told apart structurally, by the {:where ...} body)]. The brace spelling has since been withdrawn from type
first form inside: a constraint map leads with a *keyword*, and a map type position entirely ([texpr] above), so the slot after the return type can be
leads with a type [{string i32}], [{K V}] and a keyword is not a type nothing but this. A bare [{}] in *expression* position is already refused
anywhere in the language. So [Map ({v = Kw _} :: _)] in the slot after the ([expr] below), so there is nothing for it to be confused with on the other
return type is a constraint map and nothing else can be. The return-type side either.
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.
A bare [{}] in *expression* position is already refused ([expr] below), so The leading keyword is still required and still checked, because it is what
there is also nothing for a constraint map to be confused with once past tells a constraint map from a struct literal's field list, [{.x 1}], which
the return type. *) is what braces mean in the position a body starts in. *)
let constraints (body : Form.t list) : Ast.pred list * Form.t list = let constraints (body : Form.t list) : Ast.pred list * Form.t list =
match body with match body with
| ({ Form.v = Form.Map (({ Form.v = Form.Kw _; _ } :: _ as kvs)); _ } as m) | ({ 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 — \ "%s is a fixed array, which C passes as a pointer and Flan as a value — \
declare (Ptr T) and say which" declare (Ptr T) and say which"
what 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 (* 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 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. *) 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 | Named n | Enum n -> n
| Slice t -> "[" ^ to_string t ^ "]" | Slice t -> "[" ^ to_string t ^ "]"
| Array (n, t) -> Printf.sprintf "[%Ld %s]" n (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 ^ ")" | Ptr t -> "(Ptr " ^ to_string t ^ ")"
| Alloc -> "Allocator" | Alloc -> "Allocator"
| Vec t -> "(Vec " ^ to_string t ^ ")" | Vec t -> "(Vec " ^ to_string t ^ ")"

View File

@ -18,13 +18,17 @@
;; [4 f32] fixed array — a value, copies on assignment ;; [4 f32] fixed array — a value, copies on assignment
;; [f32] slice, ptr+len — a NON-OWNING view, copies the view only ;; [f32] slice, ptr+len — a NON-OWNING view, copies the view only
;; (Vec f32) owning growable, ptr+len+cap — MOVE-ONLY, carries allocator ;; (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 ;; Braces are NOT a type. {K V} used to be a second spelling of (Map K V) and
;; VALUE position {.field v ...} is a struct or condition literal — a field ;; was withdrawn: the brace's value and type meanings do not correspond the way
;; label is a dot, and the colon is left for keys. There is no map literal yet; ;; the bracket's do, and {} in type position is wanted for anonymous struct
;; a map is built with make-map and an allocator, and when a literal arrives it ;; types, {.x f32 .y f32}. In a VALUE position {.field v ...} is a struct or
;; takes {:key value}, which is why the dot is what struct construction uses. ;; 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 ;; (Ptr World) pointer
;; (Fn [f32] bool) function pointer, no captured environment ;; (Fn [f32] bool) function pointer, no captured environment
;; (Option a) union from the stdlib ;; (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\")" "(declare-c takes [xs [4 f32]] \"Takes\")"
"which C passes as a pointer and Flan as a value"; "which C passes as a pointer and Flan as a value";
shim_refuses "declare-c: a map" 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"; "which has no C representation";
shim_refuses "declare-c: a returned string" shim_refuses "declare-c: a returned string"
"(declare-c name [] string \"Name\")" "(declare-c name [] string \"Name\")"

View File

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