diff --git a/docs/RESCRIPT-ELIMINATION.adoc b/docs/RESCRIPT-ELIMINATION.adoc index 4c7ed43a..44807791 100644 --- a/docs/RESCRIPT-ELIMINATION.adoc +++ b/docs/RESCRIPT-ELIMINATION.adoc @@ -208,10 +208,12 @@ No untyped `extern raw` will be added (an arbitrary-source hole defeats affine/effect tracking — the ADR-012 contortion). No compiler change. |*ESC-02* (#246) |`JSON.t` (7) |No stdlib JSON type (`stdlib/` has `Ajv` but no `Json`). Needs a stdlib JSON type. -|*ESC-03* (#247) |`Dict.t` (6) |No stdlib `Map`/`Dict` type -(`stdlib/collections.affine` is list ops only; `stdlib/Http.affine:16` -already flags the `Dict` gap, tied to #160/#162). Needs a stdlib `Map` type — -coordinate with #160/#162. +|*ESC-03* (#247) — LANDED |`Dict.t` (6) |`stdlib/dict.affine`: keyed +container over the assoc-list shape `[(String, V)]` (the representation +`json::JObject` already uses) — `empty`/`from_pairs`/`get`/`contains`/ +`size`/`insert`/`set`/`remove`/`keys`/`values`; AOT-gated (#136). +`Dict.t` → `dict` ops over `[(String, V)]`. (#162 / STDLIB-03; the +`Http.affine:16` headers upgrade is now unblocked, additive.) |=== === Tier 4 — Cross-unit gating (a sequencing finding, language-grounded) diff --git a/docs/TECH-DEBT.adoc b/docs/TECH-DEBT.adoc index 8f834022..5e7399ed 100644 --- a/docs/TECH-DEBT.adoc +++ b/docs/TECH-DEBT.adoc @@ -146,7 +146,12 @@ complete; convergence ABI shared w/ Ephapax (no host dep); String→Json parse is the `Http` typed-boundary bridge (ADR-018), not a hand-rolled parser. Closes ESC-02 #246 (the #229 `JSON.t` target) -|STDLIB-03 |`Dict`/`Map` keyed container |S2 |open #162 +|STDLIB-03 |`Dict`/`Map` keyed container |S2 |*LANDED* (Refs #162 #247): +`stdlib/dict.affine` — keyed container over `[(String, V)]` (the +`json::JObject` representation): empty/from_pairs/get/contains/size/ +insert/set/remove/keys/values; AOT-gated (#136). Closes ESC-03 #247 (the +#229 `Dict.t` target); unblocks the additive `Http.affine` headers→Dict +upgrade |STDLIB-04 |Residual `extern` builtins → real implementations |S3 |open |=== diff --git a/stdlib/dict.affine b/stdlib/dict.affine new file mode 100644 index 00000000..da28a1f9 --- /dev/null +++ b/stdlib/dict.affine @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2025 hyperpolymath +// +// Dict - String-keyed associative map (echidna#64) +// +// Backs the ReScript->AffineScript migration's `Dict` requirement +// (echidna `[migration-roadmap.rescript-to-affinescript]`, Client.res): +// JSON object decoding returns `Dict`-shaped values and request-body +// construction builds a `Dict` imperatively then wraps it for encoding. +// +// Representation: an association list `[(String, V)]`. A String-keyed +// map is the minimum echidna#64 needs (JSON object keys are strings). +// `insert`/`set` are last-write-wins and keep at most one binding per +// key, so `get` returns the most recently set value. This mirrors the +// purely-functional, list-based style of `collections.affine` (whose +// `[(A, B)]` zip/unzip already compile through the AOT pipeline), so it +// adds no new compiler primitive, type, or extern. + +module dict; + +use prelude::{Option, Some, None}; + +// ============================================================================ +// Construction +// ============================================================================ + +/// The empty dict. +pub fn empty() -> [(String, V)] { + [] +} + +/// Build a dict from a list of pairs (later pairs win on duplicate keys). +pub fn from_pairs(pairs: [(String, V)]) -> [(String, V)] { + let mut d = []; + for (k, v) in pairs { + d = insert(d, k, v); + } + d +} + +// ============================================================================ +// Lookup +// ============================================================================ + +/// Look up a key. `None` if absent. +pub fn get(d: [(String, V)], key: String) -> Option { + for (k, v) in d { + if k == key { + return Some(v); + } + } + None +} + +/// Whether a key is present. +pub fn contains(d: [(String, V)], key: String) -> Bool { + for (k, v) in d { + if k == key { + return true; + } + } + false +} + +/// Number of bindings. +pub fn size(d: [(String, V)]) -> Int { + len(d) +} + +// ============================================================================ +// Update (immutable; returns a new dict) +// ============================================================================ + +/// Insert or replace `key`'s binding (last-write-wins). +pub fn insert(d: [(String, V)], key: String, value: V) -> [(String, V)] { + let mut rest = []; + for (k, v) in d { + if k != key { + rest = rest ++ [(k, v)]; + } + } + [(key, value)] ++ rest +} + +/// Alias of `insert`, for the imperative create-then-set-keys builder +/// pattern used in Client.res (`d = set(d, "field", v)`). +pub fn set(d: [(String, V)], key: String, value: V) -> [(String, V)] { + insert(d, key, value) +} + +/// Remove `key` if present (no-op if absent). +pub fn remove(d: [(String, V)], key: String) -> [(String, V)] { + let mut rest = []; + for (k, v) in d { + if k != key { + rest = rest ++ [(k, v)]; + } + } + rest +} + +// ============================================================================ +// Projection +// ============================================================================ + +/// All keys, in iteration order. +pub fn keys(d: [(String, V)]) -> [String] { + let mut ks = []; + for (k, v) in d { + ks = ks ++ [k]; + } + ks +} + +/// All values, in iteration order. +pub fn values(d: [(String, V)]) -> [V] { + let mut vs = []; + for (k, v) in d { + vs = vs ++ [v]; + } + vs +}