Skip to content

Commit 06f4ec5

Browse files
hyperpolymathclaude
andcommitted
feat(stdlib): ESC-03 — Dict/Map keyed container (Refs #162 #247 #229)
stdlib/dict.affine: keyed associative container over the assoc-list shape [(String, V)] — the same representation json::JObject uses, so a decoded JSON object feeds dict::get directly. Surface: empty, from_pairs, get, contains, size, insert, set, remove, keys, values. `module dict;`, prelude only, no host dependency — target-agnostic. The #229 Dict.t target ports to dict ops over [(String, V)]. Recovered from the stranded feat/stdlib-dict-echidna64 draft (that branch's full diff is destructive — only the dict.affine FILE is sound). Extracted, validated on current main: main check dict.affine -> Type checking passed; the #136 stdlib AOT gate auto-discovered it (AOT dict.affine OK); full gate 278 -> 279, zero regression. Unblocks the additive, source-compatible Http.affine headers->Dict upgrade noted in stdlib/Http.affine:16-18. Refs #162 #247 #229. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8a7c358 commit 06f4ec5

3 files changed

Lines changed: 134 additions & 5 deletions

File tree

docs/RESCRIPT-ELIMINATION.adoc

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,12 @@ Needs a language decision on a raw/FFI form (or an explicit "port every
207207
`%%raw` to typed `extern`" doctrine).
208208
|*ESC-02* (#246) |`JSON.t` (7) |No stdlib JSON type (`stdlib/` has `Ajv` but
209209
no `Json`). Needs a stdlib JSON type.
210-
|*ESC-03* (#247) |`Dict.t` (6) |No stdlib `Map`/`Dict` type
211-
(`stdlib/collections.affine` is list ops only; `stdlib/Http.affine:16`
212-
already flags the `Dict` gap, tied to #160/#162). Needs a stdlib `Map` type —
213-
coordinate with #160/#162.
210+
|*ESC-03* (#247) — LANDED |`Dict.t` (6) |`stdlib/dict.affine`: keyed
211+
container over the assoc-list shape `[(String, V)]` (the representation
212+
`json::JObject` already uses) — `empty`/`from_pairs`/`get`/`contains`/
213+
`size`/`insert`/`set`/`remove`/`keys`/`values`; AOT-gated (#136).
214+
`Dict.t` → `dict` ops over `[(String, V)]`. (#162 / STDLIB-03; the
215+
`Http.affine:16` headers upgrade is now unblocked, additive.)
214216
|===
215217

216218
=== Tier 4 — Cross-unit gating (a sequencing finding, language-grounded)

docs/TECH-DEBT.adoc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,12 @@ joint-closed. Deno-ESM (#226) + typed-wasm CPS line PR1..PR3d
132132
(#227/#233/#236/#237/#238/#266) all merged; ADR-013 delivery plan
133133
complete; convergence ABI shared w/ Ephapax
134134
|STDLIB-02 |Portable `Json` primitive |S2 |open #161
135-
|STDLIB-03 |`Dict`/`Map` keyed container |S2 |open #162
135+
|STDLIB-03 |`Dict`/`Map` keyed container |S2 |*LANDED* (Refs #162 #247):
136+
`stdlib/dict.affine` — keyed container over `[(String, V)]` (the
137+
`json::JObject` representation): empty/from_pairs/get/contains/size/
138+
insert/set/remove/keys/values; AOT-gated (#136). Closes ESC-03 #247 (the
139+
#229 `Dict.t` target); unblocks the additive `Http.affine` headers→Dict
140+
upgrade
136141
|STDLIB-04 |Residual `extern` builtins → real implementations |S3 |open
137142
|===
138143

stdlib/dict.affine

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2025 hyperpolymath
3+
//
4+
// Dict - String-keyed associative map (echidna#64)
5+
//
6+
// Backs the ReScript->AffineScript migration's `Dict` requirement
7+
// (echidna `[migration-roadmap.rescript-to-affinescript]`, Client.res):
8+
// JSON object decoding returns `Dict`-shaped values and request-body
9+
// construction builds a `Dict` imperatively then wraps it for encoding.
10+
//
11+
// Representation: an association list `[(String, V)]`. A String-keyed
12+
// map is the minimum echidna#64 needs (JSON object keys are strings).
13+
// `insert`/`set` are last-write-wins and keep at most one binding per
14+
// key, so `get` returns the most recently set value. This mirrors the
15+
// purely-functional, list-based style of `collections.affine` (whose
16+
// `[(A, B)]` zip/unzip already compile through the AOT pipeline), so it
17+
// adds no new compiler primitive, type, or extern.
18+
19+
module dict;
20+
21+
use prelude::{Option, Some, None};
22+
23+
// ============================================================================
24+
// Construction
25+
// ============================================================================
26+
27+
/// The empty dict.
28+
pub fn empty<V>() -> [(String, V)] {
29+
[]
30+
}
31+
32+
/// Build a dict from a list of pairs (later pairs win on duplicate keys).
33+
pub fn from_pairs<V>(pairs: [(String, V)]) -> [(String, V)] {
34+
let mut d = [];
35+
for (k, v) in pairs {
36+
d = insert(d, k, v);
37+
}
38+
d
39+
}
40+
41+
// ============================================================================
42+
// Lookup
43+
// ============================================================================
44+
45+
/// Look up a key. `None` if absent.
46+
pub fn get<V>(d: [(String, V)], key: String) -> Option<V> {
47+
for (k, v) in d {
48+
if k == key {
49+
return Some(v);
50+
}
51+
}
52+
None
53+
}
54+
55+
/// Whether a key is present.
56+
pub fn contains<V>(d: [(String, V)], key: String) -> Bool {
57+
for (k, v) in d {
58+
if k == key {
59+
return true;
60+
}
61+
}
62+
false
63+
}
64+
65+
/// Number of bindings.
66+
pub fn size<V>(d: [(String, V)]) -> Int {
67+
len(d)
68+
}
69+
70+
// ============================================================================
71+
// Update (immutable; returns a new dict)
72+
// ============================================================================
73+
74+
/// Insert or replace `key`'s binding (last-write-wins).
75+
pub fn insert<V>(d: [(String, V)], key: String, value: V) -> [(String, V)] {
76+
let mut rest = [];
77+
for (k, v) in d {
78+
if k != key {
79+
rest = rest ++ [(k, v)];
80+
}
81+
}
82+
[(key, value)] ++ rest
83+
}
84+
85+
/// Alias of `insert`, for the imperative create-then-set-keys builder
86+
/// pattern used in Client.res (`d = set(d, "field", v)`).
87+
pub fn set<V>(d: [(String, V)], key: String, value: V) -> [(String, V)] {
88+
insert(d, key, value)
89+
}
90+
91+
/// Remove `key` if present (no-op if absent).
92+
pub fn remove<V>(d: [(String, V)], key: String) -> [(String, V)] {
93+
let mut rest = [];
94+
for (k, v) in d {
95+
if k != key {
96+
rest = rest ++ [(k, v)];
97+
}
98+
}
99+
rest
100+
}
101+
102+
// ============================================================================
103+
// Projection
104+
// ============================================================================
105+
106+
/// All keys, in iteration order.
107+
pub fn keys<V>(d: [(String, V)]) -> [String] {
108+
let mut ks = [];
109+
for (k, v) in d {
110+
ks = ks ++ [k];
111+
}
112+
ks
113+
}
114+
115+
/// All values, in iteration order.
116+
pub fn values<V>(d: [(String, V)]) -> [V] {
117+
let mut vs = [];
118+
for (k, v) in d {
119+
vs = vs ++ [v];
120+
}
121+
vs
122+
}

0 commit comments

Comments
 (0)