From 8dc11ed6f811f72f74817dab6d5cd8644c216511 Mon Sep 17 00:00:00 2001 From: hyperpolymath Date: Tue, 19 May 2026 19:53:51 +0100 Subject: [PATCH] =?UTF-8?q?feat(stdlib):=20ESC-03=20=E2=80=94=20Dict/Map?= =?UTF-8?q?=20keyed=20container=20(Refs=20#162=20#247=20#229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/RESCRIPT-ELIMINATION.adoc | 10 +-- docs/TECH-DEBT.adoc | 7 +- stdlib/dict.affine | 122 +++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 stdlib/dict.affine 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 +}