From 70af1966a2a5e048a6f99cbe66a325d3a486be40 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 13 Sep 2026 14:58:27 +0700 Subject: [PATCH] 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. --- lib/cimport.ml | 3 ++- lib/parse.ml | 47 +++++++++++++++++++++++++++-------------- lib/shim.ml | 6 +++++- lib/types.ml | 2 +- syntax-sketch.flan | 16 ++++++++------ test/test_acceptance.ml | 2 +- test/test_flan.ml | 47 ++++++++++++++++++++++------------------- 7 files changed, 75 insertions(+), 48 deletions(-) diff --git a/lib/cimport.ml b/lib/cimport.ml index a3fede3..f95765a 100644 --- a/lib/cimport.ml +++ b/lib/cimport.ml @@ -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) diff --git a/lib/parse.ml b/lib/parse.ml index 44020fb..ca23be8 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -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) diff --git a/lib/shim.ml b/lib/shim.ml index 6ced73d..00aed2c 100644 --- a/lib/shim.ml +++ b/lib/shim.ml @@ -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. *) diff --git a/lib/types.ml b/lib/types.ml index e080d5e..ee7f714 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -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 ^ ")" diff --git a/syntax-sketch.flan b/syntax-sketch.flan index de6241e..c533067 100644 --- a/syntax-sketch.flan +++ b/syntax-sketch.flan @@ -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 diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 30f0507..bffe3cb 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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\")" diff --git a/test/test_flan.ml b/test/test_flan.ml index 679b673..a2239b7 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -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"