From 9a820d86cd7e5755b290393e6439536f02c32b3e Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sat, 12 Sep 2026 14:47:54 +0700
Subject: [PATCH 1/5] Sweep every field label from the colon spelling to the
dot
The script is in tools/ rather than thrown away, because two lanes are
writing Flan in the old spelling right now and their files need the same
pass at merge.
It works on forms, not on text: a keyword becomes a dot only where it sits
in a field-label position inside a brace, so an enum member in value
position, a map key inside an EDN string and a type-position {K V} are all
left alone. :keys keeps its colon -- it names no field.
---
calc-me.flan | 2 +-
conditions-play.flan | 2 +-
examples/core-delta-time.flan | 6 +-
examples/core-input-gamepad.flan | 16 +-
examples/core-input-gestures-testbed.flan | 58 ++---
examples/core-input-gestures.flan | 6 +-
examples/core-input-keys.flan | 4 +-
examples/core-input-virtual-controls.flan | 44 ++--
lib/ast.ml | 2 +-
lib/check.ml | 2 +-
lib/form.ml | 3 +-
lib/prelude.ml | 28 +--
sand.flan | 112 ++++-----
syntax-sketch.flan | 8 +-
test/programs/agent-longname.flan | 2 +-
test/programs/break.flan | 4 +-
test/programs/cleanup.flan | 2 +-
test/programs/conditions.flan | 8 +-
test/programs/debug-permuted.flan | 2 +-
test/programs/debug.flan | 2 +-
test/programs/destructure.flan | 22 +-
test/programs/dev-break.flan | 2 +-
test/programs/dev-locals.flan | 4 +-
test/programs/dev-loop.flan | 2 +-
test/programs/edn.flan | 2 +-
test/programs/enum-compare.flan | 2 +-
test/programs/error.flan | 2 +-
test/programs/machine.flan | 4 +-
test/programs/pkg-return.flan | 2 +-
test/programs/raylib-audio.flan | 4 +-
test/programs/raylib-ffi.flan | 128 +++++-----
test/programs/raylib-font.flan | 28 +--
test/programs/raylib-image.flan | 8 +-
test/programs/reach-walk.flan | 2 +-
test/programs/restarts.flan | 4 +-
test/programs/values.flan | 2 +-
test/programs/vec.flan | 4 +-
test/programs/virtual-controls-headless.flan | 2 +-
test/test_acceptance.ml | 12 +-
test/test_dev.ml | 8 +-
test/test_flan.ml | 40 ++--
test/test_repl.ml | 4 +-
tools/colon-to-dot.py | 237 +++++++++++++++++++
web/examples/boom.flan | 2 +-
web/examples/breakdemo.flan | 2 +-
web/examples/conds.flan | 4 +-
web/examples/geom/vec.flan | 2 +-
web/examples/pkg.flan | 4 +-
web/examples/places.flan | 4 +-
web/examples/printing.flan | 2 +-
web/examples/restart.flan | 2 +-
web/examples/structs.flan | 2 +-
52 files changed, 550 insertions(+), 312 deletions(-)
create mode 100755 tools/colon-to-dot.py
diff --git a/calc-me.flan b/calc-me.flan
index d86ebd2..bd099b7 100644
--- a/calc-me.flan
+++ b/calc-me.flan
@@ -105,7 +105,7 @@
;; ── Whole input, or nothing. Trailing junk is an error, not ignored. ──
(defn evaluate [src [u8]] (Option f64)
- (let [c (Cursor {:src src})] ; pos omitted: zeroed
+ (let [c (Cursor {.src src})] ; pos omitted: zeroed
(let [v (some (parse-expr (addr c) 1))]
(skip-spaces (addr c))
(if (= (peek (addr c)) 0)
diff --git a/conditions-play.flan b/conditions-play.flan
index 662b27d..291598a 100644
--- a/conditions-play.flan
+++ b/conditions-play.flan
@@ -15,7 +15,7 @@
;;; Two frames under the restart-case, so a transfer has something to cross.
(defn probe [n i32] i32
- (signal (AssetMissing {:id n}))
+ (signal (AssetMissing {.id n}))
100)
(defn middle [n i32] i32
diff --git a/examples/core-delta-time.flan b/examples/core-delta-time.flan
index 5f6ed65..dc24721 100644
--- a/examples/core-delta-time.flan
+++ b/examples/core-delta-time.flan
@@ -41,9 +41,9 @@
(defer (rl/close-window))
(set current-fps 60)
- (set delta-circle (rl/Vector2 {:x 0.0 :y (/ (f32 screen-height) 3.0)}))
- (set frame-circle (rl/Vector2 {:x 0.0
- :y (* (f32 screen-height) (/ 2.0 3.0))}))
+ (set delta-circle (rl/Vector2 {.x 0.0 .y (/ (f32 screen-height) 3.0)}))
+ (set frame-circle (rl/Vector2 {.x 0.0
+ .y (* (f32 screen-height) (/ 2.0 3.0))}))
(rl/set-target-fps current-fps)
diff --git a/examples/core-input-gamepad.flan b/examples/core-input-gamepad.flan
index 167362c..6e4b574 100644
--- a/examples/core-input-gamepad.flan
+++ b/examples/core-input-gamepad.flan
@@ -92,7 +92,7 @@
(defn draw-pad-background []
(rl/draw-rectangle-rounded
- (rl/Rectangle {:x 175.0 :y 110.0 :width 460.0 :height 220.0})
+ (rl/Rectangle {.x 175.0 .y 110.0 .width 460.0 .height 220.0})
0.3 16 rl/darkgray)
;; The three middle buttons and the four face buttons, as outlines. The
@@ -113,10 +113,10 @@
(rl/draw-rectangle 217 176 84 25 rl/black)
(rl/draw-rectangle-rounded
- (rl/Rectangle {:x 215.0 :y 98.0 :width 100.0 :height 10.0})
+ (rl/Rectangle {.x 215.0 .y 98.0 .width 100.0 .height 10.0})
0.5 16 rl/darkgray)
(rl/draw-rectangle-rounded
- (rl/Rectangle {:x 495.0 :y 98.0 :width 100.0 :height 10.0})
+ (rl/Rectangle {.x 495.0 .y 98.0 .width 100.0 .height 10.0})
0.5 16 rl/darkgray))
(defn draw-pad-buttons []
@@ -146,11 +146,11 @@
(when (rl/gamepad-button-down? gamepad :left-trigger-1)
(rl/draw-rectangle-rounded
- (rl/Rectangle {:x 215.0 :y 98.0 :width 100.0 :height 10.0})
+ (rl/Rectangle {.x 215.0 .y 98.0 .width 100.0 .height 10.0})
0.5 16 rl/red))
(when (rl/gamepad-button-down? gamepad :right-trigger-1)
(rl/draw-rectangle-rounded
- (rl/Rectangle {:x 495.0 :y 98.0 :width 100.0 :height 10.0})
+ (rl/Rectangle {.x 495.0 .y 98.0 .width 100.0 .height 10.0})
0.5 16 rl/red)))
(defn draw-stick [cx i32 cy i32 ax f32 ay f32 thumb-down bool]
@@ -178,9 +178,9 @@
(when (rl/key-pressed? :right) (set gamepad (+ gamepad 1)))
(let [axis-count (min 6 (rl/get-gamepad-axis-count gamepad))
- vibrate-rect (rl/Rectangle {:x 10.0
- :y (+ 90.0 (* 20.0 (f32 axis-count)))
- :width 75.0 :height 24.0})]
+ vibrate-rect (rl/Rectangle {.x 10.0
+ .y (+ 90.0 (* 20.0 (f32 axis-count)))
+ .width 75.0 .height 24.0})]
;; Draw
(rl/begin-drawing)
diff --git a/examples/core-input-gestures-testbed.flan b/examples/core-input-gestures-testbed.flan
index b21f746..833968c 100644
--- a/examples/core-input-gestures-testbed.flan
+++ b/examples/core-input-gestures-testbed.flan
@@ -179,7 +179,7 @@
(rl/draw-circle (+ last-x 80) (+ last-y 16) 10.0
(if (= last-gesture :tap) rl/blue rl/lightgray))
;; segments 0 lets raylib pick the count from the radius.
- (rl/draw-ring (rl/Vector2 {:x (f32 (+ last-x 103)) :y (f32 (+ last-y 16))})
+ (rl/draw-ring (rl/Vector2 {.x (f32 (+ last-x 103)) .y (f32 (+ last-y 16))})
6.0 11.0 0.0 360.0 0
(if (= last-gesture :drag) rl/lime rl/lightgray))
(rl/draw-circle (+ last-x 80) (+ last-y 43) 10.0
@@ -191,21 +191,21 @@
;; for pinch-in. Counter-clockwise winding, or raylib culls them.
(let [out-c (if (= last-gesture :pinch-out) rl/orange rl/lightgray)
in-c (if (= last-gesture :pinch-in) rl/violet rl/lightgray)]
- (rl/draw-triangle (rl/Vector2 {:x (f32 (+ last-x 122)) :y (f32 (+ last-y 16))})
- (rl/Vector2 {:x (f32 (+ last-x 137)) :y (f32 (+ last-y 26))})
- (rl/Vector2 {:x (f32 (+ last-x 137)) :y (f32 (+ last-y 6))})
+ (rl/draw-triangle (rl/Vector2 {.x (f32 (+ last-x 122)) .y (f32 (+ last-y 16))})
+ (rl/Vector2 {.x (f32 (+ last-x 137)) .y (f32 (+ last-y 26))})
+ (rl/Vector2 {.x (f32 (+ last-x 137)) .y (f32 (+ last-y 6))})
out-c)
- (rl/draw-triangle (rl/Vector2 {:x (f32 (+ last-x 147)) :y (f32 (+ last-y 6))})
- (rl/Vector2 {:x (f32 (+ last-x 147)) :y (f32 (+ last-y 26))})
- (rl/Vector2 {:x (f32 (+ last-x 162)) :y (f32 (+ last-y 16))})
+ (rl/draw-triangle (rl/Vector2 {.x (f32 (+ last-x 147)) .y (f32 (+ last-y 6))})
+ (rl/Vector2 {.x (f32 (+ last-x 147)) .y (f32 (+ last-y 26))})
+ (rl/Vector2 {.x (f32 (+ last-x 162)) .y (f32 (+ last-y 16))})
out-c)
- (rl/draw-triangle (rl/Vector2 {:x (f32 (+ last-x 125)) :y (f32 (+ last-y 33))})
- (rl/Vector2 {:x (f32 (+ last-x 125)) :y (f32 (+ last-y 53))})
- (rl/Vector2 {:x (f32 (+ last-x 140)) :y (f32 (+ last-y 43))})
+ (rl/draw-triangle (rl/Vector2 {.x (f32 (+ last-x 125)) .y (f32 (+ last-y 33))})
+ (rl/Vector2 {.x (f32 (+ last-x 125)) .y (f32 (+ last-y 53))})
+ (rl/Vector2 {.x (f32 (+ last-x 140)) .y (f32 (+ last-y 43))})
in-c)
- (rl/draw-triangle (rl/Vector2 {:x (f32 (+ last-x 144)) :y (f32 (+ last-y 43))})
- (rl/Vector2 {:x (f32 (+ last-x 159)) :y (f32 (+ last-y 53))})
- (rl/Vector2 {:x (f32 (+ last-x 159)) :y (f32 (+ last-y 33))})
+ (rl/draw-triangle (rl/Vector2 {.x (f32 (+ last-x 144)) .y (f32 (+ last-y 43))})
+ (rl/Vector2 {.x (f32 (+ last-x 159)) .y (f32 (+ last-y 53))})
+ (rl/Vector2 {.x (f32 (+ last-x 159)) .y (f32 (+ last-y 33))})
in-c))
;; Four pips, one per simultaneous touch raylib reports.
@@ -223,8 +223,8 @@
(if (= i 0) gesture-color rl/lightgray))))
;; The two mode buttons. Maroon means the mode that button controls is on.
- (let [b1 (rl/Rectangle {:x 53.0 :y 7.0 :width 48.0 :height 26.0})
- b2 (rl/Rectangle {:x 108.0 :y 7.0 :width 36.0 :height 26.0})]
+ (let [b1 (rl/Rectangle {.x 53.0 .y 7.0 .width 48.0 .height 26.0})
+ b2 (rl/Rectangle {.x 108.0 .y 7.0 .width 36.0 .height 26.0})]
(rl/draw-rectangle-rec b1 (if (or (= log-mode 1) (= log-mode 3))
rl/maroon rl/gray))
(rl/draw-text "Hide" 60 10 10 rl/white)
@@ -242,16 +242,16 @@
(d/draw-f32 current-angle 2 (+ (i32 prot-x) 55) (+ (i32 prot-y) 92) 20
gesture-color)
- (rl/draw-circle-v (rl/Vector2 {:x prot-x :y prot-y}) 80.0 rl/white)
- (rl/draw-line-ex (rl/Vector2 {:x (- prot-x 90.0) :y prot-y})
- (rl/Vector2 {:x (+ prot-x 90.0) :y prot-y}) 3.0 rl/lightgray)
- (rl/draw-line-ex (rl/Vector2 {:x prot-x :y (- prot-y 90.0)})
- (rl/Vector2 {:x prot-x :y (+ prot-y 90.0)}) 3.0 rl/lightgray)
- (rl/draw-line-ex (rl/Vector2 {:x (- prot-x 80.0) :y (- prot-y 45.0)})
- (rl/Vector2 {:x (+ prot-x 80.0) :y (+ prot-y 45.0)}) 3.0
+ (rl/draw-circle-v (rl/Vector2 {.x prot-x .y prot-y}) 80.0 rl/white)
+ (rl/draw-line-ex (rl/Vector2 {.x (- prot-x 90.0) .y prot-y})
+ (rl/Vector2 {.x (+ prot-x 90.0) .y prot-y}) 3.0 rl/lightgray)
+ (rl/draw-line-ex (rl/Vector2 {.x prot-x .y (- prot-y 90.0)})
+ (rl/Vector2 {.x prot-x .y (+ prot-y 90.0)}) 3.0 rl/lightgray)
+ (rl/draw-line-ex (rl/Vector2 {.x (- prot-x 80.0) .y (- prot-y 45.0)})
+ (rl/Vector2 {.x (+ prot-x 80.0) .y (+ prot-y 45.0)}) 3.0
rl/green)
- (rl/draw-line-ex (rl/Vector2 {:x (- prot-x 80.0) :y (+ prot-y 45.0)})
- (rl/Vector2 {:x (+ prot-x 80.0) :y (- prot-y 45.0)}) 3.0
+ (rl/draw-line-ex (rl/Vector2 {.x (- prot-x 80.0) .y (+ prot-y 45.0)})
+ (rl/Vector2 {.x (+ prot-x 80.0) .y (- prot-y 45.0)}) 3.0
rl/green)
(rl/draw-text "0" (+ (i32 prot-x) 96) (- (i32 prot-y) 9) 20 rl/black)
@@ -267,9 +267,9 @@
;; against cos feeding y is what rotates it the way the dial is labelled.
(unless (= current-angle 0.0)
(let [rad (/ (* (+ current-angle 90.0) pi) 180.0)]
- (rl/draw-line-ex (rl/Vector2 {:x prot-x :y prot-y})
- (rl/Vector2 {:x (+ (* angle-length (sin-f32 rad)) prot-x)
- :y (+ (* angle-length (cos-f32 rad)) prot-y)})
+ (rl/draw-line-ex (rl/Vector2 {.x prot-x .y prot-y})
+ (rl/Vector2 {.x (+ (* angle-length (sin-f32 rad)) prot-x)
+ .y (+ (* angle-length (cos-f32 rad)) prot-y)})
3.0 gesture-color))))
(defn main []
@@ -285,8 +285,8 @@
(rl/set-target-fps 60)
- (let [b1 (rl/Rectangle {:x 53.0 :y 7.0 :width 48.0 :height 26.0})
- b2 (rl/Rectangle {:x 108.0 :y 7.0 :width 36.0 :height 26.0})]
+ (let [b1 (rl/Rectangle {.x 53.0 .y 7.0 .width 48.0 .height 26.0})
+ b2 (rl/Rectangle {.x 108.0 .y 7.0 .width 36.0 .height 26.0})]
(until (rl/window-should-close?)
;; Update
(let [g (rl/get-gesture-detected)
diff --git a/examples/core-input-gestures.flan b/examples/core-input-gestures.flan
index 364c9e5..3c97431 100644
--- a/examples/core-input-gestures.flan
+++ b/examples/core-input-gestures.flan
@@ -59,9 +59,9 @@
(rl/set-target-fps 60)
- (let [touch-area (rl/Rectangle {:x 220.0 :y 10.0
- :width (- (f32 screen-width) 230.0)
- :height (- (f32 screen-height) 20.0)})]
+ (let [touch-area (rl/Rectangle {.x 220.0 .y 10.0
+ .width (- (f32 screen-width) 230.0)
+ .height (- (f32 screen-height) 20.0)})]
(until (rl/window-should-close?)
;; Update
(set last-gesture current-gesture)
diff --git a/examples/core-input-keys.flan b/examples/core-input-keys.flan
index 9f8c95d..6eabef1 100644
--- a/examples/core-input-keys.flan
+++ b/examples/core-input-keys.flan
@@ -26,8 +26,8 @@
"raylib [core] example - input keys")
(defer (rl/close-window))
- (set ball (rl/Vector2 {:x (f32 (/ screen-width 2))
- :y (f32 (/ screen-height 2))}))
+ (set ball (rl/Vector2 {.x (f32 (/ screen-width 2))
+ .y (f32 (/ screen-height 2))}))
(rl/set-target-fps 60)
diff --git a/examples/core-input-virtual-controls.flan b/examples/core-input-virtual-controls.flan
index df80dd3..aba327b 100644
--- a/examples/core-input-virtual-controls.flan
+++ b/examples/core-input-virtual-controls.flan
@@ -58,10 +58,10 @@
;; it on each axis. The C computes these at run time from padPosition; they are
;; written out here because a defconst is compile-time and the numbers are.
(defconst button-positions [button-max rl/Vector2]
- [(rl/Vector2 {:x 100.0 :y 305.0}) ; up
- (rl/Vector2 {:x 55.0 :y 350.0}) ; left
- (rl/Vector2 {:x 145.0 :y 350.0}) ; right
- (rl/Vector2 {:x 100.0 :y 395.0})]) ; down
+ [(rl/Vector2 {.x 100.0 .y 305.0}) ; up
+ (rl/Vector2 {.x 55.0 .y 350.0}) ; left
+ (rl/Vector2 {.x 145.0 .y 350.0}) ; right
+ (rl/Vector2 {.x 100.0 .y 395.0})]) ; down
(defconst player-speed f32 75.0)
@@ -70,8 +70,8 @@
(defvar player rl/Vector2)
(defn reset-player []
- (set player (rl/Vector2 {:x (/ (f32 screen-width) 2.0)
- :y (/ (f32 screen-height) 2.0)})))
+ (set player (rl/Vector2 {.x (/ (f32 screen-width) 2.0)
+ .y (/ (f32 screen-height) 2.0)})))
(defn abs-f32 [v f32] f32
(max v (- 0.0 v)))
@@ -130,24 +130,24 @@
;; [4 [3 rl/Vector2]]: a fixed array of fixed arrays of a struct, which is the
;; deepest shape any of these ten examples asks for and which works.
(defconst arrow-tris [button-max [3 rl/Vector2]]
- [[(rl/Vector2 {:x 100.0 :y 293.0})
- (rl/Vector2 {:x 91.0 :y 314.0})
- (rl/Vector2 {:x 109.0 :y 314.0})]
- [(rl/Vector2 {:x 64.0 :y 341.0})
- (rl/Vector2 {:x 43.0 :y 350.0})
- (rl/Vector2 {:x 64.0 :y 359.0})]
- [(rl/Vector2 {:x 157.0 :y 350.0})
- (rl/Vector2 {:x 136.0 :y 341.0})
- (rl/Vector2 {:x 136.0 :y 359.0})]
- [(rl/Vector2 {:x 91.0 :y 386.0})
- (rl/Vector2 {:x 100.0 :y 407.0})
- (rl/Vector2 {:x 109.0 :y 386.0})]])
+ [[(rl/Vector2 {.x 100.0 .y 293.0})
+ (rl/Vector2 {.x 91.0 .y 314.0})
+ (rl/Vector2 {.x 109.0 .y 314.0})]
+ [(rl/Vector2 {.x 64.0 .y 341.0})
+ (rl/Vector2 {.x 43.0 .y 350.0})
+ (rl/Vector2 {.x 64.0 .y 359.0})]
+ [(rl/Vector2 {.x 157.0 .y 350.0})
+ (rl/Vector2 {.x 136.0 .y 341.0})
+ (rl/Vector2 {.x 136.0 .y 359.0})]
+ [(rl/Vector2 {.x 91.0 .y 386.0})
+ (rl/Vector2 {.x 100.0 .y 407.0})
+ (rl/Vector2 {.x 109.0 .y 386.0})]])
(defconst label-colors [button-max rl/Color]
- [(rl/Color {:r 253 :g 249 :b 0 :a 255}) ; yellow, up
- (rl/Color {:r 0 :g 121 :b 241 :a 255}) ; blue, left
- (rl/Color {:r 230 :g 41 :b 55 :a 255}) ; red, right
- (rl/Color {:r 0 :g 228 :b 48 :a 255})]) ; green, down
+ [(rl/Color {.r 253 .g 249 .b 0 .a 255}) ; yellow, up
+ (rl/Color {.r 0 .g 121 .b 241 .a 255}) ; blue, left
+ (rl/Color {.r 230 .g 41 .b 55 .a 255}) ; red, right
+ (rl/Color {.r 0 .g 228 .b 48 .a 255})]) ; green, down
(defn main []
(rl/init-window screen-width screen-height
diff --git a/lib/ast.ml b/lib/ast.ml
index c37147e..9485081 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -46,7 +46,7 @@ and expr_kind =
| Field of expr * string (* (.pos c) — auto-derefs one level *)
| Call of expr * expr list
| Match of expr * arm list
- | Struct of string * (string * expr) list (* (Cursor {:src s}) *)
+ | Struct of string * (string * expr) list (* (Cursor {.src s}) *)
| Arr of expr list (* [0xE6B800FF ...] — a fixed array value *)
(* These bind names or alter control flow, so none of them can be a call. *)
| Fn of string list * expr list (* (fn [x y] ...) — non-escaping *)
diff --git a/lib/check.ml b/lib/check.ml
index 91be36e..3a26a5d 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -2486,7 +2486,7 @@ and named_call ctx ~want loc name args =
if Hashtbl.mem ctx.env.structs name || Hashtbl.mem ctx.env.unions name
then
fail loc
- "%s is a type — a struct value is written (%s {:field value ...})"
+ "%s is a type — a struct value is written (%s {.field value ...})"
name name
else if String.contains name '/' then
unimplemented loc
diff --git a/lib/form.ml b/lib/form.ml
index 6c81f11..8ee4239 100644
--- a/lib/form.ml
+++ b/lib/form.ml
@@ -17,7 +17,8 @@ and value =
| Byte of int (* \space \0 \( (0..255) *)
| List of t list (* (f x) *)
| Vec of t list (* [1 2 3] and every binding/type bracket *)
- | Map of t list (* {:key v} in value position, {K V} in type position *)
+ | Map of t list (* {.field v} a struct value, {K V} a type. The
+ colon spelling is left for map literals. *)
let make v loc = { v; loc }
diff --git a/lib/prelude.ml b/lib/prelude.ml
index 5ce9bd5..f6d32c0 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -526,10 +526,10 @@ let source = {flan|
(defn decode-rune [s [u8]] Rune
(when (= (len s) 0)
- (return (Rune {:code 0 :width 0 :ok false})))
+ (return (Rune {.code 0 .width 0 .ok false})))
(let [b0 (at s 0)]
(when (< b0 0x80)
- (return (Rune {:code (i32 b0) :width 1 :ok true})))
+ (return (Rune {.code (i32 b0) .width 1 .ok true})))
;; size 0 means "this byte cannot lead"; lo/hi are the *second* byte's
;; accepted range, which is the only place the overlong and surrogate
;; rules live. Bytes three and four are always 0x80..0xbf.
@@ -548,34 +548,34 @@ let source = {flan|
(= b0 0xf4) (do (set size 4) (set hi (u8 0x8f)))
:else (set size 0))
(when (= size 0)
- (return (Rune {:code 0 :width 1 :ok false})))
+ (return (Rune {.code 0 .width 1 .ok false})))
;; A sequence cut off by the end of the slice. Width 1, so a caller
;; scanning a buffer boundary makes progress instead of stalling.
(when (> size (len s))
- (return (Rune {:code 0 :width 1 :ok false})))
+ (return (Rune {.code 0 .width 1 .ok false})))
(let [b1 (at s 1)]
(when (or (< b1 lo) (> b1 hi))
- (return (Rune {:code 0 :width 1 :ok false})))
+ (return (Rune {.code 0 .width 1 .ok false})))
(when (= size 2)
- (return (Rune {:code (bit-or (<< (i32 (bit-and b0 0x1f)) 6)
+ (return (Rune {.code (bit-or (<< (i32 (bit-and b0 0x1f)) 6)
(i32 (bit-and b1 0x3f)))
- :width 2 :ok true})))
+ .width 2 .ok true})))
(let [b2 (at s 2)]
(when (or (< b2 0x80) (> b2 0xbf))
- (return (Rune {:code 0 :width 1 :ok false})))
+ (return (Rune {.code 0 .width 1 .ok false})))
(when (= size 3)
- (return (Rune {:code (bit-or (bit-or (<< (i32 (bit-and b0 0x0f)) 12)
+ (return (Rune {.code (bit-or (bit-or (<< (i32 (bit-and b0 0x0f)) 12)
(<< (i32 (bit-and b1 0x3f)) 6))
(i32 (bit-and b2 0x3f)))
- :width 3 :ok true})))
+ .width 3 .ok true})))
(let [b3 (at s 3)]
(when (or (< b3 0x80) (> b3 0xbf))
- (return (Rune {:code 0 :width 1 :ok false})))
- (Rune {:code (bit-or (bit-or (<< (i32 (bit-and b0 0x07)) 18)
+ (return (Rune {.code 0 .width 1 .ok false})))
+ (Rune {.code (bit-or (bit-or (<< (i32 (bit-and b0 0x07)) 18)
(bit-or (<< (i32 (bit-and b1 0x3f)) 12)
(<< (i32 (bit-and b2 0x3f)) 6)))
(i32 (bit-and b3 0x3f)))
- :width 4 :ok true})))))))
+ .width 4 .ok true})))))))
;; Decode at a byte offset. None when the offset is not on a rune boundary or
;; the bytes there are malformed, which is stricter than Odin's rune_at — that
@@ -683,7 +683,7 @@ let source = {flan|
(defstruct Split [rest [u8] sep u8 more bool])
(defn split-on-byte [s [u8] sep u8] Split
- (Split {:rest s :sep sep :more true}))
+ (Split {.rest s .sep sep .more true}))
(defn split-next! [it (Ptr Split)] (Option [u8])
(when (not (.more it))
diff --git a/sand.flan b/sand.flan
index bd3fbce..f20da9f 100644
--- a/sand.flan
+++ b/sand.flan
@@ -101,7 +101,7 @@
;; over a mutable scan position, which is what a while loop is.
(defn settle [row i32 col i32]
(let [vel (+ gravity (at velocity row col))
- some-point (rl/Vector2 {:x 15.0 :y 12})
+ some-point (rl/Vector2 {.x 15.0 .y 12})
y (min (- rows 1) (+ row (i32 vel)))]
(while (> y row)
(when (empty-at? y col)
@@ -217,18 +217,18 @@
(let [m (rl/get-mouse-position)
frame (f32 (if (rl/mouse-button-down? :left) 8.0 0.0))]
(rl/draw-texture-rec brush
- (rl/Rectangle {:x frame :y 0.0 :width 8.0 :height 8.0})
- (rl/Vector2 {:x (- (.x m) 4.0) :y (- (.y m) 4.0)})
+ (rl/Rectangle {.x frame .y 0.0 .width 8.0 .height 8.0})
+ (rl/Vector2 {.x (- (.x m) 4.0) .y (- (.y m) 4.0)})
rl/white)
(rl/draw-texture brush 20 50 rl/white)
- (rl/draw-texture-v brush (rl/Vector2 {:x 44.0 :y 50.0})
+ (rl/draw-texture-v brush (rl/Vector2 {.x 44.0 .y 50.0})
(rl/get-color (at colors current-color)))
- (rl/draw-texture-ex brush (rl/Vector2 {:x 72.0 :y 46.0}) 0.0 2.0 rl/white)))
+ (rl/draw-texture-ex brush (rl/Vector2 {.x 72.0 .y 46.0}) 0.0 2.0 rl/white)))
;; The mirrored one beside them, scaled up so the flip is visible rather
;; than eight pixels wide. If the two badges look the same, either the flip
;; did nothing or the upload took the unedited buffer.
(when brush-mirrored-ok
- (rl/draw-texture-ex brush-mirrored (rl/Vector2 {:x 110.0 :y 46.0})
+ (rl/draw-texture-ex brush-mirrored (rl/Vector2 {.x 110.0 .y 46.0})
0.0 2.0 rl/white)))
;; ── Sound ──────────────────────────────────────────────────────────
@@ -281,9 +281,9 @@
(unless audio-ok
(println "sand: no audio device — the grains are silent"))
(build-tone 40)
- (let [w (rl/Wave {:frame-count (u32 tone-frames) :sample-rate (u32 tone-rate)
- :sample-size 16 :channels 1
- :data (addr (at tone-pcm 0))})]
+ (let [w (rl/Wave {.frame-count (u32 tone-frames) .sample-rate (u32 tone-rate)
+ .sample-size 16 .channels 1
+ .data (addr (at tone-pcm 0))})]
(when audio-ok
(set tone (rl/load-sound-from-wave w))
(set tone-ok (rl/sound-valid? tone))
@@ -376,16 +376,16 @@
;; A fresh (Camera2D {}) has a zoom of 0, which is singular: both conversions
;; hand back NaN and nothing draws. 1.0 is the identity.
(defn reset-view []
- (set view (rl/Camera2D {:offset (rl/Vector2 {:x 0.0 :y 0.0})
- :target (rl/Vector2 {:x 0.0 :y 0.0})
- :rotation 0.0
- :zoom 1.0})))
+ (set view (rl/Camera2D {.offset (rl/Vector2 {.x 0.0 .y 0.0})
+ .target (rl/Vector2 {.x 0.0 .y 0.0})
+ .rotation 0.0
+ .zoom 1.0})))
(defn set-view [target-x f32 target-y f32 zoom f32]
- (set view (rl/Camera2D {:offset (.offset view)
- :target (rl/Vector2 {:x target-x :y target-y})
- :rotation (.rotation view)
- :zoom zoom})))
+ (set view (rl/Camera2D {.offset (.offset view)
+ .target (rl/Vector2 {.x target-x .y target-y})
+ .rotation (.rotation view)
+ .zoom zoom})))
;; Panning is world units per second and zooming is a factor per second, so
;; neither changes with the frame rate. That is the whole of what
@@ -482,12 +482,12 @@
(rl/draw-circle-lines-v p (+ r (f32 6.0)) tint)
;; A crosshair: two thin lines and one thick one, which is three separate
;; raylib calls with three different shapes of argument.
- (rl/draw-line-v (rl/Vector2 {:x (- x r) :y y})
- (rl/Vector2 {:x (+ x r) :y y}) tint)
- (rl/draw-line-v (rl/Vector2 {:x x :y (- y r)})
- (rl/Vector2 {:x x :y (+ y r)}) tint)
- (rl/draw-line-ex (rl/Vector2 {:x (- x (f32 4.0)) :y y})
- (rl/Vector2 {:x (+ x (f32 4.0)) :y y}) (f32 3.0) rl/white)
+ (rl/draw-line-v (rl/Vector2 {.x (- x r) .y y})
+ (rl/Vector2 {.x (+ x r) .y y}) tint)
+ (rl/draw-line-v (rl/Vector2 {.x x .y (- y r)})
+ (rl/Vector2 {.x x .y (+ y r)}) tint)
+ (rl/draw-line-ex (rl/Vector2 {.x (- x (f32 4.0)) .y y})
+ (rl/Vector2 {.x (+ x (f32 4.0)) .y y}) (f32 3.0) rl/white)
;; The exact world point: a filled dot, and one pixel of white on top of
;; it. Both are the Vector2 forms, so they land where the ring's centre
;; is and not somewhere an integer cast put them.
@@ -496,16 +496,16 @@
;; A pointer above the cursor. Counter-clockwise, because raylib culls the
;; other winding and draws nothing — which looks exactly like a broken
;; binding and is why the outline is drawn over it as a control.
- (let [tip (rl/Vector2 {:x x :y (- y (+ r (f32 26.0)))})
- left (rl/Vector2 {:x (- x (f32 12.0)) :y (- y (+ r (f32 6.0)))})
- rght (rl/Vector2 {:x (+ x (f32 12.0)) :y (- y (+ r (f32 6.0)))})]
+ (let [tip (rl/Vector2 {.x x .y (- y (+ r (f32 26.0)))})
+ left (rl/Vector2 {.x (- x (f32 12.0)) .y (- y (+ r (f32 6.0)))})
+ rght (rl/Vector2 {.x (+ x (f32 12.0)) .y (- y (+ r (f32 6.0)))})]
(rl/draw-triangle tip left rght tint)
(rl/draw-triangle-lines tip left rght rl/white))
;; And the world's own edge, so panning has something to pan against.
(rl/draw-rectangle-lines-ex
- (rl/Rectangle {:x 0.0 :y 0.0
- :width (f32 screen-width)
- :height (f32 screen-height)})
+ (rl/Rectangle {.x 0.0 .y 0.0
+ .width (f32 screen-width)
+ .height (f32 screen-height)})
(f32 2.0) (rl/get-color 0x303030FF))))
;; Drawn outside the camera, in screen pixels, so it stays put while the world
@@ -532,8 +532,8 @@
h 72
x 24
y (- (rl/get-screen-height) (+ h 24))
- panel (rl/Rectangle {:x (f32 (- x 12)) :y (f32 (- y 12))
- :width (f32 (+ w 24)) :height (f32 (+ h 24))})]
+ panel (rl/Rectangle {.x (f32 (- x 12)) .y (f32 (- y 12))
+ .width (f32 (+ w 24)) .height (f32 (+ h 24))})]
(rl/draw-rectangle-rounded panel (f32 0.2) 8 (rl/get-color 0x101018E0))
;; Both outline forms, one inside the other: the plain one has no
;; thickness in raylib 5.5 and the -ex one is where thickness went.
@@ -541,7 +541,7 @@
(rl/draw-rectangle-rounded-lines-ex panel (f32 0.2) 8 (f32 2.0)
(rl/get-color 0x6060A0FF))
(if hud-font-ok
- (rl/draw-text-ex hud-font title (rl/Vector2 {:x (f32 x) :y (f32 y)})
+ (rl/draw-text-ex hud-font title (rl/Vector2 {.x (f32 x) .y (f32 y)})
30.0 4.0 rl/white)
(rl/draw-text title x y 30 rl/white))
(rl/draw-text keys x (+ y 40) 20 (rl/get-color 0xA0A0B0FF))
@@ -565,8 +565,8 @@
(let [cp (+ 48 current-color)]
(when (> (rl/get-glyph-index hud-font cp) 0)
(rl/draw-text-codepoint hud-font cp
- (rl/Vector2 {:x (f32 (- sw 24))
- :y (f32 (- sh 96))})
+ (rl/Vector2 {.x (f32 (- sw 24))
+ .y (f32 (- sh 96))})
30.0 rl/white))))
;; A zoom read-out with no number in it, because there is no string
@@ -577,11 +577,11 @@
(let [bx (f32 (- sw 220))
by (f32 (- sh 60))
fill (* (f32 200.0) (min (f32 1.0) (/ (.zoom view) (f32 8.0))))]
- (rl/draw-rectangle-rec (rl/Rectangle {:x bx :y by :width (f32 200.0)
- :height (f32 10.0)})
+ (rl/draw-rectangle-rec (rl/Rectangle {.x bx .y by .width (f32 200.0)
+ .height (f32 10.0)})
(rl/get-color 0x202028FF))
- (rl/draw-rectangle-v (rl/Vector2 {:x bx :y by})
- (rl/Vector2 {:x fill :y (f32 10.0)})
+ (rl/draw-rectangle-v (rl/Vector2 {.x bx .y by})
+ (rl/Vector2 {.x fill .y (f32 10.0)})
(rl/get-color 0x6060A0FF))
(rl/draw-rectangle-lines (- sw 220) (- sh 60) 200 10
(rl/get-color 0x8080C0FF))
@@ -600,7 +600,7 @@
(rl/draw-ellipse ex ey (f32 26.0) (f32 12.0) (rl/get-color 0x303040FF))
(rl/draw-ellipse-lines ex ey (f32 26.0) (f32 12.0)
(rl/get-color 0x8080C0FF))
- (rl/draw-ring-lines (rl/Vector2 {:x (f32 ex) :y (f32 ey)})
+ (rl/draw-ring-lines (rl/Vector2 {.x (f32 ex) .y (f32 ey)})
(f32 30.0) (f32 34.0) spin (+ spin (f32 270.0)) 32
rl/white)))))
@@ -628,19 +628,19 @@
;; separates "centred stick" from "no pad" — both of which are a dot in
;; the middle otherwise.
(rl/draw-circle-lines (i32 ox) (i32 oy) r (rl/get-color 0x6060A0FF))
- (let [p (rl/Vector2 {:x (+ ox (* (rl/get-gamepad-axis-movement 0 :left-x) r))
- :y (+ oy (* (rl/get-gamepad-axis-movement 0 :left-y) r))})]
+ (let [p (rl/Vector2 {.x (+ ox (* (rl/get-gamepad-axis-movement 0 :left-x) r))
+ .y (+ oy (* (rl/get-gamepad-axis-movement 0 :left-y) r))})]
(rl/draw-circle-v p (f32 5.0) rl/white))
;; The two triggers as bars of different lengths, so exchanging them is
;; visible. They rest at -1 and not at 0, which raylib does not
;; normalise and neither does this — hence the +1.
(let [lt (+ (f32 1.0) (rl/get-gamepad-axis-movement 0 :left-trigger))
rt (+ (f32 1.0) (rl/get-gamepad-axis-movement 0 :right-trigger))]
- (rl/draw-rectangle-v (rl/Vector2 {:x (+ ox (f32 46.0)) :y (- oy (f32 12.0))})
- (rl/Vector2 {:x (* lt (f32 30.0)) :y (f32 8.0)})
+ (rl/draw-rectangle-v (rl/Vector2 {.x (+ ox (f32 46.0)) .y (- oy (f32 12.0))})
+ (rl/Vector2 {.x (* lt (f32 30.0)) .y (f32 8.0)})
(rl/get-color 0x8080C0FF))
- (rl/draw-rectangle-v (rl/Vector2 {:x (+ ox (f32 46.0)) :y (+ oy (f32 4.0))})
- (rl/Vector2 {:x (* rt (f32 50.0)) :y (f32 8.0)})
+ (rl/draw-rectangle-v (rl/Vector2 {.x (+ ox (f32 46.0)) .y (+ oy (f32 4.0))})
+ (rl/Vector2 {.x (* rt (f32 50.0)) .y (f32 8.0)})
(rl/get-color 0x8080C0FF)))
;; One pip per axis the pad reports, and one per face button that is
;; NOT up — gamepad-button-up? rather than -down? so the negative form
@@ -661,8 +661,8 @@
(rl/draw-circle-lines-v (rl/get-touch-position i) (f32 18.0)
(rl/get-color 0xA0A0FFFF))
(rl/draw-pixel (rl/get-touch-x) (rl/get-touch-y) rl/white)
- (rl/draw-pixel-v (rl/Vector2 {:x (f32 (rl/get-touch-point-id i))
- :y (f32 4.0)})
+ (rl/draw-pixel-v (rl/Vector2 {.x (f32 (rl/get-touch-point-id i))
+ .y (f32 4.0)})
rl/white))
;; And the gesture, if any: a bar as long as the hold has lasted, and the
@@ -673,14 +673,14 @@
8 rl/white)
(when (rl/gesture-detected? :drag)
(let [d (rl/get-gesture-drag-vector)]
- (rl/draw-line-v (rl/Vector2 {:x ox :y oy})
- (rl/Vector2 {:x (+ ox (* (.x d) (f32 200.0)))
- :y (+ oy (* (.y d) (f32 200.0)))})
+ (rl/draw-line-v (rl/Vector2 {.x ox .y oy})
+ (rl/Vector2 {.x (+ ox (* (.x d) (f32 200.0)))
+ .y (+ oy (* (.y d) (f32 200.0)))})
(rl/get-color 0xFFC000FF))))
(when (rl/gesture-detected? :pinch-in)
(let [q (rl/get-gesture-pinch-vector)]
- (rl/draw-circle-v (rl/Vector2 {:x (+ ox (* (.x q) (f32 200.0)))
- :y (+ oy (* (.y q) (f32 200.0)))})
+ (rl/draw-circle-v (rl/Vector2 {.x (+ ox (* (.x q) (f32 200.0)))
+ .y (+ oy (* (.y q) (f32 200.0)))})
(f32 4.0) rl/white))))))
(defn draw-world []
@@ -706,10 +706,10 @@
;; screen and nothing else changes.
(if scene-ok
(rl/draw-texture-rec (.texture scene)
- (rl/Rectangle {:x 0.0 :y 0.0
- :width (f32 screen-width)
- :height (f32 (- 0 screen-height))})
- (rl/Vector2 {:x 0.0 :y 0.0})
+ (rl/Rectangle {.x 0.0 .y 0.0
+ .width (f32 screen-width)
+ .height (f32 (- 0 screen-height))})
+ (rl/Vector2 {.x 0.0 .y 0.0})
rl/white)
(draw-world))
;; And everything after it is in screen pixels again.
diff --git a/syntax-sketch.flan b/syntax-sketch.flan
index 3afe994..7e08405 100644
--- a/syntax-sketch.flan
+++ b/syntax-sketch.flan
@@ -100,8 +100,8 @@
(let [text (try (read-file path))
table (try (parse-toml text))
port (try (ok-or (get table "port")
- (MissingKey {:key "port"})))]
- (Ok (Config {:port port}))))
+ (MissingKey {.key "port"})))]
+ (Ok (Config {.port port}))))
;; errdefer runs only on the Result failure path — NOT on a restart transfer
;; (spec-conditions.md §5). Pairs with explicit allocation.
@@ -109,7 +109,7 @@
(let [buf (alloc-image context/allocator)]
(errdefer (free buf))
(try (decode-png path buf))
- (Ok (Atlas {:image buf}))))
+ (Ok (Atlas {.image buf}))))
;; ── Conditions: handlers run on the signalling frame, nothing unwinds ─
;; load-texture cannot know the right recovery — an editor wants a placeholder,
@@ -125,7 +125,7 @@
(if (file-exists? path)
(rl/load-texture path)
(restart-case
- (do (signal (AssetMissing {:path path}))
+ (do (signal (AssetMissing {.path path}))
(abort "unhandled AssetMissing"))
(use-placeholder [] placeholder-texture)
(retry [] (load-texture path)))))
diff --git a/test/programs/agent-longname.flan b/test/programs/agent-longname.flan
index c1a7d2f..4e7d86e 100644
--- a/test/programs/agent-longname.flan
+++ b/test/programs/agent-longname.flan
@@ -11,5 +11,5 @@
(defn main [] i32
;; The path is overridden by FLAN_AGENT_SOCKET; a program has to name one.
(agent/start "/tmp/flan-longname.sock")
- (error (MissingYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY {:id 1}))
+ (error (MissingYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY {.id 1}))
0)
diff --git a/test/programs/break.flan b/test/programs/break.flan
index 64a8d5b..fe37c9b 100644
--- a/test/programs/break.flan
+++ b/test/programs/break.flan
@@ -10,7 +10,7 @@
(defn fetch [n i32] i32
(restart-case
- (do (error (Missing {:id n})) 0)
+ (do (error (Missing {.id n})) 0)
(use-placeholder [] -1)
(retry [] 7)))
@@ -21,7 +21,7 @@
;;; file that by-name lookup cannot produce.
(defn shadowed [n i32] i32
(restart-case
- (+ (restart-case (do (error (Missing {:id n})) 0)
+ (+ (restart-case (do (error (Missing {.id n})) 0)
(retry [] 5))
100)
(retry [] 900)))
diff --git a/test/programs/cleanup.flan b/test/programs/cleanup.flan
index 19b3a4a..4fa80d2 100644
--- a/test/programs/cleanup.flan
+++ b/test/programs/cleanup.flan
@@ -26,7 +26,7 @@
;;; and (6) a handler-bind with two clauses has to pop both, innermost-first.
;;; If either leaks, the stack keeps a frame pointing into a function that has
;;; gone, and the next signal calls into it.
-(defn deep [] i32 (signal (Missing {:id 1})) 0)
+(defn deep [] i32 (signal (Missing {.id 1})) 0)
(defn leaky [] i32
(restart-case
diff --git a/test/programs/conditions.flan b/test/programs/conditions.flan
index c578016..b2b24b8 100644
--- a/test/programs/conditions.flan
+++ b/test/programs/conditions.flan
@@ -13,9 +13,9 @@
;;; Signals twice and keeps going both times — that is the whole of §1.
(defn load-all []
- (signal (AssetMissing {:id 1}))
- (signal (AssetMissing {:id 2}))
- (signal (Corrupt {:id 3})))
+ (signal (AssetMissing {.id 1}))
+ (signal (AssetMissing {.id 2}))
+ (signal (Corrupt {.id 3})))
(defn main [] i32
;; No handler: a no-op, not an abort and not a message (§2).
@@ -36,7 +36,7 @@
;; Nesting: the inner frame does not displace the outer one, so both run.
(handler-bind [(Corrupt [c] (set other (+ other 100)))]
(handler-bind [(Corrupt [c] (set other (+ other 1000)))]
- (signal (Corrupt {:id 0}))))
+ (signal (Corrupt {.id 0}))))
(print other) (println "") ; 3 + 1000 + 100 = 1103
;; And the stack is back to what it was: no handler, no effect.
diff --git a/test/programs/debug-permuted.flan b/test/programs/debug-permuted.flan
index a6a6259..0b5c66b 100644
--- a/test/programs/debug-permuted.flan
+++ b/test/programs/debug-permuted.flan
@@ -16,7 +16,7 @@
bump))
(defn main [] i32
- (let [c (Cell {:alive true :heat 3.25 :id 7 :name "grain"})]
+ (let [c (Cell {.alive true .heat 3.25 .id 7 .name "grain"})]
(let [r (tick (addr c) 41)]
(print r) (println "")
(print (.heat c)) (println "")
diff --git a/test/programs/debug.flan b/test/programs/debug.flan
index fed2282..9e29a60 100644
--- a/test/programs/debug.flan
+++ b/test/programs/debug.flan
@@ -21,7 +21,7 @@
bump))
(defn main [] i32
- (let [c (Cell {:alive true :heat 3.25 :id 7 :name "grain"})]
+ (let [c (Cell {.alive true .heat 3.25 .id 7 .name "grain"})]
(let [r (tick (addr c) 41)]
(print r) (println "")
(print (.heat c)) (println "")
diff --git a/test/programs/destructure.flan b/test/programs/destructure.flan
index 031c74e..94ecd29 100644
--- a/test/programs/destructure.flan
+++ b/test/programs/destructure.flan
@@ -20,7 +20,7 @@
(defn make-point [] Point
(set calls (+ calls 1))
- (Point {:x 3 :y 4}))
+ (Point {.x 3 .y 4}))
(defn show2 [label string a i32 b i32]
(print label)
@@ -32,24 +32,24 @@
(defn main [] i32
;; :keys, the common case: one name per field, spelled as the field is.
- (let [{:keys [x y]} (Point {:x 1 :y 2})]
+ (let [{:keys [x y]} (Point {.x 1 .y 2})]
(show2 "keys" x y))
;; The pair form, which is what renames and what nests — a :keys entry is a
;; field name and never a pattern.
- (let [{a :x b :y} (Point {:x 10 :y 20})]
+ (let [{a .x b .y} (Point {.x 10 .y 20})]
(show2 "pairs" a b))
- (let [l (Line {:a (Point {:x 5 :y 6}) :b (Point {:x 7 :y 8})})]
- (let [{{:keys [x y]} :b} l]
+ (let [l (Line {.a (Point {.x 5 .y 6}) .b (Point {.x 7 .y 8})})]
+ (let [{{:keys [x y]} .b} l]
(show2 "nested" x y))
;; A pattern may shadow the very name it destructures, because the value is
;; read into a temporary before any of the names are bound.
- (let [{l :a} l]
+ (let [{l .a} l]
(show2 "shadow" (.x l) (.y l))))
;; A later binding sees an earlier pattern's names, as in any let.
- (let [{:keys [x]} (Point {:x 100 :y 0})
+ (let [{:keys [x]} (Point {.x 100 .y 0})
doubled (* x 2)]
(show2 "sequential" x doubled))
@@ -81,15 +81,15 @@
(print (len rest)) (println ""))
;; Patterns nest through each other: a struct inside an array.
- (let [ps [(Point {:x 1 :y 2}) (Point {:x 3 :y 4})]
- [{:keys [x]} {y :y}] ps]
+ (let [ps [(Point {.x 1 .y 2}) (Point {.x 3 .y 4})]
+ [{:keys [x]} {y .y}] ps]
(show2 "nested-in-array" x y))
;; A tail of something wider than a machine word. The corpus slices arrays of
;; i32, u8 and f32 and nothing else, so this is the one place the desugared
;; (slice xs n (len xs)) has to get a struct's stride right rather than a
;; scalar's.
- (let [ps [(Point {:x 1 :y 2}) (Point {:x 3 :y 4}) (Point {:x 5 :y 6})]
+ (let [ps [(Point {.x 1 .y 2}) (Point {.x 3 .y 4}) (Point {.x 5 .y 6})]
[first & others] ps]
(print "struct-tail ")
(print (.x first)) (print " ")
@@ -100,7 +100,7 @@
;; Evaluate-once. Two patterns, two calls, four names — one call per pattern.
;; Without the temporary each of the four names would call it again: 4, not 2.
(let [{:keys [x y]} (make-point)
- {a :x b :y} (make-point)]
+ {a .x b .y} (make-point)]
(print "calls ")
(print calls) (print " ")
(print (+ x (+ y (+ a b))))
diff --git a/test/programs/dev-break.flan b/test/programs/dev-break.flan
index 9df48b2..b8a60b6 100644
--- a/test/programs/dev-break.flan
+++ b/test/programs/dev-break.flan
@@ -14,7 +14,7 @@
(defn fetch [n i32] i32
(restart-case
- (do (error (Missing {:id n})) 0)
+ (do (error (Missing {.id n})) 0)
(use-placeholder [] -1)
(retry [] 7)))
diff --git a/test/programs/dev-locals.flan b/test/programs/dev-locals.flan
index da27e79..7a7ef4a 100644
--- a/test/programs/dev-locals.flan
+++ b/test/programs/dev-locals.flan
@@ -12,11 +12,11 @@
(defstruct Boom [why i32])
(defn look [n i64 label string] i64
- (let [p (Point {:x 1.5 :y 2.5})
+ (let [p (Point {.x 1.5 .y 2.5})
xs [10 20 30]
flag (> n 0)]
(restart-case
- (do (error (Boom {:why 7}))
+ (do (error (Boom {.why 7}))
;; Never reached before the break, so [after] is a slot with nothing
;; in it: the frame records a null for it and this is what "not bound
;; yet" has to mean.
diff --git a/test/programs/dev-loop.flan b/test/programs/dev-loop.flan
index 60287b9..d92cec1 100644
--- a/test/programs/dev-loop.flan
+++ b/test/programs/dev-loop.flan
@@ -12,7 +12,7 @@
(defstruct Missing [id i32])
(defn probe [] i64
- (signal (Missing {:id 1}))
+ (signal (Missing {.id 1}))
0)
(defn step [] i64
diff --git a/test/programs/edn.flan b/test/programs/edn.flan
index c506a64..000cfcc 100644
--- a/test/programs/edn.flan
+++ b/test/programs/edn.flan
@@ -82,7 +82,7 @@
;; rather than being returned, which is why this can be a straight line of
;; assignments with one test at the end.
(defn read-enemy [c (Ptr edn/Cursor)] Enemy
- (let [e (Enemy {:hp 0 :speed 0.0 :boss? false})]
+ (let [e (Enemy {.hp 0 .speed 0.0 .boss? false})]
(edn/expect c edn/tok-map-open)
(while (edn/ok? c)
(let [k (edn/next c)]
diff --git a/test/programs/enum-compare.flan b/test/programs/enum-compare.flan
index 45ae7f1..b737b70 100644
--- a/test/programs/enum-compare.flan
+++ b/test/programs/enum-compare.flan
@@ -22,6 +22,6 @@
(println (if (below? :hi) "hi below mid" "hi not below mid"))
;; And through a struct field, which is a different path to the same compare.
- (let [s (S {:k :hi})]
+ (let [s (S {.k :hi})]
(println (if (= (.k s) :hi) "field eq yes" "field eq no")))
0)
diff --git a/test/programs/error.flan b/test/programs/error.flan
index 9148117..2dd96f9 100644
--- a/test/programs/error.flan
+++ b/test/programs/error.flan
@@ -10,5 +10,5 @@
;; A handler that returns normally. It runs — signal's lookup is the same —
;; and it still does not answer the error.
(handler-bind [(AssetMissing [c] (println "handler ran"))]
- (error (AssetMissing {:id 1})))
+ (error (AssetMissing {.id 1})))
0)
diff --git a/test/programs/machine.flan b/test/programs/machine.flan
index 2fddcc0..59027b7 100644
--- a/test/programs/machine.flan
+++ b/test/programs/machine.flan
@@ -31,10 +31,10 @@
(set total (sum-grid))
(print total) (println "") ; 12
(print (at pal 2)) (println "") ; 30
- (let [p (P {:x 1 :y 2})] ; :y omitted is zeroed
+ (let [p (P {.x 1 .y 2})] ; :y omitted is zeroed
(bump (addr p))
(print (.x p)) (println "") ; 2
- (let [l (Line {:a p})]
+ (let [l (Line {.a p})]
(print (.y (.a l))) (println ""))) ; 2
(print (/ 7 2)) (println "") ; 3 integer divide
(print (/ (f64 7) 2.0)) (println "") ; 3.5 float divide
diff --git a/test/programs/pkg-return.flan b/test/programs/pkg-return.flan
index 48b497d..2b42e09 100644
--- a/test/programs/pkg-return.flan
+++ b/test/programs/pkg-return.flan
@@ -17,7 +17,7 @@
;;; And the one that must keep working: a lowercase qualified name in the same
;;; position is an expression, not a type.
-(defn local [] Local (Local {:n 5}))
+(defn local [] Local (Local {.n 5}))
(defn main [] i32
(let [c (fresh (bytes "[1 2]"))
diff --git a/test/programs/raylib-audio.flan b/test/programs/raylib-audio.flan
index dee76ba..dc29ff1 100644
--- a/test/programs/raylib-audio.flan
+++ b/test/programs/raylib-audio.flan
@@ -133,8 +133,8 @@
;; Nothing raylib made: four integers Flan chose and a buffer Flan owns. So
;; every number below is raylib reading THIS struct, and there is no
;; raylib-produced struct anywhere for a permutation to hide inside.
- (let [src (rl/Wave {:frame-count 8 :sample-rate 8000 :sample-size 16
- :channels 1 :data (addr (at pcm 0))})]
+ (let [src (rl/Wave {.frame-count 8 .sample-rate 8000 .sample-size 16
+ .channels 1 .data (addr (at pcm 0))})]
(show-bool "valid" (rl/wave-valid? src))
(show-wave "source" src)
diff --git a/test/programs/raylib-ffi.flan b/test/programs/raylib-ffi.flan
index c802b27..391d47b 100644
--- a/test/programs/raylib-ffi.flan
+++ b/test/programs/raylib-ffi.flan
@@ -37,10 +37,10 @@
;; The camera below is chosen so that no field is silently unpinned — offset
;; and target differ, zoom is 2.0 and not the identity 1.0, and every
;; component is a distinct dyadic value that prints exactly.
-(defconst cam (rl/Camera2D {:offset (rl/Vector2 {:x 100.0 :y 50.0})
- :target (rl/Vector2 {:x 8.0 :y 4.0})
- :rotation 0.0
- :zoom 2.0}))
+(defconst cam (rl/Camera2D {.offset (rl/Vector2 {.x 100.0 .y 50.0})
+ .target (rl/Vector2 {.x 8.0 .y 4.0})
+ .rotation 0.0
+ .zoom 2.0}))
;; Absolute values, not a round trip: screen (140,90) is world (28,24) because
;; ((140-100)/2)+8 = 28 and ((90-50)/2)+4 = 24. Swap offset and target in the
@@ -90,17 +90,17 @@
;; Rectangle, pinned completely. The intersection of (0,0,10,4) and
;; (6,1,10,10) is (6,1,4,3) — four different numbers, each derived from a
;; different pair of fields, so swapping any two fields changes the answer.
- (show-rect (rl/get-collision-rec (rl/Rectangle {:x 0.0 :y 0.0 :width 10.0 :height 4.0})
- (rl/Rectangle {:x 6.0 :y 1.0 :width 10.0 :height 10.0})))
+ (show-rect (rl/get-collision-rec (rl/Rectangle {.x 0.0 .y 0.0 .width 10.0 .height 4.0})
+ (rl/Rectangle {.x 6.0 .y 1.0 .width 10.0 .height 10.0})))
;; Texture2D, as far as a machine with no GPU can go. raylib keeps the
;; shapes texture without touching GL, and substitutes a default when
;; `texture.id`, `source.width` or `source.height` is zero — that guard is
;; the only asymmetry a headless test gets.
- (let [rect (rl/Rectangle {:x 3.5 :y 7.25 :width 11.5 :height 13.75})]
+ (let [rect (rl/Rectangle {.x 3.5 .y 7.25 .width 11.5 .height 13.75})]
;; (A) Valid, five distinct values: they come back, so the struct crosses
;; intact in both directions and raylib stored it rather than defaulting.
- (rl/set-shapes-texture (rl/Texture2D {:id 7 :width 13 :height 17 :mipmaps 2 :format 4}) rect)
+ (rl/set-shapes-texture (rl/Texture2D {.id 7 .width 13 .height 17 .mipmaps 2 .format 4}) rect)
(show-texture (rl/get-shapes-texture))
(show-rect (rl/get-shapes-texture-rectangle))
@@ -108,7 +108,7 @@
;; back. The 7 is the only distinct field in it, so this pins `format` as
;; the last field, and the substitution happening at all pins `id` as the
;; field the guard reads.
- (rl/set-shapes-texture (rl/Texture2D {:id 0 :width 13 :height 17 :mipmaps 2 :format 4}) rect)
+ (rl/set-shapes-texture (rl/Texture2D {.id 0 .width 13 .height 17 .mipmaps 2 .format 4}) rect)
(show-texture (rl/get-shapes-texture))
;; (C) width zero, id positive: still stored, because the guard does not
@@ -120,24 +120,24 @@
;; without a GL context reads width, height or mipmaps, so their order
;; among themselves is not pinned by this test. A swap there shows up as a
;; visibly wrong sprite in the interactive run, and nowhere else.
- (rl/set-shapes-texture (rl/Texture2D {:id 7 :width 0 :height 17 :mipmaps 2 :format 4}) rect)
+ (rl/set-shapes-texture (rl/Texture2D {.id 7 .width 0 .height 17 .mipmaps 2 .format 4}) rect)
(show-texture (rl/get-shapes-texture)))
;; Camera2D, each direction on its own. See the note above show-v for why
;; this is not a round trip.
- (show-v (rl/get-screen-to-world-2d (rl/Vector2 {:x 140.0 :y 90.0}) cam))
- (show-v (rl/get-world-to-screen-2d (rl/Vector2 {:x 28.0 :y 24.0}) cam))
+ (show-v (rl/get-screen-to-world-2d (rl/Vector2 {.x 140.0 .y 90.0}) cam))
+ (show-v (rl/get-world-to-screen-2d (rl/Vector2 {.x 28.0 .y 24.0}) cam))
;; And the rotated camera, which is what pins Vector2's own two fields.
- (let [spun (rl/Camera2D {:offset (rl/Vector2 {:x 100.0 :y 50.0})
- :target (rl/Vector2 {:x 8.0 :y 4.0})
- :rotation 90.0
- :zoom 2.0})]
+ (let [spun (rl/Camera2D {.offset (rl/Vector2 {.x 100.0 .y 50.0})
+ .target (rl/Vector2 {.x 8.0 .y 4.0})
+ .rotation 90.0
+ .zoom 2.0})]
(show-near "rotated screen-to-world"
- (rl/get-screen-to-world-2d (rl/Vector2 {:x 140.0 :y 90.0}) spun)
+ (rl/get-screen-to-world-2d (rl/Vector2 {.x 140.0 .y 90.0}) spun)
28.0 -16.0)
(show-near "rotated world-to-screen"
- (rl/get-world-to-screen-2d (rl/Vector2 {:x 28.0 :y 24.0}) spun)
+ (rl/get-world-to-screen-2d (rl/Vector2 {.x 28.0 .y 24.0}) spun)
60.0 90.0))
;; ── Collision, which is the best material a headless test gets ──────
@@ -146,25 +146,25 @@
;; so a wrong field order gives a wrong answer rather than the same struct
;; back. Each case below is paired with one that must come out the other
;; way, because a predicate that always said yes would pass a single case.
- (let [r (rl/Rectangle {:x 0.0 :y 0.0 :width 10.0 :height 4.0})]
+ (let [r (rl/Rectangle {.x 0.0 .y 0.0 .width 10.0 .height 4.0})]
;; Inside on both axes, then outside on y only. Swap width and height and
;; both of these flip, which is what makes the pair worth more than either.
- (show-bool "point in rect" (rl/collision-point-rec? (rl/Vector2 {:x 5.0 :y 3.0}) r))
- (show-bool "point below rect" (rl/collision-point-rec? (rl/Vector2 {:x 5.0 :y 5.0}) r))
+ (show-bool "point in rect" (rl/collision-point-rec? (rl/Vector2 {.x 5.0 .y 3.0}) r))
+ (show-bool "point below rect" (rl/collision-point-rec? (rl/Vector2 {.x 5.0 .y 5.0}) r))
;; Overlapping by one unit, then clear of it. Pins x against width.
(show-bool "rects overlap"
- (rl/collision-recs? r (rl/Rectangle {:x 9.0 :y 1.0 :width 10.0 :height 10.0})))
+ (rl/collision-recs? r (rl/Rectangle {.x 9.0 .y 1.0 .width 10.0 .height 10.0})))
(show-bool "rects apart"
- (rl/collision-recs? r (rl/Rectangle {:x 11.0 :y 1.0 :width 10.0 :height 10.0}))))
+ (rl/collision-recs? r (rl/Rectangle {.x 11.0 .y 1.0 .width 10.0 .height 10.0}))))
;; Centres five apart with radii summing to six, then seven apart. The radius
;; is a scalar beside two Vector2s, so this pins it against their fields.
(show-bool "circles touch"
- (rl/collision-circles? (rl/Vector2 {:x 0.0 :y 0.0}) 3.0
- (rl/Vector2 {:x 5.0 :y 0.0}) 3.0))
+ (rl/collision-circles? (rl/Vector2 {.x 0.0 .y 0.0}) 3.0
+ (rl/Vector2 {.x 5.0 .y 0.0}) 3.0))
(show-bool "circles clear"
- (rl/collision-circles? (rl/Vector2 {:x 0.0 :y 0.0}) 3.0
- (rl/Vector2 {:x 7.0 :y 0.0}) 3.0))
+ (rl/collision-circles? (rl/Vector2 {.x 0.0 .y 0.0}) 3.0
+ (rl/Vector2 {.x 7.0 .y 0.0}) 3.0))
;; The one that answers with a number rather than a yes: a horizontal segment
;; at y = 7 crossed by a vertical one at x = 3, so the answer is (3 7).
@@ -187,86 +187,86 @@
;;
;; What this section *does* pin is Rectangle, completely — swapping width and
;; height turns three of the four predicates below the wrong way.
- (match (rl/collision-lines (rl/Vector2 {:x 0.0 :y 7.0}) (rl/Vector2 {:x 10.0 :y 7.0})
- (rl/Vector2 {:x 3.0 :y 0.0}) (rl/Vector2 {:x 3.0 :y 10.0}))
+ (match (rl/collision-lines (rl/Vector2 {.x 0.0 .y 7.0}) (rl/Vector2 {.x 10.0 .y 7.0})
+ (rl/Vector2 {.x 3.0 .y 0.0}) (rl/Vector2 {.x 3.0 .y 10.0}))
(Some p) (show-v p)
None (println "no crossing"))
;; The rest of the collision family, each with the case that must come out
;; the other way. Bound and linking is not the same as working: a wrapper
;; whose arguments are in the wrong order links perfectly and answers
;; nonsense, and until something calls it nothing says so.
- (let [r (rl/Rectangle {:x 0.0 :y 0.0 :width 10.0 :height 4.0})]
+ (let [r (rl/Rectangle {.x 0.0 .y 0.0 .width 10.0 .height 4.0})]
;; Circle against rect: just touching at the right edge, then clear of it.
(show-bool "circle meets rect"
- (rl/collision-circle-rec? (rl/Vector2 {:x 12.0 :y 2.0}) 3.0 r))
+ (rl/collision-circle-rec? (rl/Vector2 {.x 12.0 .y 2.0}) 3.0 r))
(show-bool "circle clears rect"
- (rl/collision-circle-rec? (rl/Vector2 {:x 14.0 :y 2.0}) 3.0 r)))
+ (rl/collision-circle-rec? (rl/Vector2 {.x 14.0 .y 2.0}) 3.0 r)))
;; Circle against a segment, which is the one that pins the radius against
;; the two endpoints rather than against a single centre.
(show-bool "circle meets line"
- (rl/collision-circle-line? (rl/Vector2 {:x 5.0 :y 2.0}) 3.0
- (rl/Vector2 {:x 0.0 :y 0.0})
- (rl/Vector2 {:x 10.0 :y 0.0})))
+ (rl/collision-circle-line? (rl/Vector2 {.x 5.0 .y 2.0}) 3.0
+ (rl/Vector2 {.x 0.0 .y 0.0})
+ (rl/Vector2 {.x 10.0 .y 0.0})))
(show-bool "circle clears line"
- (rl/collision-circle-line? (rl/Vector2 {:x 5.0 :y 4.0}) 3.0
- (rl/Vector2 {:x 0.0 :y 0.0})
- (rl/Vector2 {:x 10.0 :y 0.0})))
+ (rl/collision-circle-line? (rl/Vector2 {.x 5.0 .y 4.0}) 3.0
+ (rl/Vector2 {.x 0.0 .y 0.0})
+ (rl/Vector2 {.x 10.0 .y 0.0})))
(show-bool "point in circle"
- (rl/collision-point-circle? (rl/Vector2 {:x 2.0 :y 0.0})
- (rl/Vector2 {:x 0.0 :y 0.0}) 3.0))
+ (rl/collision-point-circle? (rl/Vector2 {.x 2.0 .y 0.0})
+ (rl/Vector2 {.x 0.0 .y 0.0}) 3.0))
(show-bool "point outside circle"
- (rl/collision-point-circle? (rl/Vector2 {:x 4.0 :y 0.0})
- (rl/Vector2 {:x 0.0 :y 0.0}) 3.0))
+ (rl/collision-point-circle? (rl/Vector2 {.x 4.0 .y 0.0})
+ (rl/Vector2 {.x 0.0 .y 0.0}) 3.0))
;; A right triangle with the square corner at the origin. The inside point is
;; inside for one vertex order and not the other, so this is one of the few
;; here that notices which vertex is which.
(show-bool "point in triangle"
- (rl/collision-point-triangle? (rl/Vector2 {:x 1.0 :y 1.0})
- (rl/Vector2 {:x 0.0 :y 0.0})
- (rl/Vector2 {:x 8.0 :y 0.0})
- (rl/Vector2 {:x 0.0 :y 6.0})))
+ (rl/collision-point-triangle? (rl/Vector2 {.x 1.0 .y 1.0})
+ (rl/Vector2 {.x 0.0 .y 0.0})
+ (rl/Vector2 {.x 8.0 .y 0.0})
+ (rl/Vector2 {.x 0.0 .y 6.0})))
(show-bool "point outside triangle"
- (rl/collision-point-triangle? (rl/Vector2 {:x 7.0 :y 5.0})
- (rl/Vector2 {:x 0.0 :y 0.0})
- (rl/Vector2 {:x 8.0 :y 0.0})
- (rl/Vector2 {:x 0.0 :y 6.0})))
+ (rl/collision-point-triangle? (rl/Vector2 {.x 7.0 .y 5.0})
+ (rl/Vector2 {.x 0.0 .y 0.0})
+ (rl/Vector2 {.x 8.0 .y 0.0})
+ (rl/Vector2 {.x 0.0 .y 6.0})))
;; On the segment, then beside it. The threshold is the last argument, so a
;; wrapper that lost it among the four coordinates answers with whatever was
;; in that register.
(show-bool "point on line"
- (rl/collision-point-line? (rl/Vector2 {:x 5.0 :y 0.0})
- (rl/Vector2 {:x 0.0 :y 0.0})
- (rl/Vector2 {:x 10.0 :y 0.0}) 1))
+ (rl/collision-point-line? (rl/Vector2 {.x 5.0 .y 0.0})
+ (rl/Vector2 {.x 0.0 .y 0.0})
+ (rl/Vector2 {.x 10.0 .y 0.0}) 1))
(show-bool "point off line"
- (rl/collision-point-line? (rl/Vector2 {:x 5.0 :y 4.0})
- (rl/Vector2 {:x 0.0 :y 0.0})
- (rl/Vector2 {:x 10.0 :y 0.0}) 1))
+ (rl/collision-point-line? (rl/Vector2 {.x 5.0 .y 4.0})
+ (rl/Vector2 {.x 0.0 .y 0.0})
+ (rl/Vector2 {.x 10.0 .y 0.0}) 1))
;; The only one that crosses a *slice*, so it is the only one where ptr+len
;; has to arrive as raylib's pointer-and-count. A wrong length reads past the
;; array or stops short, and either way the square stops being a square.
- (let [square [(rl/Vector2 {:x 0.0 :y 0.0}) (rl/Vector2 {:x 8.0 :y 0.0})
- (rl/Vector2 {:x 8.0 :y 8.0}) (rl/Vector2 {:x 0.0 :y 8.0})]]
+ (let [square [(rl/Vector2 {.x 0.0 .y 0.0}) (rl/Vector2 {.x 8.0 .y 0.0})
+ (rl/Vector2 {.x 8.0 .y 8.0}) (rl/Vector2 {.x 0.0 .y 8.0})]]
(show-bool "point in poly"
- (rl/collision-point-poly? (rl/Vector2 {:x 4.0 :y 4.0}) (slice square 0 4)))
+ (rl/collision-point-poly? (rl/Vector2 {.x 4.0 .y 4.0}) (slice square 0 4)))
(show-bool "point outside poly"
- (rl/collision-point-poly? (rl/Vector2 {:x 12.0 :y 4.0}) (slice square 0 4)))
+ (rl/collision-point-poly? (rl/Vector2 {.x 12.0 .y 4.0}) (slice square 0 4)))
;; The same point against the same array, three corners instead of four:
;; inside the square, outside the triangle the first three make. This is
;; the case that proves the *length* crosses — everything above would pass
;; with a hardcoded count, or with the pointer alone.
(show-bool "in square, four corners"
- (rl/collision-point-poly? (rl/Vector2 {:x 2.0 :y 6.0}) (slice square 0 4)))
+ (rl/collision-point-poly? (rl/Vector2 {.x 2.0 .y 6.0}) (slice square 0 4)))
(show-bool "out of triangle, three"
- (rl/collision-point-poly? (rl/Vector2 {:x 2.0 :y 6.0}) (slice square 0 3))))
+ (rl/collision-point-poly? (rl/Vector2 {.x 2.0 .y 6.0}) (slice square 0 3))))
;; Parallel, so they never meet: None rather than a point nobody wrote.
- (match (rl/collision-lines (rl/Vector2 {:x 0.0 :y 0.0}) (rl/Vector2 {:x 1.0 :y 2.0})
- (rl/Vector2 {:x 5.0 :y 0.0}) (rl/Vector2 {:x 6.0 :y 2.0}))
+ (match (rl/collision-lines (rl/Vector2 {.x 0.0 .y 0.0}) (rl/Vector2 {.x 1.0 .y 2.0})
+ (rl/Vector2 {.x 5.0 .y 0.0}) (rl/Vector2 {.x 6.0 .y 2.0}))
(Some p) (show-v p)
None (println "no crossing"))
diff --git a/test/programs/raylib-font.flan b/test/programs/raylib-font.flan
index 7e75d7c..1f81afa 100644
--- a/test/programs/raylib-font.flan
+++ b/test/programs/raylib-font.flan
@@ -68,17 +68,17 @@
(defvar glyphs [3 rl/GlyphInfo])
(defn build-glyphs []
- (set (at glyph-recs 0) (rl/Rectangle {:x 0.0 :y 0.0 :width 5.0 :height 10.0}))
- (set (at glyph-recs 1) (rl/Rectangle {:x 5.0 :y 0.0 :width 7.0 :height 10.0}))
- (set (at glyph-recs 2) (rl/Rectangle {:x 12.0 :y 0.0 :width 9.0 :height 10.0}))
+ (set (at glyph-recs 0) (rl/Rectangle {.x 0.0 .y 0.0 .width 5.0 .height 10.0}))
+ (set (at glyph-recs 1) (rl/Rectangle {.x 5.0 .y 0.0 .width 7.0 .height 10.0}))
+ (set (at glyph-recs 2) (rl/Rectangle {.x 12.0 .y 0.0 .width 9.0 .height 10.0}))
;; 65 66 67 are A B C. Advances 11 and 13 for the first two; C's advance is
;; 0 on purpose, which is what sends raylib down the other branch.
- (set (at glyphs 0) (rl/GlyphInfo {:value 65 :offset-x 1 :offset-y 0
- :advance-x 11 :image (rl/Image {})}))
- (set (at glyphs 1) (rl/GlyphInfo {:value 66 :offset-x 2 :offset-y 0
- :advance-x 13 :image (rl/Image {})}))
- (set (at glyphs 2) (rl/GlyphInfo {:value 67 :offset-x 3 :offset-y 0
- :advance-x 0 :image (rl/Image {})})))
+ (set (at glyphs 0) (rl/GlyphInfo {.value 65 .offset-x 1 .offset-y 0
+ .advance-x 11 .image (rl/Image {})}))
+ (set (at glyphs 1) (rl/GlyphInfo {.value 66 .offset-x 2 .offset-y 0
+ .advance-x 13 .image (rl/Image {})}))
+ (set (at glyphs 2) (rl/GlyphInfo {.value 67 .offset-x 3 .offset-y 0
+ .advance-x 0 .image (rl/Image {})})))
(defn show-bool [name string b bool]
(print name) (print " ")
@@ -116,11 +116,11 @@
;; therefore parses rl/Font as an expression and fails with "unknown name".
;; That is a parser limit and not something this file wanted; it applies to
;; rl/Vector2 just as much as to rl/Font.
- (let [f (rl/Font {:base-size 10 :glyph-count 3 :glyph-padding 2
- :texture (rl/Texture2D {:id 1 :width 32 :height 16
- :mipmaps 1 :format 7})
- :recs (addr (at glyph-recs 0))
- :glyphs (addr (at glyphs 0))})]
+ (let [f (rl/Font {.base-size 10 .glyph-count 3 .glyph-padding 2
+ .texture (rl/Texture2D {.id 1 .width 32 .height 16
+ .mipmaps 1 .format 7})
+ .recs (addr (at glyph-recs 0))
+ .glyphs (addr (at glyphs 0))})]
;; font-valid? reads the texture id and both array pointers, so it is a
;; null check on the three things everything below dereferences.
(show-bool "valid" (rl/font-valid? f))
diff --git a/test/programs/raylib-image.flan b/test/programs/raylib-image.flan
index 873144a..f59d914 100644
--- a/test/programs/raylib-image.flan
+++ b/test/programs/raylib-image.flan
@@ -26,9 +26,9 @@
;; transposed read pass, and that is exactly the trap the collision cases fell
;; into.
-(defconst bg (rl/Color {:r 10 :g 20 :b 30 :a 255}))
-(defconst mark-a (rl/Color {:r 200 :g 0 :b 0 :a 255}))
-(defconst mark-b (rl/Color {:r 0 :g 200 :b 0 :a 255}))
+(defconst bg (rl/Color {.r 10 .g 20 .b 30 .a 255}))
+(defconst mark-a (rl/Color {.r 200 .g 0 .b 0 .a 255}))
+(defconst mark-b (rl/Color {.r 0 .g 200 .b 0 .a 255}))
;; Where the export goes and comes back from. The two optimisation levels
;; write identical bytes, so sharing one path between runs is harmless.
@@ -151,7 +151,7 @@
;; the Rectangle and the result is 1 x 2 with the mark gone.
(let [img (rl/gen-image-color 6 3 bg)]
(rl/image-draw-pixel (addr img) 5 0 mark-a)
- (rl/image-crop (addr img) (rl/Rectangle {:x 4.0 :y 0.0 :width 2.0 :height 1.0}))
+ (rl/image-crop (addr img) (rl/Rectangle {.x 4.0 .y 0.0 .width 2.0 .height 1.0}))
(show-image "cropped" img)
(show-pixel "cropped at 1,0" img 1 0)
(show-pixel "cropped at 0,0" img 0 0)
diff --git a/test/programs/reach-walk.flan b/test/programs/reach-walk.flan
index a82c111..0a35f50 100644
--- a/test/programs/reach-walk.flan
+++ b/test/programs/reach-walk.flan
@@ -29,7 +29,7 @@
;;; Signals with nothing to return, so the clause body is the only way past.
(defn missing [] i32
- (error (Nope {:id 1})))
+ (error (Nope {.id 1})))
(defn pick [] i32
(restart-case (missing)
diff --git a/test/programs/restarts.flan b/test/programs/restarts.flan
index 0b6b6ba..7765f44 100644
--- a/test/programs/restarts.flan
+++ b/test/programs/restarts.flan
@@ -16,7 +16,7 @@
;;; The signalling end. Two frames below the restart-case, so the transfer has
;;; something to cross.
(defn load [n i32] i32
- (signal (AssetMissing {:id n}))
+ (signal (AssetMissing {.id n}))
100)
;;; §5: this defer runs whether the call below returns or transfers, and it
@@ -46,7 +46,7 @@
;;; signalling version needs does not exist here.
(defn strict [n i32] i32
(restart-case
- (do (error (AssetMissing {:id n}))
+ (do (error (AssetMissing {.id n}))
;; unreachable — error is Never, so nothing after it runs
0)
(use-placeholder [] -2)))
diff --git a/test/programs/values.flan b/test/programs/values.flan
index 1a711fc..0118047 100644
--- a/test/programs/values.flan
+++ b/test/programs/values.flan
@@ -5,7 +5,7 @@
(defvar arr [3 i32])
(defn main [] i32
- (let [a (P {:x 1})]
+ (let [a (P {.x 1})]
(let [b a] ; a copy, not an alias
(set (.x a) 99)
(print (.x b)) (println ""))) ; 1
diff --git a/test/programs/vec.flan b/test/programs/vec.flan
index 4a8811e..bc7193c 100644
--- a/test/programs/vec.flan
+++ b/test/programs/vec.flan
@@ -76,8 +76,8 @@
;; A second element type over the same runtime, and a struct element, so
;; that size_of and align_of are doing work rather than both being 4.
(let [ps (vec-new Point)]
- (push ps (Point {:x 1 :y 2}))
- (push ps (Point {:x 3 :y 4}))
+ (push ps (Point {.x 1 .y 2}))
+ (push ps (Point {.x 3 .y 4}))
(println (len ps)) ; 2
(println (.y (at ps 1))) ; 4
(free ps))
diff --git a/test/programs/virtual-controls-headless.flan b/test/programs/virtual-controls-headless.flan
index cb39acd..c4a0762 100644
--- a/test/programs/virtual-controls-headless.flan
+++ b/test/programs/virtual-controls-headless.flan
@@ -48,7 +48,7 @@
(while (<= y sweep-y1)
(let [x sweep-x0]
(while (<= x sweep-x1)
- (let [p (rl/Vector2 {:x (f32 x) :y (f32 y)})]
+ (let [p (rl/Vector2 {.x (f32 x) .y (f32 y)})]
(vc/move-player (vc/nearest-button p) dt))
(set x (+ x sweep-step))))
(set y (+ y sweep-step))))
diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml
index 0b9f47f..e2156bd 100644
--- a/test/test_acceptance.ml
+++ b/test/test_acceptance.ml
@@ -150,15 +150,15 @@ let () =
let println_out =
"plain string\nplain bytes\n42\n-7\n5\n18446744073709551615\n3.5\n\
-0.25\ntrue\nfalse\n()\n:green\n:red\n\n(some 0)\nnone\n\
- (Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y -2}) :tags [ 0 42 0]})\n\
+ (Blob {.id 7 .name \"sandy \\\"quoted\\\"\" .pos (V {.x 1.5 .y -2}) .tags [ 0 42 0]})\n\
[ 0 0 9 0]\n[ 0 0 0 0 0 0 0 0 ...]\n[ 0 9 0]\n\
- (D1 {:d (D2 {:d (D3 {:d (D4 {:d (D5 {:n ...})})})})})\n[ 0 0]\n\
+ (D1 {.d (D2 {.d (D3 {.d (D4 {.d (D5 {.n ...})})})})})\n[ 0 0]\n\
[ 0 0]\n[ 9 0]\n"
(* The escape buffer is 1024 and the input is 1100 x's, so this is the
truncation: the ellipsis goes *inside* the quotes, and the count is
spelled out rather than pasted so that a change to the buffer or to
the reserve shows up here as a number and not as a wall of x. *)
- ^ "(Long {:s \"" ^ String.make 1014 'x' ^ "...\"})\n"
+ ^ "(Long {.s \"" ^ String.make 1014 'x' ^ "...\"})\n"
^ "abc\n"
in
outputs "println, every arm" "programs/println.flan" println_out;
@@ -1749,7 +1749,7 @@ ERR@7 unexpected token: not the kind the caller was reading
in
let cell = "(defstruct Cell [alive bool heat f64 id i32 name string])\n" in
let cell' = "(defstruct Cell [name string id i32 alive bool heat f64])\n" in
- let body = "(defn main [] i32 (let [c (Cell {:id 1})] (i32 (.id c))))\n" in
+ let body = "(defn main [] i32 (let [c (Cell {.id 1})] (i32 (.id c))))\n" in
layout_case "DWARF offsets agree with LLVM: a mixed struct" (cell ^ body)
"Cell" [ "alive"; "heat"; "id"; "name" ];
(* The same struct, permuted. If the offsets came from anywhere but the
@@ -1759,7 +1759,7 @@ ERR@7 unexpected token: not the kind the caller was reading
layout_case "DWARF offsets agree with LLVM: nesting and fixed arrays"
("(defstruct P [x i32 y i32])\n\
(defstruct Board [tag u8 cells [4 P] here P edge (Ptr P) seen (Option i64)])\n\
- (defn main [] i32 (let [b (Board {:tag 1})] (i32 (.tag b))))\n")
+ (defn main [] i32 (let [b (Board {.tag 1})] (i32 (.tag b))))\n")
"Board" [ "tag"; "cells"; "here"; "edge"; "seen" ];
(* Permuting the fields must actually move them. Asserting that the two
@@ -1903,7 +1903,7 @@ ERR@7 unexpected token: not the kind the caller was reading
(defn pick [xs [i32] i i32] i32 (at xs i))\n\
(defn fetch [n i32] i32\n\
\ (restart-case\n\
- \ (do (error (Missing {:id n})) 0)\n\
+ \ (do (error (Missing {.id n})) 0)\n\
\ (use-value [v i32 s string] (do (print s) v))\n\
\ (use-placeholder [] -1)))\n\
(defn run [] i32\n\
diff --git a/test/test_dev.ml b/test/test_dev.ml
index 84a8662..02d708e 100644
--- a/test/test_dev.ml
+++ b/test/test_dev.ml
@@ -650,7 +650,7 @@ let () =
its loop, so a body that errors stops it — and take the exit. *)
(match
ask
- "(:op \"eval\" :code \"(defn step [] i64 (restart-case (do (error (Missing {:id 9})) 0) (use-placeholder [] -1)))\" :file \"/tmp/buf.flan\")"
+ "(:op \"eval\" :code \"(defn step [] i64 (restart-case (do (error (Missing {.id 9})) 0) (use-placeholder [] -1)))\" :file \"/tmp/buf.flan\")"
with
| r when status r <> "ok" ->
fail "installing a body that errors: %s"
@@ -775,7 +775,7 @@ let () =
let want =
[ ("n", "i64", "3");
("label", "string", "\"hello\"");
- ("p", "Point", "(Point {:x 1.5 :y 2.5})");
+ ("p", "Point", "(Point {.x 1.5 .y 2.5})");
("xs", "[3 i32]", "[ 10 20 30]");
("flag", "bool", "true") ]
in
@@ -815,7 +815,7 @@ let () =
holding [p]'s value and nothing would say so. *)
let r =
ask
- "(:op \"eval\" :code \"(defn look [n i64 label string] i64 (let [q (Point {:x 9.0 :y 9.0}) ys [1 2 3] mark (< n 0)] (restart-case (do (error (Boom {:why 7})) (let [after (i64 99)] after)) (carry-on [] 5))))\" :file \"/tmp/buf.flan\")"
+ "(:op \"eval\" :code \"(defn look [n i64 label string] i64 (let [q (Point {.x 9.0 .y 9.0}) ys [1 2 3] mark (< n 0)] (restart-case (do (error (Boom {.why 7})) (let [after (i64 99)] after)) (carry-on [] 5))))\" :file \"/tmp/buf.flan\")"
in
if status r <> "ok" then
fail "installing a renamed body while stopped: %s"
@@ -1038,7 +1038,7 @@ let () =
in
let r =
request c
- "(:op \"eval\" :code \"(defn step [] i64 (error (Missing {:id 3})))\" :file \"/tmp/disasm.flan\")"
+ "(:op \"eval\" :code \"(defn step [] i64 (error (Missing {.id 3})))\" :file \"/tmp/disasm.flan\")"
in
if status r <> "ok" then
fail "installing a body that errors: %s"
diff --git a/test/test_flan.ml b/test/test_flan.ml
index 8e1b1dc..3f8cd98 100644
--- a/test/test_flan.ml
+++ b/test/test_flan.ml
@@ -113,11 +113,11 @@ let () =
(* ── Sequences ─────────────────────────────────────────────────── *)
reads "list" "(+ 1 2)" "(+ 1 2)";
reads "vector" "[1 2 3]" "[1 2 3]";
- reads "map literal" "{:src src :pos 0}" "{:src src :pos 0}";
+ reads "map literal" "{.src src .pos 0}" "{.src src .pos 0}";
reads "type notation" "[4 f32]" "[4 f32]";
reads "nested type" "[rows [cols u32]]" "[rows [cols u32]]";
reads "commas as space" "[1, 2, 3]" "[1 2 3]";
- reads "nested" "(a (b [c {:d e}]))" "(a (b [c {:d e}]))";
+ reads "nested" "(a (b [c {.d e}]))" "(a (b [c {.d e}]))";
(* ── Trivia ────────────────────────────────────────────────────── *)
reads "line comment" "; nope\n42" "42";
@@ -173,8 +173,8 @@ let () =
| _ -> []
in
let corpus =
- "(invoke-restart 'skip-form) (a 'b [c 'd] {:e 'f}) '(g 'h) \
- `(i ~j ~@k) `(l `(m ~n)) [`o ~p] {:q `r} (f a~b x`y)"
+ "(invoke-restart 'skip-form) (a 'b [c 'd] {.e 'f}) '(g 'h) \
+ `(i ~j ~@k) `(l `(m ~n)) [`o ~p] {.q `r} (f a~b x`y)"
in
check "no sigils leak into names"
(bad_names (Form.make (Form.List (read corpus))
@@ -314,7 +314,7 @@ let () =
(match (parse1 "(.pos c)").e with
| Field ({ e = Var "c"; _ }, "pos") -> ()
| _ -> check "field access" false);
- (match (parse1 "(Cursor {:src s :pos 0})").e with
+ (match (parse1 "(Cursor {.src s .pos 0})").e with
| Struct ("Cursor", [ ("src", _); ("pos", _) ]) -> ()
| _ -> check "struct literal" false);
(match (parse1 "[1 2 3]").e with
@@ -611,12 +611,12 @@ let () =
(* ── Structs, fields and auto-deref ────────────────────────────── *)
let cursor = "(defstruct Cursor [src [u8] pos i32]) " in
accepts "struct literal, omitted field zeroed"
- (cursor ^ "(defn f [s [u8]] Cursor (Cursor {:src s}))");
+ (cursor ^ "(defn f [s [u8]] Cursor (Cursor {.src s}))");
rejects_check "unknown field"
- (cursor ^ "(defn f [s [u8]] Cursor (Cursor {:nope s}))")
+ (cursor ^ "(defn f [s [u8]] Cursor (Cursor {.nope s}))")
~needle:"has no field nope";
rejects_check "field given twice"
- (cursor ^ "(defn f [s [u8]] Cursor (Cursor {:pos 0 :pos 1}))")
+ (cursor ^ "(defn f [s [u8]] Cursor (Cursor {.pos 0 .pos 1}))")
~needle:"given twice";
accepts "field through a pointer auto-derefs"
(cursor ^ "(defn f [c (Ptr Cursor)] i32 (.pos c))");
@@ -634,7 +634,7 @@ let () =
"(defconst k 1) (defn f [] (set k 2))" ~needle:"is a constant";
accepts "addr of a local gives a pointer"
(cursor ^ "(defn g [c (Ptr Cursor)] i32 (.pos c)) \
- (defn f [s [u8]] i32 (let [c (Cursor {:src s})] (g (addr c))))");
+ (defn f [s [u8]] i32 (let [c (Cursor {.src s})] (g (addr c))))");
rejects_check "addr of a non-place"
"(defn f [] (addr (+ 1 2)))" ~needle:"addr takes the address of a place";
@@ -818,7 +818,7 @@ let () =
accepts "handler-bind over a struct condition"
"(defstruct C [id i32]) (defvar n i64)\n\
- (defn f [] (handler-bind [(C [c] (set n 1))] (signal (C {:id 2}))))";
+ (defn f [] (handler-bind [(C [c] (set n 1))] (signal (C {.id 2}))))";
(* Matching is by type and there is no hierarchy, so a condition has to be a
struct — an integer would have nothing to match against. *)
rejects_check "signalling a non-struct"
@@ -830,18 +830,18 @@ let () =
usable as a restart-case body's fall-through. *)
accepts "error in value position"
"(defstruct C [id i32])\n\
- (defn f [] i32 (error (C {:id 1})))";
+ (defn f [] i32 (error (C {.id 1})))";
(* And signal is not: it is Unit, whatever it finds. *)
rejects_check "signal in value position"
"(defstruct C [id i32])\n\
- (defn f [] i32 (signal (C {:id 1})))" ~needle:"expected i32";
+ (defn f [] i32 (signal (C {.id 1})))" ~needle:"expected i32";
(* A handler is lifted into a function of its own, so the establishing
function's locals are not there. Capturing them is a closure, which is
milestone 5 — until then it is refused for the reason it is refused for
rather than as an unknown name. *)
rejects_check "a handler capturing a local"
"(defstruct C [id i32])\n\
- (defn f [] (let [n 0] (handler-bind [(C [c] (set n 1))] (signal (C {:id 2})))))"
+ (defn f [] (let [n 0] (handler-bind [(C [c] (set n 1))] (signal (C {.id 2})))))"
~needle:"a handler cannot see n";
(* The frames are popped on the way out of the body, so an early exit would
leave them on the stack pointing into a function that has gone. *)
@@ -860,7 +860,7 @@ let () =
accepts "restart-case with a clause that transfers into it"
"(defstruct C [id i32])\n\
- (defn g [] i32 (signal (C {:id 1})) 0)\n\
+ (defn g [] i32 (signal (C {.id 1})) 0)\n\
(defn f [] i32 (restart-case (g) (skip [] 7)))\n\
(defn h [] i32 (handler-bind [(C [c] (invoke-restart 'skip))] (f)))";
(* §3: the body and every clause yield the whole form, so they have to agree
@@ -926,17 +926,17 @@ let () =
accepts "struct pattern with :keys"
(pt ^ "(defn f [p Point] i32 (let [{:keys [x y]} p] (+ x y)))");
accepts "struct pattern with a name/:field pair"
- (pt ^ "(defn f [p Point] i32 (let [{a :x b :y} p] (+ a b)))");
+ (pt ^ "(defn f [p Point] i32 (let [{a .x b .y} p] (+ a b)))");
accepts "a nested struct pattern"
- (line ^ "(defn f [l Line] i32 (let [{{:keys [x y]} :a} l] (+ x y)))");
+ (line ^ "(defn f [l Line] i32 (let [{{:keys [x y]} .a} l] (+ x y)))");
(* A later binding sees an earlier pattern's names, as in any let. *)
accepts "a binding after a pattern sees its names"
(pt ^ "(defn f [p Point] i32 (let [{:keys [x]} p y (+ x 1)] y))");
(* Shadowing works because the value goes into a temporary first. *)
accepts "a pattern may shadow the name it destructures"
- (line ^ "(defn f [a Line] i32 (let [{a :a} a] (.x a)))");
+ (line ^ "(defn f [a Line] i32 (let [{a .a} a] (.x a)))");
accepts "a pattern over a call"
- (pt ^ "(defn mk [] Point (Point {:x 1 :y 2}))\n\
+ (pt ^ "(defn mk [] Point (Point {.x 1 .y 2}))\n\
(defn f [] i32 (let [{:keys [x y]} (mk)] (+ x y)))");
rejects_check "a field the struct does not have"
@@ -952,7 +952,7 @@ let () =
(pt ^ "(defn f [p Point] i32 (let [{} p] 0))")
~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))")
+ (pt ^ "(defn f [p Point] i32 (let [{.x} p] 0))")
~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))")
@@ -981,7 +981,7 @@ let () =
"(defn f [] i32 (let [xs [1 2] [a b & r] xs] (+ a (+ b (len r)))))";
accepts "a struct pattern nested in an array pattern"
(pt ^ "(defn f [ps [2 Point]] i32 \
- (let [[{:keys [x]} {y :y}] ps] (+ x y)))");
+ (let [[{:keys [x]} {y .y}] ps] (+ x y)))");
rejects_check "an array pattern that names too few elements"
"(defn f [] i32 (let [xs [1 2 3] [a b] xs] (+ a b)))"
diff --git a/test/test_repl.ml b/test/test_repl.ml
index b8c889f..f642277 100644
--- a/test/test_repl.ml
+++ b/test/test_repl.ml
@@ -96,9 +96,9 @@ let () =
this would otherwise come back as -1. *)
value "u64 at its maximum" "big" "18446744073709551615";
(* A struct, nested, with a fixed array inside it. *)
- value "a struct" "(.pos b)" "(V {:x 1.5 :y 0})";
+ value "a struct" "(.pos b)" "(V {.x 1.5 .y 0})";
value "a nested struct" "b"
- "(Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y 0}) :tags [ 0 42 0]})";
+ "(Blob {.id 7 .name \"sandy \\\"quoted\\\"\" .pos (V {.x 1.5 .y 0}) .tags [ 0 42 0]})";
value "a fixed array" "arr" "[ 0 0 9 0]";
(* A slice's length is not known until it runs, so this one renders
through a loop rather than by unrolling. *)
diff --git a/tools/colon-to-dot.py b/tools/colon-to-dot.py
new file mode 100755
index 0000000..b7a4b3f
--- /dev/null
+++ b/tools/colon-to-dot.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python3
+"""Rewrite struct field labels from the colon spelling to the dot spelling.
+
+`{:x 1.0 :y 2.0}` becomes `{.x 1.0 .y 2.0}`, and the destructuring pair
+`{inner :field}` becomes `{inner .field}`. `:keys` is left alone: it names no
+field, it is an instruction to the compiler, so the dot keeps exactly one
+meaning -- "this names a field".
+
+Only keywords that sit in a *field-label* position inside a brace form are
+touched. Enum members, map keys and every keyword inside a string literal or a
+comment are left as they are. The lexing rules here mirror lib/reader.ml.
+
+Re-runnable: converting an already-converted file is a no-op, so this can be
+run again over files a parallel branch wrote in the old spelling.
+
+ tools/colon-to-dot.py ... # rewrite .flan in place
+ tools/colon-to-dot.py --check ... # report, change nothing
+ tools/colon-to-dot.py --in-strings ... # Flan inside "..." literals
+ tools/colon-to-dot.py --raw-ml ... # Flan in a {flan|...|flan} block
+
+A directory is walked for `.flan` files only. An `.ml` file is converted when it
+is named on the command line, which is how the Flan source embedded in
+`lib/prelude.ml` and in the tests gets swept; it is deliberately not automatic,
+because an OCaml record written `{ v: value }` would look like a field label to
+the scan. Read the diff when sweeping `.ml`.
+"""
+
+import sys, os
+
+DELIM = set('()[]{}";`~ \t\n\r,')
+OPENERS = {'(': ')', '[': ']', '{': '}'}
+CLOSERS = {')', ']', '}'}
+
+
+class Atom:
+ def __init__(self, start, end, text):
+ self.start, self.end, self.text = start, end, text
+ self.tok = start # never moves; `start` may slide onto a quote sigil
+
+ def label(self):
+ """The keyword name if this atom is a `:kw`, else None."""
+ t = self.text
+ return t[1:] if len(t) > 1 and t[0] == ':' else None
+
+
+class Seq:
+ def __init__(self, open_char, start):
+ self.open_char, self.start = open_char, start
+ self.end = start
+ self.items = []
+
+ def label(self):
+ return None
+
+
+def lex_forms(src, i, end, stop=None):
+ """Read forms from src[i:end] until `stop` (a closing char) or exhaustion.
+
+ Returns (items, next_index). A quote/quasiquote/unquote prefix is folded
+ into the form it applies to, so a quoted value stays one element.
+ """
+ items = []
+ n = end
+ pending_prefix = None # start offset of a sigil awaiting its form
+
+ def push(node):
+ nonlocal pending_prefix
+ if pending_prefix is not None:
+ node.start = pending_prefix
+ pending_prefix = None
+ items.append(node)
+
+ while i < n:
+ c = src[i]
+ if c in ' \t\n\r,':
+ i += 1
+ elif c == ';': # line comment
+ while i < n and src[i] != '\n':
+ i += 1
+ elif c == '"': # string literal
+ j = i + 1
+ while j < n and src[j] != '"':
+ j += 2 if src[j] == '\\' else 1
+ j = min(j + 1, n)
+ push(Atom(i, j, src[i:j]))
+ i = j
+ elif c == '\\': # character literal
+ j = i + 1
+ if j < n:
+ j += 1 # always one char
+ while j < n and src[j] not in DELIM:
+ j += 1
+ push(Atom(i, j, src[i:j]))
+ i = j
+ elif c in "'`~": # quote sugar
+ if pending_prefix is None:
+ pending_prefix = i
+ i += 2 if (c == '~' and i + 1 < n and src[i + 1] == '@') else 1
+ elif c in OPENERS:
+ node = Seq(c, i)
+ node.items, i = lex_forms(src, i + 1, n, OPENERS[c])
+ node.end = i
+ push(node)
+ elif c in CLOSERS:
+ return items, i + 1
+ else: # symbol or keyword
+ j = i
+ while j < n and src[j] not in DELIM:
+ j += 1
+ if j == i:
+ j = i + 1
+ push(Atom(i, j, src[i:j]))
+ i = j
+ return items, n
+
+
+def collect(node, out):
+ """Walk the form tree, recording the offsets of every colon to rewrite."""
+ if isinstance(node, Seq):
+ if node.open_char == '{':
+ items = node.items
+ k = 0
+ while k < len(items):
+ a = items[k]
+ b = items[k + 1] if k + 1 < len(items) else None
+ a_label = a.label()
+ b_label = b.label() if b is not None else None
+ if a_label == 'keys':
+ pass # a directive, not a field
+ elif a_label is not None:
+ out.append(a.tok) # {:field value}
+ elif b_label is not None and b_label != 'keys':
+ out.append(b.tok) # {pattern :field}
+ k += 2
+ for it in node.items:
+ collect(it, out)
+
+
+def convert(src):
+ items, _ = lex_forms(src, 0, len(src))
+ out = []
+ for it in items:
+ collect(it, out)
+ if not out:
+ return src, 0
+ chars = list(src)
+ for off in out:
+ assert chars[off] == ':', "expected ':' at offset %d" % off
+ chars[off] = '.'
+ return ''.join(chars), len(out)
+
+
+def convert_in_ocaml_strings(src):
+ """Convert Flan source that sits inside ordinary OCaml `"..."` literals.
+
+ The tests hold their Flan snippets that way, so the plain scan skips right
+ over them. Each literal's raw text is scanned on its own, with `\\"` masked
+ to a same-length filler first so an escaped quote cannot be mistaken for the
+ start of a Flan string. Masking preserves length, so offsets map back 1:1.
+ """
+ chars = list(src)
+ total = 0
+ i, n = 0, len(src)
+ while i < n:
+ c = src[i]
+ if c == '"':
+ j = i + 1
+ while j < n and src[j] != '"':
+ j += 2 if src[j] == '\\' else 1
+ body = src[i + 1:j]
+ masked = body.replace('\\"', '\\x')
+ _, offs = _offsets(masked)
+ for off in offs:
+ pos = i + 1 + off
+ if chars[pos] == ':':
+ chars[pos] = '.'
+ total += 1
+ i = j + 1
+ elif c == '(' and i + 1 < n and src[i + 1] == '*': # OCaml comment
+ i += 2
+ else:
+ i += 1
+ return ''.join(chars), total
+
+
+def _offsets(src):
+ items, _ = lex_forms(src, 0, len(src))
+ out = []
+ for it in items:
+ collect(it, out)
+ return src, out
+
+
+def walk(paths):
+ for p in paths:
+ if os.path.isdir(p):
+ for root, dirs, files in os.walk(p):
+ dirs[:] = [d for d in dirs
+ if d not in ('_build', '.git', 'vendor', 'node_modules')]
+ for f in sorted(files):
+ if f.endswith('.flan'):
+ yield os.path.join(root, f)
+ else:
+ yield p
+
+
+def main(argv):
+ check = '--check' in argv
+ in_strings = '--in-strings' in argv
+ allow_raw_ml = '--raw-ml' in argv
+ paths = [a for a in argv[1:] if not a.startswith('--')] or ['.']
+ total_files = total_sites = 0
+ for path in walk(paths):
+ if path.endswith('.ml') and not in_strings and not allow_raw_ml:
+ sys.stderr.write(
+ "%s: an .ml file needs --in-strings (its Flan is inside OCaml "
+ "string literals), or --raw-ml if the Flan is in a {flan|...|flan} "
+ "block. A plain scan would read OCaml's `::` as a field label.\n"
+ % path)
+ return 2
+ with open(path, encoding='utf-8') as fh:
+ src = fh.read()
+ new, n = convert_in_ocaml_strings(src) if in_strings else convert(src)
+ if n:
+ total_files += 1
+ total_sites += n
+ print("%s: %d" % (path, n))
+ if not check:
+ with open(path, 'w', encoding='utf-8') as fh:
+ fh.write(new)
+ verb = "would convert" if check else "converted"
+ print("%s %d field labels across %d files" % (verb, total_sites, total_files))
+ return 1 if (check and total_sites) else 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/web/examples/boom.flan b/web/examples/boom.flan
index ca75da6..7771015 100644
--- a/web/examples/boom.flan
+++ b/web/examples/boom.flan
@@ -2,7 +2,7 @@
(defn load [n i32] i32
(restart-case
- (do (error (Missing {:id n})) ; Never — only a transfer gets past
+ (do (error (Missing {.id n})) ; Never — only a transfer gets past
0)
(use-placeholder [] -1)
(retry [] 7)))
diff --git a/web/examples/breakdemo.flan b/web/examples/breakdemo.flan
index be13438..7f7ae55 100644
--- a/web/examples/breakdemo.flan
+++ b/web/examples/breakdemo.flan
@@ -4,7 +4,7 @@
(defn load [n i32] i32
(restart-case
- (do (error (Missing {:id n}))
+ (do (error (Missing {.id n}))
0)
(use-placeholder [] -1)
(retry [] 7)))
diff --git a/web/examples/conds.flan b/web/examples/conds.flan
index 75d3589..5537d56 100644
--- a/web/examples/conds.flan
+++ b/web/examples/conds.flan
@@ -3,8 +3,8 @@
(defvar seen i64)
(defn load-all []
- (signal (AssetMissing {:id 1})) ; Unit — the caller carries on
- (signal (AssetMissing {:id 2})))
+ (signal (AssetMissing {.id 1})) ; Unit — the caller carries on
+ (signal (AssetMissing {.id 2})))
(defn main []
(load-all) ; no handler: a no-op
diff --git a/web/examples/geom/vec.flan b/web/examples/geom/vec.flan
index 956560c..c990b1d 100644
--- a/web/examples/geom/vec.flan
+++ b/web/examples/geom/vec.flan
@@ -2,4 +2,4 @@
(defstruct V2 [x f32 y f32])
(defn add [a V2 b V2] V2
- (V2 {:x (+ (.x a) (.x b)) :y (+ (.y a) (.y b))}))
+ (V2 {.x (+ (.x a) (.x b)) .y (+ (.y a) (.y b))}))
diff --git a/web/examples/pkg.flan b/web/examples/pkg.flan
index 635493b..d55033b 100644
--- a/web/examples/pkg.flan
+++ b/web/examples/pkg.flan
@@ -3,7 +3,7 @@
(import g "geom")
(defn main []
- (let [v (g/add (g/V2 {:x 3.0 :y 0.0})
- (g/V2 {:x 0.0 :y 4.0}))]
+ (let [v (g/add (g/V2 {.x 3.0 .y 0.0})
+ (g/V2 {.x 0.0 .y 4.0}))]
(print (g/length v))
(println "")))
diff --git a/web/examples/places.flan b/web/examples/places.flan
index 913d9b7..10ce89a 100644
--- a/web/examples/places.flan
+++ b/web/examples/places.flan
@@ -6,13 +6,13 @@
;; `set` takes a fixed list of forms, not an extensible setf.
(defn main []
- (let [e (Enemy {:hp 10 :name "slime"})
+ (let [e (Enemy {.hp 10 .name "slime"})
p (addr e)]
(set spawned (+ spawned 1)) ; a local or a defvar
(set (.hp e) 7) ; a struct field
(set (.hp p) 8) ; through a (Ptr Enemy) — derefs one level
(set (at room 2) 5) ; a fixed array or slice element
- (set (deref p) (Enemy {:hp 3 :name "wisp"})) ; a whole-object store
+ (set (deref p) (Enemy {.hp 3 .name "wisp"})) ; a whole-object store
(print (.hp e)) (println "")
(println (.name e))
diff --git a/web/examples/printing.flan b/web/examples/printing.flan
index eab17a2..206cd79 100644
--- a/web/examples/printing.flan
+++ b/web/examples/printing.flan
@@ -7,7 +7,7 @@
(defn main []
(println 42) ; an i32, uncast
(println 1.5)
- (println (Enemy {:hp 3 :name "wisp" :key :left}))
+ (println (Enemy {.hp 3 .name "wisp" .key :left}))
(println (look-up :space))
(println (look-up :left))
(print "no newline: ") (println true))
diff --git a/web/examples/restart.flan b/web/examples/restart.flan
index b60ee75..564f8c8 100644
--- a/web/examples/restart.flan
+++ b/web/examples/restart.flan
@@ -3,7 +3,7 @@
(defvar cleanups i64)
(defn load [n i32] i32
- (signal (AssetMissing {:id n}))
+ (signal (AssetMissing {.id n}))
100)
(defn middle [n i32] i32
diff --git a/web/examples/structs.flan b/web/examples/structs.flan
index 53ea21c..278377d 100644
--- a/web/examples/structs.flan
+++ b/web/examples/structs.flan
@@ -11,7 +11,7 @@
(set (.pos c) (+ (.pos c) 1))) ; field access derefs one level
(defn main []
- (let [c (Cursor {:src (bytes "hi")})] ; pos omitted, so pos is 0
+ (let [c (Cursor {.src (bytes "hi")})] ; pos omitted, so pos is 0
(print (peek (addr c))) (println "")
(advance (addr c))
(print (peek (addr c))) (println "")))
From 8e4735659203096e50c99b2b85d3e14ee48553b2 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sat, 12 Sep 2026 14:51:57 +0700
Subject: [PATCH 2/5] A field label is a dot now, and the colon is refused
where one was
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
kept two jobs -- field label and enum member -- and this leaves it with
one, keys, which is what a map literal will want.
The old spelling is refused rather than quietly accepted, and the refusal
names the new one. Two accepted spellings is how two spellings become
permanent, and this repo rejects what it does not support and says why.
:keys keeps its colon. It names no field -- it is an instruction to the
compiler that happens to sit in the same brace -- so leaving it alone is
what lets the dot mean exactly one thing.
render.ml prints the dot too, or a struct the daemon shows would not be
Flan anyone could paste back.
---
examples/core-input-multitouch.flan | 2 +-
lib/parse.ml | 52 +++++++++++++++++++++--------
lib/render.ml | 2 +-
syntax-sketch.flan | 6 ++--
test/test_flan.ml | 4 +--
tools/colon-to-dot.py | 4 ++-
vendor/edn/edn.flan | 8 ++---
vendor/raylib/raylib.flan | 52 ++++++++++++++---------------
8 files changed, 79 insertions(+), 51 deletions(-)
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 ─────────────────────────────────────────────────────────
From cb757868b40f1cabb117246374576bb981559936 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sat, 12 Sep 2026 14:55:47 +0700
Subject: [PATCH 3/5] Keep the printed struct a colon; it is a wire format
Emacs reads back
render.ml's output is parsed by emacs/flan-inspect.el, which hard-codes
the colon when it reads a field out of a rendered struct. Moving the
printer on its own would break inspection in the dev loop without
breaking a test that says so, so the printer waits and moves with its
reader, in the Emacs lane.
The sweep could not tell a rendered *expectation* from a Flan *source*
snippet -- both are strings in a test -- so it converted both. The suite
named every one it got wrong, and those are back.
emacs/test-flan-dev.el:415 is the one edit inside emacs/: Flan source sent
to the daemon for eval, which the parser now refuses in the old spelling.
One label, in a fixture.
---
emacs/test-flan-dev.el | 2 +-
lib/render.ml | 6 +++++-
test/test_acceptance.ml | 6 +++---
test/test_dev.ml | 2 +-
test/test_flan.ml | 14 ++++++++++++++
test/test_repl.ml | 4 ++--
6 files changed, 26 insertions(+), 8 deletions(-)
diff --git a/emacs/test-flan-dev.el b/emacs/test-flan-dev.el
index 7a89d58..4dbb798 100644
--- a/emacs/test-flan-dev.el
+++ b/emacs/test-flan-dev.el
@@ -412,7 +412,7 @@ is written instead — the real `message' call the real command makes."
;; that errors stops it on its own game thread, in a frame of its own — not
;; inside anything this client asked for. Nothing tells Emacs.
(flan-dev--eval
- "(defn step [] i64 (restart-case (do (error (Missing {:id 7})) 0) (use-placeholder [] -1)))"
+ "(defn step [] i64 (restart-case (do (error (Missing {.id 7})) 0) (use-placeholder [] -1)))"
"form")
(let ((deadline (+ (float-time) 20)))
(while (and (not flan-dev--stopped) (< (float-time) deadline))
diff --git a/lib/render.ml b/lib/render.ml
index f8f6377..1346521 100644
--- a/lib/render.ml
+++ b/lib/render.ml
@@ -147,7 +147,11 @@ 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 ^ " ") ]
+ (* Still a colon, deliberately. This printed form is a wire
+ format: emacs/flan-inspect.el parses it back, and it
+ hard-codes the colon. Moving the printer to the dot has
+ to land with that reader, in the Emacs lane. *)
+ @ [ lit (":" ^ f.Tast.fname ^ " ") ]
@ render c (depth + 1) v)
shown)
in
diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml
index e2156bd..04efbf4 100644
--- a/test/test_acceptance.ml
+++ b/test/test_acceptance.ml
@@ -150,15 +150,15 @@ let () =
let println_out =
"plain string\nplain bytes\n42\n-7\n5\n18446744073709551615\n3.5\n\
-0.25\ntrue\nfalse\n()\n:green\n:red\n\n(some 0)\nnone\n\
- (Blob {.id 7 .name \"sandy \\\"quoted\\\"\" .pos (V {.x 1.5 .y -2}) .tags [ 0 42 0]})\n\
+ (Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y -2}) :tags [ 0 42 0]})\n\
[ 0 0 9 0]\n[ 0 0 0 0 0 0 0 0 ...]\n[ 0 9 0]\n\
- (D1 {.d (D2 {.d (D3 {.d (D4 {.d (D5 {.n ...})})})})})\n[ 0 0]\n\
+ (D1 {:d (D2 {:d (D3 {:d (D4 {:d (D5 {:n ...})})})})})\n[ 0 0]\n\
[ 0 0]\n[ 9 0]\n"
(* The escape buffer is 1024 and the input is 1100 x's, so this is the
truncation: the ellipsis goes *inside* the quotes, and the count is
spelled out rather than pasted so that a change to the buffer or to
the reserve shows up here as a number and not as a wall of x. *)
- ^ "(Long {.s \"" ^ String.make 1014 'x' ^ "...\"})\n"
+ ^ "(Long {:s \"" ^ String.make 1014 'x' ^ "...\"})\n"
^ "abc\n"
in
outputs "println, every arm" "programs/println.flan" println_out;
diff --git a/test/test_dev.ml b/test/test_dev.ml
index 02d708e..15b3d8c 100644
--- a/test/test_dev.ml
+++ b/test/test_dev.ml
@@ -775,7 +775,7 @@ let () =
let want =
[ ("n", "i64", "3");
("label", "string", "\"hello\"");
- ("p", "Point", "(Point {.x 1.5 .y 2.5})");
+ ("p", "Point", "(Point {:x 1.5 :y 2.5})");
("xs", "[3 i32]", "[ 10 20 30]");
("flag", "bool", "true") ]
in
diff --git a/test/test_flan.ml b/test/test_flan.ml
index 3c0a14d..a4e501b 100644
--- a/test/test_flan.ml
+++ b/test/test_flan.ml
@@ -618,6 +618,12 @@ let () =
rejects_check "field given twice"
(cursor ^ "(defn f [s [u8]] Cursor (Cursor {.pos 0 .pos 1}))")
~needle:"given twice";
+ (* The old spelling is refused rather than quietly accepted, and the refusal
+ names the new one. Two accepted spellings is how two spellings become
+ permanent, and the colon is wanted for keys. *)
+ rejects_check "a field label written with a colon"
+ (cursor ^ "(defn f [s [u8]] Cursor (Cursor {:src s}))")
+ ~needle:"a field label is written .src, not :src";
accepts "field through a pointer auto-derefs"
(cursor ^ "(defn f [c (Ptr Cursor)] i32 (.pos c))");
accepts "set through a pointer"
@@ -957,6 +963,14 @@ let () =
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";
+ (* The same refusal on the destructuring side: a field is a field wherever it
+ is named, so the rule is not half-applied. :keys is the one that keeps its
+ colon, and the test below it says so. *)
+ rejects_check "a destructured field written with a colon"
+ (pt ^ "(defn f [p Point] i32 (let [{a :x} p] a))")
+ ~needle:"a field label is written .x, not :x";
+ accepts ":keys keeps its colon, naming no field"
+ (pt ^ "(defn f [p Point] i32 (let [{:keys [x y]} p] (+ x y)))");
(* 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/test/test_repl.ml b/test/test_repl.ml
index f642277..b8c889f 100644
--- a/test/test_repl.ml
+++ b/test/test_repl.ml
@@ -96,9 +96,9 @@ let () =
this would otherwise come back as -1. *)
value "u64 at its maximum" "big" "18446744073709551615";
(* A struct, nested, with a fixed array inside it. *)
- value "a struct" "(.pos b)" "(V {.x 1.5 .y 0})";
+ value "a struct" "(.pos b)" "(V {:x 1.5 :y 0})";
value "a nested struct" "b"
- "(Blob {.id 7 .name \"sandy \\\"quoted\\\"\" .pos (V {.x 1.5 .y 0}) .tags [ 0 42 0]})";
+ "(Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y 0}) :tags [ 0 42 0]})";
value "a fixed array" "arr" "[ 0 0 9 0]";
(* A slice's length is not known until it runs, so this one renders
through a loop rather than by unrolling. *)
From e992491799eec0d1da1bffb952ee78c77e823988 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sat, 12 Sep 2026 15:00:34 +0700
Subject: [PATCH 4/5] The colon belongs to keys; the prose, the page and the
sweep agree now
web/index.html's Flan blocks convert and its output blocks do not, which
is the same split render.ml makes: the printed form keeps the colon until
the Emacs inspector that reads it moves too. Same in BUILT.md.
plan.org, spec-conditions.md and spec-memory.md carried struct literals in
the old spelling and now do not.
NEXT.md decision 6 is struck, and batch item 2 with it, naming what to run
at merge. BUILT.md says why the colon belongs to keys -- mostly that a map
literal wants {:key value}, and two literals sharing one syntax would have
left the reader asking the checker which it was looking at.
The sweep was not idempotent and is now: {.k :hi} -- a field already
converted, holding an enum member -- read as a destructuring pair on a
second run and ate the member. A re-run over a lane's files would have
corrupted them silently, which is exactly what the tool exists to do
safely.
---
BUILT.md | 45 +++++++++++++++++-
NEXT.md | 34 ++++++-------
plan.org | 4 +-
spec-conditions.md | 2 +-
spec-memory.md | 2 +-
.../__pycache__/colon-to-dot.cpython-313.pyc | Bin 0 -> 11286 bytes
tools/colon-to-dot.py | 14 ++++++
web/index.html | 22 ++++-----
8 files changed, 86 insertions(+), 37 deletions(-)
create mode 100644 tools/__pycache__/colon-to-dot.cpython-313.pyc
diff --git a/BUILT.md b/BUILT.md
index 5f882f8..60c69ec 100644
--- a/BUILT.md
+++ b/BUILT.md
@@ -1570,7 +1570,7 @@ And a `Vec` does not cross to C: handing a header that owns storage to C hands o
### `StorageExhausted` went in *with* `Vec`, not after it
No allocating operation returns an error and none can fail silently. When the allocator cannot satisfy a request the
-operation signals `(StorageExhausted {:bytes n :align a :allocator id})` with `error` — whose type is `Never` — inside a
+operation signals `(StorageExhausted {.bytes n .align a .allocator id})` with `error` — whose type is `Never` — inside a
`restart-case` offering `retry`. One rule over every allocating operation, which is what keeps `push` and `reserve` at
`Unit`, `clone` at the container, and no signature anywhere growing a `Result`.
@@ -1688,7 +1688,7 @@ rule without an exception: *no allocating operation returns an error*. There is
no error code — `slurp`'s type is `(Vec u8)` and `barf`'s is `Unit`.
**Two failures, two conditions, and the guards nest rather than merge.** Allocation failure is `StorageExhausted` under
-`retry`, unchanged and reused. File failure is `FileError {:path :op :reason}` under `retry` and `use-value`. They stay
+`retry`, unchanged and reused. File failure is `FileError {.path .op .reason}` under `retry` and `use-value`. They stay
apart because they ask two different answerable questions: the handler that grows an arena is not the handler that
supplies another path, and collapsing them would make one handler guess which it was looking at. `file_guard` in
check.ml is `alloc_guard`'s shape built from the same nodes — a `while`, a `restart-case` and an `error` — so the
@@ -2042,3 +2042,44 @@ on `window is not defined` — which says the module is live and says nothing ab
correct for a page, which is torn down by the tab closing, and it is worth knowing before reading anything into it.
- **Canvas size against `screen-width`/`screen-height`.** The shell is a string in `Build` and its canvas is not sized
from the program, so 900x600 may be letterboxed or cropped.
+
+## The colon belongs to keys, so a field label is a dot
+
+`{.x 1.0 .y 2.0}` is how a struct is constructed, and `{inner .field}` is how a pattern names one. The colon is gone
+from both, and what is left of it is one job: keys — map keys and enum members.
+
+**What was wrong with the colon.** Nothing, taken alone. The problem was that it had two jobs and the dot had one.
+`(.x v)` already read a field; `{:x 1.0}` also named a field, while `:space` named an enum member. So the dot meant
+"field" and the colon meant "field, or member, depending". Moving the label to the dot leaves each mark with one
+meaning, and it costs nothing to read because **the delimiter already disambiguates**: `(.x v)` is a list and
+therefore a call and therefore an access; `{.x 1.0}` is a brace form and therefore a construction. There is no
+position where the two could be confused, which is why the same spelling can serve both.
+
+**Why it had to land before `Map`, and this is the real reason.** A map literal wants to be `{:key value}`. While
+struct construction owned that exact spelling, a map literal and a struct literal were *the same syntax*, and the
+only thing that could tell them apart was what the checker expected at that position. That is a context-sensitive
+grammar for no gain. Reserving the colon for keys keeps the two visibly distinct at the reader, before any type is
+known. Doing it after `Map` landed would have meant changing both; doing it first meant changing one.
+
+**`:keys` kept its colon, and that is the point rather than an inconsistency.** `{:keys [x y]}` is the one thing in a
+brace that is not a field name — it is an instruction to the compiler that happens to sit there, and it takes a
+vector rather than a value. Giving it a dot would have made the dot mean "a field, or the word keys". Leaving it a
+colon lets the dot mean exactly one thing, *this names a field*, which is the whole reason the colon was given up.
+Everything else Clojure puts in that position — `:as`, `:or`, `:strs`, `:syms` — is still refused by its own name.
+
+**The old spelling is refused, not accepted quietly**, and the refusal names the new one: `a field label is written
+.x, not :x — the colon is for keys`. Two accepted spellings is how two spellings become permanent, and the standing
+rule here is that what is not supported is rejected explicitly with the reason. Both refusals are tested by their
+reason, on the construction side and on the destructuring side, which is what stops the colon drifting back.
+
+**The sweep is a tool, not a one-off.** `tools/colon-to-dot.py` converted 681 labels across 45 `.flan` files,
+`vendor/`, and the Flan embedded in `lib/prelude.ml` and the tests. It works on *forms*, not on text: a keyword
+becomes a dot only where it sits in a field-label position inside a brace, so an enum member in value position
+(`{.k :hi}`), a genuine EDN map inside a string (`test/programs/edn.flan`), and a type-position `{K V}` are all left
+alone. It was kept in the tree because several lanes branched before it and their Flan needs the same pass at merge.
+
+**What it deliberately did not change: the printed form.** `render.ml` still prints `(V {:x 1.5 :y 0})`. That string
+is a wire format — `emacs/flan-inspect.el` parses it back and hard-codes the colon when it reads a field out — so
+moving the printer alone would break struct inspection in the dev loop without breaking any test that says so. The
+printer moves when its reader does, in the Emacs lane. It is the one place the old spelling is still correct, and
+the reason is worth keeping: **a format with two ends only changes at both.**
diff --git a/NEXT.md b/NEXT.md
index 5b0d339..1f82cee 100644
--- a/NEXT.md
+++ b/NEXT.md
@@ -277,23 +277,16 @@ length is not known until the file is read and therefore cannot exist before an
## Decided later the same day, and queued
-**6. A field label is written with a dot, not a colon, and the colon is reserved for keys.** `{.x 1.0 .y 2.0}`
-replaces `{:x 1.0 :y 2.0}` in struct construction, and the same change applies where destructuring names a field.
-The delimiter is what makes it unambiguous, which is the author's argument and it holds: `(.x v)` is a call and
-therefore an access, `{.x 1.0}` is a brace form and therefore a construction. Today the dot already means "read a
-field" and the colon means both "field label" and "enum member", which is the ambiguity the change removes.
+~~**6. A field label is written with a dot, not a colon, and the colon is reserved for keys.**~~ **Done.**
+`{.x 1.0 .y 2.0}` is struct construction and `{inner .field}` is destructuring; the old spelling is refused, and the
+refusal names the new one. `:keys` kept its colon — it names no field, so leaving it alone is what lets the dot mean
+exactly one thing. 681 labels across 45 `.flan` files, `vendor/` and the Flan embedded in `lib/prelude.ml` and the
+tests. `Map` is now free to take `{:key value}` without colliding with struct literals. See BUILT.md, "The colon
+belongs to keys".
-**The reason to do it before `Map`, not after.** A map literal will want to be `{:key value}`. If struct
-construction owns that exact spelling, map literals and struct literals are the same syntax and the checker has to
-tell them apart from context. Reserving the colon for keys — map keys and enum members — keeps the two visibly
-distinct. Doing this after `Map` lands means changing both; doing it now means changing one.
-
-Cost: ~284 sites across 45 `.flan` files, mechanical. **Queued rather than started** only because it touches nearly
-every Flan file in the repo, including ones a running lane held. Run it when the tree is quiet, before step 4.
-
-One thing to decide with it: destructuring uses the colon two ways — `{inner :field}` names a field, which should
-become a dot like any other field, and `{:keys [x y]}` where `:keys` is an instruction to the compiler rather than a
-field name, which arguably stays a colon. Settle both in the same pass rather than leaving the rule half-applied.
+**Two things it left behind.** `render.ml` still *prints* a struct with colons, deliberately:
+`emacs/flan-inspect.el:165` parses that output and hard-codes the colon, so the printer has to move with its reader
+and that belongs to the Emacs lane. `emacs/MANUAL.md` and `flan-mode.el`'s font-lock also still show the colon.
**7. `Map` follows Odin's implementation.** Read `base/runtime/dynamic_map_internal.odin` before writing any of it;
the checkout is at `~/Repositories/Odin`. Three properties are the ones worth copying, and they are stated in its own
@@ -416,9 +409,10 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
from this, and a red suite stops being a signal quickly. The handoff says to start by printing both sides of the
comparison in `Dev.locals`.
-2. **The colon-to-dot change.** Cheap, mechanical, ~284 sites across 45 files — and it must land **before** `Map`, or
- map literals and struct literals collide and both have to change instead of one. It gets more expensive every day
- more Flan is written. See the decision above for the destructuring wrinkle to settle in the same pass.
+2. ~~**The colon-to-dot change.**~~ **Done**, and `Map` is unblocked: `{:key value}` is free. The sweep is
+ `tools/colon-to-dot.py`, kept rather than thrown away, because the lanes that branched before it wrote Flan in the
+ old spelling and their files want the same pass at merge — `python3 tools/colon-to-dot.py .` over the tree, and
+ `--in-strings` for a `test/*.ml` that embeds Flan.
3. **`Map`, and the `defer` relaxation.** `Map` is step 4 of the container build order and finishes what `Vec` started.
The `defer` change — permitting it in a `let` whose extent is the function body — is small, independent, and is the
@@ -854,7 +848,7 @@ expander last, on 6's unions.
with it — `rt_die` is the non-dev path too, where there is no listener and nothing to deadlock against, so whether it
should be `_exit` unconditionally or only under `--dev` is a decision rather than a typo.
-- **`(A {:x 1})` on a union variant says "unknown struct A"** rather than the union refusal `check_struct` plainly
+- **`(A {.x 1})` on a union variant says "unknown struct A"** rather than the union refusal `check_struct` plainly
intends — `env` has no table of variant names. A diagnostics bug, not a backend death.
### Test blind spots, from a mutation pass
diff --git a/plan.org b/plan.org
index 2cd59a6..1a6bf92 100644
--- a/plan.org
+++ b/plan.org
@@ -117,9 +117,9 @@ world.
~(clone x)~. Value structs are the snapshot / undo / replay story; they need no
separate type.
- Literals live in read-only memory.
-- Struct literals name fields: ~(Cursor {:src src :pos 0})~. *Omitted fields are
+- Struct literals name fields: ~(Cursor {.src src .pos 0})~. *Omitted fields are
zeroed*, as in Odin — the same rule as a declaration with no initialiser, so
- ~(Cursor {:src src})~ is complete and means ~pos~ is 0.
+ ~(Cursor {.src src})~ is complete and means ~pos~ is 0.
- *Zero is initialisation (ZII), with an opt-out.* No initialiser means
all-bytes-zero. ~(defvar buf [65536 u8] uninit)~ skips it, exactly as Odin's
~---~ does, for a large buffer that is about to be overwritten. ~uninit~ is
diff --git a/spec-conditions.md b/spec-conditions.md
index 5aedd48..1391c8e 100644
--- a/spec-conditions.md
+++ b/spec-conditions.md
@@ -25,7 +25,7 @@ must produce its type on the *fall-through* path too:
(if (file-exists? path)
(rl/load-texture path)
(restart-case
- (do (signal (AssetMissing {:path path}))
+ (do (signal (AssetMissing {.path path}))
(abort "unhandled AssetMissing")) ; fall-through must not return
(use-placeholder [] placeholder-texture)
(retry [] (load-texture path)))))
diff --git a/spec-memory.md b/spec-memory.md
index deecd60..72f3f66 100644
--- a/spec-memory.md
+++ b/spec-memory.md
@@ -385,7 +385,7 @@ fixed here; nothing is built that needs it yet.
the allocator cannot satisfy a request, the operation signals
```
-(StorageExhausted {:bytes n :align a :allocator id})
+(StorageExhausted {.bytes n .align a .allocator id})
```
with `error`, whose type is `Never` (spec-conditions.md §2), inside a
diff --git a/tools/__pycache__/colon-to-dot.cpython-313.pyc b/tools/__pycache__/colon-to-dot.cpython-313.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5c381bfcaaf5e931178733dcb9ddb218c26e27ad
GIT binary patch
literal 11286
zcmcIqYfv25mF}MDdC)VwG$4?Kq~;|w3Jk`U{6LQwBV<`N5HT8?7{$=cG%(W8gSuyg
zgyIy*kIfL-(GXeDC~{()WV5)|yQ`#9yQ-~yIN78sRr?2m9L=_=#Hn?q%Kwm*#OwUp
z@7(SlK*FxgR^=Al>AsJ1&pr3NZ{I5}7CDp>;jd5qT`kA`H@(=!Wj);c01sEVFsE^0
zKJ0jw*Le2p&>Sb7bzIoFk2gnZLRbj9!tSsq*WA?{#uRPk!s0%MHO|A@
zd}#9*+PvY)aPd~|6t5M9OSW>F7%oNa3zwnxhs#kHhc}@v2?tP@o-7MjwD)tIT+eYQ
z`Mx>}*EK(N+{se>^L-6#8m&hgWBXU)MjfM$;AT06P4_92T0EsldP+-2Q_@IWNyMat
zJgg*iX+%qoOQ|tMiY60DRnjMvL?W(^N~t7kjU`iswvZ?eU1&ciwS^v%+RsZ5haP%)
zNE%k6$#F%OhAxE6o{-hkEXiujgrS%Vr!@jRA;&eqiL0ujN$m`mbzsbnkkt{AhT2an
z=k*~eu1g7JBqhl}Q)!ptDM^)SVKphTvCY(kL9!~vRdc~OHe~GsNKV8Piq>g<%i4H;m=)_|R`iC_+sz@VAZCsKyMNB2r(J>_!lDgFNxHPVe4=bA9ERD+(
zv;gfSCMjnXt+3uIBCIUyxANfxoiaYY4zG{vG6;isIZhLCguN1Ra3(JU>U
z0NutEj2n+@8YU;=!>yVk#}q9zp1>yilollMFb>C9+g3%B;c^&*c%tYoR<#J>McpFB^K8Xg+d`3St#|AW^sOKB*FMXnn>UX
zJ_TM&3yvgudSkc*Zz8Fsnx*KNtd1(!FqlLg{Zx2ywZ%Lw-2@5>+lbanM@+OBf7Ai}
zI{QfVPlc(;lP%+k2jJ1h3ot?A3$*Jm0_+#*;bmz!k&K=e#SRI?QlhCOI19%#DJM?T
zF@g_<7@dZ!GfINhLsEx|=JABtNaI&eesW9!cc(;4PGS%tc6!8##Jo@>O0zUM7RT{o
z%vf@gb+b+D$+QMxAf}JSuzXw_qJf3z~b>6?7Xy2WaqNJqAY!pk3&hR=L-X>I&Dt
zDdNyTr(`W#Z$f{runWTqXDXyLzP%^tU{p4o2}Q;8Cf1zNsauk?0KK}HQ8GkE81}py&H6*A
zrn&e1WtX0rc_zazS8vO8TzAivUl-@j%$L8Jn%BQ~A#)~Mp52s5FL)2!6i{iKF)1jR
zr_0Wnj#w*4)$^8zPJxUjPC*Hvw0@N?93Q#MFQvuE>JU_sItP~|D6m<~B
zL|YXY!v&*0t_Pj$9JL@uvQC}mLxfBk5Wx&Ta4c|nD_V3SA-KFTwBW4+kC)_x*G2I6
zE8cu%RYrJKWW-s==0q2^BLNRrI14we0dGN`p`g_O`)@;%xPdMBuB4ssX0Qblww9Hi4#{RfeERhR>*dz}_5LIXqiN2p&adP%&(wQ}P{
z5r-;-`EfB)6cP72NG+c7s;>Tuc1Xk1y!`LbVmx80x+Cteb3dMB50gfr1&5*XPxHM$
zK#$9I!OfX6Hf+udyW1x*2Xu!7TXU>2;I(yQAa~su2Z%2NqK83z!J0=Hc=}U>m5iQf6fWwu*R$u*dTZy=dYC(4vp5&=07I-45$s?@^+a5C+^VkX
zuj=O`9)a6Olti&LmxQ^D77{Qwap>IubDS4jgD7WXg|juW8W2pF9Je1B$<-T-h1S?`
zEbWBG{~5UU{wFXM*?EGVC&VaVM8F6Ao5MavLue~@Vn8Y)FuDWlexfN_81?-d(36d%50WY_#yBCV#>K799QW~8X-k1?2aBcAJZ!PmEdHK#Ltm!9YLg_#KgF2
z#fdC9Lp&!>OeErnl2S<)&&VdMnB^_PLLokbA%;m-$bkuoJ?pe8i51!1drzV|uF~6lLbNH2G`R%ot
zp1imCl7GgZJ@Kx0E7QkQF6PE+kW#oF`wd<+_&S3>`61Yvn{qY{eaYbQ_hhihaE`}S
zW~L3-;jZVN={7vQpX=)B>I*ZKZxj^}VtZijgyHFYt~ZP!L7(9SR~UTUY!`?j3|`e9
zVe6vfl-7nvbLzu*(n$tk9?kOPlct&cERCV+aT-zqBtM?!R{UIX>2z1#>znS#S8kc^
z`J>=+ZCfefeEwG&^ZwGABN;yL^Jm42pUwM=v*j~KzSfa1F3Hwk?9XosTyD&DUOO@u
zyWW#8E6*OEb!Yh%Cl{!&+rQoO@%z?b^mojl`|
zFL$fd&80_sh!HkniorrJB~l2G>yi?Z$U&THnX^&(c0YxGcXh(J~z?$9bId*Be**
zc+Ov*t<5|>-OD(e3`e1m#wNMESAy0nYfURRP#9+$0P>`%$SY8mTwlTbnXObKAm~T`
zPgs9J^{@7;a9C7l#JOr-A`aUKFq8Ftn5=q2N);l`u#-&JCys?N>904(&p1)0JjU^;Vikj=1&^RfMqN0r&(`I>92Q77b@I
zoia@Wf7;+>gC90L@}L<5dWNkBM?9sIKVvIR%f~-PP^n7eU;hBrH1~n0JlpWP^R}mE
z(NmN6m0mhDb0`;H@=5uM>Z`|Qk7c?bmP}G#i{=_HpSm2%S8SO*n&tBijifH#?!4jp
z+ha=&k7m0_w!iV*&BN&1y6ugoYfaaKSx>%c_nR;0YU%a$;LQd!1h(czmI4icpRcT0
ztlXWisG@~(XWmF(OTSyuXtvJ3aMQiia%f%WEp!$%a}_kRiPDwON;Ma#y1M7JJ(;JL
z{ekRLzw+1Ih34MAQpqkIT`1&~F&)66b-fMwWU
zciVEUK=%8eZ^Ec<$QE&M*wt5XVNHCfj<{^8Tn!N-PP>h{ZSEeESFtiTwM98M0d7Ou
zuBp8Vc?z&OvAfh5Gx(5!tOm2W8sk*rBtuBV^^_r;O2*ZoN9#b7#hruNv-EPD2D!+|
z)4{s{Mq9V|()41$;VccUy@;NR_}9ORYMNUvt#Lj8lx
zgJU$AXW;?nvB|0;69mP-AuCOE_U~ni{~w{uolNn&A};f&x)!rp8H#h2
zSqt$C6NGO?L@+F1sy-X*h>r}I>hG^%49w!p7_D4M@os+&F>J(PVe06XOxA_rVTMn2
z_BMhRBHI;MtN_)F?w}8C>=|@33WQK#oXa#4y$~V`y3?XFtqxj;{Tqr$A+r~8m@$l;
z#<6yq9BC%;t$en9A*9=PHApzeRT-IfGX*`wviw8HFteK*9YGA`|IGCE7+rh7dc-vg
zrW%K{4=ci;PRi#Cw^z8vKw_Md(UN{Tj!P^pK01~X*#!##jN?v8M$+9{r3w8qa_@+a
zDCVJQDrA?grpQE1sz}nuQ)AN5$+{t2Kk2v*z~z)AOF9yHEl4Vkq7xS_xIe=W@^*BI
zhNTringAd(s3NFHzz@@{5-itgB*kGKX+I)fHBu4)X(s>K^;Jmf##&@C5w_@xb{6rL
zxft80sZ(@zA2uxGPNuEBEhL&+=rM2zGn*PY_RMHbIZoE$|C~k|N()m(cHZFkXeaOs
zofM%q(n*E~_c(MBgk+_)iuOE>DK=FT^eMDaDw)D%*Hbj#CNKIft_@S17au2k_y3AC-Vbcfwaun;$Fm1AhY<^1Jvn=F
zu5rF{sj@Y5WO?(B+}WkgyJnuwbi7x!E$c)`k*l3ma?#n-cnMU`w$BCLg^jLl$ac*h
zgT>DspFQ@Ct~*6sbc|W;)(C
zV=cJojquQ4ZCu&4M%(80E%>AbL3&WrlHOrvnke{}HPii0CjAc66e2^*!pnk{1B(?b
z8{>*q+s2Zlj1kPZ^d;b`QT5OG*C~zqXIZlMeI=`L3;3l_h#2+wzxQ>t)^idF5#r!D-GK}kzsn*z#$2{uONX{P?uOa2|p6YGf_{GOMrXcr8`%NrKtgw;s3ly8
zplsaP8t{YG;J*Bq;3cka%v2B0Xpt#4T0`x4Y!WF
zXC-EMk?he)VVg?JW4LInZtxR^pe2*=voJ?DTy!;#R`_HO`coR+`myS#c+kI#>O=70
zd&Qe@3*;*%C+E_^nS;6V+rDj!zHQ5mO@H&kcVC!4{(~16%WAViuIrAQYx>MRj^Fky
z|J~SJd49*vg{Ef~cRcfvldIYOX6z2@$qKW_vz`yO?3{CbXM45-;r`XB*{Qjv+f{p(
zs`f1Jklq-*Hu^^5T4H|h(vC-NRX=ji#cgf6=i_!gw6Y1F`zuf1t>Q`oAKw*eosV^R
z5!_cg#7-Cg6TjHWd4ICiO|`TeEkE@?fqGpQuAmw=g9Gus4>`???w_CE;v^6e;S1wBBFr_(K&*dc&M8J<=LXWk~yKwEco;
zOLea}K$%lkR$G}X`7aTFVV^|pZ(^@*>oUgO|3&89c?RG_nk8J^|H@v+`kh1yr@_wa
zpMb;KeU-4CIjx-m!~09bXXA;Jyl-#azcx>O{Tks?INfDx@fy6!`)^0wd!3(%86vcZ
z$KEO7Nms>TPlK&dgb}WmYv$|`F`Ux~bj?njlf?D>=%8~aH^P7N2qff@Mm0wTH}y<;
ztdCx%{4Fi3U#m=cTUyNTOqioJ<-jZRs(=qNz4$<5>R%i6cJc%9%sv@G{$0UGQEQ=4
zYF@WKb1C@5rYpSu8zw#2OzC}lP4g05Su<1mgzs4>(Z9wWHoxMaZF(JKInia`2=mml}?ScJRampt45QQM4xigwkVXHoGLcKSzen_WIZUcsk%lTW7a~%
z0#OaFpVgB#+b8WI8}Tib(oq2e4RC{Q^t-tDuv4QT!nE&9NO2=yNG|CE*o*_>#xjJEc&;@
zKP}%mw|B9;Y5G_`uzjv#G0-^uT)w&{cRbg3&Aa5^|1EE}?H~Ma`EQ)Ky}SL=@oU~i
z|Nh(l!|yn6rZHKnT@Y%gpIt7i$VR^D%LtTVBI{Fm+qZMkxAX7Ce><@3FTHeZ=Gbk2
z&7!|%#o;LL;}MPrs_uB4z2RiH-YNxzU9g-xrRCC{OB($AH7+Zuh@L`=GnHgG!|
zWRAoA5MU3J2b-Kc)Lr@X7l^|Pu?U5~qd-XwPIEBg84;t_8VeQZJJ
zf-fjYiv>U7EXJTDun&!&AaKs9;4S6sftnc5><>{
z>Aba_s0;z$4yLp((>sjHnp=Fsd%*n4;sgBlyUjkn
z;cm?izV_}@ybG;mepEZQ@~tcFoZ!z)EjhR31 1 and t[0] == ':' else None
+ def converted(self):
+ """True if this atom is already a `.field` label."""
+ t = self.text
+ return len(t) > 1 and t[0] == '.' and not t[1].isdigit()
+
class Seq:
def __init__(self, open_char, start):
@@ -52,6 +57,9 @@ class Seq:
def label(self):
return None
+ def converted(self):
+ return False
+
def lex_forms(src, i, end, stop=None):
"""Read forms from src[i:end] until `stop` (a closing char) or exhaustion.
@@ -129,6 +137,12 @@ def collect(node, out):
pass # a directive, not a field
elif a_label is not None:
out.append(a.tok) # {:field value}
+ elif a.converted():
+ pass
+ # Already `{.field value}`. Without this the pair would fall
+ # to the rule below and an enum member in value position --
+ # `{.k :hi}` -- would be read as a destructuring label and
+ # converted on a second run. Re-running must be a no-op.
elif b_label is not None and b_label != 'keys':
out.append(b.tok) # {pattern :field}
k += 2
diff --git a/web/index.html b/web/index.html
index dbb2f25..4068d17 100644
--- a/web/index.html
+++ b/web/index.html
@@ -381,13 +381,13 @@ heap is involved.
;; `set` takes a fixed list of forms, not an extensible setf.
(defn main []
- (let [e (Enemy {:hp 10 :name "slime"})
+ (let [e (Enemy {.hp 10 .name "slime"})
p (addr e)]
(set spawned (+ spawned 1)) ; a local or a defvar
(set (.hp e) 7) ; a struct field
(set (.hp p) 8) ; through a (Ptr Enemy) — derefs one level
(set (at room 2) 5) ; a fixed array or slice element
- (set (deref p) (Enemy {:hp 3 :name "wisp"})) ; a whole-object store
+ (set (deref p) (Enemy {.hp 3 .name "wisp"})) ; a whole-object store
(print (.hp e)) (println "")
(println (.name e))
@@ -506,7 +506,7 @@ its fields, and omitted fields are zeroed.
(set (.pos c) (+ (.pos c) 1))) ; field access derefs one level
(defn main []
- (let [c (Cursor {:src (bytes "hi")})] ; pos omitted, so pos is 0
+ (let [c (Cursor {.src (bytes "hi")})] ; pos omitted, so pos is 0
(print (peek (addr c))) (println "")
(advance (addr c))
(print (peek (addr c))) (println "")))
@@ -753,7 +753,7 @@ user-supplied printer to choose between.
(defn main []
(println 42) ; an i32, uncast
(println 1.5)
- (println (Enemy {:hp 3 :name "wisp" :key :left}))
+ (println (Enemy {.hp 3 .name "wisp" .key :left}))
(println (look-up :space))
(println (look-up :left))
(print "no newline: ") (println true))
@@ -848,7 +848,7 @@ and no ceremony.
(defstruct V2 [x f32 y f32])
(defn add [a V2 b V2] V2
- (V2 {:x (+ (.x a) (.x b)) :y (+ (.y a) (.y b))}))
+ (V2 {.x (+ (.x a) (.x b)) .y (+ (.y a) (.y b))}))
;; geom/len.flan — a second file in the same directory shares one top-level
;; scope: it does not import vec.flan, and the order of the two does not matter.
@@ -860,8 +860,8 @@ and no ceremony.
(import g "geom")
(defn main []
- (let [v (g/add (g/V2 {:x 3.0 :y 0.0})
- (g/V2 {:x 0.0 :y 4.0}))]
+ (let [v (g/add (g/V2 {.x 3.0 .y 0.0})
+ (g/V2 {.x 0.0 .y 4.0}))]
(print (g/length v))
(println "")))
@@ -934,8 +934,8 @@ normally leaves the signaller to carry on — the accumulation case:
(defvar seen i64)
(defn load-all []
- (signal (AssetMissing {:id 1})) ; Unit — the caller carries on
- (signal (AssetMissing {:id 2})))
+ (signal (AssetMissing {.id 1})) ; Unit — the caller carries on
+ (signal (AssetMissing {.id 2})))
(defn main []
(load-all) ; no handler: a no-op
@@ -959,7 +959,7 @@ first, before the clause body starts.
(defvar cleanups i64)
(defn load [n i32] i32
- (signal (AssetMissing {:id n}))
+ (signal (AssetMissing {.id n}))
100)
(defn middle [n i32] i32
@@ -1055,7 +1055,7 @@ and an exit status of 134:
(defn load [n i32] i32
(restart-case
- (do (error (Missing {:id n})) ; Never — only a transfer gets past
+ (do (error (Missing {.id n})) ; Never — only a transfer gets past
0)
(use-placeholder [] -1)
(retry [] 7)))
From 73fb16bfa30d13d8e05600c5c91443e603ab6bcc Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sat, 12 Sep 2026 15:03:35 +0700
Subject: [PATCH 5/5] Two comments the sweep could not reach, and a handoff
note that was wrong
check.ml's prose carried struct literals in the old spelling in two
comments the form-level scan does not see, OCaml comments not being forms.
The Emacs handoff said MANUAL.md and flan-mode.el's font-lock still show
the colon. MANUAL.md does not mention a struct literal at all. font-lock
does have something, but it is the opposite of what was written: it colours
:name as a constant and has no rule for .name, so a field label is now
unfontified rather than wrongly coloured. Said accurately, with the line.
runtime/flan_rt.c:256 also shows {:name ...} and is left alone on purpose --
it describes the *printed* form, which still uses colons and is correct.
---
BUILT.md | 2 +-
NEXT.md | 14 ++++++++------
lib/check.ml | 4 ++--
3 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/BUILT.md b/BUILT.md
index 60c69ec..4476e0c 100644
--- a/BUILT.md
+++ b/BUILT.md
@@ -2073,7 +2073,7 @@ rule here is that what is not supported is rejected explicitly with the reason.
reason, on the construction side and on the destructuring side, which is what stops the colon drifting back.
**The sweep is a tool, not a one-off.** `tools/colon-to-dot.py` converted 681 labels across 45 `.flan` files,
-`vendor/`, and the Flan embedded in `lib/prelude.ml` and the tests. It works on *forms*, not on text: a keyword
+`vendor/` included, and 94 more in the Flan embedded in `lib/prelude.ml` and the tests. It works on *forms*, not on text: a keyword
becomes a dot only where it sits in a field-label position inside a brace, so an enum member in value position
(`{.k :hi}`), a genuine EDN map inside a string (`test/programs/edn.flan`), and a type-position `{K V}` are all left
alone. It was kept in the tree because several lanes branched before it and their Flan needs the same pass at merge.
diff --git a/NEXT.md b/NEXT.md
index 1f82cee..62fb3a1 100644
--- a/NEXT.md
+++ b/NEXT.md
@@ -280,13 +280,15 @@ length is not known until the file is read and therefore cannot exist before an
~~**6. A field label is written with a dot, not a colon, and the colon is reserved for keys.**~~ **Done.**
`{.x 1.0 .y 2.0}` is struct construction and `{inner .field}` is destructuring; the old spelling is refused, and the
refusal names the new one. `:keys` kept its colon — it names no field, so leaving it alone is what lets the dot mean
-exactly one thing. 681 labels across 45 `.flan` files, `vendor/` and the Flan embedded in `lib/prelude.ml` and the
-tests. `Map` is now free to take `{:key value}` without colliding with struct literals. See BUILT.md, "The colon
-belongs to keys".
+exactly one thing. 681 labels across 45 `.flan` files including `vendor/`, plus 94 more in the Flan embedded in
+`lib/prelude.ml` and the tests. `Map` is now free to take `{:key value}` without colliding with struct literals. See
+BUILT.md, "The colon belongs to keys".
-**Two things it left behind.** `render.ml` still *prints* a struct with colons, deliberately:
-`emacs/flan-inspect.el:165` parses that output and hard-codes the colon, so the printer has to move with its reader
-and that belongs to the Emacs lane. `emacs/MANUAL.md` and `flan-mode.el`'s font-lock also still show the colon.
+**What it left for the Emacs lane, both verified.** `render.ml` still *prints* a struct with colons, deliberately:
+`emacs/flan-inspect.el:165` parses that output and hard-codes the colon when it reads a field out, so the printer
+has to move in the same commit as its reader. And `flan-mode.el:61` font-locks `:name` as a constant with nothing
+matching `.name`, so a field label is now unfontified where it used to be coloured. Neither is urgent; both belong
+with whoever next opens `emacs/`.
**7. `Map` follows Odin's implementation.** Read `base/runtime/dynamic_map_internal.odin` before writing any of it;
the checkout is at `~/Repositories/Odin`. Three properties are the ones worth copying, and they are stated in its own
diff --git a/lib/check.ml b/lib/check.ml
index 3a26a5d..f2773a5 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -1384,7 +1384,7 @@ and fold_left_prim ctx ~want loc name p ok what args =
No allocating operation returns an error and none can fail silently. When
the allocator cannot satisfy a request the operation signals
- (StorageExhausted {:bytes n :align a :allocator id})
+ (StorageExhausted {.bytes n .align a .allocator id})
with [error] — whose type is Never — inside a [restart-case] offering
[retry]. That is one rule over every allocating operation, which is what
@@ -2309,7 +2309,7 @@ and named_call ctx ~want loc name args =
1. It does NOT check UTF-8, because `string` does not claim UTF-8. The
prelude settles this: valid-utf8? is an ordinary function you call when
you care, decode-rune/rune-at/rune-count all take [u8] rather than
- string, and decode-rune answers {:ok false :width 1} on a malformed
+ string, and decode-rune answers {.ok false .width 1} on a malformed
byte rather than assuming its input is well-formed. The one place the
runtime treats a string differently from a byte slice is
flan_escape_bytes, for a string nested in a printed structure, and that