diff --git a/NEXT.md b/NEXT.md index deeff41..f373265 100644 --- a/NEXT.md +++ b/NEXT.md @@ -267,6 +267,49 @@ this closes nothing off. That section stays open for the record but is no longer ~~**5. File I/O — `slurp` and `barf` — is the next stdlib work**~~ **Built.** It was the next stdlib work, after `Vec`, because `slurp` returns a string whose length is not known until the file is read and therefore cannot exist before an allocator does. +## 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. + +**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. + +**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 +header comment: + +- **Open-addressed Robin Hood hashing at a 75% load factor.** No buckets, no per-entry allocation, and probe + distances stay even because a later arrival steals a slot from an earlier one. +- **Cache-line-aligned `Map_Cell` packing**, so no single key or value ever straddles a cache line and a linear probe + walks memory in a cache-friendly order. This is the part a hand-rolled open-addressed map usually gets wrong. +- **`uintptr` throughout** for sizes, masks and offsets, to keep sign-extension and masking instructions out of the + probe loop. + +Its static/dynamic split is the same type-erasure this project already committed to: `Map_Info` carries size, +alignment and offsets, and the compiler emits the hash and equality pair per key type. `spec-memory.md`'s +structural-key restriction holds this to the built-in key set, so there is no dispatch to design. + +**Why this will not be Python's dict.** Worth recording because it is the question that prompted the decision. +Python's dict algorithm is fine; what makes it slow is that every key and value is a separately allocated, reference- +counted object, and hashing and comparison go through indirect calls that cannot be inlined. Flan stores raw bytes +and compiles the hash and comparison concretely at each use. That difference is most of the gap before any +algorithmic cleverness. **jank is not the model** — it is Clojure, so its maps are persistent with structural +sharing, which plan.org rules out by name because shared structure destroys the clear ownership that is the whole +reason there is no collector. + ## Blocked and unfinished Everything below was found, decided or half-built and then stopped. Each says what blocks it. Nothing here is a