diff --git a/examples/core-input-multitouch.flan b/examples/core-input-multitouch.flan index 44be21b..5345054 100644 --- a/examples/core-input-multitouch.flan +++ b/examples/core-input-multitouch.flan @@ -13,7 +13,7 @@ ;;;; It is a top-level `defvar` and not a local, which is NOT a stylistic ;;;; choice. A `let` binding takes no type annotation, so the only way to make ;;;; a fixed array inside a function is to initialise it from a literal with -;;;; every element written out — ten `(rl/Vector2 {:x 0.0 :y 0.0})`s here, and +;;;; every element written out — ten `(rl/Vector2 {.x 0.0 .y 0.0})`s here, and ;;;; thirty-two in the gestures testbed. A zeroed local array of a given type ;;;; cannot be spelled. Static storage is what the C's `= { 0 }` gets anyway. ;;;; diff --git a/lib/parse.ml b/lib/parse.ml index 229dd93..a9347c6 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -111,7 +111,7 @@ let rec expr (f : Form.t) : Ast.expr = (* In value position brackets are a fixed-array literal; in type position they are a slice or array type. Position disambiguates, as with {}. *) | Vec items -> mk (Ast.Arr (List.map expr items)) - | Map _ -> fail f "a bare map is not an expression; write (Type {:field v})" + | Map _ -> fail f "a bare map is not an expression; write (Type {.field v})" | List [] -> fail f "() is not an expression" | List (head :: args) -> form f mk head args @@ -343,11 +343,11 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr = | [ target ] -> mk (Ast.Field (expr target, field)) | _ -> fail f "field access is (.%s value)" field) - (* ── struct literal: (Cursor {:src s :pos 0}) ───────────────────── *) + (* ── struct literal: (Cursor {.src s .pos 0}) ───────────────────── *) | Sym name when args <> [] && is_map (List.hd args) -> (match args with | [ { v = Map kvs; _ } ] -> mk (Ast.Struct (name, struct_fields f kvs)) - | _ -> fail f "a struct literal is (%s {:field value ...})" name) + | _ -> fail f "a struct literal is (%s {.field value ...})" name) (* ── anything else is a call ────────────────────────────────────── *) | _ -> mk (Ast.Call (expr head, List.map expr args)) @@ -410,11 +410,18 @@ and destructure (p : Form.t) (v : Ast.expr) : Ast.binding list = {:keys [x y]} over a struct or [a b] over a fixed array" (Form.to_string p) -(* {:keys [x y]} and {inner :field}, over a struct. Clojure's map destructuring +(* {:keys [x y]} and {inner .field}, over a struct. Clojure's map destructuring with Flan's structs standing in for its maps: [:keys] is the common case and the pair form is what nests, since a [:keys] entry is a name and never a pattern. Everything else Clojure puts in this position — [:as], [:or], - [:strs], [:syms] — is refused by name where it is written. *) + [:strs], [:syms] — is refused by name where it is written. + + [:keys] keeps its colon while [.field] takes the dot, and the split is the + point rather than an inconsistency: [.field] names a field of the struct, + [:keys] names no field at all — it is an instruction to the compiler that + happens to sit in the same brace. Keeping them apart leaves the dot meaning + exactly one thing, "this names a field", which is the whole reason the + colon was given up here. *) and dmap (p : Form.t) (t : Ast.expr) (items : Form.t list) : Ast.binding list = let ex loc e : Ast.expr = { Ast.e; loc } in let field loc name = ex loc (Ast.Field (t, name)) in @@ -438,7 +445,7 @@ and dmap (p : Form.t) (t : Ast.expr) (items : Form.t list) : Ast.binding list = | _ -> Loc.fail n.loc ":keys binds field names, and %s is not one — a nested pattern \ - is written {%s :field}" + is written {%s .field}" (Form.to_string n) (Form.to_string n) in { Ast.bname = name; bty = None; bval = field n.loc name; bloc = n.loc } @@ -449,15 +456,22 @@ and dmap (p : Form.t) (t : Ast.expr) (items : Form.t list) : Ast.binding list = ignore rest; Loc.fail bad.loc ":%s is not implemented in a destructuring pattern — a struct pattern \ - is {:keys [x y]} or {name :field}, and nothing else" k - | pat :: ({ v = Kw fld; _ } as fform) :: rest -> - destructure pat (field fform.loc fld) @ go rest + is {:keys [x y]} or {name .field}, and nothing else" k + | pat :: ({ v = Sym s; _ } as fform) :: rest + when String.length s > 1 && s.[0] = '.' -> + destructure pat (field fform.loc (String.sub s 1 (String.length s - 1))) + @ go rest + | pat :: ({ v = Kw fld; _ } as bad) :: _ -> + ignore pat; + Loc.fail bad.loc + "a field label is written .%s, not :%s — the colon is for keys, and a \ + struct pattern binds {name .%s}" fld fld fld | pat :: other :: _ -> Loc.fail other.loc - "expected :field after %s, found %s — a struct pattern binds \ - {name :field}" (Form.to_string pat) (Form.to_string other) + "expected .field after %s, found %s — a struct pattern binds \ + {name .field}" (Form.to_string pat) (Form.to_string other) | [ odd ] -> - Loc.fail odd.loc "%s has no :field — a struct pattern comes in pairs" + Loc.fail odd.loc "%s has no .field — a struct pattern comes in pairs" (Form.to_string odd) in if items = [] then @@ -543,12 +557,22 @@ and no_duplicates (p : Form.t) (bs : Ast.binding list) = in ignore p; go [] bs +(* A field label is a dot, never a colon. The delimiter is what disambiguates: + [(.x v)] is a call and therefore an access, [{.x 1.0}] is a brace form and + therefore a construction. The colon is left for keys — map keys and enum + members — so the two never share a spelling. *) and struct_fields f (items : Form.t list) : (string * Ast.expr) list = let rec go = function | [] -> [] - | { v = Kw k; _ } :: value :: rest -> (k, expr value) :: go rest + | { v = Sym s; _ } :: value :: rest + when String.length s > 1 && s.[0] = '.' -> + (String.sub s 1 (String.length s - 1), expr value) :: go rest + | ({ v = Kw k; _ } as bad) :: _ :: _ -> + Loc.fail bad.loc + "a field label is written .%s, not :%s — the colon is for keys, and a \ + struct value is (Type {.%s value ...})" k k k | other :: _ :: _ -> - Loc.fail other.loc "expected :field, found %s" (Form.to_string other) + Loc.fail other.loc "expected .field, found %s" (Form.to_string other) | [ odd ] -> Loc.fail odd.loc "field %s has no value" (Form.to_string odd) in ignore f; go items diff --git a/lib/render.ml b/lib/render.ml index ec3fe2a..f8f6377 100644 --- a/lib/render.ml +++ b/lib/render.ml @@ -147,7 +147,7 @@ let rec render c depth (e : Tast.expr) : Tast.expr list = (fun i (f : Tast.field) -> let v = { Tast.e = Tast.Field (e, i); ty = f.Tast.fty; loc } in (if i = 0 then [] else [ lit " " ]) - @ [ lit (":" ^ f.Tast.fname ^ " ") ] + @ [ lit ("." ^ f.Tast.fname ^ " ") ] @ render c (depth + 1) v) shown) in diff --git a/syntax-sketch.flan b/syntax-sketch.flan index 7e08405..def2402 100644 --- a/syntax-sketch.flan +++ b/syntax-sketch.flan @@ -20,8 +20,10 @@ ;; {string i32} owning hashmap — move-only, shorthand for (Map string i32) ;; ;; 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. There is no -;; map literal — a map is built with make-map and an allocator. +;; 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. ;; (Ptr World) pointer ;; (Fn [f32] bool) function pointer, no captured environment ;; (Option a) union from the stdlib diff --git a/test/test_flan.ml b/test/test_flan.ml index 3f8cd98..3c0a14d 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -953,10 +953,10 @@ let () = ~needle:"an empty struct pattern {} binds nothing"; rejects_check "a field name with no pattern before it" (pt ^ "(defn f [p Point] i32 (let [{.x} p] 0))") - ~needle:"has no :field"; + ~needle:"has no .field"; rejects_check "a pattern with no field name after it" (pt ^ "(defn f [p Point] i32 (let [{a b} p] 0))") - ~needle:"expected :field after a"; + ~needle:"expected .field after a"; (* Clojure's other map-destructuring keys. Each is refused by its own name: "unexpected form" would leave the author guessing which of the four they diff --git a/tools/colon-to-dot.py b/tools/colon-to-dot.py index b7a4b3f..44d9e8e 100755 --- a/tools/colon-to-dot.py +++ b/tools/colon-to-dot.py @@ -195,8 +195,10 @@ def walk(paths): for p in paths: if os.path.isdir(p): for root, dirs, files in os.walk(p): + # vendor/ is not third-party: edn, raylib and agent are this + # repo's own packages, written in Flan, and they convert too. dirs[:] = [d for d in dirs - if d not in ('_build', '.git', 'vendor', 'node_modules')] + if d not in ('_build', '.git', 'node_modules')] for f in sorted(files): if f.endswith('.flan'): yield os.path.join(root, f) diff --git a/vendor/edn/edn.flan b/vendor/edn/edn.flan index 72df9df..11d91c1 100644 --- a/vendor/edn/edn.flan +++ b/vendor/edn/edn.flan @@ -158,7 +158,7 @@ ;; ── Construction ──────────────────────────────────────────────────── (defn cursor [src [u8]] Cursor - (Cursor {:src src :pos 0 :err err-none :err-pos 0 :depth 0})) + (Cursor {.src src .pos 0 .err err-none .err-pos 0 .depth 0})) (defn ok? [c (Ptr Cursor)] bool (= (.err c) err-none)) @@ -248,10 +248,10 @@ (slice (.src c) p p)) (defn token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token - (Token {:kind kind :text (slice (.src c) lo hi) :pos p})) + (Token {.kind kind .text (slice (.src c) lo hi) .pos p})) (defn error-token [c (Ptr Cursor)] Token - (Token {:kind tok-error :text (empty-at c (.err-pos c)) :pos (.err-pos c)})) + (Token {.kind tok-error .text (empty-at c (.err-pos c)) .pos (.err-pos c)})) ;; Whitespace, commas, and `;` comments, which run to the newline or to the end ;; of input — a comment on the last line of a file with no trailing newline is @@ -376,7 +376,7 @@ (when (> (.depth c) 0) (fail c err-unbalanced (.pos c)) (return (error-token c))) - (return (Token {:kind tok-eof :text (empty-at c (.pos c)) :pos (.pos c)}))) + (return (Token {.kind tok-eof .text (empty-at c (.pos c)) .pos (.pos c)}))) (let [s (.src c) lo (.pos c) diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index 6275b72..f8ed5ad 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -133,8 +133,8 @@ ;; directions at once. (declare-c fade [color Color alpha f32] Color "Fade") -(defconst black (Color {:r 0 :g 0 :b 0 :a 255})) -(defconst white (Color {:r 255 :g 255 :b 255 :a 255})) +(defconst black (Color {.r 0 .g 0 .b 0 .a 255})) +(defconst white (Color {.r 255 .g 255 .b 255 .a 255})) ;; raylib's own named palette, from raylib.h's CLITERAL macros. These are the ;; only entries in this file that are not a function, a layout or an enum, and @@ -143,32 +143,32 @@ ;; hex that cannot be diffed against the C it came from. They are values and ;; not calls — a Color is four bytes with no packing question — so unlike ;; get-color they cost nothing at run time and need no window. -(defconst lightgray (Color {:r 200 :g 200 :b 200 :a 255})) -(defconst gray (Color {:r 130 :g 130 :b 130 :a 255})) -(defconst darkgray (Color {:r 80 :g 80 :b 80 :a 255})) -(defconst yellow (Color {:r 253 :g 249 :b 0 :a 255})) -(defconst gold (Color {:r 255 :g 203 :b 0 :a 255})) -(defconst orange (Color {:r 255 :g 161 :b 0 :a 255})) -(defconst pink (Color {:r 255 :g 109 :b 194 :a 255})) -(defconst red (Color {:r 230 :g 41 :b 55 :a 255})) -(defconst maroon (Color {:r 190 :g 33 :b 55 :a 255})) -(defconst green (Color {:r 0 :g 228 :b 48 :a 255})) -(defconst lime (Color {:r 0 :g 158 :b 47 :a 255})) -(defconst darkgreen (Color {:r 0 :g 117 :b 44 :a 255})) -(defconst skyblue (Color {:r 102 :g 191 :b 255 :a 255})) -(defconst blue (Color {:r 0 :g 121 :b 241 :a 255})) -(defconst darkblue (Color {:r 0 :g 82 :b 172 :a 255})) -(defconst purple (Color {:r 200 :g 122 :b 255 :a 255})) -(defconst violet (Color {:r 135 :g 60 :b 190 :a 255})) -(defconst darkpurple (Color {:r 112 :g 31 :b 126 :a 255})) -(defconst beige (Color {:r 211 :g 176 :b 131 :a 255})) -(defconst brown (Color {:r 127 :g 106 :b 79 :a 255})) -(defconst darkbrown (Color {:r 76 :g 63 :b 47 :a 255})) -(defconst magenta (Color {:r 255 :g 0 :b 255 :a 255})) +(defconst lightgray (Color {.r 200 .g 200 .b 200 .a 255})) +(defconst gray (Color {.r 130 .g 130 .b 130 .a 255})) +(defconst darkgray (Color {.r 80 .g 80 .b 80 .a 255})) +(defconst yellow (Color {.r 253 .g 249 .b 0 .a 255})) +(defconst gold (Color {.r 255 .g 203 .b 0 .a 255})) +(defconst orange (Color {.r 255 .g 161 .b 0 .a 255})) +(defconst pink (Color {.r 255 .g 109 .b 194 .a 255})) +(defconst red (Color {.r 230 .g 41 .b 55 .a 255})) +(defconst maroon (Color {.r 190 .g 33 .b 55 .a 255})) +(defconst green (Color {.r 0 .g 228 .b 48 .a 255})) +(defconst lime (Color {.r 0 .g 158 .b 47 .a 255})) +(defconst darkgreen (Color {.r 0 .g 117 .b 44 .a 255})) +(defconst skyblue (Color {.r 102 .g 191 .b 255 .a 255})) +(defconst blue (Color {.r 0 .g 121 .b 241 .a 255})) +(defconst darkblue (Color {.r 0 .g 82 .b 172 .a 255})) +(defconst purple (Color {.r 200 .g 122 .b 255 .a 255})) +(defconst violet (Color {.r 135 .g 60 .b 190 .a 255})) +(defconst darkpurple (Color {.r 112 .g 31 .b 126 .a 255})) +(defconst beige (Color {.r 211 .g 176 .b 131 .a 255})) +(defconst brown (Color {.r 127 .g 106 .b 79 .a 255})) +(defconst darkbrown (Color {.r 76 .g 63 .b 47 .a 255})) +(defconst magenta (Color {.r 255 .g 0 .b 255 .a 255})) ;; Alpha 0, so it is invisible rather than a colour — raylib's own name for it. -(defconst blank (Color {:r 0 :g 0 :b 0 :a 0})) +(defconst blank (Color {.r 0 .g 0 .b 0 .a 0})) ;; raylib's off-white background, which is what every example clears to. -(defconst raywhite (Color {:r 245 :g 245 :b 245 :a 255})) +(defconst raywhite (Color {.r 245 .g 245 .b 245 .a 255})) ;; ── Drawing ─────────────────────────────────────────────────────────