Below was from an exploration with an LLM investigating which part is slow in the current compilation process.
Key summary
Using Nickel for the keymap-ncl-to-json compilation pass is too slow for complex keymaps.
- We'll still keep
keymap.ncl for authoring keymaps.
- Treat
keymap.json as the stable IR.
- Explore native lowering to that IR.
- JSON → Rust source codegen (
keymap-codegen) is comparatively cheap and can stay in Nickel for now.
Product constraints
| Layer |
Decision |
Why |
keymap.ncl authoring + contracts |
Keep (must) |
Preferred authoring surface; contract validation belongs with authoring |
Dense keymap.json |
Keep as stable IR |
Real API: serde / Cucumber / full-profile Vec system already consume it |
| ncl → json passes in Nickel |
Too slow; explore native |
~10s on rich maps; tiny IR; interpreter tax |
| json → rust codegen in Nickel |
Defer rewrite |
~1s; not the bottleneck; fak/kirei lineage still works |
Measurements (casual wall-clock)
Casual profiling only (time around nickel export / existing scripts). Nickel 1.16 has no built-in profiler. Each --field export is a cold process. (Although I'd note: the full integration test suite is affected by cold processes). Order-of-magnitude; local machine (M4 Macbook Air) measured on 2026-07-26.
(tests/ncl/keymap-36key-rgoulter is my keymap; relatively sophisticated. Miryoku-ish, with some chording).
Fixture focus: tests/ncl/keymap-36key-rgoulter (real-ish layered map: 36 keys, ~28 KB JSON — not a large IR).
| Stage |
What |
Wall |
| Binary start |
nickel --version |
~4 ms |
| Load / surface only |
--field=layers (almost no passes) |
~1.0–1.3 s |
| Full ncl → json |
keymap-ncl-to-json.sh / --field=json_keymap |
~10 s (rich 36-key) |
| json → rust |
keymap-codegen.sh |
~1.2 s |
| Simpler maps ncl → json |
e.g. 2-key named / 48-key basic |
~1–2.5 s |
Stage walk (36-key rgoulter; cold export per field; Δ ≈ new work after shared load):
| Forced field |
Wall |
Approx marginal |
layers |
~1.5 s |
load + author surface |
layered_keys / chorded_keys |
~1.6–2.3 s |
early passes |
automation_transform |
~4.2 s |
+automation |
json_keymap |
~10 s |
+~6 s named-layers, pad, to_json_value |
Reading: startup is negligible. A large fixed cost (~1s) is loading/evaluating the Nickel program graph. Variable cost scales with keymap richness, not raw key count alone (48-key basic ~2.3s vs 36-key rich ~10s). The bulk of the expensive path sits after automation_transform, inside json_keymap.
This is not “slow because the problem instance is huge.” Same logical work in a native language on a 36-node IR should be milliseconds-class; Nickel pays large constant factors (interpreter, contracts, repeated tree walks, dynamic key-module dispatch, record/array updates).
Target shape
keymap.ncl --[Nickel: author + contracts; stop before heavy passes]--> handoff
--[native: lower to dense keymap.json]---------------------> keymap.json
--[Nickel or later Rust: codegen]--------------------------> keymap.rs
~1 s? ~ms–tens of ms ~1 s
A product budget of ~1s Nickel + cheap native lower + ~1s codegen is reasonable for keymap/firmware builds once the hot passes leave Nickel.
Methodology (repro)
time ncl/scripts/keymap-ncl-to-json.sh tests/ncl/keymap-36key-rgoulter
time ncl/scripts/keymap-codegen.sh tests/ncl/keymap-36key-rgoulter
for f in layers layers_of_keys layered_keys chorded_keys \
automation_transform json_keymap; do
echo -n "$f "
time nickel export --format=json \
--import-path=ncl --import-path=tests/ncl/keymap-36key-rgoulter \
keymap-ncl-to-json.ncl keymap.ncl --field="$f" >/dev/null
done
Optional: add temporary exportable intermediates inside json_keymap (post–named-layers, post-pad, post–to_json_value) to split the ~6s tail further.
Recommended exploration
Spike (primary): Nickel keeps authoring + contracts; export a handoff IR; Rust lowers to dense keymap.json with snapshot parity against tests/ncl/**/keymap.json.
Related paths
- Frontend:
ncl/keymap-ncl-to-json.ncl, ncl/passes/*
- Backend:
ncl/keymap-codegen.ncl, ncl/key_system/
- Scripts:
ncl/scripts/keymap-ncl-to-json.sh, keymap-codegen.sh
- Helper (combined eval):
smart-keymap-nickel-helper
- Local write-up:
notes/nickel-compiler-passes-perf.org
Discussion: Nickel used for three roles
Nickel currently does three jobs that got conflated:
- Authoring DSL — keep.
- Compiler frontend (ncl → dense JSON) — too slow; main target.
- Compiler backend (JSON → Rust source) — acceptable cost; defer.
(1) and (2) sharing a language was interesting and coherent with fak/kirei; measurements say (2) is not worth keeping in Nickel long-term if multi-second (or 10s+) maps are normal.
Below was from an exploration with an LLM investigating which part is slow in the current compilation process.
Key summary
Using Nickel for the
keymap-ncl-to-jsoncompilation pass is too slow for complex keymaps.keymap.nclfor authoring keymaps.keymap.jsonas the stable IR.keymap-codegen) is comparatively cheap and can stay in Nickel for now.Product constraints
keymap.nclauthoring + contractskeymap.jsonMeasurements (casual wall-clock)
Casual profiling only (
timearoundnickel export/ existing scripts). Nickel 1.16 has no built-in profiler. Each--fieldexport is a cold process. (Although I'd note: the full integration test suite is affected by cold processes). Order-of-magnitude; local machine (M4 Macbook Air) measured on 2026-07-26.(
tests/ncl/keymap-36key-rgoulteris my keymap; relatively sophisticated. Miryoku-ish, with some chording).Fixture focus:
tests/ncl/keymap-36key-rgoulter(real-ish layered map: 36 keys, ~28 KB JSON — not a large IR).nickel --version--field=layers(almost no passes)keymap-ncl-to-json.sh/--field=json_keymapkeymap-codegen.shStage walk (36-key rgoulter; cold export per field; Δ ≈ new work after shared load):
layerslayered_keys/chorded_keysautomation_transformjson_keymapto_json_valueReading: startup is negligible. A large fixed cost (~1s) is loading/evaluating the Nickel program graph. Variable cost scales with keymap richness, not raw key count alone (48-key basic ~2.3s vs 36-key rich ~10s). The bulk of the expensive path sits after
automation_transform, insidejson_keymap.This is not “slow because the problem instance is huge.” Same logical work in a native language on a 36-node IR should be milliseconds-class; Nickel pays large constant factors (interpreter, contracts, repeated tree walks, dynamic key-module dispatch, record/array updates).
Target shape
A product budget of ~1s Nickel + cheap native lower + ~1s codegen is reasonable for keymap/firmware builds once the hot passes leave Nickel.
Methodology (repro)
Optional: add temporary exportable intermediates inside
json_keymap(post–named-layers, post-pad, post–to_json_value) to split the ~6s tail further.Recommended exploration
Spike (primary): Nickel keeps authoring + contracts; export a handoff IR; Rust lowers to dense
keymap.jsonwith snapshot parity againsttests/ncl/**/keymap.json.Related paths
ncl/keymap-ncl-to-json.ncl,ncl/passes/*ncl/keymap-codegen.ncl,ncl/key_system/ncl/scripts/keymap-ncl-to-json.sh,keymap-codegen.shsmart-keymap-nickel-helpernotes/nickel-compiler-passes-perf.orgDiscussion: Nickel used for three roles
Nickel currently does three jobs that got conflated:
(1) and (2) sharing a language was interesting and coherent with fak/kirei; measurements say (2) is not worth keeping in Nickel long-term if multi-second (or 10s+) maps are normal.