From e992491799eec0d1da1bffb952ee78c77e823988 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sat, 12 Sep 2026 15:00:34 +0700
Subject: [PATCH] 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)))