Skip to content

perf: typed MapKey enum removes routing-tsp str(j) tax on numeric maps#267

Merged
danieljohnmorris merged 6 commits into
mainfrom
fix/numeric-map-keys
May 14, 2026
Merged

perf: typed MapKey enum removes routing-tsp str(j) tax on numeric maps#267
danieljohnmorris merged 6 commits into
mainfrom
fix/numeric-map-keys

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Adds a typed MapKey { Text(String), Int(i64) } to ilo's map representation. Before this change, Value::Map was keyed by String, so any program iterating a map by a numeric index had to call k=str j before every mget/mset. The routing-tsp benchmark (and any agent-written code that builds an index lookup table) paid that tax on every iteration.

With typed keys: mset m 7 v and mget m 7 work directly. Floats floor to i64 at the builtin boundary (matching at xs i); NaN/Infinity raise a runtime error. Text and Int are distinct keys: Int(1) and Text("1") no longer collide. Bool keys are stringified to Text (a bool map is always shorter as a two-arm ?, so the surface syntax isn't worth the lexer ambiguity).

JSON serialisation stringifies numeric keys (JS/Python convention). The round-trip remains lossy, since JSON object keys are always strings, deserialising back to a Record with text fields.

Repro before/after

Before (routing-tsp pattern):

loop>n;m=mmap;j=0;@<j 10{k=str j;m=mset m k j;j=+j 1};r=mget m (str 5);?r{n v:v;_:-1}

After: no stringification needed.

loop>n;m=mmap;j=0;@<j 10{m=mset m j j;j=+j 1};r=mget m 5;?r{n v:v;_:-1}

What's in the diff

  • interpreter: introduce typed MapKey enum for Value::Map (280e389): Value::Map becomes Arc<HashMap<MapKey, Value>>. Adds MapKey::from_value for the builtin boundary, Ord for deterministic mkeys/mvals order, and stringifies numeric keys on to_json.
  • vm: thread MapKey through bytecode and Cranelift JIT (1f4a547): HeapObj::Map switches to HashMap<MapKey, NanVal>. OP_MGET/MSET/MHAS/MDEL/MKEYS/MVALS build typed keys via new nanval_to_map_key helper. RC=1 in-place mset fast path is preserved. JIT helpers now report runtime errors on bad key types (matching tree/VM).
  • verify: infer map key type from declarations and builtin args (192390e): drops the hard-coded "key must be t" constraint; mkeys returns L<declared-key-type>; frq xs:L a yields M a n.
  • test + example: cross-engine coverage for typed map keys (63999b3): 15 new cross-engine tests; new examples/numeric-map-keys.ilo; updated regression_frq and examples/frq.ilo for the typed-key semantics.

Test plan

  • cargo fmt --check clean
  • cargo build --release --features cranelift clean
  • cargo test --release --features cranelift passes (full suite green: ~2900 lib tests + every integration crate)
  • cargo clippy --release --features cranelift --all-targets -- -D warnings clean
  • examples/numeric-map-keys.ilo exercises all five entry points across tree, VM, and cranelift
  • regression_numeric_map_keys covers mset, mget, mhas, mdel, mkeys, mvals, len, jdmp, float-floor, Int-vs-Text distinctness across all three engines

Follow-ups

  • The from_json path still produces Record for generic objects rather than Value::Map. That's untouched here, but if/when the JSON shape grows a M n _ variant, we'd want to populate MapKey::Int from numeric-looking string keys.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 72.14485% with 100 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/interpreter/mod.rs 72.13% 34 Missing ⚠️
src/verify.rs 65.65% 34 Missing ⚠️
src/vm/mod.rs 73.33% 32 Missing ⚠️

📢 Thoughts on this report? Let us know!

Adds MapKey { Text(String), Int(i64) } and switches Value::Map from
Arc<HashMap<String, Value>> to Arc<HashMap<MapKey, Value>>. Numbers floor
to i64 at the builtin boundary (matching at xs i); NaN/Infinity are
rejected as runtime errors. Bool keys are stringified to Text — two-arm
bool maps are always shorter as a guard.

mget/mset/mhas/mdel/mkeys/mvals/grp/frq build typed keys through
MapKey::from_value at the boundary. JSON serialisation stringifies
numeric keys (JS/Python convention) — round-trip remains lossy since
JSON object keys are always strings.

Removes the routing-tsp tax: callers no longer need 'k=str j' before
every iteration when keying by a loop index.
HeapObj::Map switches to HashMap<MapKey, NanVal>. Adds nanval_to_map_key
and map_key_to_nanval helpers to convert at the NanVal boundary, mirroring
the tree-walker's MapKey::from_value.

OP_MGET/MSET/MHAS/MDEL/MKEYS/MVALS all build a typed key from the NanVal
register before dispatch. Wrong-type (non-text, non-number) and non-finite
keys raise VmError::Type uniformly across the tree, VM, and Cranelift
backends. RC=1 in-place mset fast path is preserved.

JIT helpers (jit_mget, jit_mset, jit_mhas, jit_mdel) report runtime errors
on bad key types instead of silently returning nil, matching tree/VM
semantics.
Drops the hard-coded 'key must be t' constraint on mget/mset/mhas/mdel.
is_valid_map_key_arg now accepts any scalar (Text or Number) and checks
compatibility against the declared map key type. Maps declared as M n _
accept numeric keys without an ILO-T013 error.

mkeys returns L<declared-key-type> instead of the old hard-coded L t.
mvals returns L<declared-value-type>. frq xs:L a yields M a n (key type
follows the list element type), with Bool flattened to Text since bools
are stringified at the MapKey boundary.
regression_numeric_map_keys: 15 cross-engine tests covering mset/mget,
mhas (present + missing), mdel, mkeys (sorted determinism), mvals, len,
jdmp (numeric keys stringify in JSON), float-floor-to-int, and Int-vs-Text
distinctness via frq. Each test runs tree, VM, and cranelift.

examples/numeric-map-keys.ilo: demonstrates the typed-key surface end to
end across all engines (hist, has-key, del-key, key-count, sorted-keys).

regression_frq + examples/frq.ilo: updated to reflect typed keys. Numeric
frq inputs produce M n n (lookup with the bare number), and Int/Text are
now distinct keys with no collision on shared print form.
@danieljohnmorris danieljohnmorris merged commit f3d6f08 into main May 14, 2026
4 of 5 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/numeric-map-keys branch May 14, 2026 16:34
danieljohnmorris added a commit that referenced this pull request May 15, 2026
- slc / take / drop accept negative indices counting from end (bounds
  clamp), matching at xs i. Closes the quant-trader fencepost and the
  slc xs -np 1 np ergonomics gap (#266).
- Map keys are typed: text or integer. mset m 7 v and mget m 7 work
  directly, no str conversion. Int(1) and Text("1") are distinct.
  Float keys floor to i64; jdmp stringifies numeric keys for JSON (#267).
- Add map / flt / fld to the builtin reference. All HOFs (map, flt,
  fld, srt, grp, uniqby, partition, flatmap) now work cross-engine
  on tree, VM, Cranelift JIT, and AOT (#274 #277 #278 #279 #280 #283).
- New Inline lambdas subsection: Phase 1 literals are cross-engine,
  Phase 2 closure capture is tree-only with automatic fallthrough
  surfacing ILO-R012 on VM and Cranelift (#265 #284).
- AOT-compiled binaries from ilo compile now strip the top-level
  ~/^ wrapper byte-for-byte the same as in-process runners (#281).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant