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 0000000..5c381bf Binary files /dev/null and b/tools/__pycache__/colon-to-dot.cpython-313.pyc differ diff --git a/tools/colon-to-dot.py b/tools/colon-to-dot.py index 44d9e8e..d680f0b 100755 --- a/tools/colon-to-dot.py +++ b/tools/colon-to-dot.py @@ -42,6 +42,11 @@ class Atom: t = self.text return t[1:] if len(t) > 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)))