Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/RESCRIPT-ELIMINATION.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion docs/TECH-DEBT.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
|===

Expand Down
122 changes: 122 additions & 0 deletions stdlib/dict.affine
Original file line number Diff line number Diff line change
@@ -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<V>() -> [(String, V)] {
[]
}

/// Build a dict from a list of pairs (later pairs win on duplicate keys).
pub fn from_pairs<V>(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<V>(d: [(String, V)], key: String) -> Option<V> {
for (k, v) in d {
if k == key {
return Some(v);
}
}
None
}

/// Whether a key is present.
pub fn contains<V>(d: [(String, V)], key: String) -> Bool {
for (k, v) in d {
if k == key {
return true;
}
}
false
}

/// Number of bindings.
pub fn size<V>(d: [(String, V)]) -> Int {
len(d)
}

// ============================================================================
// Update (immutable; returns a new dict)
// ============================================================================

/// Insert or replace `key`'s binding (last-write-wins).
pub fn insert<V>(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<V>(d: [(String, V)], key: String, value: V) -> [(String, V)] {
insert(d, key, value)
}

/// Remove `key` if present (no-op if absent).
pub fn remove<V>(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<V>(d: [(String, V)]) -> [String] {
let mut ks = [];
for (k, v) in d {
ks = ks ++ [k];
}
ks
}

/// All values, in iteration order.
pub fn values<V>(d: [(String, V)]) -> [V] {
let mut vs = [];
for (k, v) in d {
vs = vs ++ [v];
}
vs
}
Loading