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.
This commit is contained in:
parent
cb757868b4
commit
e992491799
45
BUILT.md
45
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.**
|
||||
|
||||
34
NEXT.md
34
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
|
||||
|
||||
4
plan.org
4
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
|
||||
|
||||
@ -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)))))
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
tools/__pycache__/colon-to-dot.cpython-313.pyc
Normal file
BIN
tools/__pycache__/colon-to-dot.cpython-313.pyc
Normal file
Binary file not shown.
@ -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
|
||||
|
||||
@ -381,13 +381,13 @@ heap is involved.</p>
|
||||
|
||||
;; `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.</p>
|
||||
(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 "")))</code></pre>
|
||||
@ -753,7 +753,7 @@ user-supplied printer to choose between.</p>
|
||||
(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))</code></pre>
|
||||
@ -848,7 +848,7 @@ and no ceremony.</p>
|
||||
(defstruct V2 [x f32 y f32])
|
||||
|
||||
(defn add [a V2 b V2] V2
|
||||
(V2 {:x (+ (.x a) (.x b)) :y (+ (.y a) (.y b))}))</code></pre>
|
||||
(V2 {.x (+ (.x a) (.x b)) .y (+ (.y a) (.y b))}))</code></pre>
|
||||
|
||||
<pre><code>;; 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.</p>
|
||||
(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 "")))</code></pre>
|
||||
|
||||
@ -934,8 +934,8 @@ normally leaves the signaller to carry on — the accumulation case:</p>
|
||||
(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.</p>
|
||||
(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:</p>
|
||||
|
||||
(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)))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user