Skip to content

Commit 8dc11ed

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 ec19653 commit 8dc11ed

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
@@ -208,10 +208,12 @@ No untyped `extern raw` will be added (an arbitrary-source hole defeats
208208
affine/effect tracking — the ADR-012 contortion). No compiler change.
209209
|*ESC-02* (#246) |`JSON.t` (7) |No stdlib JSON type (`stdlib/` has `Ajv` but
210210
no `Json`). Needs a stdlib JSON type.
211-
|*ESC-03* (#247) |`Dict.t` (6) |No stdlib `Map`/`Dict` type
212-
(`stdlib/collections.affine` is list ops only; `stdlib/Http.affine:16`
213-
already flags the `Dict` gap, tied to #160/#162). Needs a stdlib `Map` type —
214-
coordinate with #160/#162.
211+
|*ESC-03* (#247) — LANDED |`Dict.t` (6) |`stdlib/dict.affine`: keyed
212+
container over the assoc-list shape `[(String, V)]` (the representation
213+
`json::JObject` already uses) — `empty`/`from_pairs`/`get`/`contains`/
214+
`size`/`insert`/`set`/`remove`/`keys`/`values`; AOT-gated (#136).
215+
`Dict.t` → `dict` ops over `[(String, V)]`. (#162 / STDLIB-03; the
216+
`Http.affine:16` headers upgrade is now unblocked, additive.)
215217
|===
216218

217219
=== 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
@@ -146,7 +146,12 @@ complete; convergence ABI shared w/ Ephapax
146146
(no host dep); String→Json parse is the `Http` typed-boundary bridge
147147
(ADR-018), not a hand-rolled parser. Closes ESC-02 #246 (the #229
148148
`JSON.t` target)
149-
|STDLIB-03 |`Dict`/`Map` keyed container |S2 |open #162
149+
|STDLIB-03 |`Dict`/`Map` keyed container |S2 |*LANDED* (Refs #162 #247):
150+
`stdlib/dict.affine` — keyed container over `[(String, V)]` (the
151+
`json::JObject` representation): empty/from_pairs/get/contains/size/
152+
insert/set/remove/keys/values; AOT-gated (#136). Closes ESC-03 #247 (the
153+
#229 `Dict.t` target); unblocks the additive `Http.affine` headers→Dict
154+
upgrade
150155
|STDLIB-04 |Residual `extern` builtins → real implementations |S3 |open
151156
|===
152157

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)