diff --git a/.claude/rules/c-runtime-memory.md b/.claude/rules/c-runtime-memory.md
index b14f9e168..9cfbdc0e3 100644
--- a/.claude/rules/c-runtime-memory.md
+++ b/.claude/rules/c-runtime-memory.md
@@ -92,7 +92,7 @@ iteration, or a collector that quietly stops working).
record, `store_put` refused to re-insert it, and the caller was told 1).
- **`eigs_json_encode` borrows its argument and `eigs_json_parse_value` returns
- an owned ref.** `eigs_json_encode(make_num(x))` leaks the `make_num`; a parsed
+ an owned ref.** `eigs_json_encode(make_float(x))` leaks the `make_float`; a parsed
Value must be decref'd on *every* path including early returns, and the value
read out of it before the decref. Three of the five call sites in
`ext_http.c` had this wrong (#731) — if you touch one, re-audit the rest.
@@ -148,11 +148,15 @@ iteration, or a collector that quietly stops working).
- **Container elements are `EigsSlot` storage (#1665 arm D); read them only
through `src/container.h`, and know which getter owns what.** Every rule
below was bought by a real bug during the migration:
- - `list_get_ref` / `dict_value_get_ref` return **NULL for a number** by
- contract. Test `list_elem_type` / `slot_type` before dereferencing, and
- never write `get_ref(...)->type == VAL_NUM` (always false). Ragged tensor
- rows crashed `relu`/`gather` this way (B5), and the DMG in-place
- register-write fast path silently never fired (B2, +38% Ir).
+ - `list_get_ref` / `dict_value_get_ref` return **NULL for an immediate
+ number**, a double or an SMI (`src/container.h`:133 and :137). A boxed
+ int (|i| >= 2^47, since #1758 R1.3) is a heap slot, so it comes back as a
+ non-NULL `VAL_INT` Value. Test `list_elem_type` / `slot_type` before
+ dereferencing. Never write `get_ref(...)->type == VAL_FLOAT` (always
+ false), and never read `get_ref(...)->type == VAL_INT` as "an int" (it is
+ true for a boxed int only, never for an SMI). Ragged tensor rows crashed
+ `relu`/`gather` this way (B5), and the DMG in-place register-write fast
+ path silently never fired (B2, +38% Ir).
- A view (`list_get_view`, `slot_view`, `EIGS_VIEW`, `BUILTIN_*_VIEW`) is
**stack storage**: never return it, store it, or incref-and-keep it, and
a `BUILTIN_*_VIEW` compound literal dies at its enclosing block. A view in
diff --git a/.github/workflows/phase2.yml b/.github/workflows/phase2.yml
new file mode 100644
index 000000000..760b14991
--- /dev/null
+++ b/.github/workflows/phase2.yml
@@ -0,0 +1,81 @@
+name: phase2
+
+# #1758: landed MAP rows, the pre-registered builtin table on matched
+# capability profiles, the C readers and the stdlib differential. The shared
+# phase2_check.py helper derives the complete declared MAP prefix.
+on:
+ push:
+ branches: [phase2-numbers]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ phase2:
+ name: phase2 (MAP + builtins + readers + lib)
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ services:
+ db:
+ image: postgres:16
+ env:
+ POSTGRES_USER: eigs
+ POSTGRES_PASSWORD: eigs_test
+ POSTGRES_DB: eigs_test
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd "pg_isready -U eigs"
+ --health-interval 5s
+ --health-timeout 5s
+ --health-retries 10
+ env:
+ DATABASE_URL: postgres://eigs:eigs_test@127.0.0.1:5432/eigs_test
+ steps:
+ # Every branch and tag is needed: origin/main and v0.46.0.
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0
+ - name: Fetch the approved builtin bar object
+ run: |
+ git fetch --no-tags origin 0a73adb0612f8612d75393891ae2ba2e1c803663
+ git cat-file -e '0a73adb0612f8612d75393891ae2ba2e1c803663^{tree}'
+ - name: Install matched capability dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libpq-dev zlib1g-dev libsdl2-2.0-0 libsdl2-mixer-2.0-0 libsdl2-ttf-2.0-0
+ - name: Build tag, main and branch profiles serially; run the phase2 lane
+ run: |
+ set -euo pipefail
+ export XDG_CACHE_HOME="$RUNNER_TEMP/cache"
+ make --no-print-directory observer-anchor
+ anchor="$XDG_CACHE_HOME/eigs-oracle-anchor/$(sha256sum tools/observer_oracle/build_anchor.sh | cut -d' ' -f1)/src/eigenscript"
+ mkdir -p "$RUNNER_TEMP/main" "$RUNNER_TEMP/tag-profiles"
+ git archive origin/main | tar -x -C "$RUNNER_TEMP/main"
+ git archive v0.46.0 | tar -x -C "$RUNNER_TEMP/tag-profiles"
+ make -C "$RUNNER_TEMP/main" > "$RUNNER_TEMP/main-build.log" 2>&1 \
+ || { tail -n 50 "$RUNNER_TEMP/main-build.log"; echo "::error::the origin/main build failed"; exit 1; }
+ # The table uses matching capability builds; a capability-unavailable
+ # transcript is a failure, including on the tag (ruling A3).
+ for profile in server zlib db; do
+ target="$profile"
+ [ "$profile" != db ] || target=server-db
+ make -C "$RUNNER_TEMP/tag-profiles" "$target" > "$RUNNER_TEMP/tag-$profile-build.log" 2>&1 \
+ || { tail -n 50 "$RUNNER_TEMP/tag-$profile-build.log"; echo "::error::the tag $profile build failed"; exit 1; }
+ printf -v "TAG_BIN_${profile^^}" '%s' "$RUNNER_TEMP/tag-profiles/build/$target/eigenscript"
+ export "TAG_BIN_${profile^^}"
+ done
+ export CC_GUARD_LOG="$RUNNER_TEMP/cc-guard.log"
+ : > "$CC_GUARD_LOG"
+ for profile in server zlib db; do
+ target="$profile"
+ [ "$profile" != db ] || target=server-db
+ PATH="$PWD/tools/cc-guard-bin:$PATH" make "$target"
+ printf -v "BRANCH_BIN_${profile^^}" '%s' "$PWD/build/$target/eigenscript"
+ export "BRANCH_BIN_${profile^^}"
+ done
+ # Restore the release alias before the MAP and library checks.
+ PATH="$PWD/tools/cc-guard-bin:$PATH" make build
+ bash tools/cc_guard.sh --report
+ bash tools/phase2_ci.sh "$anchor" "$RUNNER_TEMP/main/src/eigenscript" src/eigenscript
diff --git a/CLAUDE.md b/CLAUDE.md
index 306ed7c9b..f85dcae76 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -47,7 +47,7 @@ make tsan-http # TSan *with* ext_http+model+net (CI gate: EIGS_SUITE_SECTIONS='
make http # http+model variant — run tests/test_http_server.sh
make zlib # DEFLATE decompression (`inflate`) via system zlib (-lz)
make jit-smoke # standalone emitter tests (jit_smoke.c stubs all helpers)
-bash tools/jit_diff.sh # JIT vs INTERPRETER differential over the corpus (tape-replayed; CI job)
+bash tools/jit_diff.sh # JIT vs INTERPRETER differential over the corpus (CI job; a pair still unstable on a rerun is ledgered COULD_NOT_ADJUDICATE, never tape-replayed: #2122)
make freestanding-check # 2-stage symbol gate for the EigenOS profile (docs/FREESTANDING.md)
make freestanding-libc-diff # mini-libc/libm vs glibc oracle (src/freestanding/)
make poison # 0xAA uninit-read hunter build; run the suite with MALLOC_PERTURB_=170
diff --git a/Makefile b/Makefile
index 325dc486a..f3802ef19 100644
--- a/Makefile
+++ b/Makefile
@@ -36,7 +36,7 @@ LDFLAGS := -pie -Wl,-z,relro,-z,now -lm -lpthread
endif
SRC_DIR := src
-SOURCES := $(SRC_DIR)/eigenscript.c $(SRC_DIR)/lexer.c $(SRC_DIR)/parser.c $(SRC_DIR)/builtins.c $(SRC_DIR)/builtins_buf.c $(SRC_DIR)/builtins_host.c $(SRC_DIR)/builtins_tensor.c $(SRC_DIR)/fsutil.c $(SRC_DIR)/hash.c $(SRC_DIR)/alloc.c $(SRC_DIR)/state.c $(SRC_DIR)/strbuf.c $(SRC_DIR)/ext_store.c $(SRC_DIR)/fmt.c $(SRC_DIR)/lint.c $(SRC_DIR)/lint_host.c $(SRC_DIR)/chunk.c $(SRC_DIR)/compiler.c $(SRC_DIR)/vm.c $(SRC_DIR)/task.c $(SRC_DIR)/jit.c $(SRC_DIR)/jit_checked.c $(SRC_DIR)/trace.c $(SRC_DIR)/eigs_embed.c $(SRC_DIR)/repl.c $(SRC_DIR)/step.c $(SRC_DIR)/tape_read.c $(SRC_DIR)/bundle.c $(SRC_DIR)/main.c
+SOURCES := $(SRC_DIR)/eigenscript.c $(SRC_DIR)/lexer.c $(SRC_DIR)/parser.c $(SRC_DIR)/builtins.c $(SRC_DIR)/builtins_int.c $(SRC_DIR)/builtins_buf.c $(SRC_DIR)/builtins_host.c $(SRC_DIR)/builtins_tensor.c $(SRC_DIR)/fsutil.c $(SRC_DIR)/hash.c $(SRC_DIR)/alloc.c $(SRC_DIR)/state.c $(SRC_DIR)/strbuf.c $(SRC_DIR)/ext_store.c $(SRC_DIR)/fmt.c $(SRC_DIR)/lint.c $(SRC_DIR)/lint_host.c $(SRC_DIR)/chunk.c $(SRC_DIR)/compiler.c $(SRC_DIR)/vm.c $(SRC_DIR)/task.c $(SRC_DIR)/jit.c $(SRC_DIR)/jit_checked.c $(SRC_DIR)/trace.c $(SRC_DIR)/eigs_embed.c $(SRC_DIR)/repl.c $(SRC_DIR)/step.c $(SRC_DIR)/tape_read.c $(SRC_DIR)/bundle.c $(SRC_DIR)/main.c
HOSTED_SOURCES := $(SOURCES) $(SRC_DIR)/ext_gfx.c
MODEL_SRC := $(SRC_DIR)/model_io.c $(SRC_DIR)/model_infer.c $(SRC_DIR)/model_train.c
BINARY := $(SRC_DIR)/eigenscript
@@ -90,7 +90,7 @@ define AUX_REFRESH
done
endef
-.PHONY: db-params-test all build server server-db full http net gfx zlib lib amalgamation tsan test test-changed precheck sandbox-intern-test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp lsp-asan dap jit-smoke embed-smoke embed-smoke-asan embed-smoke-asan-server embed-smoke-gfx embed-concurrent asan asan-server valgrind pgo poison freestanding-check freestanding-libc-diff asan-http asan-gfx tsan-server tsan-http nativefn-test arming-mt-test embed-roads print-% sigpipe-contract-test sigpipe-partial-test ui-sdl-input-gfx gfx-x11-regressions pool-off asan-pool-off pool-off-num pool-off-callenv jit-checked jit-checked-plant
+.PHONY: db-params-test all build server server-db full http net gfx zlib lib amalgamation tsan test test-changed precheck sandbox-intern-test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp lsp-asan dap jit-smoke embed-smoke embed-smoke-asan embed-smoke-asan-server embed-smoke-gfx embed-concurrent asan asan-server valgrind pgo poison freestanding-check freestanding-libc-diff asan-http asan-gfx tsan-server tsan-http nativefn-test arming-mt-test embed-roads print-% sigpipe-contract-test sigpipe-partial-test ui-sdl-input-gfx gfx-x11-regressions pool-off asan-pool-off pool-off-num pool-off-callenv jit-checked jit-checked-plant smi-trap
# ---- Per-variant objdir engine (#740) -------------------------------------
# The engine's rules are defined before `all`, so pin the default goal.
@@ -196,7 +196,15 @@ SRC_V_jit-checked := $(HOSTED_SOURCES)
FLAGS_jit-checked := $(CFLAGS) $(DEFS_OFF) -DEIGENSCRIPT_EXT_GFX=1 -DEIGS_JIT_CHECKED $(VERDEF)
LIBS_jit-checked := $(LDFLAGS) -ldl
-VARIANTS := release server server-db zlib asan asan-server tsan tsan-server valgrind poison pool-off asan-pool-off pool-off-num pool-off-callenv jit-checked
+# #1758: compatibility target for the pre-R1.3 int-source gate. R1.3 retires
+# EIGS_SMI_TRAP now that integer literals are producers; slot_as_ptr still
+# checks tags through EIGS_SLOT_CHECKED. Historical R1.1/R1.2 calibrations
+# build their pinned pre-flip source rather than treating this as a trap.
+SRC_V_smi-trap := $(HOSTED_SOURCES)
+FLAGS_smi-trap := $(CFLAGS) $(DEFS_OFF) -DEIGENSCRIPT_EXT_GFX=1 -DEIGS_SMI_TRAP -DEIGS_SLOT_CHECKED $(VERDEF)
+LIBS_smi-trap := $(LDFLAGS) -ldl
+
+VARIANTS := release server server-db zlib asan asan-server tsan tsan-server valgrind poison pool-off asan-pool-off pool-off-num pool-off-callenv jit-checked smi-trap
# Objects depend on Makefile+VERSION so a flag or version-string change
# rebuilds; header edits are covered by the generated .d files.
@@ -244,6 +252,50 @@ build/$(SLOT_TEST_VARIANT)/test_container_slots: tests/test_container_slots.c $(
container-slot-test: build/$(SLOT_TEST_VARIANT)/test_container_slots
@echo "Container slot API test built: $<"
+# #1758 R1.1: the C slot test (docs/design/phase2-numbers.md 2.5). It includes
+# the single-file amalgamation so the static slot classifiers are reachable;
+# tests/phase2/slot_layer.sh runs it and compares the entries it examined with
+# the population tools/phase2_inventory.py derives. The release flavour turns
+# on slot_as_ptr's refusal with -DEIGS_SLOT_CHECKED; ASan turns it on itself.
+# Both carry -Wall -Wextra, and the runner fails on any warning: release
+# compiles the amalgamation at -O0 and ASan at -O1, the two levels #1775
+# found warnings at (and -O0 is about 41 s against -O2's 162 s).
+PHASE2_SLOT_VARIANT ?= release
+PHASE2_SLOT_FLAGS_release := -Wall -Wextra $(WERROR_FLAGS) -O0 -g -DEIGS_SLOT_CHECKED
+PHASE2_SLOT_FLAGS_asan := -Wall -Wextra $(ASAN_FLAGS)
+build/phase2/eigenscript_all.c: $(filter-out $(CLI_ONLY),$(SOURCES)) $(wildcard $(SRC_DIR)/*.h) tools/amalgamate.sh Makefile
+ bash tools/amalgamate.sh build/phase2
+build/phase2/slot_layer_%: tests/phase2/slot_layer.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread
+.PHONY: phase2-slot-test
+phase2-slot-test: build/phase2/slot_layer_$(PHASE2_SLOT_VARIANT)
+ @echo "phase2 slot test built: $<"
+
+# #1758 R1.6a: final reader C fixture, through the no-SDL amalgamation.
+PHASE2_READERS_VARIANT ?= release
+PHASE2_READERS_FIXTURE ?= readers
+build/phase2/readers_%: tests/phase2/readers.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread
+# R1.6d-D1: raw stored payloads through the actual graphics/audio readers.
+build/phase2/gfx_audio_boundary_%: tests/phase2/gfx_audio_boundary.c src/ext_gfx.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread -ldl
+build/phase2/gfx_byte_boundary_%: tests/phase2/gfx_byte_boundary.c src/ext_gfx.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread -ldl
+# R1.6z-D1 public getter bit-round-trip gate; compile outside timed MAP rows.
+build/phase2/embed_buffer_get_raw_%: tests/phase2/embed_buffer_get_raw.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread
+# Q7 lands in R1.6e; an absent source is a build failure once selected.
+build/phase2/q7_readers_%: tests/phase2/q7_readers.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread
+# N6: actual shared writer plus Python repr/bit/truncation oracle cases.
+build/phase2/num_text_%: tests/phase2/num_text.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread
+# R1.7-D4: signed-zero constant keys are compared by bits.
+build/phase2/const_dedup_bits_%: tests/phase2/const_dedup_bits.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread
+.PHONY: phase2-readers-test
+phase2-readers-test: build/phase2/$(PHASE2_READERS_FIXTURE)_$(PHASE2_READERS_VARIANT)
+ @echo "phase2 reader test built: $<"
# #1992: side-effect-free observer normalization must stay bit-identical to
# num_guard's non-strict VALUE mapping. Link the selected suite variant so
# the test exercises the real thread-local flags and strict-mode state.
@@ -480,13 +532,16 @@ observer-anchor:
# observer-anchor builds, or reuses, in the user cache. FLOOR=, on the command
# line or in the environment, names another floor; an empty FLOOR skips the floor
# and the anchor. EXPECT=ieee,int64 fails when an expected capability is absent
-# (#1758). CI calls score.py directly, through tools/observer_oracle/ci_lane.sh
-# (the required check `observer oracle (floor)`).
+# (#1758). Both pass --declared tests/phase2/oracle_expect.txt: Phase 2's known-
+# wrong capability verdicts, each with its recover round (#1830); an undeclared
+# wrong verdict or a stale declaration fails, and #1776 merges only when the file
+# is empty (tools/phase2_cost.sh --final). CI calls score.py directly, through
+# tools/observer_oracle/ci_lane.sh (the required check `observer oracle (floor)`).
.PHONY: observer-oracle
observer-oracle: FLOOR ?= tools/observer_oracle/floor.json
observer-oracle: build
$(if $(FLOOR),$(OBSERVER_ANCHOR))
- python3 tools/observer_oracle/score.py src/eigenscript $(if $(FLOOR),--floor $(FLOOR)) $(if $(EXPECT),--expect $(EXPECT))
+ python3 tools/observer_oracle/score.py src/eigenscript $(if $(FLOOR),--floor $(FLOOR)) $(if $(EXPECT),--expect $(EXPECT)) --declared tests/phase2/oracle_expect.txt
install-gfx: install
@echo "NOTE: 'make install-gfx' is a compatibility alias for 'make install'."
@@ -514,7 +569,7 @@ $(SRC_DIR)/lsp_stdlib_index.h: $(wildcard lib/*.eigs) tools/gen_lsp_stdlib_index
# Builtin half of the same idea (#742): names from the registration seams +
# ext_names.h, hover detail from the signature comments. Reserved report
# forms are excluded using lexer.c. Commit the generated header for review.
-$(SRC_DIR)/lsp_builtin_index.h: $(SRC_DIR)/builtins.c $(SRC_DIR)/builtins_host.c \
+$(SRC_DIR)/lsp_builtin_index.h: $(SRC_DIR)/builtins.c $(SRC_DIR)/builtins_int.c $(SRC_DIR)/builtins_host.c \
$(SRC_DIR)/hash.c $(SRC_DIR)/ext_store.c $(SRC_DIR)/ext_names.h \
$(SRC_DIR)/lexer.c tools/gen_lsp_builtin_index.sh
bash tools/gen_lsp_builtin_index.sh
@@ -745,6 +800,12 @@ jit-checked: build/jit-checked/eigenscript
$(call RELINK,jit-checked)
@echo "EigenScript $(VERSION) (jit-checked: emitted slot witnesses) built. Binary: $(BINARY)"
+# #1758 R1.1 (see FLAGS_smi-trap): build it in a scratch tree, then
+# tools/phase2_check.sh --through R1.1 --smi-trap /src/eigenscript ...
+smi-trap: build/smi-trap/eigenscript
+ $(call RELINK,smi-trap)
+ @echo "EigenScript $(VERSION) (smi-trap compatibility: checked slots; source trap retired in R1.3) built. Binary: $(BINARY)"
+
# Profile-guided optimization. Builds an instrumented binary, runs the
# DMG cpu_instrs workload to collect branch/edge counters, then rebuilds
@@ -898,6 +959,10 @@ build/$(HISTORY_BOUNDARY_VARIANT)/test_trace_history_boundary: tests/test_trace_
trace-history-boundary-test: build/$(HISTORY_BOUNDARY_VARIANT)/test_trace_history_boundary
@echo "Trace history boundary test built: $<"
+# R1.6d-D1: actual net_send client with raw stored bytes and a real socketpair.
+build/phase2/net_byte_boundary_%: tests/phase2/net_byte_boundary.c src/ext_net.c build/phase2/eigenscript_all.c tools/werror_flags.txt
+ $(CC) $(PHASE2_SLOT_FLAGS_$*) -Ibuild/phase2 -o $@ $< -lm -lpthread
+
# Witnesses for tools/make_lock_check.sh (#1940). With AFTER set, the recipe
# refuses to run unless that file exists, so a second make proves it started
# only after the first make's recipe wrote its DONE file. SESSION records the
diff --git a/README.md b/README.md
index 682789107..ebfc3948a 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,22 @@
+
+Width arithmetic uses explicit wrapping, checked and saturating builtins.
+N5 converts integral floats, then checks the named type range without masking.
+Width results are ints; checked overflow raises `value`. `u64` uses int64 bit
+patterns. `fround` returns a float after double-to-float32-to-double conversion
+(ints convert to double first). See [the width builtins](docs/BUILTINS.md#width-arithmetic-n9).
+
+```eigenscript
+print of [i64_wrapping_add of [9223372036854775807, 1], u8_wrapping_add of [255, 1]]
+print of (fround of 0.1)
+```
+```output
+[-9223372036854775808, 0]
+0.10000000149011612
+```
+
@@ -33,6 +49,54 @@ evaluations have separate stop scopes (see `docs/EMBEDDING.md`).
`vm_run_bytecode` raises a catchable `value` error naming a rejected chunk descriptor; a valid program may still return `null`. `sandbox_run` reports descriptor rejection in its structured `{ok: false, error: ...}` result. Its optional fourth limit, `max_work`, bounds cumulative bytecode instructions across function calls and callback re-entry (default 10,000,000), independently of loop and allocation limits. This meters VM work, not elapsed time: a blocking native callback must return before the sandbox can stop.
+The positions and counts of `char_at`, `substr`, `get_at`, `set_at`,
+`copy_into`, `fill` and the `list_*` mutation/slice builtins use exact conversion
+(N5). So do handle fields, bytecode ABI/counts/bytes, sandbox limits and numeric
+exit codes. Integral floats are accepted; fractions and values outside the
+receiving C type raise `value` before indexing or effects.
+`recv_timeout` rounds milliseconds half away from zero and raises beyond the
+C long range, with no one-year cap. `usleep` rounds microseconds and raises
+beyond the C int range. `record_history` returns its previous flag as an int;
+nonzero numeric flags still enable history. `task_detach` and sandbox error lines return ints.
+`read_bytes` produces int bytes; `write_bytes` requires exact bytes in 0..255
+and returns an int count. File caps and corpus/stream counts follow N5; an
+integral stream count outside its cap still raises `limit`. Subprocess exit
+statuses are ints, and timeout seconds round to representable poll milliseconds.
+
+```eigenscript
+print of (substr of ["hello", 1.0, 2.0])
+try:
+ print of (substr of ["hello", 1.5, 2])
+catch e:
+ print of e.kind
+print of (num_type of (record_history of 0.5))
+print of (usleep of 1.5)
+try:
+ usleep of 1e300
+catch e:
+ print of e.kind
+path is mktemp of null
+written is write_bytes of [path, [65.0, 66.0]]
+bytes is read_bytes of path
+print of [num_type of written, num_type of bytes[0], str_from_bytes of bytes]
+try:
+ write_bytes of [path, [65.5]]
+catch e:
+ print of e.kind
+remove_file of path
+```
+```output
+el
+value
+int
+null
+value
+["int", "int", "AB"]
+value
+```
+
+Runtime-published counts and indices are exact ints (S9-K1): `__loop_iterations__`, live and historical `when` assignment counts, aggregate `what` lengths, trajectory `window`, error `line`, and scheduler trace `seq` and task ids. Scheduler `tick` remains a continuous virtual time.
+
Sandbox execution does not update shared temporal history: assignment values, names, counts and observer snapshots stay outside that history even when recording is armed. Ordinary tape assignment records still emit. Host and trusted descriptor history recording resumes normally outside the sandbox; sandbox temporal reads remain refused.
## Try it in your browser
@@ -164,6 +228,44 @@ Comparisons, `not` and predicates return `true`/`false`, a `bool`. A bool is
not a number: `true + 1` and `(x > 0) == 1` raise, so test a bool directly
(`if x > 0:`). See [docs/SPEC.md](docs/SPEC.md#booleans-comparison-and-logic).
+
+Bitwise operators and `bit_*` builtins return ints, including when an
+operand is an integral float. They use Lua 5.4's 64-bit shift rules: `>>`
+zero-fills; a negative count shifts in the opposite direction; a count of
+64 or more, or -64 or less, gives zero. Each operand must be an exact int64;
+a fractional or out-of-range float raises `value`.
+
+```eigenscript
+print of [1 << 64, 1 << -1, -1 >> 1, 8 >> -1, 1 << 63]
+print of [bit_shr of [-1, 1], bit_shl of [8, -1], num_type of (3.0 & 1)]
+```
+```output
+[0, 0, 9223372036854775807, 16, -9223372036854775808]
+[9223372036854775807, 4, "int"]
+```
+
+`//` floors the quotient; `%` has the divisor's sign. `//=` applies the same
+operation to its assignment target, like `/=`. Zero divisors raise `value`
+for int, float and mixed operands. Two ints produce an int for
+`//` and `%`; a float operand produces a float. `/` always produces a float.
+Float and mixed `//` use Python’s `divmod` quotient correction: `1.0 // 0.1`
+is `9.0`, and `1e-300 // -1e300` is `-1.0`. `//=` uses the same rule.
+
+```eigenscript
+x is -7
+x //= 2
+print of [x, 7 // -2, -7 % 3, 7 % -3, -7.5 % 2]
+print of [num_type of (7 // 2), num_type of (7.0 // 2)]
+y is 1.0
+y //= 0.1
+print of [1.0 // 0.1, 1 // 0.1, 1e-300 // -1e300, y, num_type of y]
+```
+```output
+[-4, -4, 2, -2, 0.5]
+["int", "float"]
+[9.0, 9.0, -1.0, 9.0, "float"]
+```
+
### Ask Your Code
Every value quietly remembers how it has been changing — and you don't pay
@@ -256,8 +358,14 @@ by `python3 reports/convergence_loops/report1990.py`. See
> `improving` uses the ML loss convention that lower is better: the value
> must be decreasing, with steps contracting toward a limit. Growth is never
> improving, even when it rises toward a plateau. For
-> non-numeric values the observer's entropy reading applies — a value
-> locating itself *from the inside*, with no external goal. The full
+> non-numeric values, `converged` and ordinarily `stable` compare bounded,
+> detached assignment snapshots; the other predicates and `report` read
+> entropy. A bounded snapshot containing a NaN buffer cannot converge;
+> `stable` instead uses the entropy-window rule after a full bounded
+> comparison window (S10-D1). Refusal clears that eligibility, preserving
+> `false` / `true` / `false` for the matmul scenario's equality, stability
+> and convergence. Finite mutation checks and oversized-snapshot refusals
+> remain. The full model is in
> [docs/OBSERVER.md](docs/OBSERVER.md); the precise predicate semantics
> (what `converged` requires, how it differs from `equilibrium`, the
> N-step window) are in [docs/PREDICATES.md](docs/PREDICATES.md).
@@ -340,16 +448,26 @@ and returns a buffer when every tensor operand was one. `zeros of n` returns a
buffer (`zeros of [rows, cols]` still returns the nested list) — numeric work
wants the flat container, and that is the name it reaches for.
-EigenScript numbers are finite by construction. Operations that would overflow
+EigenScript has exact signed int64 values and finite float64 numbers.
+Decimal integer literals fitting int64 are exact; a point, exponent or
+out-of-range decimal literal gives a float. Hex integer literals wrap
+modulo 2^64. `type of` returns `"num"` for both, and `num_type of` distinguishes `"int"`
+from `"float"` (null for a non-number). Int `+ - *` raise `value` on
+overflow; unary minus and `abs` raise on INT64_MIN. Comparisons between
+ints and floats are exact, including beyond 2^53; mixed arithmetic converts
+ints to floats. Float operations that would overflow
to infinity saturate at `+/-1e308`. Strict mode is the default: an
out-of-domain call (`sqrt of -1`), a `NaN` result or a wrong-typed builtin
argument (`abs of "x"`) raises a catchable error. Run with `EIGS_STRICT=0` to
-get the finite stand-ins instead: `NaN` becomes `0`, `sqrt of -1` is `0`, and
-`asin`/`acos` clamp their inputs. The same rule applies when a raw tensor-kernel
-result is read from a `buffer`; one stored element never changes meaning with
-the operator that consumes it. Structural buffer equality and scalar reductions
-normalize each input by this rule too, as do mixed buffer/list materialization
-and numeric byte or sample conversion. A raised read stops subsequent work.
+get the finite stand-ins instead: `NaN` becomes float `0.0`, `sqrt of -1` is `0.0`, and
+`asin`/`acos` clamp their inputs. Buffer consumers apply the same normalization,
+except `sum`, `get_at`, indexing, iteration, `dot` and `write_bytes`, which refuse non-finite
+buffer elements in every strict mode (N13).
+Structural buffer equality compares stored doubles directly with IEEE equality;
+list/dict self-equality follows that rule through nested and cyclic containers.
+The other scalar reductions normalize each input, as do mixed buffer/list materialization
+and the remaining numeric byte or sample conversions. `write_bytes` instead
+requires exact bytes in 0..255 (N5/Q1). A raised read stops subsequent work.
`type_mismatch` error by default; `EIGS_STRICT=0` retains numeric-zero
substitution. `str_from_bytes` stops at numeric NUL, without inspecting later
elements; byte codecs consume the complete list.
@@ -361,6 +479,132 @@ The optional model extension raises a catchable `value` error when
model's `max_seq_len`. Training applies the same limit to the combined input
and output lengths; callers must choose their context window explicitly.
+Empty `min`/`max` raises `value` in both call forms (R1.6b-D2); empty
+`sum` remains int zero (C2). `sign_extend` reads exact int64 arguments and
+raises `value` if its existing subtraction underflows (R1.6b-D1).
+
+Counts and indices are ints. `floor`, `ceil` and half-away-from-zero `round`
+return an int when the result fits int64. `min`/`max` return the winning
+argument with its kind; the first argument wins a tie. `num` requires a
+complete numeric-literal string (N10). JSON integer text fitting int64
+decodes exactly (N14).
+
+The [C embedding API](docs/EMBEDDING.md#value-handles) follows Lua’s numeric
+accessors: `eigs_value_new_int` constructs an exact int64, `eigs_value_is_int`
+tests the stored kind (`lua_isinteger`), and `eigs_value_as_int` reports
+whether a number converts exactly to int64 (`lua_tointegerx`).
+`eigs_value_as_num` converts either kind to double (`lua_tonumber`);
+`EIGS_TYPE_NUM` covers both.
+
+LSP hover on a numeric literal reports `int` or `float` from the literal’s
+kind: `3.0` remains a float even though its value is integral.
+
+```eigenscript
+print of [type of 3, num_type of 3, num_type of 3.0]
+print of [9007199254740993, 3.0, -0.0]
+print of [num_type of (len of "abc"), num_type of (floor of -2.5)]
+print of [num of "9007199254740993", num_type of (num of "3.0")]
+print of [num_type of (min of [3, 3.0]), num_type of (min of [3.0, 3])]
+print of (json_path of ["{\"n\":9223372036854775807}", "n"])
+try:
+ min of []
+catch e:
+ print of [e.kind, (index_of of [e.message, "min"]) >= 0]
+try:
+ print of (num of "12abc")
+catch e:
+ print of e.kind
+```
+```output
+["num", "int", "float"]
+[9007199254740993, 3.0, -0.0]
+["int", "int"]
+[9007199254740993, "float"]
+["int", "float"]
+9223372036854775807
+["value", true]
+value
+```
+
+
+`add`, `subtract` and `multiply` preserve exact ints for scalar operands and
+for each pair of list elements, with `value` on int64 overflow. `negative`
+preserves int scalar and list elements and raises at INT64_MIN. Buffer
+arithmetic remains float. A buffer kernel stops at its first raised result,
+releases its partial output and does not read later elements (#1417).
+An all-int `sum` returns an exact int, while a
+float anywhere in a nested input selects float addition for the entire
+reduction. A buffer selects float addition too. An empty list's sum is int
+zero, in both call forms. `sum` refuses non-finite buffer elements with a
+`value` error in every strict mode (N13). In strict mode, it stops at the first NaN with a "not a number" diagnostic and sets `math_flags.invalid`; later elements are not read (#1417).
+
+```eigenscript
+exact is sum of [9007199254740993, 1]
+print of [exact, num_type of exact]
+mixed is sum of [9223372036854775807, 1, 0.5]
+print of [num_type of mixed, mixed == 9223372036854775808.0]
+print of [num_type of (sum of []), num_type of (sum of ([]))]
+print of (add of [[9007199254740993, 2], [1, 3]])
+try:
+ negative of (-9223372036854775807 - 1)
+catch e:
+ print of e.kind
+```
+```output
+[9007199254740994, "int"]
+["float", true]
+["int", "int"]
+[9007199254740994, 5]
+value
+```
+
+Store handles carry integer `_store_id` and `_store_gen` fields. Every store operation requires exact, in-range integer handle fields; integral floats convert, while fractional, non-finite or out-of-range numeric fields raise `value` before store access (R1.6f-D1). `store_count` returns an integer.
+
+`store_get`, `store_delete`, and `store_update` accept string keys or exact
+int64 numeric keys. Integral floats convert; fractional or out-of-range floats
+raise `value` before lookup or mutation. Numeric keys retain all int64 digits.
+Stored decimal integer text within int64 decodes as an int; a decimal point or
+exponent selects a float. Buffer payloads remain floats. Store records preserve
+int64 digits and use shortest round-trip float text, including `.0` for
+integral floats and the sign of `-0.0`, so `3.0` reads back as a float.
+
+See the [stored-record example](docs/SPEC.md#stored-records).
+
+PostgreSQL integer columns (`smallint`, `integer`, `bigint`, `oid`) return
+exact int64 values through `db_query_value`; `db_query_json` retains their
+exact decimal digits (N14, Q-DB253). A `bigint` beyond 2^53 is an int,
+including both int64 boundaries. `numeric` remains text, and finite real
+columns returned directly remain floats. Float parameters use N6 shortest round-trip text, preserving `.0` and
+signed zero. Store records use the same float writer and exact int64 digits
+(N14, #1762). The executable DB examples and boundary assertions
+in `tests/test_db.eigs` run on the server-db profile with PostgreSQL.
+
+TCP handles, ports and sent-byte counts are integers. Numeric socket handles, ports, receive counts and outgoing bytes require exact integers (integral floats convert); bytes must be in 0..255. `net_send` validates the entire payload before sending any byte. Poll timeouts are continuous milliseconds rounded to the C int range; an unrepresentable duration raises `value`.
+
+`http_early_bind` and `http_serve` require an exact integer port in 0..65535; integral floats convert and port zero requests an ephemeral port. Fractional or out-of-range ports raise `value` before binding. `shared_delete` and `shared_size` return integer counts.
+
+N6 prints each float with Python's shortest round-trip spelling: integral
+floats retain `.0`, negative zero is `-0.0`, and scientific notation starts
+below `1e-4` or at `1e16`. Ints keep their exact decimal digits. `print`,
+`write`, `str`, f-strings and JSON share the number writer. The executed
+examples are `tests/phase2/rows/N6.float-repr.eigs` and
+`tests/phase2/rows/N6.str-fstring.eigs`.
+
+The HTTP shared counter `shared_incr` follows numeric `+`: two int operands
+return an exact int and int64 overflow raises `value` without changing the
+stored counter (A). A missing counter starts at int zero. A float operand
+produces a float. The stored operand is its JSON-decoded value; N6 preserves an
+integer-valued float as a float by retaining its `.0`. Executable shared-counter examples in
+`tests/test_http.eigs` cover exact values, both overflow directions and the
+next operation after a raise.
+
+`http_post` also preserves every digit of JSON integer header values within
+int64 before sending them as text (N14, B6). The executed loopback example
+is `tests/phase2/builtins/builtins/http_post.eigs`.
+
+Builtin `sort` sorts a list in place; `sort_by` returns a new list using numeric keys.
+Both are stable on every platform: equal elements or keys keep their input order.
+
## Standard Library
The machine-checkable EigenCore-0 reference frontend lives in [`spec/lean/`](spec/lean/).
@@ -621,6 +865,9 @@ spawn occurrences to match workers. Tape associations carry state grouping;
event order does not choose replay identity. Hosts explicitly advance replay
sessions between evaluations; unread sibling outcomes prevent advance. See [docs/TRACE.md](docs/TRACE.md)
and [docs/EMBEDDING.md](docs/EMBEDDING.md) for the binding and version contract.
+Tape format v9 preserves integer/float kinds, chunked module sources, and
+length-framed oversized builtin answers;
+older formats are refused.
Replay also refuses side-effecting boundaries the tape cannot reconstruct:
in particular, `mktemp` raises before creating a temporary file.
Recorded builtins validate arguments before their tape boundary, so a call
@@ -715,3 +962,88 @@ Codespace and a green CI run can't drift.
> Launching a Codespace bills to *your* account's free monthly hours — you can
> open this org repo from a personal account without org Codespaces billing.
+
+Tensor sizes and indices follow N5: an integral float is accepted and a
+fraction raises `value`. `gather` keeps the selected list cell's int/float
+kind; buffer elements and numeric tensor calculations are floats. `shape`
+returns integer dimension counts. `mean` and `norm` refuse non-finite buffer
+cells with `value` in both strict modes.
+
+```eigenscript
+g is gather of [[[1, 2], [3, 4]], [1.0, 0]]
+print of [g, num_type of g[0], num_type of (shape of g)[0]]
+try:
+ gather of [[10, 20], 0.5]
+catch e:
+ print of e.kind
+```
+```output
+[[2, 3], "int", "int"]
+value
+```
+
+Buffer sizes, indices, offsets and counts follow N5. Byte inputs to
+`str_from_bytes`, `f64_from_bytes`, `inflate` and `buf_from_pcm16le` must be
+exact integers in 0..255; fractions and out-of-range bytes raise `value`.
+`buf_len`, `f64_to_bytes` and `inflate` return integer counts or bytes;
+buffer storage and scalar buffer reads are floats. `buf_get`, `buf_peak`,
+`buf_dot` and the byte/PCM codecs refuse non-finite input cells with `value`.
+
+Model token ids and generation limits follow N5 and the C int range;
+fractional ids or token counts raise `value`. `eigen_generate` returns int
+token ids, while `eigen_eval_loss` returns a float. Temperature, top_p and
+training learning rates remain float quantities. Model JSON weights stay
+float data; the loader status strings are unchanged.
+
+Graphics and audio magnitudes round half away from zero and raise `value`
+when the result exceeds the C call's range. This refusal happens even before
+SDL is loaded:
+
+```eigenscript
+try:
+ gfx_delay of 1e300
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Stored buffer data boundary
+
+Buffer kernels can retain non-finite data. Scalar indexing and iteration
+raise `value` when that data is read, in every strict mode (N13).
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+try:
+ print of p[0]
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Buffer equality
+
+Buffers compare their stored doubles with IEEE equality: NaN differs from
+everything, even itself; infinities compare by sign; signed zeros are equal.
+Equality never raises or changes math flags, including under strict mode.
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+print of (p == p)
+print of (p == g)
+sgd_update of [p, p, 1.0]
+print of (p == p)
+```
+```output
+true
+false
+false
+```
diff --git a/bench/check_regression.sh b/bench/check_regression.sh
index 040ce48f3..c53f68869 100755
--- a/bench/check_regression.sh
+++ b/bench/check_regression.sh
@@ -1,13 +1,14 @@
#!/bin/bash
# Deterministic perf regression gate (#398). Wall-clock medians flake on shared
# CI runners, so the GATE uses cachegrind instruction counts (Ir) instead:
-# same binary + same input => identical Ir, run to run, machine to machine. A
-# regression is then a diffable fact, not a statistical argument. Fails if any
-# workload's Ir exceeds its baseline: --vs (CI, same job) by more than THRESHOLD_PCT, the checked-in file by more than FILE_THRESHOLD_PCT.
+# same binary + same input in one environment gives a diffable instruction
+# count; glibc ifunc selection can vary across hosts (#1939). The --vs
+# threshold is THRESHOLD_PCT; checked-in files use FILE_THRESHOLD_PCT.
#
# bench/check_regression.sh # gate: compare vs bench/baseline.txt
# bench/check_regression.sh --update # regenerate the baseline
# bench/check_regression.sh --selftest # prove a 2x-work pessimization is caught
+# bench/check_regression.sh --list-vs # derived --vs workload/tier population
set -u
DIR="$(cd "$(dirname "$0")" && pwd)"
EIGS="${EIGENSCRIPT:-$DIR/../src/eigenscript}"
@@ -30,12 +31,18 @@ FILE_THRESHOLD_PERMILLE=$(( FILE_THRESHOLD_PCT * 10 ))
# above and below stay exact integer permille comparisons.
pct() { awk -v a="$1" -v b="$2" 'BEGIN { printf "%.3f", (a - b) * 100 / b }'; }
-ir_of() { # ir_of -> instruction count
+# Drop caller EIGS_* settings, then select the requested tier explicitly.
+# --vs measures the union of bench/*.eigs (default JIT) and every declared
+# workload/tier pair, so its own CI log can re-declare every max_ci (#1939).
+ir_of() { # ir_of [jit|off] -> instruction count
+ local tier="${3:-jit}" tier_env=
+ case "$tier" in jit) ;; off) tier_env=EIGS_JIT_OFF=1 ;; *) return 2 ;; esac
# Capture the producer before parsing it. This is the standard portable
# shell pattern when a pipeline must preserve an earlier command's status
# (rather than relying on the pipeline's final command or bash pipefail).
out=$(mktemp) || return
- "$VALGRIND" --tool=cachegrind --cachegrind-out-file=/dev/null "$1" "$2" >"$out" 2>&1
+ env $(compgen -e | grep '^EIGS_' | sed 's/^/-u /') $tier_env \
+ "$VALGRIND" --tool=cachegrind --cachegrind-out-file=/dev/null "$1" "$2" >"$out" 2>&1
status=$?
if [ "$status" -ne 0 ]; then
cat "$out" >&2
@@ -110,21 +117,43 @@ case "${1:-}" in
reference="$scratch/reference"
printf '#!/bin/sh\nexit 0\n' > "$current"
cp "$current" "$reference"
+ # Equal synthetic counts would make real declared costs STALE. Keep the
+ # full population and give each declared row counts inside its own band.
cat > "$profiler" <<'EOF'
-#!/bin/sh
-while [ "$#" -gt 2 ]; do shift; done
-printf '%s %s\n' "$(basename "$1")" "$(basename "$2")" >> "$MEASURE_LOG"
-echo 'I refs: 1,000'
+#!/usr/bin/env python3
+from decimal import Decimal
+import os
+from pathlib import Path
+import sys
+
+root = Path(os.environ['BENCH_SELFTEST_ROOT']).resolve()
+sys.path.insert(0, str(root / 'tools'))
+from phase2_cost_declarations import NOISE, parse_declarations
+
+binary, file = map(Path, sys.argv[-2:])
+workload = file.resolve().relative_to(root).as_posix()
+tier = 'off' if os.environ.get('EIGS_JIT_OFF') == '1' else 'jit'
+with open(os.environ['MEASURE_LOG'], 'a') as log:
+ log.write(f'{binary.name}\t{workload}\t{tier}\n')
+declarations = parse_declarations((root / 'tests/phase2/cost_expect.txt').read_text())
+declaration = declarations.get((workload, tier))
+count = 1000000
+if binary.name == 'current' and declaration:
+ delta = max(Decimal(0), declaration.max_ci - NOISE)
+ count = int(Decimal(count) * (1 + delta / 100))
+print(f'I refs: {count}')
EOF
chmod +x "$profiler" "$current" "$reference"
- if ! MEASURE_LOG="$measure_log" VALGRIND="$profiler" EIGENSCRIPT="$current" "$0" --vs "$reference" >/dev/null; then
+ if ! BENCH_SELFTEST_ROOT="$DIR/.." MEASURE_LOG="$measure_log" VALGRIND="$profiler" EIGENSCRIPT="$current" "$0" --vs "$reference" >"$scratch/population.out" 2>&1; then
+ cat "$scratch/population.out"
rm -rf "$scratch"
echo "SELFTEST FAILED: valid derived benchmark population was rejected"; exit 1
fi
expected="$scratch/expected"
- for f in "$DIR"/*.eigs; do
- printf 'current %s\nreference %s\n' "$(basename "$f")" "$(basename "$f")"
- done | sort > "$expected"
+ "$0" --list-vs > "$scratch/population" || exit 1
+ while IFS=$'\t' read -r workload tier; do
+ printf 'current\t%s\t%s\nreference\t%s\t%s\n' "$workload" "$tier" "$workload" "$tier"
+ done < "$scratch/population" | sort > "$expected"
if ! sort "$measure_log" | cmp -s - "$expected"; then
rm -rf "$scratch"
echo "SELFTEST FAILED: a workload/tier was skipped or measured more than once"; exit 1
@@ -132,25 +161,46 @@ EOF
rm -rf "$scratch"
echo "SELFTEST OK: every derived workload/tier is measured once"
exit 0 ;;
---vs)
- # Compare THIS binary ($EIGENSCRIPT) against a reference binary ($2) on the
- # same workloads. Both built in the same environment => the Ir difference is
- # a pure code-change signal (this is what CI runs: PR binary vs origin/main
- # binary, so no cross-machine baseline drift). Fails on > THRESHOLD_PCT.
- ref="$2"; fail=0
+--vs|--list-vs)
+ # Base and candidate use this gate's own measurement recipe. Its CI
+ # counts, including off-tier declarations, are the source of max_ci.
+ ref="${2:-}"; fail=0; used=0
+ if [ "$1" = --vs ] && [ -z "$ref" ]; then echo " FAIL: --vs requires a reference binary"; exit 2; fi
+ decl=$(bash "$DIR/../tools/phase2_cost.sh" --declared all --method ci) \
+ || { echo " FAIL: cannot read the declared costs (tools/phase2_cost.sh --declared all --method ci)"; exit 2; }
+ ndecl=$(awk 'NF {n++} END {print n+0}' <<< "$decl")
+ work=$(mktemp -d "${TMPDIR:-/tmp}/eigs-bench-vs.XXXXXX") || exit 2
+ trap 'rm -rf "$work"' EXIT
for f in "$DIR"/*.eigs; do
- name=$(basename "$f" .eigs)
- a=$(ir_of "$EIGS" "$f") || exit $?
- b=$(ir_of "$ref" "$f") || exit $?
- inc=$(pct "$a" "$b")
- if [ $(( (a - b) * 1000 )) -gt $(( THRESHOLD_PERMILLE * b )) ]; then
- echo " REGRESSION: $name Ir ${b} -> ${a} (+${inc}% > ${THRESHOLD_PCT}%)"; fail=1
- else
- echo " ok: $name Ir ${a} (${inc}% vs ref)"
- fi
+ printf 'bench/%s\tjit\n' "$(basename "$f")" >> "$work/population.unsorted"
done
- [ "$fail" -eq 0 ] && echo "no instruction-count regressions vs reference (threshold ${THRESHOLD_PCT}%)"
+ awk -F'\t' 'NF {print $1 "\t" $2}' <<< "$decl" >> "$work/population.unsorted"
+ LC_ALL=C sort -u "$work/population.unsorted" > "$work/population"
+ if [ "$1" = --list-vs ]; then cat "$work/population"; exit 0; fi
+ nrows=$(wc -l < "$work/population")
+ echo " tiers: explicit jit/off (caller EIGS_* unset); column: max_ci; stale_below_max=0.5; declared rows: $ndecl; measured rows: $nrows"
+ while IFS=$'\t' read -r workload tier; do
+ f="$DIR/../$workload"
+ [ -f "$f" ] || { echo " FAIL: missing workload: $workload"; exit 2; }
+ a=$(ir_of "$EIGS" "$f" "$tier") || exit $?
+ b=$(ir_of "$ref" "$f" "$tier") || exit $?
+ [[ "$a" =~ ^[1-9][0-9]*$ && "$b" =~ ^[1-9][0-9]*$ ]] \
+ || { echo " FAIL: $workload $tier: no instruction count (cand='$a' ref='$b')"; exit 2; }
+ row=$(awk -F'\t' -v w="$workload" -v t="$tier" '$1 == w && $2 == t' <<< "$decl")
+ if [ -n "$row" ]; then used=$((used + 1)); fi
+ # One exact two-sided predicate; selects max_ci for both tiers.
+ python3 "$DIR/../tools/phase2_cost_declarations.py" --method ci \
+ --row "$workload" "$tier" "$b" "$a"
+ rc=$?
+ [ "$rc" -le 1 ] || exit "$rc"
+ [ "$rc" -eq 0 ] || fail=1
+ done < "$work/population"
+ # Every declaration was measured exactly once, regardless of workload dir
+ # or tier. The union's population is derived, never hand-maintained.
+ [ "$used" -eq "$ndecl" ] || { echo " FAIL: declared rows=$ndecl applied=$used"; exit 2; }
+ [ "$fail" -eq 0 ] && echo "no instruction-count regressions vs reference (threshold ${THRESHOLD_PCT}%; declared rows applied=$used; measured rows=$nrows)"
exit $fail ;;
+
esac
# Default (local): compare against the checked-in baseline. NOTE the baseline is
diff --git a/changes/added/1758-int64-flip.md b/changes/added/1758-int64-flip.md
new file mode 100644
index 000000000..3fc75f60b
--- /dev/null
+++ b/changes/added/1758-int64-flip.md
@@ -0,0 +1,5 @@
+- Integer literals now retain exact int64 values, with exact int/float
+ comparisons and checked `+`, `-`, `*`, negation and `abs` that raise `value`
+ on int64 overflow. `num_type` reports `"int"`, `"float"` or `null`; `type`
+ continues to report `"num"` for either numeric kind. Trace tape format v7
+ preserves numeric kinds and refuses older tapes (#1758).
diff --git a/changes/added/1758-n9-width-arithmetic.md b/changes/added/1758-n9-width-arithmetic.md
new file mode 100644
index 000000000..437d46488
--- /dev/null
+++ b/changes/added/1758-n9-width-arithmetic.md
@@ -0,0 +1 @@
+- Add explicit wrapping, checked and saturating add/sub/mul for signed and unsigned 8/16/32/64-bit widths, unsigned u64 comparison/division/remainder/decimal formatting, and float32 rounding with `fround` (#1758 N9). Operands use N5 exact conversion and type range checks; width results are ints.
diff --git a/changes/added/2120-seed-random-env.md b/changes/added/2120-seed-random-env.md
new file mode 100644
index 000000000..44cc2f2e7
--- /dev/null
+++ b/changes/added/2120-seed-random-env.md
@@ -0,0 +1 @@
+- Add `EIGS_SEED_RANDOM` to make the process-wide random stream reproducible for differential testing and reproduction (#2120).
diff --git a/changes/breaking/1758-n8-lua-bitwise.md b/changes/breaking/1758-n8-lua-bitwise.md
new file mode 100644
index 000000000..8364e6add
--- /dev/null
+++ b/changes/breaking/1758-n8-lua-bitwise.md
@@ -0,0 +1 @@
+- Bitwise operators and `bit_*` builtins now return exact int64 values. Shifts follow Lua 5.4: right shifts zero-fill, negative counts reverse direction, and counts outside (-64, 64) give zero. Fractional or out-of-range operands still raise `value` (#1740, #1758 N8).
diff --git a/changes/changed/1758-number-text.md b/changes/changed/1758-number-text.md
new file mode 100644
index 000000000..12a1d034c
--- /dev/null
+++ b/changes/changed/1758-number-text.md
@@ -0,0 +1 @@
+- Float text now uses Python-style shortest round-trip spelling across display, f-strings and JSON (N6), preserving integral float `.0` and signed zero. Integer text remains exact. Store records and SQL parameters use the same writer, fixing dropped digits (#1762). Trace number records use the same spelling without a tape format version change (T1). Constant dedup preserves binary64 bits, including distinct signed zeros.
diff --git a/changes/changed/1758-phase2-store-http-net.md b/changes/changed/1758-phase2-store-http-net.md
new file mode 100644
index 000000000..376956e6b
--- /dev/null
+++ b/changes/changed/1758-phase2-store-http-net.md
@@ -0,0 +1 @@
+- Store, HTTP and TCP numeric arguments use exact integer readers: fractional identifiers, ports, counts and bytes raise `value`; counts, ports and handles return integers. TCP byte values must be in 0..255 and an invalid payload sends nothing.
diff --git a/changes/changed/1758-r1-4-floor-division.md b/changes/changed/1758-r1-4-floor-division.md
new file mode 100644
index 000000000..04193efa2
--- /dev/null
+++ b/changes/changed/1758-r1-4-floor-division.md
@@ -0,0 +1 @@
+- Add floor division `//` and `//=`, returning an exact int for two ints and a float otherwise. `%` now uses floor modulo with the divisor's sign. Integer floor-division overflow raises; `INT64_MIN % -1` is 0. Zero divisors still raise for every numeric kind until N11 (#1758).
diff --git a/changes/changed/1758-r16b-numeric-builtins.md b/changes/changed/1758-r16b-numeric-builtins.md
new file mode 100644
index 000000000..72e1f80c5
--- /dev/null
+++ b/changes/changed/1758-r16b-numeric-builtins.md
@@ -0,0 +1,9 @@
+- Numeric builtins preserve exact int64 results and use exact integer arguments. `num` now refuses partial or empty numeric text (N10); JSON, store and database integer text decodes as int64 (N14). Scalar/list tensor arithmetic and shared counters raise on int64 overflow. Empty `min`/`max` now raises `value` in both call forms; `sign_extend` raises on subtraction underflow (R1.6b-D1/D2).
+- EigenStore numeric lookup, update, and delete keys use all int64 digits (N5), including keys above 2^53. Fractional or out-of-range floats raise before record changes. Record integer text decodes exactly (N14); float persistence text remains unchanged until R1.7.
+- HTTP POST JSON integer headers retain all int64 digits (N14, B6, R1.6b-D3); the pre-R1.6 decoder could round integer values above 2^53.
+- CI follow-up: construct ranges with an int64 induction value and a descending element count, avoiding a redundant loop counter while preserving bounds, integer results, limits, and ownership.
+- CI follow-up: preserve stored float payload formatting, including canonical NaN, until the scheduled N13 encoder refusal; remove the migrated abs raw-read exception.
+- CI follow-up: keep validate.is_alpha recognizing letters after N10 by removing an unused numeric-text conversion; retain its string membership checks.
+- CI follow-up: retain literal min/max guard names in the shared reducer so the strict guard census and its probes agree.
+- CI follow-up: retain the first strict NaN diagnostic and invalid flag when `sum` reads a buffer (#1417), before reading a later infinity. Checked int64 sum overflow remains a `value` error.
+- CI follow-up: stop shared tensor buffer kernels after their first raised result, releasing partial buffers and preserving the original diagnostic and math flags (#1417).
diff --git a/changes/changed/1758-r16c-builtins.md b/changes/changed/1758-r16c-builtins.md
new file mode 100644
index 000000000..770e1ee16
--- /dev/null
+++ b/changes/changed/1758-r16c-builtins.md
@@ -0,0 +1,6 @@
+- Numeric builtins now use their final integer or float readers (#1758, R1.6c).
+ Integer-valued positions, counts, handles, bytecode fields and bytes reject
+ fractions instead of truncating; continuous durations use checked rounding.
+ Count, index and status results are ints; buffer data remains float.
+ Migrated buffer readers refuse non-finite payloads (N13), retaining the
+ first strict-NaN diagnostic and INVALID flag.
diff --git a/changes/changed/1758-r16d-tensor-buffer-model.md b/changes/changed/1758-r16d-tensor-buffer-model.md
new file mode 100644
index 000000000..16170e5b6
--- /dev/null
+++ b/changes/changed/1758-r16d-tensor-buffer-model.md
@@ -0,0 +1 @@
+- Tensor sizes and indices require exact integer values (N5). `gather` preserves selected list cells' numeric kinds, and `shape` returns integer dimensions. Float tensor calculations convert integer inputs to float storage. Buffer windows and byte codecs check exact integer inputs; bytes outside 0..255 raise instead of wrapping. Buffer lengths, `f64_to_bytes` and `inflate` byte lists use integer values. Model token ids and generation counts require exact integer inputs, and generation returns integer token ids.
diff --git a/changes/changed/1758-r16e-gfx-numeric-readers.md b/changes/changed/1758-r16e-gfx-numeric-readers.md
new file mode 100644
index 000000000..8632c4eec
--- /dev/null
+++ b/changes/changed/1758-r16e-gfx-numeric-readers.md
@@ -0,0 +1 @@
+- Migrate graphics and audio numeric arguments to the Phase 2 exact integer and Q7 readers: clip complete geometry before rounding, clamp bounded quantities, and refuse unrepresentable magnitudes. Buffer-backed audio and framebuffer reads reject stored non-finite samples at the scalar boundary.
diff --git a/changes/changed/1758-r16z-bridge.md b/changes/changed/1758-r16z-bridge.md
new file mode 100644
index 000000000..e074a8e0d
--- /dev/null
+++ b/changes/changed/1758-r16z-bridge.md
@@ -0,0 +1,2 @@
+- Buffer indexing and iteration refuse stored non-finite elements in every strict mode. Buffer equality compares stored doubles with IEEE equality, without raises or math flags; embedding buffer reads return the exact stored double, including NaN payloads, infinities and signed zero. The transitional numeric bridge and caller-less helpers are removed (#1758, R1.6z).
+- Container self-equality follows IEEE buffer equality through nested and cyclic lists/dicts, with a visited scan that keeps shared structures linear (R1.6z-D2).
diff --git a/changes/changed/1758-r18-embed-ints.md b/changes/changed/1758-r18-embed-ints.md
new file mode 100644
index 000000000..96b36fc94
--- /dev/null
+++ b/changes/changed/1758-r18-embed-ints.md
@@ -0,0 +1,6 @@
+- The embedding API adds exact int64 construction and conversion
+(`eigs_value_new_int`, `eigs_value_as_int`) and an int-kind predicate
+(`eigs_value_is_int`), following Lua's numeric C API. Numeric literal hover
+distinguishes int and float. The iterator's hidden int position again uses
+an in-place immediate update, recovering the S9-K1 fast path. Phase 2 cost
+declarations have separate local and CI maxima measured by each gate.
diff --git a/changes/fixed/1758-number-text-amalgamation.md b/changes/fixed/1758-number-text-amalgamation.md
new file mode 100644
index 000000000..ce32dd20d
--- /dev/null
+++ b/changes/fixed/1758-number-text-amalgamation.md
@@ -0,0 +1 @@
+- Size N6 float-renderer scratch text for the complete unsigned significand and signed exponent format, keeping the two-file amalgamation warning-free at every tested optimization level.
diff --git a/changes/fixed/1758-runtime-counts-int.md b/changes/fixed/1758-runtime-counts-int.md
new file mode 100644
index 000000000..2732822eb
--- /dev/null
+++ b/changes/fixed/1758-runtime-counts-int.md
@@ -0,0 +1 @@
+- Publish runtime counts and indices as exact ints (S9-K1): loop and assignment counts, historical assignment ordinals, aggregate interrogative lengths, trajectory window sizes, error source lines and scheduler sequence/task metadata. Hidden iterator positions and index-miss sentinels also keep their integer kind.
diff --git a/changes/fixed/1758-stable-sort.md b/changes/fixed/1758-stable-sort.md
new file mode 100644
index 000000000..c04ba30db
--- /dev/null
+++ b/changes/fixed/1758-stable-sort.md
@@ -0,0 +1 @@
+- `sort` now preserves the input order of equal elements on every platform, including mixed int/float ties. Both `sort` and `sort_by` guarantee stable ordering (the #1758 R1.7 fix-forward sort ruling).
diff --git a/changes/fixed/1775-amalgamation-warnings.md b/changes/fixed/1775-amalgamation-warnings.md
new file mode 100644
index 000000000..535276718
--- /dev/null
+++ b/changes/fixed/1775-amalgamation-warnings.md
@@ -0,0 +1 @@
+- The single-file amalgamation (`make amalgamation`, `build/eigenscript_all.c`) compiles without warnings at `-O0` and `-O1` with `-Wall -Wextra`, as it already did at `-O2`. `-O0` reported a missing return at the end of `builtin_tensor_gather`, and `-O1` reported seven possibly uninitialized locals in the bitwise and index paths of the VM; none was reachable, but an embedder building the two-file artifact with warnings on saw them (#1775).
diff --git a/changes/fixed/1882-float-floor-division.md b/changes/fixed/1882-float-floor-division.md
new file mode 100644
index 000000000..619b68a9f
--- /dev/null
+++ b/changes/fixed/1882-float-floor-division.md
@@ -0,0 +1 @@
+- Float and mixed `//` and `//=` now use Python’s `divmod` quotient correction. Decimal boundaries such as `1.0 // 0.1` return `9.0`, and an underflowed negative ratio such as `1e-300 // -1e300` returns `-1.0`. Integer arithmetic and the current zero-divisor errors are unchanged. (#1882)
diff --git a/changes/internal/1758-phase2-cost.md b/changes/internal/1758-phase2-cost.md
new file mode 100644
index 000000000..7afaeaff2
--- /dev/null
+++ b/changes/internal/1758-phase2-cost.md
@@ -0,0 +1,2 @@
+- Keep float iterator exits ahead of int refusals and retain inlined index validation and reference acquisition; measure the CodSpeed workload list in both execution tiers (#1758).
+- Apply the machine's graphics-only memory ceiling in the Phase 2 corpus comparison without limiting thread-stack reservations (#1758).
diff --git a/changes/internal/1758-phase2-r1-2e.md b/changes/internal/1758-phase2-r1-2e.md
new file mode 100644
index 000000000..5d2e17e29
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2e.md
@@ -0,0 +1 @@
+- Admit int Values in the tensor bridge.
diff --git a/changes/internal/1758-phase2-r1-2f.md b/changes/internal/1758-phase2-r1-2f.md
new file mode 100644
index 000000000..86ee604b2
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2f.md
@@ -0,0 +1 @@
+- Admit int Values in the buffer bridge.
diff --git a/changes/internal/1758-phase2-r1-2g.md b/changes/internal/1758-phase2-r1-2g.md
new file mode 100644
index 000000000..6b3fa6ff2
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2g.md
@@ -0,0 +1 @@
+- Admit int Values in the host bridge.
diff --git a/changes/internal/1758-phase2-r1-2j.md b/changes/internal/1758-phase2-r1-2j.md
new file mode 100644
index 000000000..e8b1ff545
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2j.md
@@ -0,0 +1 @@
+- Migrate the JIT float guards and standalone emitter stub.
diff --git a/changes/internal/1758-phase2-r1-2k.md b/changes/internal/1758-phase2-r1-2k.md
new file mode 100644
index 000000000..7347aed5b
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2k.md
@@ -0,0 +1 @@
+- Give constant deduplication and disassembly exact int arms.
diff --git a/changes/internal/1758-phase2-r1-2l.md b/changes/internal/1758-phase2-r1-2l.md
new file mode 100644
index 000000000..2df619c1d
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2l.md
@@ -0,0 +1 @@
+- Migrate float tape tokens and retain the v7 int refusal.
diff --git a/changes/internal/1758-phase2-r1-2m.md b/changes/internal/1758-phase2-r1-2m.md
new file mode 100644
index 000000000..4f95853f5
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2m.md
@@ -0,0 +1 @@
+- Migrate the store encoder and numeric bridge.
diff --git a/changes/internal/1758-phase2-r1-2n.md b/changes/internal/1758-phase2-r1-2n.md
new file mode 100644
index 000000000..efe359c6f
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2n.md
@@ -0,0 +1 @@
+- Admit int Values in the embedding number bridge.
diff --git a/changes/internal/1758-phase2-r1-2o.md b/changes/internal/1758-phase2-r1-2o.md
new file mode 100644
index 000000000..03a4e6dfd
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2o.md
@@ -0,0 +1 @@
+- Migrate extension numeric guards and exact database int encoding.
diff --git a/changes/internal/1758-phase2-r1-2q.md b/changes/internal/1758-phase2-r1-2q.md
new file mode 100644
index 000000000..fc7b0b0d0
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2q.md
@@ -0,0 +1 @@
+- Finish the VM Value number migration: exact int readers and explicit uncatchable refusals for arithmetic and iterator state whose semantics land in R1.3.
diff --git a/changes/internal/1758-phase2-r1-2z.md b/changes/internal/1758-phase2-r1-2z.md
new file mode 100644
index 000000000..f1245c940
--- /dev/null
+++ b/changes/internal/1758-phase2-r1-2z.md
@@ -0,0 +1 @@
+- Remove the transitional Value numeric aliases after migrating all consumers; retain the number bridge until R1.6.
diff --git a/changes/internal/1758-phase2-r16a-readers.md b/changes/internal/1758-phase2-r16a-readers.md
new file mode 100644
index 000000000..bce9cb324
--- /dev/null
+++ b/changes/internal/1758-phase2-r16a-readers.md
@@ -0,0 +1,5 @@
+- Add the Phase 2 final numeric readers and an opt-in bridge deprecation meter; builtin callers and behavior are unchanged.
+
+Declare the bar's probe directory as owned by tools/phase2_builtins.sh in the
+corpus exclusion ledger, so the generic differential preserves its standalone
+program population while the bar supplies probe fixtures and profiles.
diff --git a/changes/internal/1758-phase2-slot-layer.md b/changes/internal/1758-phase2-slot-layer.md
new file mode 100644
index 000000000..bacf4edb9
--- /dev/null
+++ b/changes/internal/1758-phase2-slot-layer.md
@@ -0,0 +1,2 @@
+- #1758 Phase 2 round R1.1, the slot layer: the SMI tag (`0xFFFA`, an int in [-2^47, 2^47)) and its readers exist, and every
+ slot reader decided what an int does there, but nothing produces an int yet, so no program behaves differently.
diff --git a/changes/internal/1758-phase2-value-builtins.md b/changes/internal/1758-phase2-value-builtins.md
new file mode 100644
index 000000000..238d39493
--- /dev/null
+++ b/changes/internal/1758-phase2-value-builtins.md
@@ -0,0 +1 @@
+- Prepare builtin number guards for both Value kinds while retaining the double bridge; preserve exact integer text and numeric sort keys for the future int producer.
diff --git a/changes/internal/1758-phase2-value-gfx.md b/changes/internal/1758-phase2-value-gfx.md
new file mode 100644
index 000000000..ec1618500
--- /dev/null
+++ b/changes/internal/1758-phase2-value-gfx.md
@@ -0,0 +1 @@
+- Prepare graphics and audio numeric guards to admit ints through the unchanged bridge. Pin the audio corpus program to SDL's dummy driver so inherited disk audio settings cannot create sdlaudio.raw in the source tree.
diff --git a/changes/internal/1758-phase2-value-layer.md b/changes/internal/1758-phase2-value-layer.md
new file mode 100644
index 000000000..e7c31a985
--- /dev/null
+++ b/changes/internal/1758-phase2-value-layer.md
@@ -0,0 +1 @@
+- #1758 Phase 2 round R1.2, the Value layer: the number Value type is renamed (`VAL_FLOAT`), an exact int64 Value type (`VAL_INT`) exists, and every switch and type test over a value's type decides what an int does there; nothing produces an int yet (R1.3), so no program behaves differently.
diff --git a/changes/internal/1758-phase2-value-readers.md b/changes/internal/1758-phase2-value-readers.md
new file mode 100644
index 000000000..7e88004bc
--- /dev/null
+++ b/changes/internal/1758-phase2-value-readers.md
@@ -0,0 +1 @@
+- Complete the allocation-free int readers: SMI type and Value views preserve VAL_INT and exact digits, and slot_int_value extracts boxed int64 values without converting through double.
diff --git a/changes/internal/1758-sync5.md b/changes/internal/1758-sync5.md
new file mode 100644
index 000000000..5627a83f6
--- /dev/null
+++ b/changes/internal/1758-sync5.md
@@ -0,0 +1 @@
+- Merge main through 88a1c911 into Phase2: combine integer-kind and chunked-source tapes as v8, retain side-effect-free replay, remove the rm alias, and register set_exit_code for R1.6c (R1.6-S1/S2).
diff --git a/changes/internal/1848-stdlib-real-valued-literals.md b/changes/internal/1848-stdlib-real-valued-literals.md
new file mode 100644
index 000000000..7c5242f96
--- /dev/null
+++ b/changes/internal/1848-stdlib-real-valued-literals.md
@@ -0,0 +1,2 @@
+- No entry: `rest_energy`, `poisson_pmf`/`poisson_cdf` and `fmt_num`/`fmt_percent` keep computing in floats
+ under #1758's unreleased int literals, so they answer as the last release does (#1848).
diff --git a/changes/internal/1863-container-get-ref-comments.md b/changes/internal/1863-container-get-ref-comments.md
new file mode 100644
index 000000000..909f18dbf
--- /dev/null
+++ b/changes/internal/1863-container-get-ref-comments.md
@@ -0,0 +1 @@
+- Comments only: `src/container.h` states that `list_get_ref` and `dict_value_get_ref` return NULL for a double or an SMI, and a boxed int's `VAL_INT` Value otherwise, and that container storage holds boxed ints outside the SMI range (#1863). No behaviour changes.
diff --git a/changes/internal/2120-seeded-arms.md b/changes/internal/2120-seeded-arms.md
new file mode 100644
index 000000000..c331f46a3
--- /dev/null
+++ b/changes/internal/2120-seeded-arms.md
@@ -0,0 +1 @@
+- `jit_diff` runs every arm with one `EIGS_SEED_RANDOM`, so a program that draws a random value is compared like any other instead of reading as a divergence about 1 run in 8; this replaces the temporary two-file waiver, and `test_chunk_verify_stack.eigs`, which a replay-first rule would have masked, pins the OBS row (#2120).
diff --git a/changes/internal/2122-gate-safe-jit-diff.md b/changes/internal/2122-gate-safe-jit-diff.md
new file mode 100644
index 000000000..5fc157976
--- /dev/null
+++ b/changes/internal/2122-gate-safe-jit-diff.md
@@ -0,0 +1 @@
+- `jit_diff` now ledgers unstable comparisons that trace/replay cannot safely adjudicate without changing the observer gate, while allowing such a pair to agree on a later run (#2122).
diff --git a/codspeed.yml b/codspeed.yml
index b98c34d67..3691509b2 100644
--- a/codspeed.yml
+++ b/codspeed.yml
@@ -28,13 +28,13 @@
# make && codspeed run -m simulation --cycle-estimation=false # every target
# (CLI 5.3.1 has no per-target selector; the ids exist for the report only)
#
-# Workloads: bench/*.eigs are the five Ir-gate workloads; the three tests/
-# benches are the JIT's own shapes (CLAUDE.md "Benchmarks"), dmg_shape being
-# the consumer stand-in for the DMG emulator's hot loop.
+# In-repo workloads: every bench/*.eigs and tests/bench_*.eigs file.
+# tools/phase2_cost.sh reads this same set and measures each in both tiers.
+# dmg_shape is the consumer stand-in for the DMG emulator's hot loop.
$schema: https://raw.githubusercontent.com/CodSpeedHQ/codspeed/refs/heads/main/schemas/codspeed.schema.json
benchmarks:
- # --- the five Ir-gate workloads (bench/) ---
+ # --- the Ir-gate workloads (bench/) ---
- name: "scalar_loop"
id: scalar-loop
exec: ./src/eigenscript bench/scalar_loop.eigs
@@ -54,7 +54,17 @@ benchmarks:
id: unobserved-armed-loop
exec: ./src/eigenscript bench/unobserved_armed_loop.eigs
- # --- the JIT's own shapes (tests/) ---
+ - name: "alloc_heavy"
+ id: alloc-heavy
+ exec: ./src/eigenscript bench/alloc_heavy.eigs
+ - name: "conservative_observed_loop"
+ id: conservative-observed-loop
+ exec: ./src/eigenscript bench/conservative_observed_loop.eigs
+ - name: "conservative_unobserved_loop"
+ id: conservative-unobserved-loop
+ exec: ./src/eigenscript bench/conservative_unobserved_loop.eigs
+
+ # --- runtime and JIT workloads (tests/) ---
- name: "bench_perf"
id: bench-perf
exec: ./src/eigenscript tests/bench_perf.eigs
@@ -65,6 +75,16 @@ benchmarks:
id: idxset
exec: ./src/eigenscript tests/bench_idxset.eigs
+ - name: "bench_buf_mix"
+ id: buf-mix
+ exec: ./src/eigenscript tests/bench_buf_mix.eigs
+ - name: "bench_buf_resample"
+ id: buf-resample
+ exec: ./src/eigenscript tests/bench_buf_resample.eigs
+ - name: "bench_pcm_codec"
+ id: pcm-codec
+ exec: ./src/eigenscript tests/bench_pcm_codec.eigs
+
# --- a REAL consumer: the DMG Game Boy emulator running Blargg's cpu_instrs ---
# The in-repo dmg_shape bench flatters the JIT ~10x against the emulator it
# names (measured 2026-09-21 under cachegrind: JIT removes 48% of the
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 226a2e8d9..f28a4509a 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -218,9 +218,10 @@ EigenScript uses reference counting plus numeric and environment freelists.
(`__ATOMIC_RELAXED` increment, `__ATOMIC_ACQ_REL` decrement). When the
refcount reaches zero, `free_value` tears down the value and its children.
-**Numeric freelist.** Freed `VAL_NUM` values are placed in a per-thread
-freelist (up to 4096 entries) and reused by `make_num`, avoiding
-malloc/free churn in arithmetic-heavy loops.
+**Numeric freelist.** Freed `VAL_FLOAT` values are placed in a per-thread
+freelist (up to 4096 entries) and reused by `make_float`, avoiding
+malloc/free churn in arithmetic-heavy loops. `VAL_INT` boxes are freed
+directly; immediate floats and SMIs need no heap reference count.
**Environment freelist.** Freed `Env` structs are cached per-thread (up to
1024 entries) and reused by `env_new`, avoiding allocation in tight
diff --git a/docs/BUILTINS.md b/docs/BUILTINS.md
index a83d4d3c8..1cf631b18 100644
--- a/docs/BUILTINS.md
+++ b/docs/BUILTINS.md
@@ -45,13 +45,21 @@ audio (`audio_open`, `audio_close`, `audio_pause`, `audio_play`,
`audio_capture_close`, `audio_stream_open`, `audio_stream_push`,
`audio_stream_queued`, `audio_stream_clear`, `audio_stream_close`), and
+N6 prints each float with Python's shortest round-trip spelling: integral
+floats retain `.0`, negative zero is `-0.0`, and scientific notation starts
+below `1e-4` or at `1e16`. Ints keep their exact decimal digits. `print`,
+`write`, `str`, f-strings and JSON share the number writer. The executed
+examples are `tests/phase2/rows/N6.float-repr.eigs` and
+`tests/phase2/rows/N6.str-fstring.eigs`.
+
+
## Runtime status and control
| Function | Usage | Description |
|---|---|---|
| `math_flags` | `math_flags of null` | Return sticky `overflow`, `invalid`, and `underflow` numeric-status booleans. |
| `must_not_yield` | `must_not_yield of fn` | Run a callback while rejecting cooperative suspension. |
-| `inflate` | `inflate of bytes` | Decode raw DEFLATE bytes when zlib support is enabled. |
+| `inflate` | `inflate of bytes` | Input bytes follow N5, then 0..255; output byte values are ints. Decode raw DEFLATE bytes when zlib support is enabled. |
## Core Language
@@ -60,43 +68,54 @@ audio (`audio_open`, `audio_close`, `audio_pause`, `audio_play`,
| Name | Signature | Description |
|------|-----------|-------------|
| `print` | `print of value` | Output value to stdout with newline |
-| `len` | `len of value` | Length of string or list count |
+| `len` | `len of value` | Exact int length of string, list, dict, buffer or text builder |
| `str` | `str of value` | Convert to string representation |
-| `num` | `num of value` | Convert to number (parse string or coerce). `num of "inf"` saturates to `1e308`; `num of "nan"` raises a `value` error naming `num` by default, while under `EIGS_STRICT=0` it is `0` and sets `math_flags.invalid` (#971). |
+| `num` | `num of value` | Parse complete numeric-literal text (N10): decimal integer text fitting int64 and wrapped hex integers give ints; a point/exponent or decimal overflow gives a float. Optional sign and surrounding ASCII whitespace are accepted. Partial/empty text, separators, 0b/0o, hex floats, inf and nan raise `value` in every strict mode. Numbers retain their kind; existing non-text coercions remain. |
| `type` | `type of value` | Return type name: "num", "str", "list", "dict", "buffer", "text_builder", "fn", "builtin", "none" (the null value — SPEC.md is normative and its gated example prints `none`; the string `"null"` is never produced) |
+| `num_type` | `num_type of value` | Return "int" for an int64, "float" for a binary64, or `null` for any other value. Both numeric kinds still have `type of` "num" (#1758 N7). |
| `assert` | `assert of [cond, msg]` | Raise catchable error `"ASSERT FAIL: "` if condition is false |
-| `exit` | `exit of N` | Terminate the program with exit code `N` (default 0). **Uncatchable** — a `try`/`catch` does not intercept it — and unwinds through normal teardown, so it is leak-clean even with live closures. Code after it does not run. The request is scoped to the evaluating thread and cleared at each host eval entry, so under the embedding API a script that calls `exit` does not disable `try`/`catch` for the host's *next* eval (#739). |
-| `set_exit_code` | `set_exit_code of N` | Set the status (integer 0..255) used when the process ends gracefully, without ending it. The last call wins. Explicit `exit` and runtime failures retain their own status. |
+| `exit` | `exit of N` | Terminate the program with exit code `N` (default 0). **Uncatchable** — a `try`/`catch` does not intercept it — and unwinds through normal teardown, so it is leak-clean even with live closures. Code after it does not run. The request is scoped to the evaluating thread and cleared at each host eval entry, so under the embedding API a script that calls `exit` does not disable `try`/`catch` for the host's *next* eval (#739). A supplied numeric code follows N5, then the C int range; invalid codes raise catchably before requesting exit. |
+| `set_exit_code` | `set_exit_code of N` | Set the status (integer 0..255) used when the process ends gracefully, without ending it. The last call wins. Explicit `exit` and runtime failures retain their own status. Integral floats are accepted; fractions raise `value` (N5). |
| `coalesce` | `coalesce of [value, default]` | Return value unless empty/null, else default |
| `eval` | `eval of code_string` | Execute EigenScript code, return result |
| `throw` | `throw of message` | Raise catchable error |
Numeric values are finite by construction. `NaN` collapses to `0`, and
-overflow or infinity saturates at `+/-1e308`. This applies to numeric
-literals, `num` conversion, scalar arithmetic, tensor arithmetic, and the
+overflow or infinity saturates at `+/-1e308`. This applies to float-producing numeric
+literals, scalar arithmetic, tensor arithmetic, and the
numeric fast paths used by reassignment and `unobserved` blocks.
+Counts, byte codes and indices (`len`, `ord`, `index_of`, `list_index_of`),
+`range` elements, channel/thread handle id and generation fields, `spawn`'s
+numeric `done` flag, and task ids (`task_spawn`, `task_self`) are ints.
+`task_sched_seed` accepts an exact int64 seed by N5; no double conversion
+loses its low bits. `scan_ints` returns exact int64 tokens, skips non-integer
+tokens, and raises `value` naming any integer token outside int64.
+`abs` preserves its input kind and raises at INT64_MIN. Numeric sorting and
+`sort_by` retain exact int64 ordering.
+
### Lists
| Name | Signature | Description |
|------|-----------|-------------|
| `append` | `append of [list, item]` | Append item to list (mutates list) |
| `concat` | `concat of [a, b]` | Concatenate two lists into new list |
-| `range` | `range of n` or `range of [start, end]` | Generate integer list [0..n) or [start..end) |
-| `set_at` | `set_at of [list, index, value]` | Set element at index (mutates list); negative indices count from the end, like `[]`. Also takes a **buffer** in the first position — `set_at of [buf, i, v]`, or `set_at of [shaped_buf, row, col, v]` — where a non-number value is refused (`cannot store str in a buffer`) |
-| `get_at` | `get_at of [list, index]` | Get element at index; negative indices count from the end, like `[]`. Also takes a **buffer**: `get_at of [buf, i]`, or `get_at of [shaped_buf, row, col]` |
-| `copy_into` | `copy_into of [dest, offset, src]` | Copy src elements into dest starting at offset — a list into a list, or a buffer / list of numbers into a buffer. Returns dest. Wrong arity, a non-number offset (the doc used to list `[dest, src, offset]`, the code has always read `[dest, offset, src]`), a bad type, or a non-number element bound for a buffer RAISES (#1069; it silently returned null) |
-| `list_slice` | `list_slice of [list, start, end]` | New list with the elements of [start, end) — dual of `copy_into`. Negative indices count from the end, like `[]`; bounds then clamp to [0, len]. `start >= end` gives `[]`. Never raises on bounds |
-| `num_copy` | `num_copy of value` | Create independent copy of numeric value |
-| `hex` | `hex of n` or `hex of [n, nibbles]` | Uppercase hex string of a non-negative integer, zero-padded to `nibbles` (never truncated). Raises on negatives, fractions, non-numbers |
+| `range` | `range of n` or `range of [start, end]` | Generate int elements for [0..n), [start..end), or [start..end) by step. Every bound/step follows N5: integral floats convert, fractions raise `value`, then C int bounds and the existing element cap apply. |
+| `set_at` | `set_at of [list, index, value]` | Set element at index (mutates list); negative indices count from the end, like `[]`. Also takes a **buffer** in the first position — `set_at of [buf, i, v]`, or `set_at of [shaped_buf, row, col, v]` — where a non-number value is refused (`cannot store str in a buffer`). Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
+| `get_at` | `get_at of [list, index]` | Get element at index; negative indices count from the end, like `[]`. Also takes a **buffer**: `get_at of [buf, i]`, or `get_at of [shaped_buf, row, col]`. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). Buffer elements must be finite; non-finite cells raise `value` in every strict mode (N13). |
+| `copy_into` | `copy_into of [dest, offset, src]` | Copy src elements into dest starting at offset — a list into a list, or a buffer / list of numbers into a buffer. Returns dest. Wrong arity, a non-number offset (the doc used to list `[dest, src, offset]`, the code has always read `[dest, offset, src]`), a bad type, or a non-number element bound for a buffer RAISES (#1069; it silently returned null). Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
+| `list_slice` | `list_slice of [list, start, end]` | New list with the elements of [start, end) — dual of `copy_into`. Negative indices count from the end, like `[]`; bounds then clamp to [0, len]. `start >= end` gives `[]`. Clamped integer bounds remain valid. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
+| `num_copy` | `num_copy of value` | Create an independent numeric copy, preserving int/float kind and every int64 bit |
+| `hex` | `hex of n` or `hex of [n, nibbles]` | Exact uppercase hex digits for any non-negative int64. Value and optional nibble count follow N5; fractions raise, integral floats convert. Width clamps to 0..16, never truncating digits. |
| `sort` | `sort of list` | Stably sort an all-number or all-string list in-place (numeric / lexicographic); equal elements keep their input order on every platform. Mixed or non-scalar elements raise — use `sort_by` for records. Returns the list. A non-list argument raises by default; under `EIGS_STRICT=0` it is handed back unchanged (#971). |
-| `list_truncate` | `list_truncate of [list, new_len]` | Shrink list in-place to new_len items. No-op if new_len >= length. Returns the list |
-| `list_remove_at` | `list_remove_at of [list, index]` | Remove element at index, shift tail down (mutates). No-op if out of bounds. Returns the list |
-| `list_insert_at` | `list_insert_at of [list, index, value]` | Insert value at index, shift tail up (mutates) — dual of `list_remove_at`. `index == len` appends; any other out-of-bounds index is a no-op. Returns the list |
-| `list_index_of` | `list_index_of of [list, value]` | Index of the first element structurally equal to `value` (the same comparison `==` uses — nested lists and dicts match by structure), -1 if none |
+| `list_truncate` | `list_truncate of [list, new_len]` | Shrink list in-place to new_len items. No-op if new_len >= length. Returns the list. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
+| `list_remove_at` | `list_remove_at of [list, index]` | Remove element at index, shift tail down (mutates). No-op if out of bounds. Returns the list. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
+| `list_insert_at` | `list_insert_at of [list, index, value]` | Insert value at index, shift tail up (mutates) — dual of `list_remove_at`. `index == len` appends; any other out-of-bounds index is a no-op. Returns the list. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
+| `list_index_of` | `list_index_of of [list, value]` | First structural match index as an int, or int -1 when absent; exact numeric equality (N2). |
| `list_contains` | `list_contains of [list, value]` | `true` if any element structurally equals `value`, else `false` — the list counterpart of the string-only `contains` |
| `sort_by` | `sort_by of [list, key_fn]` | Stably sort list by numeric keys from key_fn, evaluated once per element; equal keys keep their input order on every platform. Returns a new sorted list |
| `dispatch` | `dispatch of [table, key, arg]` | Index list `table` by numeric `key` and call the resulting function with `arg` — a jump table (mirrors the `OP_DISPATCH` fast path). `key` must be a number. An ordinary builtin, not a special form: a user binding of the name wins (#459 — the fast path steps aside for any unit that rebinds `dispatch`, references `eval`, or compiles against an env where it is already rebound), and the parenthesized `dispatch of ([t, k, a])` form is one argument per #355/#405 |
+| `fill` | `fill of [count, value]` | Construct a list of repeated values. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
### Strings
@@ -104,14 +123,14 @@ numeric fast paths used by reassignment and `unobserved` blocks.
|------|-----------|-------------|
| `str_lower` | `str_lower of s` | Convert to lowercase |
| `str_upper` | `str_upper of s` | Convert to uppercase |
-| `char_at` | `char_at of [s, index]` | Single character at index as string ("" if out of range); negative indices count from the end, like `[]` |
+| `char_at` | `char_at of [s, index]` | Single character at index as string ("" if out of range); negative indices count from the end, like `[]`. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
| `contains` | `contains of [haystack, needle]` | `true` if haystack contains needle, else `false` (non-string operands are `false`, never a spurious match) |
| `starts_with` | `starts_with of [s, prefix]` | `true` if s starts with prefix, else `false` |
| `ends_with` | `ends_with of [s, suffix]` | `true` if s ends with suffix, else `false` |
-| `index_of` | `index_of of [haystack, needle]` | First index of needle in haystack, or -1 (non-string operands are -1) |
-| `substr` | `substr of [s, start, length]` | Extract substring |
+| `index_of` | `index_of of [haystack, needle]` | First byte index as an int, or int -1 when absent (legacy non-string fallback is -1) |
+| `substr` | `substr of [s, start, length]` | Extract substring. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
| `split` | `split of [s, delim]` | Split string by delimiter into list. A non-string `s` or `delim` raises by default; under `EIGS_STRICT=0`, `s` splits as `""` (so answers `[""]`) and `delim` falls back to `" "` (#971). |
-| `scan_int_tokens` | `scan_int_tokens of s` or `scan_int_tokens of [s, comment_marker]` | Token rows `[text, line, col, start, end, is_int, value]` |
+| `scan_int_tokens` | `scan_int_tokens of s` or `scan_int_tokens of [s, comment_marker]` | Token rows `[text, line, col, start, end, is_int, value]`; all numeric fields are ints. Integer text beyond int64 raises `value` naming the token (C3). |
| `trim` | `trim of s` | Strip leading/trailing whitespace |
| `str_replace` | `str_replace of [s, old, new]` | Replace all occurrences of old with new |
| `chr` | `chr of byte` | One-byte string from a byte value 1–255 (the writing inverse of `ord`). Raises outside 1–255 — including 0, since strings are NUL-terminated — and on fractions; for a Unicode *codepoint* use `utf8_encode` (lib/utf8.eigs). |
@@ -154,7 +173,103 @@ builtin-call forms below are retained for backward compatibility.
| `bit_not` | `bit_not of x` | Bitwise NOT (prefer `~x`) |
| `bit_shl` | `bit_shl of [a, b]` | Left shift (prefer `a << b`) |
| `bit_shr` | `bit_shr of [a, b]` | Unsigned right shift (prefer `a >> b`) |
-| `sign_extend` | `sign_extend of [val, bits]` | Sign-extend val from given bit width. E.g. `sign_extend of [0xFF, 8]` returns -1 |
+| `sign_extend` | `sign_extend of [val, bits]` | Exact int64 result from the existing 1..32-bit sign-extension operation; value and width follow N5 (integral floats convert, fractions raise). Its subtraction is checked: int64 underflow raises `value`, naming `sign_extend` (R1.6b-D1). Bits above the width retain the existing behavior (#1896 is separate). |
+
+### Width arithmetic (N9)
+
+Binary width operations take exactly `[a, b]`. Integral floats convert by N5;
+fractions, floats outside int64 and operands outside the named type raise
+`value`. Wrong shapes and non-number operands raise `type_mismatch` in both
+strict modes. There is no operand masking. `i64` and `u64` accept every int64;
+a `u64` uses its int64 bit pattern (`-1` is unsigned max). Wrapping reduces
+modulo the width; checked overflow raises `value` naming the builtin;
+saturating clamps to the type range. Width results are ints. Unsigned division
+and remainder by zero raise `value` with `division by zero`.
+
+`u64_to_string` and `fround` are unary and accept the usual one-element list
+form. `fround` converts an int to double first, then through float32 and back
+to double. Until R2.2, float32 overflow follows the current float-result guard
+(clamp at ±1e308 and set `math_flags.overflow`). Rust std is the numeric oracle.
+
+| Function | Usage | Description |
+|---|---|---|
+| `i8_wrapping_add` | `i8_wrapping_add of [a, b]` | N9: i8 wrapping add; N5 then type range without masking; exact int result (Rust std). |
+| `i8_wrapping_sub` | `i8_wrapping_sub of [a, b]` | N9: i8 wrapping sub; N5 then type range without masking; exact int result (Rust std). |
+| `i8_wrapping_mul` | `i8_wrapping_mul of [a, b]` | N9: i8 wrapping mul; N5 then type range without masking; exact int result (Rust std). |
+| `i8_checked_add` | `i8_checked_add of [a, b]` | N9: i8 checked add; N5 then type range without masking; exact int result (Rust std). |
+| `i8_checked_sub` | `i8_checked_sub of [a, b]` | N9: i8 checked sub; N5 then type range without masking; exact int result (Rust std). |
+| `i8_checked_mul` | `i8_checked_mul of [a, b]` | N9: i8 checked mul; N5 then type range without masking; exact int result (Rust std). |
+| `i8_saturating_add` | `i8_saturating_add of [a, b]` | N9: i8 saturating add; N5 then type range without masking; exact int result (Rust std). |
+| `i8_saturating_sub` | `i8_saturating_sub of [a, b]` | N9: i8 saturating sub; N5 then type range without masking; exact int result (Rust std). |
+| `i8_saturating_mul` | `i8_saturating_mul of [a, b]` | N9: i8 saturating mul; N5 then type range without masking; exact int result (Rust std). |
+| `i16_wrapping_add` | `i16_wrapping_add of [a, b]` | N9: i16 wrapping add; N5 then type range without masking; exact int result (Rust std). |
+| `i16_wrapping_sub` | `i16_wrapping_sub of [a, b]` | N9: i16 wrapping sub; N5 then type range without masking; exact int result (Rust std). |
+| `i16_wrapping_mul` | `i16_wrapping_mul of [a, b]` | N9: i16 wrapping mul; N5 then type range without masking; exact int result (Rust std). |
+| `i16_checked_add` | `i16_checked_add of [a, b]` | N9: i16 checked add; N5 then type range without masking; exact int result (Rust std). |
+| `i16_checked_sub` | `i16_checked_sub of [a, b]` | N9: i16 checked sub; N5 then type range without masking; exact int result (Rust std). |
+| `i16_checked_mul` | `i16_checked_mul of [a, b]` | N9: i16 checked mul; N5 then type range without masking; exact int result (Rust std). |
+| `i16_saturating_add` | `i16_saturating_add of [a, b]` | N9: i16 saturating add; N5 then type range without masking; exact int result (Rust std). |
+| `i16_saturating_sub` | `i16_saturating_sub of [a, b]` | N9: i16 saturating sub; N5 then type range without masking; exact int result (Rust std). |
+| `i16_saturating_mul` | `i16_saturating_mul of [a, b]` | N9: i16 saturating mul; N5 then type range without masking; exact int result (Rust std). |
+| `i32_wrapping_add` | `i32_wrapping_add of [a, b]` | N9: i32 wrapping add; N5 then type range without masking; exact int result (Rust std). |
+| `i32_wrapping_sub` | `i32_wrapping_sub of [a, b]` | N9: i32 wrapping sub; N5 then type range without masking; exact int result (Rust std). |
+| `i32_wrapping_mul` | `i32_wrapping_mul of [a, b]` | N9: i32 wrapping mul; N5 then type range without masking; exact int result (Rust std). |
+| `i32_checked_add` | `i32_checked_add of [a, b]` | N9: i32 checked add; N5 then type range without masking; exact int result (Rust std). |
+| `i32_checked_sub` | `i32_checked_sub of [a, b]` | N9: i32 checked sub; N5 then type range without masking; exact int result (Rust std). |
+| `i32_checked_mul` | `i32_checked_mul of [a, b]` | N9: i32 checked mul; N5 then type range without masking; exact int result (Rust std). |
+| `i32_saturating_add` | `i32_saturating_add of [a, b]` | N9: i32 saturating add; N5 then type range without masking; exact int result (Rust std). |
+| `i32_saturating_sub` | `i32_saturating_sub of [a, b]` | N9: i32 saturating sub; N5 then type range without masking; exact int result (Rust std). |
+| `i32_saturating_mul` | `i32_saturating_mul of [a, b]` | N9: i32 saturating mul; N5 then type range without masking; exact int result (Rust std). |
+| `i64_wrapping_add` | `i64_wrapping_add of [a, b]` | N9: i64 wrapping add; N5 then type range without masking; exact int result (Rust std). |
+| `i64_wrapping_sub` | `i64_wrapping_sub of [a, b]` | N9: i64 wrapping sub; N5 then type range without masking; exact int result (Rust std). |
+| `i64_wrapping_mul` | `i64_wrapping_mul of [a, b]` | N9: i64 wrapping mul; N5 then type range without masking; exact int result (Rust std). |
+| `i64_checked_add` | `i64_checked_add of [a, b]` | N9: i64 checked add; N5 then type range without masking; exact int result (Rust std). |
+| `i64_checked_sub` | `i64_checked_sub of [a, b]` | N9: i64 checked sub; N5 then type range without masking; exact int result (Rust std). |
+| `i64_checked_mul` | `i64_checked_mul of [a, b]` | N9: i64 checked mul; N5 then type range without masking; exact int result (Rust std). |
+| `i64_saturating_add` | `i64_saturating_add of [a, b]` | N9: i64 saturating add; N5 then type range without masking; exact int result (Rust std). |
+| `i64_saturating_sub` | `i64_saturating_sub of [a, b]` | N9: i64 saturating sub; N5 then type range without masking; exact int result (Rust std). |
+| `i64_saturating_mul` | `i64_saturating_mul of [a, b]` | N9: i64 saturating mul; N5 then type range without masking; exact int result (Rust std). |
+| `u8_wrapping_add` | `u8_wrapping_add of [a, b]` | N9: u8 wrapping add; N5 then type range without masking; exact int result (Rust std). |
+| `u8_wrapping_sub` | `u8_wrapping_sub of [a, b]` | N9: u8 wrapping sub; N5 then type range without masking; exact int result (Rust std). |
+| `u8_wrapping_mul` | `u8_wrapping_mul of [a, b]` | N9: u8 wrapping mul; N5 then type range without masking; exact int result (Rust std). |
+| `u8_checked_add` | `u8_checked_add of [a, b]` | N9: u8 checked add; N5 then type range without masking; exact int result (Rust std). |
+| `u8_checked_sub` | `u8_checked_sub of [a, b]` | N9: u8 checked sub; N5 then type range without masking; exact int result (Rust std). |
+| `u8_checked_mul` | `u8_checked_mul of [a, b]` | N9: u8 checked mul; N5 then type range without masking; exact int result (Rust std). |
+| `u8_saturating_add` | `u8_saturating_add of [a, b]` | N9: u8 saturating add; N5 then type range without masking; exact int result (Rust std). |
+| `u8_saturating_sub` | `u8_saturating_sub of [a, b]` | N9: u8 saturating sub; N5 then type range without masking; exact int result (Rust std). |
+| `u8_saturating_mul` | `u8_saturating_mul of [a, b]` | N9: u8 saturating mul; N5 then type range without masking; exact int result (Rust std). |
+| `u16_wrapping_add` | `u16_wrapping_add of [a, b]` | N9: u16 wrapping add; N5 then type range without masking; exact int result (Rust std). |
+| `u16_wrapping_sub` | `u16_wrapping_sub of [a, b]` | N9: u16 wrapping sub; N5 then type range without masking; exact int result (Rust std). |
+| `u16_wrapping_mul` | `u16_wrapping_mul of [a, b]` | N9: u16 wrapping mul; N5 then type range without masking; exact int result (Rust std). |
+| `u16_checked_add` | `u16_checked_add of [a, b]` | N9: u16 checked add; N5 then type range without masking; exact int result (Rust std). |
+| `u16_checked_sub` | `u16_checked_sub of [a, b]` | N9: u16 checked sub; N5 then type range without masking; exact int result (Rust std). |
+| `u16_checked_mul` | `u16_checked_mul of [a, b]` | N9: u16 checked mul; N5 then type range without masking; exact int result (Rust std). |
+| `u16_saturating_add` | `u16_saturating_add of [a, b]` | N9: u16 saturating add; N5 then type range without masking; exact int result (Rust std). |
+| `u16_saturating_sub` | `u16_saturating_sub of [a, b]` | N9: u16 saturating sub; N5 then type range without masking; exact int result (Rust std). |
+| `u16_saturating_mul` | `u16_saturating_mul of [a, b]` | N9: u16 saturating mul; N5 then type range without masking; exact int result (Rust std). |
+| `u32_wrapping_add` | `u32_wrapping_add of [a, b]` | N9: u32 wrapping add; N5 then type range without masking; exact int result (Rust std). |
+| `u32_wrapping_sub` | `u32_wrapping_sub of [a, b]` | N9: u32 wrapping sub; N5 then type range without masking; exact int result (Rust std). |
+| `u32_wrapping_mul` | `u32_wrapping_mul of [a, b]` | N9: u32 wrapping mul; N5 then type range without masking; exact int result (Rust std). |
+| `u32_checked_add` | `u32_checked_add of [a, b]` | N9: u32 checked add; N5 then type range without masking; exact int result (Rust std). |
+| `u32_checked_sub` | `u32_checked_sub of [a, b]` | N9: u32 checked sub; N5 then type range without masking; exact int result (Rust std). |
+| `u32_checked_mul` | `u32_checked_mul of [a, b]` | N9: u32 checked mul; N5 then type range without masking; exact int result (Rust std). |
+| `u32_saturating_add` | `u32_saturating_add of [a, b]` | N9: u32 saturating add; N5 then type range without masking; exact int result (Rust std). |
+| `u32_saturating_sub` | `u32_saturating_sub of [a, b]` | N9: u32 saturating sub; N5 then type range without masking; exact int result (Rust std). |
+| `u32_saturating_mul` | `u32_saturating_mul of [a, b]` | N9: u32 saturating mul; N5 then type range without masking; exact int result (Rust std). |
+| `u64_wrapping_add` | `u64_wrapping_add of [a, b]` | N9: u64 wrapping add; N5 then type range without masking; exact int result (Rust std). |
+| `u64_wrapping_sub` | `u64_wrapping_sub of [a, b]` | N9: u64 wrapping sub; N5 then type range without masking; exact int result (Rust std). |
+| `u64_wrapping_mul` | `u64_wrapping_mul of [a, b]` | N9: u64 wrapping mul; N5 then type range without masking; exact int result (Rust std). |
+| `u64_checked_add` | `u64_checked_add of [a, b]` | N9: u64 checked add; N5 then type range without masking; exact int result (Rust std). |
+| `u64_checked_sub` | `u64_checked_sub of [a, b]` | N9: u64 checked sub; N5 then type range without masking; exact int result (Rust std). |
+| `u64_checked_mul` | `u64_checked_mul of [a, b]` | N9: u64 checked mul; N5 then type range without masking; exact int result (Rust std). |
+| `u64_saturating_add` | `u64_saturating_add of [a, b]` | N9: u64 saturating add; N5 then type range without masking; exact int result (Rust std). |
+| `u64_saturating_sub` | `u64_saturating_sub of [a, b]` | N9: u64 saturating sub; N5 then type range without masking; exact int result (Rust std). |
+| `u64_saturating_mul` | `u64_saturating_mul of [a, b]` | N9: u64 saturating mul; N5 then type range without masking; exact int result (Rust std). |
+| `u64_compare` | `u64_compare of [a, b]` | N9: N5 int64 bit patterns; unsigned order gives int -1, 0 or 1 (Rust std). |
+| `u64_div` | `u64_div of [a, b]` | N9: N5 int64 bit patterns; unsigned quotient as int64 bits; zero raises value (Rust std). |
+| `u64_rem` | `u64_rem of [a, b]` | N9: N5 int64 bit patterns; unsigned remainder as int64 bits; zero raises value (Rust std). |
+| `u64_to_string` | `u64_to_string of x` | N9: N5 int64 bit pattern to unsigned decimal string (Rust std). |
+| `fround` | `fround of x` | N9: int converts to f64 first; f64 to f32 to f64, float result; current result guard until R2.2. |
### Buffers
@@ -163,33 +278,34 @@ Compact typed arrays of doubles with O(1) indexed access. Iterable with
| Name | Signature | Description |
|------|-----------|-------------|
-| `buffer` | `buffer of count` | Create zero-filled buffer of given size (or `buffer of [rows, cols]` for a shaped one). A non-numeric size raises by default; under `EIGS_STRICT=0` it makes an empty buffer (#971). |
-| `buf_get` | `buf_get of [buf, index]` | Read element; out-of-range raises `index_range` (#502 — folding to 0 was indistinguishable from a real stored 0), matching the `buf[i]` operator |
-| `buf_set` | `buf_set of [buf, index, value]` | Write element |
-| `buf_len` | `buf_len of buf` | Return buffer element count |
-| `buf_from_list` | `buf_from_list of list` | Convert numeric list to buffer |
-| `buf_copy` | `buf_copy of [src, src_off, dst, dst_off, count]` | Bulk copy between buffers (`memmove`, overlap-safe). Out-of-range windows / negative count raise (`index_range`/`value`); count 0 is a no-op |
-| `buf_mix` | `buf_mix of [dst, src, dst_off, src_off, count, gain]` | In-place mix: `dst[dst_off+i] += src[src_off+i] * gain` over the window — the audio mix-down kernel (#597). VM-identical arithmetic (byte-equal to the interpreted loop); same-buffer overlap runs forward in index order. Bad windows raise |
-| `buf_scale_range` | `buf_scale_range of [b, off, count, gain]` | In-place multiply over a window: `b[off+i] *= gain` (fades, normalize). Bad windows raise |
-| `buf_fill` | `buf_fill of [b, off, count, value]` | Bulk store over a window: `b[off+i] = value`. Bad windows raise |
-| `buf_peak` | `buf_peak of [b, off, count]` | Max absolute value over a window (normalize/meter scans); 0 for an empty window. Bad windows raise |
-| `buf_dot` | `buf_dot of [a, b, a_off, b_off, count]` | Windowed dot product: sum of `a[a_off+i] * b[b_off+i]` (YIN autocorrelation). Like `dot`, summation order/association is **unspecified** (backends may reassociate); no-NaN/Inf preserved. Bad windows raise |
-| `buf_from_pcm16le` | `buf_from_pcm16le of [bytes, byte_off, count]` | Decode `count` little-endian signed 16-bit PCM samples starting at `byte_off` into a NEW float buffer: `v = b0 + 256*b1`, two's-complement fold at 32768, `v / 32767` — bit-identical to the interpreted per-sample loop it replaces (#602, the WAV-import kernel). Bad windows raise |
-| `buf_to_pcm16le` | `buf_to_pcm16le of [floats, off, count]` | Encode `count` samples into a NEW byte buffer (2 bytes/sample, LE): clamp to [-1, 1], `round(x*32767)`, two's complement via +65536 — bit-identical to the interpreted write loop (#602). Bad windows raise |
-| `buf_deinterleave` | `buf_deinterleave of [src, channel, nch, count?]` | Every `nch`-th sample starting at index `channel` into a NEW buffer (interleaved channel split). `count` defaults to the full available tail; bad `channel`/`nch` raise `value`, count overrun raises `index_range` (#602) |
-| `buf_resample_linear` | `buf_resample_linear of [src, dst_len]` | Endpoint-inclusive **linear** resample into a NEW buffer: `pos = i*(n-1)/(dst_len-1)`, lerp between the floor/ceil neighbors — bit-identical to DeslanStudio's interpreted `ab_resample_linear` (#603). The kernel is linear interpolation, **not** Fourier/sinc resampling (the consumer-documented divergence from `scipy.signal.resample`; no anti-aliasing). `dst_len` 0 → empty buffer; empty `src` with `dst_len > 0` raises `value` |
-| `read_bytes_buf` | `read_bytes_buf of path` / `read_bytes_buf of [path, max_bytes]` | Read binary file as a buffer. The default cap is 10 MiB; `max_bytes` raises it up to the shared 512 MiB hard ceiling. An over-cap `io` error names the size, active cap, and `[path, max_bytes]` escape. (A buffer stores one double per byte, so the ceiling bounds worst-case amplification.) Missing, unreadable, and directory paths raise `io` with the path and OS reason; an empty file returns an empty buffer |
-| `write_bytes` | `write_bytes of [path, {, append}]` | Write raw bytes to a file. Binary-clean (NUL written verbatim, unlike `write_text`). `append` (default 0): 0 truncates, nonzero appends. Returns bytes written; I/O failure raises `io` with the path and OS reason. Trace-recorded result. |
+| `buffer` | `buffer of count` | Dimensions and size follow N5, then the C int range. Create zero-filled buffer of given size (or `buffer of [rows, cols]` for a shaped one). A non-numeric size raises by default; under `EIGS_STRICT=0` it makes an empty buffer (#971). |
+| `buf_get` | `buf_get of [buf, index]` | The index follows N5; the returned element is float. Read element; out-of-range raises `index_range` (#502 — folding to 0 was indistinguishable from a real stored 0), matching the `buf[i]` operator |
+| `buf_set` | `buf_set of [buf, index, value]` | The index follows N5; stored values convert to float (Q2). Write element |
+| `buf_len` | `buf_len of buf` | Returns an integer count. Return buffer element count |
+| `buf_from_list` | `buf_from_list of list` | Integer cells convert to float storage (Q2). Convert numeric list to buffer |
+| `buf_copy` | `buf_copy of [src, src_off, dst, dst_off, count]` | Offsets/counts follow N5; fractions raise `value`. Bulk copy between buffers (`memmove`, overlap-safe). Out-of-range windows / negative count raise (`index_range`/`value`); count 0 is a no-op |
+| `buf_mix` | `buf_mix of [dst, src, dst_off, src_off, count, gain]` | Offsets/counts follow N5; fractions raise `value`. In-place mix: `dst[dst_off+i] += src[src_off+i] * gain` over the window — the audio mix-down kernel (#597). VM-identical arithmetic (byte-equal to the interpreted loop); same-buffer overlap runs forward in index order. Bad windows raise |
+| `buf_scale_range` | `buf_scale_range of [b, off, count, gain]` | Offsets/counts follow N5; fractions raise `value`. In-place multiply over a window: `b[off+i] *= gain` (fades, normalize). Bad windows raise |
+| `buf_fill` | `buf_fill of [b, off, count, value]` | Offsets/counts follow N5; fractions raise `value`. Bulk store over a window: `b[off+i] = value`. Bad windows raise |
+| `buf_peak` | `buf_peak of [b, off, count]` | Offsets/counts follow N5; fractions raise `value`. Max absolute value over a window (normalize/meter scans); 0 for an empty window. Bad windows raise |
+| `buf_dot` | `buf_dot of [a, b, a_off, b_off, count]` | Offsets/counts follow N5; fractions raise `value`. Windowed dot product: sum of `a[a_off+i] * b[b_off+i]` (YIN autocorrelation). Like `dot`, summation order/association is **unspecified** (backends may reassociate); no-NaN/Inf preserved. Bad windows raise |
+| `buf_from_pcm16le` | `buf_from_pcm16le of [bytes, byte_off, count]` | Input bytes follow N5, then 0..255. Offsets/counts follow N5. Decode `count` little-endian signed 16-bit PCM samples starting at `byte_off` into a NEW float buffer: `v = b0 + 256*b1`, two's-complement fold at 32768, `v / 32767` — bit-identical to the interpreted per-sample loop it replaces (#602, the WAV-import kernel). Bad windows raise |
+| `buf_to_pcm16le` | `buf_to_pcm16le of [floats, off, count]` | Offsets/counts follow N5. Encode `count` samples into a NEW byte buffer (2 bytes/sample, LE): clamp to [-1, 1], `round(x*32767)`, two's complement via +65536 — bit-identical to the interpreted write loop (#602). Bad windows raise |
+| `buf_deinterleave` | `buf_deinterleave of [src, channel, nch, count?]` | Channel/count arguments follow N5. Every `nch`-th sample starting at index `channel` into a NEW buffer (interleaved channel split). `count` defaults to the full available tail; bad `channel`/`nch` raise `value`, count overrun raises `index_range` (#602) |
+| `buf_resample_linear` | `buf_resample_linear of [src, dst_len]` | Destination length follows N5. Endpoint-inclusive **linear** resample into a NEW buffer: `pos = i*(n-1)/(dst_len-1)`, lerp between the floor/ceil neighbors — bit-identical to DeslanStudio's interpreted `ab_resample_linear` (#603). The kernel is linear interpolation, **not** Fourier/sinc resampling (the consumer-documented divergence from `scipy.signal.resample`; no anti-aliasing). `dst_len` 0 → empty buffer; empty `src` with `dst_len > 0` raises `value` |
+| `read_bytes_buf` | `read_bytes_buf of path` / `read_bytes_buf of [path, max_bytes]` | Read binary file as a buffer. The default cap is 10 MiB; `max_bytes` raises it up to the shared 512 MiB hard ceiling. An over-cap `io` error names the size, active cap, and `[path, max_bytes]` escape. (A buffer stores one double per byte, so the ceiling bounds worst-case amplification.) Missing, unreadable, and directory paths raise `io` with the path and OS reason; an empty file returns an empty buffer. The cap follows N5: integral floats convert; fractions raise `value`. Byte data remains float buffer storage (Q2). |
+| `write_bytes` | `write_bytes of [path, {, append}]` | Write raw bytes to a file. Binary-clean (NUL written verbatim, unlike `write_text`). `append` (default 0): 0 truncates, nonzero appends. Returns bytes written; I/O failure raises `io` with the path and OS reason. Trace-recorded result. Every byte follows N5 then 0..255; fractions and out-of-range bytes raise `value` before I/O or replay. The written count is an int. |
| `rename` | `rename of [old, new]` | Rename/replace a file. Atomic on POSIX (`rename(2)`) — a crash leaves either the old or the new file whole, never a mix; basis for crash-safe swaps. Returns `true`; I/O failure raises `io` with both paths and the OS reason. Trace-recorded result. |
| `remove_file` | `remove_file of path` | Delete a file. Returns `true`; I/O failure raises `io` with the path and OS reason. Trace-recorded result. |
+| `dot` | `dot of [a, b]` | Dot product of float buffers, returning a float; summation association is unspecified. Non-finite input cells raise `value` in every strict mode (N13). |
### Self-hosting
| Name | Signature | Description |
|------|-----------|-------------|
-| `vm_run_bytecode` | `vm_run_bytecode of ` | Assemble a chunk from a descriptor and run it on the C VM, returning the result. Descriptor: `[abi, code, constants, functions?, param_count?, name?, local_names?]` — `abi` is the **bytecode ABI revision** the producer was built against (currently `1`; see below); `code` is a list of byte ints (opcodes + little-endian operands, 16-bit except `OP_LINE`'s, which is 32-bit since #630); `constants` is the pool; `functions` is a list of nested descriptors referenced by `OP_CLOSURE` (nested descriptors carry **no** `abi` element); `local_names` (slot order) sizes the call frame and names parameters. The minimal `[abi, code, constants]` form is a flat module chunk. The bridge for an EigenScript-written compiler: emit bytecode as data, execute it on the same VM (and JIT) the C compiler's output uses. Caller supplies a well-formed chunk ending in `OP_RETURN`. **The chunk must also be stack-balanced**. `vm_run_bytecode` rejects an invalid chunk by raising a catchable `value` error whose diagnostic names the malformed field or stack-verification failure; `sandbox_run` returns its structured rejection `{ok: false, error: {kind: "value", message: "invalid chunk descriptor", line: 0}}` instead. No instruction may be reached with fewer operands on the stack than it consumes, and the stack depth must be the same on every path into a given instruction — the JVM/Wasm rule, which keeps verification linear. So an `if`/`else` whose two arms leave different depths is rejected, as is a `CALL` that cannot see its own callee; the depth an instruction runs at is measured from the chunk's own frame base, and consuming below it would reach the caller's operands. This is the shape the C compiler already emits — a conditional's arms each push their value before the join — so a producer that mirrors compiler output needs no change. **A descriptor whose `abi` is missing or does not match the runtime raises (kind `value`) rather than executing** — #704: opcode numbers *and* operand widths are the bytecode ABI, and before the stamp a producer built against an older revision ran misaligned garbage at exit 0 with no error. Producers must hardcode the revision as a literal; a producer that reads the runtime's current value back would always agree and the check would be decoration. A non-list descriptor also raises (it silently returned `null` before #704). |
-| `sandbox_run` | `sandbox_run of [descriptor_or_source, max_iterations?, max_bytes?, max_work?]` | Run an ABI-stamped descriptor, or compile a source string with the C compiler and run it, in the same sealed-root allowlist. Returns `{ok:true,result:...}` or `{ok:false,error:{kind,message,line,file}}`; source front-end failures have kind `parse`, while policy/budget failures have kind `sandbox`. Budgets are deterministic and cumulative for the whole call: loop back edges default to **1,000,000**, allocation/output bytes default to **256 MiB**, and executed VM instructions (including function-call entry/dispatch and callback re-entry) default to **10,000,000**. Source preparation additionally has a deterministic compile-phase sub-budget under `max_bytes`: **1 MiB + 128 bytes per source byte** is reserved as the worst-case lexer/parser/compiler/chunk working set, and a source whose reservation exceeds `max_bytes` is refused before tokenization. This is deliberately not a wall-clock deadline; native blocking calls are excluded, preserving replay determinism. |
-| `record_history` | `record_history of flag` | Enable (nonzero) / disable (0) per-assignment history recording that `prev of x` and ` is x at ` temporal queries read (sets both value- and observer-state history). The C compiler auto-enables it when compiling a temporal query; a self-hosted compiler calls this. The flag must be a number — a non-numeric flag raises (it is not silently treated as disable). Returns the previous setting. |
+| `vm_run_bytecode` | `vm_run_bytecode of ` | Assemble a chunk from a descriptor and run it on the C VM, returning the result. Descriptor: `[abi, code, constants, functions?, param_count?, name?, local_names?]` — `abi` is the **bytecode ABI revision** the producer was built against (currently `1`; see below); `code` is a list of byte ints (opcodes + little-endian operands, 16-bit except `OP_LINE`'s, which is 32-bit since #630); `constants` is the pool; `functions` is a list of nested descriptors referenced by `OP_CLOSURE` (nested descriptors carry **no** `abi` element); `local_names` (slot order) sizes the call frame and names parameters. The minimal `[abi, code, constants]` form is a flat module chunk. The bridge for an EigenScript-written compiler: emit bytecode as data, execute it on the same VM (and JIT) the C compiler's output uses. Caller supplies a well-formed chunk ending in `OP_RETURN`. **The chunk must also be stack-balanced**. `vm_run_bytecode` rejects an invalid chunk by raising a catchable `value` error whose diagnostic names the malformed field or stack-verification failure; `sandbox_run` returns its structured rejection `{ok: false, error: {kind: "value", message: "invalid chunk descriptor", line: 0}}` instead. No instruction may be reached with fewer operands on the stack than it consumes, and the stack depth must be the same on every path into a given instruction — the JVM/Wasm rule, which keeps verification linear. So an `if`/`else` whose two arms leave different depths is rejected, as is a `CALL` that cannot see its own callee; the depth an instruction runs at is measured from the chunk's own frame base, and consuming below it would reach the caller's operands. This is the shape the C compiler already emits — a conditional's arms each push their value before the join — so a producer that mirrors compiler output needs no change. **A descriptor whose `abi` is missing or does not match the runtime raises (kind `value`) rather than executing** — #704: opcode numbers *and* operand widths are the bytecode ABI, and before the stamp a producer built against an older revision ran misaligned garbage at exit 0 with no error. Producers must hardcode the revision as a literal; a producer that reads the runtime's current value back would always agree and the check would be decoration. A non-list descriptor also raises (it silently returned `null` before #704). ABI and parameter counts follow N5 and the C int range; code bytes follow N5 and 0..255, without truncation or wrapping. |
+| `record_history` | `record_history of flag` | Enable (nonzero) / disable (0) per-assignment history recording that `prev of x` and ` is x at ` temporal queries read (sets both value- and observer-state history). The C compiler auto-enables it when compiling a temporal query; a self-hosted compiler calls this. The flag must be a number — a non-numeric flag raises (it is not silently treated as disable). Returns the previous setting as an int; fractional numeric flags retain nonzero truthiness. |
+| `sandbox_run` | `sandbox_run of [descriptor_or_source, max_iterations?, max_bytes?, max_work?]` | Run an ABI-stamped descriptor, or compile a source string with the C compiler and run it, in the same sealed-root allowlist. Returns `{ok:true,result:...}` or `{ok:false,error:{kind,message,line,file}}`; source front-end failures have kind `parse`, while policy/budget failures have kind `sandbox`. Budgets are deterministic and cumulative for the whole call: loop back edges default to **1,000,000**, allocation/output bytes default to **256 MiB**, and executed VM instructions (including function-call entry/dispatch and callback re-entry) default to **10,000,000**. Source preparation additionally has a deterministic compile-phase sub-budget under `max_bytes`: **1 MiB + 128 bytes per source byte** is reserved as the worst-case lexer/parser/compiler/chunk working set, and a source whose reservation exceeds `max_bytes` is refused before tokenization. This is deliberately not a wall-clock deadline; native blocking calls are excluded, preserving replay determinism. Iteration, byte and work limits require exact integers (N5); fractions raise `value`. Error line numbers are ints. |
### Bytes ↔ values
@@ -198,9 +314,9 @@ For serialization: reconstruct strings/floats from raw bytes (the inverse of an
| Name | Signature | Description |
|------|-----------|-------------|
-| `str_from_bytes` | `str_from_bytes of ` | Build a string from raw byte values (0–255) — the list form of `chr` (`chr of n` == `str_from_bytes of [n]` for 1–255), inverting an `ord`-over-bytes loop. Nonnumeric list elements raise `type_mismatch` by default and convert to numeric zero under `EIGS_STRICT=0`. Strings are NUL-terminated: a numeric byte that converts to `0` ends conversion, and later elements are not inspected — keep NUL-bearing binary in a buffer. |
-| `f64_to_bytes` | `f64_to_bytes of x` | List of 8 ints: the big-endian IEEE-754 encoding of double `x` (network byte order, portable across host endianness). A non-number raises by default; under `EIGS_STRICT=0` it encodes as `0.0` (#971). |
-| `f64_from_bytes` | `f64_from_bytes of ` | Decode a double from the first 8 big-endian IEEE-754 bytes; the input must contain at least 8 bytes. Inverse of `f64_to_bytes`. A `NaN` bit pattern raises a `value` error by default; under `EIGS_STRICT=0` it collapses to `0` and sets `math_flags.invalid` (#971). |
+| `str_from_bytes` | `str_from_bytes of ` | Input bytes follow N5, then 0..255; fractions/out-of-range bytes raise `value`. Build a string from raw byte values (0–255) — the list form of `chr` (`chr of n` == `str_from_bytes of [n]` for 1–255), inverting an `ord`-over-bytes loop. Nonnumeric list elements raise `type_mismatch` by default and convert to numeric zero under `EIGS_STRICT=0`. Strings are NUL-terminated: a numeric byte that converts to `0` ends conversion, and later elements are not inspected — keep NUL-bearing binary in a buffer. |
+| `f64_to_bytes` | `f64_to_bytes of x` | An integer input first converts to nearest f64 (Q2). List of 8 ints: the big-endian IEEE-754 encoding of double `x` (network byte order, portable across host endianness). A non-number raises by default; under `EIGS_STRICT=0` it encodes as `0.0` (#971). |
+| `f64_from_bytes` | `f64_from_bytes of ` | Input bytes follow N5, then 0..255; fractions/out-of-range bytes raise `value`. Decode a double from the first 8 big-endian IEEE-754 bytes; the input must contain at least 8 bytes. Inverse of `f64_to_bytes`. A `NaN` bit pattern raises a `value` error by default; under `EIGS_STRICT=0` it collapses to `0` and sets `math_flags.invalid` (#971). |
Buffers also support direct indexing (`buf[i]`, `buf[i] is val`) and
compound assignment (`buf[i] += val`).
@@ -228,9 +344,9 @@ Requires the `zlib` build (`make zlib`, `-DEIGENSCRIPT_EXT_ZLIB=1
| Name | Signature | Description |
|------|-----------|-------------|
| `json_encode` | `json_encode of value` | Serialize value to JSON string. Raises on a value nested deeper than 200 levels — which includes any **cyclic** value (`dict_set of [d, "self", d]`, `append of [a, a]`), since a cycle has no depth. Catchable. |
-| `json_decode` | `json_decode of s` | Parse JSON string to value. Raises past the same 200-level limit, so a document that decodes always re-encodes. `\uXXXX` surrogate pairs are combined into one code point; unpaired surrogates, an escaped NUL, and malformed `\u` escapes raise (strict decode — lenient callers such as `json_path` receive the complete document with U+FFFD in place of the bad scalar). |
+| `json_decode` | `json_decode of s` | Parse JSON string to value. Integer text fitting int64 decodes exactly as int; decimal overflow, a point or exponent produces float (N14). Raises past the same 200-level limit, so a document that decodes always re-encodes. `\uXXXX` surrogate pairs are combined into one code point; unpaired surrogates, an escaped NUL, and malformed `\u` escapes raise (strict decode — lenient callers such as `json_path` receive the complete document with U+FFFD in place of the bad scalar). |
| `json_build` | `json_build of [k1, v1, k2, v2, ...]` | Build JSON object from key-value pairs. `json_build of null` is `{}`; any other non-list raises by default and answers `{}` under `EIGS_STRICT=0` (#971). |
-| `json_path` | `json_path of [json_str, "dot.path"]` | Extract nested value by dot-notation path; `""` when there is no value at that path (absent key, index out of range, JSON `null`). The document is parsed leniently: a malformed document is walked as far as it parsed, so a parse failure also answers `""` or a partial value. By default, a document that `json_decode` would reject (structural error, a repaired `\u` scalar, trailing garbage) raises a catchable `value` error `json_path: invalid JSON at position N` instead (#971 Phase C); JSON `false`/`null`/absent keys stay answers in both modes. |
+| `json_path` | `json_path of [json_str, "dot.path"]` | Extract nested value as text by dot-notation path, with exact int64 digits (N14); `""` when there is no value at that path (absent key, index out of range, JSON `null`). The document is parsed leniently: a malformed document is walked as far as it parsed, so a parse failure also answers `""` or a partial value. By default, a document that `json_decode` would reject (structural error, a repaired `\u` scalar, trailing garbage) raises a catchable `value` error `json_path: invalid JSON at position N` instead (#971 Phase C); JSON `false`/`null`/absent keys stay answers in both modes. |
## Dictionaries
@@ -268,14 +384,14 @@ Query a binding's assignment history. Always on for top-level bindings;
|------|--------|---------|
| `prev` | `prev of x` | Value of `x` just before its most recent assignment |
| `at` | `what is x at L` | State at or before line `L` — works with all six interrogatives and `prev` |
-| `state_at` | `state_at of line` | Dict of every tracked binding's value at or before `line` |
+| `state_at` | `state_at of line` | Dict of every tracked binding's value at or before `line`. Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
## Observer
| Name | Signature | Description |
|------|-----------|-------------|
| `observe` | `observe of value` | Return [status, entropy, dH, prev_dH] snapshot |
-| `get_observer_thresholds` | `get_observer_thresholds of null` | `[dh_zero, dh_small, h_low]` |
+| `get_observer_thresholds` | `get_observer_thresholds of null` | `[dh_zero, dh_small, h_low]`. Numeric inputs convert to float; numeric results are floats. |
| `classify` | `classify of t` or `classify of [t, "entropy"]` | Classify a trajectory snapshot (from `trajectory of x`, #421): value-channel label by default, entropy-channel with `"entropy"`. Raises `type_mismatch` on a non-snapshot — a bare value never silently classifies |
**`report` and `report_value` are reserved** (#1102). They cannot be bound or
@@ -326,13 +442,13 @@ Boolean keywords that check the most recently observed value:
| `file_exists` | `file_exists of "path"` | `true` if the path exists (any kind: file, directory, device, fifo), `false` otherwise. A `stat` probe — never blocks (#1070: the old `fopen` probe hung on a reader-less fifo). Trace-recorded, so replay is deterministic (#585) |
| `is_dir` | `is_dir of "path"` | `true` if the path names a directory, `false` for a plain file / missing path (#576 — replaces the `file_exists of "path/."` probe). Trace-recorded, so replay is deterministic |
| `is_file` | `is_file of "path"` | `true` iff the path names a REGULAR file (`S_ISREG`); `false` for a directory, a device/fifo/socket, a missing path, or a non-string. `read_file_util` admits only regular files, so this is the probe a driver uses to match that contract (#1058). Trace-recorded, so replay is deterministic |
-| `read_text` | `read_text of path` / `read_text of [path, max_bytes]` | Read file or stream contents as a string. The default cap is 10 MiB; `max_bytes` raises it up to the shared 512 MiB hard ceiling. Unseekable streams (including `/dev/stdin`, pipes, and FIFOs) are read in chunks to EOF. Missing, unreadable, directory, and over-cap paths raise `io`; over-cap errors name the size, active cap, and `[path, max_bytes]` escape. An empty input returns `""` |
+| `read_text` | `read_text of path` / `read_text of [path, max_bytes]` | Read file or stream contents as a string. The default cap is 10 MiB; `max_bytes` raises it up to the shared 512 MiB hard ceiling. Unseekable streams (including `/dev/stdin`, pipes, and FIFOs) are read in chunks to EOF. Missing, unreadable, directory, and over-cap paths raise `io`; over-cap errors name the size, active cap, and `[path, max_bytes]` escape. An empty input returns `""`. The cap follows N5: integral floats convert; fractions raise `value`. |
| `read_line` | `read_line of null` | Blocking byte-exact line read from **stdin**: the returned string includes its line terminator (`\n`, with a preceding `\r` preserved), or `null` at EOF. Thus a terminated final line differs from an unterminated one, and an empty line is `"\n"`. Pipes and redirected regular files have identical semantics. Trace-recorded, so replay is deterministic |
-| `read_bytes` | `read_bytes of path` / `read_bytes of [path, max_bytes]` | Read a file's or stream's raw bytes as a list of integers 0–255. The default cap is 10 MiB; `max_bytes` raises it up to the shared 512 MiB hard ceiling. Unseekable streams are read in chunks to EOF. Missing, unreadable, directory, and over-cap paths raise `io`; over-cap errors name the size, active cap, and `[path, max_bytes]` escape. An empty input returns `[]`. Trace-recorded, so replay is deterministic |
+| `read_bytes` | `read_bytes of path` / `read_bytes of [path, max_bytes]` | Read a file's or stream's raw bytes as a list of integers 0–255. The default cap is 10 MiB; `max_bytes` raises it up to the shared 512 MiB hard ceiling. Unseekable streams are read in chunks to EOF. Missing, unreadable, directory, and over-cap paths raise `io`; over-cap errors name the size, active cap, and `[path, max_bytes]` escape. An empty input returns `[]`. Trace-recorded, so replay is deterministic. The cap follows N5: integral floats convert; fractions raise `value`. Returned byte values are ints (N1/Q1). |
| `write_text` | `write_text of ["path", text]` | Write string to file (`true` on success); I/O failure raises `io` with the path and OS reason. Trace-recorded result |
-| `exec_capture` | `exec_capture of ["cmd", "arg1", ...]` | Run subprocess, return [exit_code, stdout_text]. No shell (direct exec). Child stdin is /dev/null. Returns [-1, ""] on failure, [-2, partial] on timeout. 10 MB output cap. Timeout form: `exec_capture of [["cmd", ...], seconds]` |
+| `exec_capture` | `exec_capture of ["cmd", "arg1", ...]` | Run subprocess, return [exit_code, stdout_text]. No shell (direct exec). Child stdin is /dev/null. Returns [-1, ""] on failure, [-2, partial] on timeout. 10 MB output cap. Timeout form: `exec_capture of [["cmd", ...], seconds]`. Exit statuses are ints. Seconds convert to milliseconds rounded half away from zero; an interval outside the poll API C int range raises `value` before starting the child (Q7 magnitude). |
| `env_get` | `env_get of "VAR_NAME"` | Get environment variable (empty string if unset) |
-| `random_hex` | `random_hex of n` | Generate n random hex characters from /dev/urandom (`""` for `n <= 0` or `n > 256`). A non-number `n` raises by default; under `EIGS_STRICT=0` it answers `""` (#971). |
+| `random_hex` | `random_hex of n` | Generate n random hex characters from /dev/urandom (`""` for `n <= 0` or `n > 256`). A non-number `n` raises by default; under `EIGS_STRICT=0` it answers `""` (#971). The count follows N5, then the C int range: integral floats convert; fractions raise `value` before replay or randomness. |
| `try_parse` | `try_parse of code_string` | `true` if string is valid EigenScript syntax, `false` otherwise |
| `mkdir` | `mkdir of "path"` | Create directory (and parents). `true` on success; I/O failure raises `io` with the path and OS reason. Trace-recorded: replay serves the recorded result and does not re-create the directory (#585) |
| `ls` | `ls of "path"` | List non-hidden directory entries as bytewise-sorted strings (the order of `LC_ALL=C ls -1`). I/O failure raises `io` with the path and OS reason. Trace-recorded, so replay is deterministic (#585) |
@@ -350,8 +466,8 @@ has the shared 10,000,000-element limit.
| Name | Signature | Description |
|------|-----------|-------------|
-| `stream_open` | `stream_open of ["path", count]` | Open file, write header for an integral `count` from 1 through 10,000,000 float64 values. Counts outside that range or fractional counts raise a catchable `limit` error naming the path and cap, including under `EIGS_STRICT=0`. Returns `true` on success, `false` on an I/O failure. One stream per **thread**: opening a second closes the first, and an unclosed stream is flushed and closed when the thread ends (#739) |
-| `stream_write` | `stream_write of value` | Append one float64 to the open stream. `true` on success, `false` on failure |
+| `stream_open` | `stream_open of ["path", count]` | Open file, write header for an integral `count` from 1 through 10,000,000 float64 values. Fractional counts raise `value` (N5); integral floats convert. Integral counts outside the cap raise a catchable `limit` error naming the path and cap, including under `EIGS_STRICT=0`. Returns `true` on success, `false` on an I/O failure. One stream per **thread**: opening a second closes the first, and an unclosed stream is flushed and closed when the thread ends (#739) |
+| `stream_write` | `stream_write of value` | Append one float64 to the open stream. `true` on success, `false` on failure. Numeric inputs convert to float storage (Q2). |
| `stream_close` | `stream_close of null` | Close the stream. `true` on success, `false` on failure |
`load_file` and `import` resolve an absolute path as-is; otherwise they try the
@@ -380,18 +496,18 @@ stdlib roots. See [Modules](SPEC.md#modules) for import collision handling.
| Name | Signature | Description |
|------|-----------|-------------|
-| `random` | `random of null` | Random float in [0, 1) |
-| `random_int` | `random_int of [lo, hi]` | Random integer in [lo, hi] inclusive; raises on non-finite or out-of-int64 bounds and on a span over 2^31. A malformed argument (not `[lo, hi]`, or non-numeric bounds) raises by default; under `EIGS_STRICT=0` it answers `0` (#971). |
-| `seed_random` | `seed_random of n` | Seed the RNG for deterministic sequences |
+| `random` | `random of null` | Random float in [0, 1). Numeric inputs convert to float; numeric results are floats. |
+| `random_int` | `random_int of [lo, hi]` | Random int in [lo, hi] inclusive. Bounds follow N5: integral floats convert; fractions and out-of-int64 values raise `value`. The existing span limit is 2^31. Malformed arguments raise by default and return int 0 under `EIGS_STRICT=0`. |
+| `seed_random` | `seed_random of n` | Seed the RNG from an exact int64 (N5); fractional floats raise. The existing srand48 generator uses the low 32 seed bits. `random` is not cryptographic. The `eigenscript` CLI reads `EIGS_SEED_RANDOM=` at startup and seeds the shared stream with it before the program runs, which makes it predictable for the whole process (for differential testing and reproduction); an unset or empty value does nothing. A nonempty value is accepted when `strtol` parses the entire value as a representable decimal `long`; leading whitespace and a sign (such as `+7`) are accepted, but trailing characters and out-of-range values are startup errors (exit 2). This environment variable is CLI-only: embedders call `seed_random`. Under `EIGS_TRACE` seeded draws are recorded; under `EIGS_REPLAY` recorded values are served and this knob is irrelevant. |
## Time
| Name | Signature | Description |
|------|-----------|-------------|
-| `monotonic_ns` | `monotonic_ns of null` | Nanoseconds from `CLOCK_MONOTONIC` (jump-free) |
-| `monotonic_ms` | `monotonic_ms of null` | Milliseconds from `CLOCK_MONOTONIC` |
-| `clock_unix` | `clock_unix of null` | Seconds since the Unix epoch (float, wall clock; tape-captured for replay) |
-| `usleep` | `usleep of microseconds` | Pause execution |
+| `monotonic_ns` | `monotonic_ns of null` | Exact int64 nanoseconds from `CLOCK_MONOTONIC` (jump-free); raises `value` if the nanosecond count overflows int64. |
+| `monotonic_ms` | `monotonic_ms of null` | Milliseconds from `CLOCK_MONOTONIC`. Numeric inputs convert to float; numeric results are floats. |
+| `clock_unix` | `clock_unix of null` | Seconds since the Unix epoch (float, wall clock; tape-captured for replay). Numeric inputs convert to float; numeric results are floats. |
+| `usleep` | `usleep of microseconds` | Pause execution. The continuous duration rounds half away from zero; a result outside the C int range raises `value` (Q7 magnitude). Nonpositive durations return immediately. |
## Trace & Replay
@@ -420,20 +536,20 @@ automatically at exit.
| Name | Signature | Description |
|------|-----------|-------------|
-| `abs` | `abs of x` | Absolute value |
-| `min` | `min of [n1, n2, ...]` | Smallest of a list of numbers (any length >= 1) |
-| `max` | `max of [n1, n2, ...]` | Largest of a list of numbers (any length >= 1) |
-| `floor` | `floor of x` | Round down to integer |
-| `ceil` | `ceil of x` | Round up to integer |
-| `round` | `round of x` | Round to nearest integer |
-| `sin` | `sin of x` | Sine (radians) |
-| `cos` | `cos of x` | Cosine (radians) |
-| `tan` | `tan of x` | Tangent (radians) |
-| `asin` | `asin of x` | Inverse sine; input is clamped to [-1, 1] |
-| `acos` | `acos of x` | Inverse cosine; input is clamped to [-1, 1] |
-| `atan` | `atan of x` | Inverse tangent |
-| `atan2` | `atan2 of [y, x]` | Two-argument inverse tangent |
-| `pi` | `pi of null` | The constant π (3.14159265...) |
+| `abs` | `abs of x` | Absolute value, preserving int or float kind. `abs` of INT64_MIN raises `value` because its positive magnitude does not fit in int64 (#1758 N4b). |
+| `min` | `min of [n1, n2, ...]` | Winning numeric argument, retaining its int/float kind; compare int64 exactly, including mixed-kind comparisons. First argument wins ties. Empty input raises `value`, naming the builtin, in both call forms and both strict modes (R1.6b-D2). |
+| `max` | `max of [n1, n2, ...]` | Winning numeric argument, retaining its int/float kind; compare int64 exactly, including mixed-kind comparisons. First argument wins ties. Empty input raises `value`, naming the builtin, in both call forms and both strict modes (R1.6b-D2). |
+| `floor` | `floor of x` | Round down; int inputs remain exact. A float result fitting int64 becomes an int; otherwise it remains float. |
+| `ceil` | `ceil of x` | Round up; int inputs remain exact. A float result fitting int64 becomes an int; otherwise it remains float. |
+| `round` | `round of x` | Round half away from zero; int inputs remain exact. A float result fitting int64 becomes an int; otherwise it remains float. |
+| `sin` | `sin of x` | Sine (radians). Numeric inputs convert to float; numeric results are floats. |
+| `cos` | `cos of x` | Cosine (radians). Numeric inputs convert to float; numeric results are floats. |
+| `tan` | `tan of x` | Tangent (radians). Numeric inputs convert to float; numeric results are floats. |
+| `asin` | `asin of x` | Inverse sine; input is clamped to [-1, 1]. Numeric inputs convert to float; numeric results are floats. |
+| `acos` | `acos of x` | Inverse cosine; input is clamped to [-1, 1]. Numeric inputs convert to float; numeric results are floats. |
+| `atan` | `atan of x` | Inverse tangent. Numeric inputs convert to float; numeric results are floats. |
+| `atan2` | `atan2 of [y, x]` | Two-argument inverse tangent. Numeric inputs convert to float; numeric results are floats. |
+| `pi` | `pi of null` | The constant π (3.14159265...). Numeric inputs convert to float; numeric results are floats. |
## Tensor Math
@@ -444,7 +560,8 @@ boxed `Value` per number, and the container the JIT and the AOT compile
against. Every builtin in this section that accepts a flat numeric list accepts
a buffer in the same position; a 1-D buffer reads as a 1-D tensor and a shaped
buffer (`buffer of [r, c]`, `reshape of [buf, r, c]`) as its `r x c` 2-D
-tensor. The numbers are identical either way.
+tensor. Buffers store floats; integer list elements stay exact through
+`add`, `subtract`, `multiply`, `negative` and all-int `sum`.
**Container of the result**: a builtin that returns a tensor returns a
*buffer* when **every** tensor operand was a buffer, and a *list* otherwise
@@ -460,12 +577,12 @@ heterogeneous or nested data.
| Name | Signature | Description |
|------|-----------|-------------|
-| `add` | `add of [a, b]` | Element-wise addition |
-| `subtract` | `subtract of [a, b]` | Element-wise subtraction |
-| `multiply` | `multiply of [a, b]` | Element-wise multiplication |
+| `add` | `add of [a, b]` | Element-wise addition; two int scalar or list elements produce an exact int, raising `value` on int64 overflow. Buffer arithmetic stays float. |
+| `subtract` | `subtract of [a, b]` | Element-wise subtraction; two int scalar or list elements produce an exact int, raising `value` on int64 overflow. Buffer arithmetic stays float. |
+| `multiply` | `multiply of [a, b]` | Element-wise multiplication; two int scalar or list elements produce an exact int, raising `value` on int64 overflow. Buffer arithmetic stays float. |
| `divide` | `divide of [a, b]` | Element-wise division; a zero denominator raises `divide: division by zero` by default (as the `/` operator does), while under `EIGS_STRICT=0` it returns 0; overflow saturates (#971). |
| `pow` | `pow of [base, exp]` | Element-wise exponentiation; overflow saturates. A negative base with a fractional exponent raises a `value` error naming `pow` by default; under `EIGS_STRICT=0` its `NaN` collapses to `0` and sets `math_flags.invalid` (#971). |
-| `negative` | `negative of t` | Element-wise negation |
+| `negative` | `negative of t` | Element-wise negation; scalar and list ints remain exact ints, raising `value` at INT64_MIN. Float and buffer elements remain floats. |
The arithmetic builtins listed above take shaped **buffers** wherever they take a flat
numeric list (#1093/#973), through **one** implementation — the same
@@ -482,7 +599,8 @@ have (the one place the two containers still differ: buffers truncate flat, so
`tests/test_autograd.eigs`); a non-numeric partner (a string, a dict) raises by
default and answers `0` under `EIGS_STRICT=0`. Before #1093 only equal-count `add` had a buffer path and
every other buffer case answered a silent `0`. Same `num_guard` kernels
-either way, so the numbers are byte-identical.
+for float inputs either way. Integer list arithmetic is exact; storing an int
+in a buffer first converts it to float and can round it.
### Functions
@@ -500,18 +618,20 @@ either way, so the numbers are byte-identical.
| Name | Signature | Description |
|------|-----------|-------------|
-| `matmul` | `matmul of [a, b]` | Matrix multiplication. Two shaped buffers multiply on the flat data and give a buffer; a 1-D left operand gives a 1-D result. Mixed list/buffer operands give a list. An accumulation that reaches `inf - inf` is `NaN`; by default that raises a catchable `value` error naming `matmul` (#971). Kernels may retain raw non-finite values in buffer storage, but every scalar read applies the finite-number guard (#1417): infinity saturates at ±`1e308`, and with strict mode off a `NaN` becomes `0` and sets `math_flags.invalid`, matching the list result. |
+| `matmul` | `matmul of [a, b]` | Matrix multiplication. Two shaped buffers multiply on the flat data and give a buffer; a 1-D left operand gives a 1-D result. Mixed list/buffer operands give a list. An accumulation that reaches `inf - inf` is `NaN`; by default that raises a catchable `value` error naming `matmul` (#971). Kernels may retain raw non-finite values in buffer storage. Indexing and iteration, like `get_at`, refuse stored NaN and infinity with `value` in every strict mode (N13). Strict NaN keeps the first-NaN diagnostic and INVALID flag; the interpreter, JIT and leaf fallback agree on the error source line. |
| `matmul_at` | `matmul_at of [a, b]` | `aᵀ·b` without materialising the transpose: `a` is `(m × k)`, `b` is `(m × n)`, result `(k × n)` — the weight gradient `dW = Xᵀ·dY` of a linear layer. Buffers and nested lists; byte-identical to `matmul` of the explicitly transposed operand (same tiled kernel order). Two 1-D operands give their `(k × n)` outer product. Shape/type/size errors raise like `matmul` (#973) |
| `matmul_bt` | `matmul_bt of [a, b]` | `a·bᵀ`: `a` is `(m × k)`, `b` is `(n × k)`, result `(m × n)` — the input gradient `dX = dY·Wᵀ`. A 1-D left operand is a row vector and yields a 1-D result, as for `matmul` (#973) |
-| `gather` | `gather of [matrix, indices, dim]` | Gather one element per row: `out[i] = matrix[i][indices[i]]`. `matrix` may be a shaped buffer and `indices` a list or a buffer; a shaped-buffer `matrix` gives a buffer. `gather of [vec, i]` on a 1-D tensor returns element `i`. An **out-of-range index raises `index_range`** — in every form, list or buffer (#973/#1093, settled at integration: there is no element there, and `scatter_add` raises on the same index). A row that is not a row (a 1-D tensor in the per-row form) still answers `0.0` for that row |
-| `scatter_add` | `scatter_add of [dst, indices, values]` | The gradient of `gather`, accumulated **in place** into the buffer `dst` (returned). A shaped `[rows × cols]` dst does `dst[i][indices[i]] += values[i]` per row; a 1-D dst does the flat `dst[indices[j]] += values[j]`. `values` is a buffer, a list of numbers, or one number broadcast to every index; repeats accumulate. Lengths must line up exactly — a non-scalar `values` shorter or longer than `indices`, or a per-row `dst` whose row count differs from the index count, raises `value` rather than truncating (a dropped gradient entry is a silent wrong number). Every index is validated **before** anything is written, so a raise (`index_range`, `value`, or `type_mismatch` for a non-buffer dst / non-numeric index or value) leaves `dst` untouched (#973) |
+| `gather` | `gather of [matrix, indices, dim]` | N5 exact indices (integral floats accepted; fractions raise `value`). Selected list cells retain their numeric kind; buffer elements are floats. Gather one element per row: `out[i] = matrix[i][indices[i]]`. `matrix` may be a shaped buffer and `indices` a list or a buffer; a shaped-buffer `matrix` gives a buffer. `gather of [vec, i]` on a 1-D tensor returns element `i`. An **out-of-range index raises `index_range`** — in every form, list or buffer (#973/#1093, settled at integration: there is no element there, and `scatter_add` raises on the same index). A row that is not a row (a 1-D tensor in the per-row form) still answers `0.0` for that row |
+| `scatter_add` | `scatter_add of [dst, indices, values]` | N5 exact indices (fractions raise `value`). The gradient of `gather`, accumulated **in place** into the buffer `dst` (returned). A shaped `[rows × cols]` dst does `dst[i][indices[i]] += values[i]` per row; a 1-D dst does the flat `dst[indices[j]] += values[j]`. `values` is a buffer, a list of numbers, or one number broadcast to every index; repeats accumulate. Lengths must line up exactly — a non-scalar `values` shorter or longer than `indices`, or a per-row `dst` whose row count differs from the index count, raises `value` rather than truncating (a dropped gradient entry is a silent wrong number). Every index is validated **before** anything is written, so a raise (`index_range`, `value`, or `type_mismatch` for a non-buffer dst / non-numeric index or value) leaves `dst` untouched (#973) |
+
+Buffer arithmetic stops at its first raised kernel result and releases its partial output; later elements are not read (#1417).
### Reductions
| Name | Signature | Description |
|------|-----------|-------------|
-| `mean` | `mean of t` | Average of all elements (list, nested list or buffer; an empty buffer or list is `0.0`) |
-| `sum` | `sum of t` | Sum of all elements (list, nested list or buffer) |
+| `mean` | `mean of t` | Non-finite buffer cells raise `value` in both strict modes (N13). Average of all elements (list, nested list or buffer; an empty buffer or list is `0.0`) |
+| `sum` | `sum of t` | Sum of all elements (list, nested list or buffer). An all-int input returns an exact int and raises `value` on int64 overflow. A float anywhere in a nested input makes the reduction float from the start; buffers produce floats and refuse non-finite elements with `value` in every strict mode (N13). In strict mode the first NaN sets `math_flags.invalid` and raises "not a number" before reading later elements (#1417). An empty list returns int `0`. |
### Construction
@@ -519,9 +639,9 @@ either way, so the numbers are byte-identical.
|------|-----------|-------------|
| `zeros` | `zeros of n` or `zeros of [rows, cols]` | `zeros of n` returns a **buffer** of `n` zeros (`type of` is `buffer`, `print of` shows ``); `zeros of [rows, cols]` returns the nested-list 2-D tensor. Breaking change in #1093 — `zeros of n` used to return a list; write `[0 for i in range of n]` if you need that. Both spellings cap at 10,000,000 elements and charge the sandbox budget |
| `zeros_like` | `zeros_like of t` | Zero tensor matching `t`'s shape **and container**: a buffer gives a buffer (shape preserved), a list gives a list, a number gives `0.0` |
-| `random_normal` | `random_normal of [rows, cols, scale]` | Gaussian random tensor |
-| `shape` | `shape of t` | Return dimensions as list |
-| `reshape` | `reshape of [buffer, rows, cols]` | New numeric buffer with the same data reinterpreted as `rows`×`cols` (requires `rows*cols == count`; `null` otherwise) |
+| `random_normal` | `random_normal of [rows, cols, scale]` | Gaussian random float tensor; dimensions follow N5 (integral floats accepted; fractions raise `value`). Dimension refusal precedes the trace-tape boundary |
+| `shape` | `shape of t` | Return dimensions as a list of exact integers |
+| `reshape` | `reshape of [buffer, rows, cols]` | Rows/columns follow N5, then the C int range. New numeric buffer with the same data reinterpreted as `rows`×`cols` (requires `rows*cols == count`; `null` otherwise) |
### Persistence
@@ -545,13 +665,13 @@ either way, so the numbers are byte-identical.
| Name | Signature | Description |
|------|-----------|-------------|
-| `token_name` | `token_name of id` | Return token type name by ID (`"?"` for an unknown id). A non-number raises by default; under `EIGS_STRICT=0` it answers `"?"` too (#971). |
+| `token_name` | `token_name of id` | Return token type name by ID (`"?"` for an unknown id). A non-number raises by default; under `EIGS_STRICT=0` it answers `"?"` too (#971). Integer positions, lengths and counts accept integral floats; fractions or values outside the C int range raise `value` (N5). |
## Corpus Preparation
| Name | Signature | Description |
|------|-----------|-------------|
-| `build_corpus` | `build_corpus of [files, top_n, stream_path, vocab_path]` | Three-pass C-backed corpus builder: tokenise `files`, emit top-`n` vocabulary and stream-format token IDs. The token stream, including file separators, is capped at 10,000,000 elements; exceeding it raises a catchable `limit` error naming the stream path and cap, including under `EIGS_STRICT=0`, before opening the stream file |
+| `build_corpus` | `build_corpus of [files, top_n, stream_path, vocab_path]` | Three-pass C-backed corpus builder: tokenise `files`, emit top-`n` vocabulary and stream-format token IDs. The token stream, including file separators, is capped at 10,000,000 elements; exceeding it raises a catchable `limit` error naming the stream path and cap, including under `EIGS_STRICT=0`, before opening the stream file. Vocabulary, slot and integer-literal counts follow N5 and the C int range; fractions raise `value`. Returned stream/identifier/file counts are ints. |
## Optional: Network Extension (TCP sockets)
@@ -583,6 +703,10 @@ backlog before `net_accept` runs (see `examples/net_echo.eigs`).
Sockets left open at exit are closed by the runtime's handle-table
drain. UDP is not yet exposed.
+TCP handles, ports and sent-byte counts are integers. Numeric socket handles, ports, receive counts and outgoing bytes require exact integers (integral floats convert); bytes must be in 0..255. `net_send` validates the entire payload before sending any byte. Poll timeouts are continuous milliseconds rounded to the C int range; an unrepresentable duration raises `value`.
+
+`http_early_bind` and `http_serve` require an exact integer port in 0..65535; integral floats convert and port zero requests an ephemeral port. Fractional or out-of-range ports raise `value` before binding. `shared_delete` and `shared_size` return integer counts.
+
## Optional: HTTP Extension
Requires full build. Provides an embedded HTTP server.
@@ -621,10 +745,10 @@ axis. Three controls bound it:
| `http_static` | `http_static of [prefix, directory]` | Serve static files (realpath-confined to `directory`). Register before `http_serve`: once serving it raises, including from a `code` route (#1140) |
| `http_early_bind` | `http_early_bind of port`, `http_early_bind of null`, or `http_early_bind of [port, "/livez"]` | Bind/listen before initialization. `null` defaults to 5000; a positive `PORT` environment override still wins. During init every request receives **503 Service Unavailable** with `Retry-After: 1`; never a fake 200 for arbitrary paths. Only an explicitly supplied liveness path answers GET/HEAD with 200, `text/plain`, body `OK` (HEAD has no body), by exact request-target match, including any query string. The path must start with `/` and contain no whitespace, CR, LF or NUL; invalid paths raise. The two scalar spellings configure no liveness path. Init-window capacity is the same number as the serving connection cap (`HTTP_MAX_CONCURRENT_CONNS`, 256): an idle or slow client cannot delay any other in-flight answer, a newcomer past that cap is accepted and answered a bodyless 503 (Content-Length 0, identical on the wire for every method including HEAD) immediately, and a client that never sends a complete request line is answered 503 at a 1 s per-client deadline. Init-window replies (liveness 200, init 503, capacity shed, handoff/teardown drain) are best-effort to a peer that is not reading: the responder never waits on a client. Once `http_serve` takes over, normal routing applies even to that path (404 unless registered) |
| `http_response_header` | `http_response_header of [name, value]` | Register before `http_serve`, including before or after `http_early_bind`; returns `"response header registered"`. Both arguments must be strings. Name: RFC 7230 token, **1–64 bytes**. Value: **0–1024 bytes**, visible ASCII, space or tab only; **no CR/LF/NUL**. Runtime-owned and runtime-emitted names (`Content-Length`, `Content-Type`, `Transfer-Encoding`, `Connection`, `Cache-Control`, `Retry-After`, `Allow`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`) are refused case-insensitively. An existing name is **replaced case-insensitively**, including at capacity; at most **16** distinct headers. Every violation raises a loud error naming the builtin and rule; a rejected registration prevents `http_serve` from starting even if caught. Registered headers accompany **every** response: routes (string/file/code/authed), static files, HEAD, OPTIONS, errors, load shedding, init 503s and optional liveness 200s. Content-Type, Content-Length, status and body bytes are unchanged. Registration is deterministic configuration and adds no tape records |
-| `http_serve` | `http_serve of port` | Start blocking HTTP server |
+| `http_serve` | `http_serve of port` | Start blocking HTTP server on an exact integer port in 0..65535 (integral floats convert). |
| `http_request_body` | `http_request_body of null` | Get current request body |
| `http_session_id` | `http_session_id of null` | Get current session ID |
-| `http_post` | `http_post of [url, headers, body]` | HTTP POST via curl (no shell). `headers` is a JSON **string**, either an object (`"{\"X-Key\": \"v\"}"`) or a flat alternating array (`"[\"X-Key\", \"v\"]"`); `""` means no headers. Any other parsed shape raises `type_mismatch` rather than silently sending none (#755). Max 32 headers; CR/LF is stripped from both halves |
+| `http_post` | `http_post of [url, headers, body]` | HTTP POST via curl (no shell). `headers` is a JSON **string**, either an object (`"{\"X-Key\": \"v\"}"`) or a flat alternating array (`"[\"X-Key\", \"v\"]"`); `""` means no headers. Any other parsed shape raises `type_mismatch` rather than silently sending none (#755). JSON integer header values within int64 retain every digit when sent as text (N14, B6). Max 32 headers; CR/LF is stripped from both halves |
| `http_request_headers` | `http_request_headers of null` | Get current request headers |
### Per-worker code routes
@@ -660,11 +784,11 @@ server down with it. Total bytes are bounded by
| `shared_set` | `shared_set of [key, value]` | Store `value` (JSON-encoded). Returns `null` if over byte cap. |
| `shared_get` | `shared_get of key` | Return stored value (re-parsed) or `null` if absent. |
| `shared_has` | `shared_has of key` | Return `true` if key present, else `false`. |
-| `shared_delete` | `shared_delete of key` | Remove key; return `1` if removed, `0` if absent. |
+| `shared_delete` | `shared_delete of key` | Remove key; return integer `1` if removed, `0` if absent. |
| `shared_keys` | `shared_keys of null` | Return list of keys. |
-| `shared_size` | `shared_size of null` | Return current key count. |
+| `shared_size` | `shared_size of null` | Return current integer key count. |
| `shared_clear` | `shared_clear of null` | Drop all entries. |
-| `shared_incr` | `shared_incr of [key, delta]` | Atomic single-lock read-modify-write. Missing key treated as `0`. Returns new value, or `null` if existing value is non-numeric. |
+| `shared_incr` | `shared_incr of [key, delta]` | Atomic single-lock read-modify-write. Missing key treated as int `0`. Two ints produce an exact int; a float operand produces a float. Int64 overflow raises `value` without changing the counter. Returns new value, or `null` if existing value is non-numeric. |
Individual op atomicity is guaranteed by the mutex. For
read-modify-write atomicity use `shared_incr`; `shared_get`+`shared_set`
@@ -693,35 +817,45 @@ first use, so no SDL2 headers or link-time SDL dependency are needed.
| Name | Signature | Description |
|------|-----------|-------------|
-| `gfx_open` | `gfx_open of [width, height, title]` | Open window and renderer. Returns `true` on success or `false` when libSDL2 is unavailable. A non-numeric size raises by default; under `EIGS_STRICT=0` it returns `false` (#1007 — it used to be read without a type check, so a string opened a `0x0` window and still answered `true`). |
+| `gfx_open` | `gfx_open of [width, height, title]` | Open window and renderer. Continuous sizes round half away from zero; a rounded size outside the C int range raises `value`. Returns `true` on success or `false` when libSDL2 is unavailable. A non-numeric size raises by default; under `EIGS_STRICT=0` it returns `false` (#1007 — it used to be read without a type check, so a string opened a `0x0` window and still answered `true`). |
| `gfx_close` | `gfx_close of null` | Destroy window and quit SDL |
-| `gfx_clear` | `gfx_clear of [r, g, b]` / `gfx_clear of null` | Clear backbuffer to color; `null` clears to black |
-| `gfx_rect` | `gfx_rect of [x, y, w, h, r, g, b]` or `[..., a]` | Filled rectangle |
-| `gfx_line` | `gfx_line of [x1, y1, x2, y2, r, g, b]` | Line segment |
-| `gfx_point` | `gfx_point of [x, y, r, g, b]` | Single pixel |
-| `gfx_circle` | `gfx_circle of [cx, cy, radius, r, g, b]` | Filled circle (midpoint) |
-| `gfx_rrect` | `gfx_rrect of [x, y, w, h, radius, r, g, b]` or `[..., a]` | Filled rounded rectangle (scanline corner fill); radius clamps to half the smaller dimension, `radius 0` = plain rect |
-| `gfx_clip` | `gfx_clip of [x, y, w, h]` / `gfx_clip of null` | Set / clear the render clip rectangle |
-| `gfx_read` | `gfx_read of [x, y]` | Read back one rendered pixel as `[r, g, b]` — the render-decode oracle primitive (#823). Reads the current back buffer: call after drawing, **before** `gfx_present`. Null with no window or a failed read. Nondeterministic input (font raster, driver), so it records/replays on the trace tape |
+| `gfx_clear` | `gfx_clear of [r, g, b]` / `gfx_clear of null` | Clear backbuffer to color; channels clamp to 0..255, then round half away from zero. `null` clears to black |
+| `gfx_rect` | `gfx_rect of [x, y, w, h, r, g, b]` or `[..., a]` | Filled rectangle; intersect the whole rectangle with the drawable area in float space, then round |
+| `gfx_line` | `gfx_line of [x1, y1, x2, y2, r, g, b]` | Line segment; clip with Liang-Barsky in float space, then round |
+| `gfx_point` | `gfx_point of [x, y, r, g, b]` | Single pixel; an outside point draws nothing, and a surviving coordinate rounds half away from zero |
+| `gfx_circle` | `gfx_circle of [cx, cy, radius, r, g, b]` | Filled circle; clip continuous spans to visible rows and columns before rounding, so a huge radius does work bounded by the drawable area |
+| `gfx_rrect` | `gfx_rrect of [x, y, w, h, radius, r, g, b]` or `[..., a]` | Filled rounded rectangle; clip continuous spans to the drawable area, then round. Radius clamps to half the smaller dimension; `radius 0` = plain rect |
+| `gfx_clip` | `gfx_clip of [x, y, w, h]` / `gfx_clip of null` | Intersect the continuous clip rectangle with the drawable area, then round; `null` clears it |
+| `gfx_read` | `gfx_read of [x, y]` | Read back one rendered pixel as integer channels `[r, g, b]`; coordinates round half away from zero and an outside point returns null — the render-decode oracle primitive (#823). Reads the current back buffer: call after drawing, **before** `gfx_present`. Null with no window or a failed read. Nondeterministic input (font raster, driver), so it records/replays on the trace tape |
| `gfx_text` | `gfx_text of [x, y, text, r, g, b]` or `[..., scale]` | Text. Proportional antialiased TTF when libSDL2_ttf + a font are available (#593); the 5x7 bitmap font otherwise — see the font note below the table |
-| `gfx_text_width` | `gfx_text_width of [text, scale?]` or `of "text"` | Pixel width of `text` under the active text renderer: TTF metrics when active, `len * 6 * scale` in bitmap mode. Works before `gfx_open` |
-| `gfx_text_height` | `gfx_text_height of scale?` | Pixel line height under the active text renderer: TTF font height when active, `7 * scale` in bitmap mode |
+| `gfx_text_width` | `gfx_text_width of [text, scale?]` or `of "text"` | Integer pixel width under the active text renderer: TTF metrics when active, `len * 6 * scale` in bitmap mode. Scale rounds half away from zero; a rounded scale outside the C int range raises `value`. Works before `gfx_open`. |
+| `gfx_text_height` | `gfx_text_height of scale?` | Integer pixel line height under the active text renderer: TTF font height when active, `7 * scale` in bitmap mode. Scale rounds half away from zero; a rounded scale outside the C int range raises `value`. |
| `gfx_present` | `gfx_present of null` | Flip backbuffer to screen |
-| `gfx_poll` | `gfx_poll of null` | Return next event as dict (`quit`, `keydown`, `keyup`, `mousemove`, `mousedown`, `mouseup`, `wheel`, `resize`), or null. Key, mouse, and wheel events carry `shift`/`ctrl`/`alt` (bools); wheel `x`/`y` are scroll deltas |
-| `gfx_ticks` | `gfx_ticks of null` | Milliseconds since `SDL_Init` |
-| `gfx_delay` | `gfx_delay of ms` | Sleep for ms (SDL-coordinated) |
+| `gfx_poll` | `gfx_poll of null` | Return next event as dict (`quit`, `keydown`, `keyup`, `mousemove`, `mousedown`, `mouseup`, `wheel`, `resize`), or null. Key, mouse, and wheel events carry `shift`/`ctrl`/`alt` (bools); event coordinates, dimensions, scancodes and button codes are ints. Wheel `x`/`y` are scroll deltas |
+| `gfx_ticks` | `gfx_ticks of null` | Milliseconds since `SDL_Init`, as an int; int `0` before SDL is loaded |
+| `gfx_delay` | `gfx_delay of ms` | Sleep for continuous milliseconds, rounded half away from zero. A rounded delay outside 0..4294967295 raises `value`. |
| `gfx_title` | `gfx_title of "text"` | Update window title |
-| `gfx_fb` | `gfx_fb of [buf, w, h, x, y, scale]` | Blit buffer (palette indices 0-3) as scaled texture |
-| `ppu_render_frame` | `ppu_render_frame of [mem_buf, fb_buf]` | Full Game Boy PPU render (BG/window/sprites) into framebuffer |
+| `gfx_fb` | `gfx_fb of [buf, w, h, x, y, scale]` | Blit a palette buffer; `w` and `h` require exact integers in the C `int` range. The continuous destination is clipped before rounding and retains the original source-pixel mapping |
+| `ppu_render_frame` | `ppu_render_frame of [mem_buf, fb_buf]` | Full Game Boy PPU render (BG/window/sprites) into framebuffer; each memory byte must be an exact integer in 0..255 |
+
+**Continuous geometry and colour (Q7).** Drawing coordinates, dimensions,
+scale and colour channels accept ints or floats. Shapes are clipped as a whole
+against the drawable area in float space before half-away-from-zero rounding;
+an empty intersection draws nothing. Colour channels clamp to 0..255 before
+rounding. Nonfinite numeric inputs raise `value` in both strict modes. Bitmap
+text clips each glyph cell, including scales too large for the TTF integer size
+API. Fractional drawing scales also use this continuous bitmap-cell path.
**Wrong-typed and wrong-arity arguments (#1007).** Every builtin in this
extension that takes an argument at all — the drawing calls, the text calls,
the framebuffer blit, the PPU renderer, and the whole audio surface below —
type-checks its arguments *before* reading them, and by default a
wrong type, a short argument list, a wrong-shaped argument *container* (a
-number or a string where a list belonged) or an out-of-domain value raises a
-catchable `type` error naming the builtin and the shape it wanted. Under `EIGS_STRICT=0`, the
-answer is byte-identical to before: the drawing calls still
+number or a string where a list belonged) raises a catchable `type_mismatch`
+error naming the builtin and the shape it wanted. This paragraph describes the
+legacy type/shape guards. Numeric N5, Q1 and Q7 validation instead raises `value`
+in both strict modes, as specified above and in each row. Under `EIGS_STRICT=0`,
+the type/shape guards retain their legacy stand-ins: the drawing calls still
answer `null`, the generators still answer an empty list, the device calls
still answer `0` or the device id they already answered.
@@ -746,8 +880,9 @@ draw a **black** rectangle where red was asked for — silently, in both modes.
That read is gone in both modes; an unchecked union pun is not behaviour
anything can depend on.
-So be precise about what "byte-identical" covers: the **returned value** is
-unchanged in every case, and so is anything the call *reports*. What a
+For these legacy wrong-type probes, "byte-identical" covers the returned
+stand-in value and anything the call reports; numeric result kinds follow
+the individual contracts above. What a
rejected call **draws** is not, and cannot be — a 48-bit pointer read as a
double is a subnormal that truncates to 0, so the parent painted a shape at
coordinate 0, in colour 0, or at scale 1, and this build paints nothing at
@@ -784,7 +919,7 @@ raises under `EIGS_STRICT`, and a pinned documented answer does not.
**Text rendering and fonts (#593).** `gfx_text` lazily loads
`libSDL2_ttf-2.0.so.0` on first use and renders proportional antialiased
text (`TTF_RenderUTF8_Blended`) when both the library and a font file are
-present. Font selection: the `EIGS_GFX_FONT` environment variable
+present and the drawing scale is an integer supported by its size API. Font selection: the `EIGS_GFX_FONT` environment variable
(absolute path to a `.ttf`) wins; when it is set but unreadable the
runtime warns once and stays on the bitmap font (a nonexistent path is
the deterministic off-switch). Otherwise a short list of common system
@@ -796,6 +931,37 @@ should measure through `gfx_text_width`/`gfx_text_height` (as `lib/ui.eigs`
does) rather than assuming the `6 * scale` monospace advance. Text
rendering is output-only: no trace-tape records in either mode.
+## EigenStore
+
+EigenStore stores record dicts in named collections. A nonempty string `_id`
+field supplies an explicit record key; otherwise `store_put` generates one.
+
+| Name | Signature | Description |
+|------|-----------|-------------|
+| `store_open` | `store_open of path` | Open/create a store; return a handle dict with integer id/generation fields. |
+| `store_close` | `store_close of handle` | Flush and close the exact handle; return `null`. |
+| `store_collections` | `store_collections of handle` | Return collection names for the exact handle. |
+| `store_count` | `store_count of [handle, collection]` | Return the integer live-record count (zero for an unknown collection). |
+| `store_drop` | `store_drop of [handle, collection]` | Drop a collection; return whether it existed. |
+| `store_put` | `store_put of [handle, collection, record]` | Write a record dict, add its `_id` field, and return the string key. Int64 fields are stored exactly. |
+| `store_get` | `store_get of [handle, collection, key]` | Return the record dict, or `null` for an absent key. Numeric keys convert exactly to int64 and use their full decimal digits. |
+| `store_delete` | `store_delete of [handle, collection, key]` | Delete the record and return `true`, or `false` when absent. Numeric keys follow the same rule as `store_get`. |
+| `store_update` | `store_update of [handle, collection, key, record]` | Replace the record and return `true`, or `false` when absent. Numeric keys follow the same rule as `store_get`. An oversized or non-dict replacement leaves the old record intact. |
+| `store_query` | `store_query of [handle, collection]` | Return the collection's records as a list of dicts. |
+
+Store handles carry integer `_store_id` and `_store_gen` fields. Every store operation requires exact, in-range integer handle fields; integral floats convert, while fractional, non-finite or out-of-range numeric fields raise `value` before store access (R1.6f-D1). `store_count` returns an integer.
+
+`store_get`, `store_delete`, and `store_update` accept string keys or exact
+int64 numeric keys. Integral floats convert; fractional or out-of-range floats
+raise `value` before lookup or mutation. Numeric keys retain all int64 digits.
+Stored decimal integer text within int64 decodes as an int; a decimal point or
+exponent selects a float. Buffer payloads remain floats. Float persistence keeps
+its existing text format until R1.7, so a whole float such as `3.0` may read back
+as an int during this transition.
+
+An invalid, closed, or stale store handle raises `value`. For a complete
+example, see [stored records](SPEC.md#stored-records).
+
## Optional: Database Extension
Requires full build with libpq. PostgreSQL client.
@@ -803,9 +969,9 @@ Requires full build with libpq. PostgreSQL client.
| Name | Signature | Description |
|------|-----------|-------------|
| `db_connect` | `db_connect of null` | Connect via DATABASE_URL env var; returns a status JSON, never raises |
-| `db_query_value` | `db_query_value of sql` or `db_query_value of [sql, p1, p2]` | Execute query, return row 0 col 0 typed by its SQL type; `null` for SQL NULL, `""` for no rows |
+| `db_query_value` | `db_query_value of sql` or `db_query_value of [sql, p1, p2]` | Execute query, return row 0 col 0 typed by its SQL type: exact int64 for integer columns, float for finite real columns; `null` for SQL NULL, `""` for no rows |
| `db_execute` | `db_execute of sql` or `db_execute of [sql, p1, p2]` | Execute command with optional params; returns `"ok"` |
-| `db_query_json` | `db_query_json of sql` or `db_query_json of [sql, p1, p2]` | Execute query, return all rows as a JSON array of objects, each value typed by its SQL type |
+| `db_query_json` | `db_query_json of sql` or `db_query_json of [sql, p1, p2]` | Execute query, return all rows as a JSON array of objects, each value typed by its SQL type; integer columns retain exact int64 digits |
### Failures raise (#888)
@@ -848,7 +1014,7 @@ Values carry their column's SQL type rather than arriving as strings:
|---|---|---|
| NULL (any column type) | `null` | Distinct from `""` — checked before the type |
| `boolean` | `true` / `false` → `true` / `false` (bool) | `if row.is_admin:` means what it reads as |
-| `smallint`, `integer`, `bigint`, `oid` | number | `bigint` past 2^53 **raises** — see below |
+| `smallint`, `integer`, `bigint`, `oid` | int | Exact across the full SQL type's range, including `bigint` beyond 2^53 |
| `real`, `double precision` | number | `NaN`/`Infinity` arrive as strings; JSON has no literal for them |
| `numeric` | **string** | Deliberate — see below |
| everything else | string | text, date, uuid, json, … unchanged |
@@ -858,20 +1024,18 @@ row's value: a column that decoded as a number for row 1 and a string for
row 100 would break `row.n + 1` on data rather than on schema.
**`numeric` stays a string.** It is PostgreSQL's arbitrary-precision decimal
-— the money type — and an EigenScript number is a binary double, which
-cannot hold `numeric(38,10)` or even `0.1` exactly. Preserving the digits
+— the money type — and neither int64 nor binary float can hold every
+`numeric(38,10)` value exactly. Preserving the digits
is the safe default; `SELECT amount::float8` is the one-token opt-in to a
number when approximate is fine. Note `avg()` and `sum(numeric)` return
`numeric`, so those want the cast; `count(*)` and `sum(integer)` return
-`bigint` and are already numbers.
+`bigint` and arrive as ints.
-**A `bigint` past 2^53 raises** instead of silently rounding, naming the
-column and the fix:
-
-```
-Error query.eigs:3: db: column 'id' value 9007199254740993 exceeds the exact-integer
-range of a number (2^53); select it as text (id::text) to keep the digits
-```
+**Integer columns preserve every int64 digit.** `db_query_value` returns an
+int directly, and `db_query_json` writes the exact decimal digits so that
+`json_decode` returns an int. This includes both int64 boundaries and values
+such as `9007199254740993`; no cast to text is needed to preserve an integer
+key. Numeric parameters also bind ints as exact digits.
Before this, every value was a string: SQL `false` arrived as `"f"`, which
is a non-empty string and therefore **truthy**, so `if row.is_admin:` passed
@@ -887,9 +1051,9 @@ Requires full build. Transformer model inference and training.
| `eigen_model_load` | `eigen_model_load of "path.json"` | Load model weights from JSON |
| `eigen_model_loaded` | `eigen_model_loaded of null` | `true` if model loaded, `false` otherwise |
| `eigen_model_info` | `eigen_model_info of null` | JSON with model config and stats |
-| `eigen_generate` | `eigen_generate of [prompt, temp, max_tokens]` | Generate text from prompt. Every prompt token id must be in `[0, vocab_size)` or it raises `index_range`. Raises when the prompt exceeds the model's `max_seq_len`. |
-| `eigen_eval_loss` | `eigen_eval_loss of [prompt, target]` | Return the target token's cross-entropy loss. Every prompt and target token id must be in `[0, vocab_size)` or it raises `index_range`. Raises when the prompt exceeds the model's `max_seq_len`. |
-| `native_train_step_builtin` | `native_train_step_builtin of [input, output, lr]` | Single training step. Every input and output token id must be in `[0, vocab_size)` or it raises `index_range` before training. Raises when the combined input and output length exceeds the model's `max_seq_len`. |
+| `eigen_generate` | `eigen_generate of [prompt, temp, max_tokens]` | Token ids and max_tokens follow N5 then C int range; output token ids are ints. Generate text from prompt. Every prompt token id must be in `[0, vocab_size)` or it raises `index_range`. Raises when the prompt exceeds the model's `max_seq_len`. |
+| `eigen_eval_loss` | `eigen_eval_loss of [prompt, target]` | Token ids follow N5 then C int range; loss is float. Return the target token's cross-entropy loss. Every prompt and target token id must be in `[0, vocab_size)` or it raises `index_range`. Raises when the prompt exceeds the model's `max_seq_len`. |
+| `native_train_step_builtin` | `native_train_step_builtin of [input, output, lr]` | Token ids follow N5 then C int range; learning rate remains float. Single training step. Every input and output token id must be in `[0, vocab_size)` or it raises `index_range` before training. Raises when the combined input and output length exceeds the model's `max_seq_len`. |
| `model_save_weights` | `model_save_weights of "path.json"` | Save model weights to JSON |
| `model_load_weights` | `model_load_weights of "path.json"` | Load model weights (alias) |
@@ -898,23 +1062,23 @@ Requires full build. Transformer model inference and training.
| Name | Signature | Description |
|------|-----------|-------------|
| `spawn` | `spawn of fn` or `spawn of [fn, arg1, ...]` | Spawn a thread running `fn`. Bare-fn form passes no args; list form passes `arg1...` positionally. Missing trailing params bind to `null`. Extra args follow the direct-call rule (docs/SPEC.md, Functions): a callee of 2+ parameters raises `call passes 3 arguments but the callee takes 2` at the spawn site (`spawn of [two, 1, 2, 99]`), and a 1-parameter callee receives the whole list (`spawn of [one, 5, 6]` binds `[5, 6]`). Args are shared by reference (unlike channel sends, which copy) — see thread-safety note below. Returns a thread handle dict. |
-| `thread_join` | `thread_join of handle` | Block until the thread completes. Returns the thread function's copied return value, or re-raises a worker's uncaught error with the same kind and message. Catching that raise observes the worker failure, matching `task_join`. |
+| `thread_join` | `thread_join of handle` | Block until the thread completes. Returns the thread function's copied return value, or re-raises a worker's uncaught error with the same kind and message. Catching that raise observes the worker failure, matching `task_join`. Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
| `channel` | `channel of null` | Create a bounded FIFO channel (capacity 64). Returns a channel handle dict. |
-| `send` | `send of [channel, value]` | Send a value to the channel. Blocks if full. Sending to a **closed** channel raises a catchable `value` error (rather than silently dropping the value); `recv` on a closed empty channel returns `null` (EOF-like). |
-| `recv` | `recv of channel` | Receive a value from the channel. **Blocks** until a value is available or the channel is closed. |
-| `try_recv` | `try_recv of channel` | Non-blocking receive. Returns the value if available, `null` if the channel is empty. |
-| `recv_timeout` | `recv_timeout of [channel, ms]` | Bounded-wait receive. Returns the value if one arrives before `ms` milliseconds elapse, else `null`. A close while waiting also returns `null`. Fractional `ms` is honored (ns precision on Linux); negative `ms` degenerates to a `try_recv`. |
-| `close_channel` | `close_channel of channel` | Close the channel. Wakes all blocked senders/receivers. |
-| `channel_closed` | `channel_closed of channel` | Returns `true` if closed, `false` otherwise. An unknown or reclaimed channel is closed (`true`). A value that is not a channel handle raises by default; under `EIGS_STRICT=0` it answers `true` (#971). |
+| `send` | `send of [channel, value]` | Send a value to the channel. Blocks if full. Sending to a **closed** channel raises a catchable `value` error (rather than silently dropping the value); `recv` on a closed empty channel returns `null` (EOF-like). Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
+| `recv` | `recv of channel` | Receive a value from the channel. **Blocks** until a value is available or the channel is closed. Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
+| `try_recv` | `try_recv of channel` | Non-blocking receive. Returns the value if available, `null` if the channel is empty. Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
+| `recv_timeout` | `recv_timeout of [channel, ms]` | Bounded-wait receive. Returns the value if one arrives before `ms` milliseconds elapse, else `null`. A close while waiting also returns `null`. negative `ms` degenerates to a `try_recv`. The timeout rounds half away from zero to milliseconds (Q7 magnitude); values outside the C long range raise `value`. The previous one-year cap is removed. |
+| `close_channel` | `close_channel of channel` | Close the channel. Wakes all blocked senders/receivers. Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
+| `channel_closed` | `channel_closed of channel` | Returns `true` if closed, `false` otherwise. An unknown or reclaimed channel is closed (`true`). A value that is not a channel handle raises by default; under `EIGS_STRICT=0` it answers `true` (#971). Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
| `task_spawn` | `task_spawn of fn` or `task_spawn of [fn, arg1, ...]` | Create a cooperative task (#408) running `fn` on the single OS thread — deterministic by construction, unlike `spawn`'s OS thread. Args are deep-COPIED (share-nothing, like channel sends), not shared by reference. Returns a numeric task id. A ready task can run when the current task explicitly yields, suspends in a blocking scheduler operation, or finishes. |
-| `task_alive` | `task_alive of id` | Returns `true` while the task is runnable or suspended, `false` once it has finished (or for an unknown id). |
+| `task_alive` | `task_alive of id` | Returns `true` while the task is runnable or suspended, `false` once it has finished (or for an unknown id). Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
| `task_self` | `task_self of null` | The **running task's own id** (a number, in the same integer space `task_spawn` returns; the main task is 0, including before any task has been spawned). Lets a worker hand out its own id as a reply address — the message-link pattern a mailbox otherwise cannot express (#526). Deterministic — reads scheduler state, records no nondeterminism. |
| `task_yield` | `task_yield of null` | Cooperatively hand control to the next ready task; this task resumes round-robin. A no-op when no task has been spawned. Forbidden inside a nested evaluation (raises `value`). |
-| `task_join` | `task_join of id` | Block until task `id` finishes, then return its deep-copied result — or re-raise its uncaught error (as the same `{kind, message, line}` it died with). Joining an already-finished task returns immediately; an unknown id (or self) returns null. All tasks blocked with none runnable is a `deadlock` error, not a hang — catchable by a `try`/`catch` around the join on the main task (`e.kind == "deadlock"`); terminal only if unhandled. |
-| `task_send` | `task_send of [id, value]` | Append a deep-copied message to task `id`'s unbounded FIFO mailbox, waking it if it waits in `task_recv`. Returns `true` if delivered, `false` if `id` is finished/unknown (a silent drop — send-to-dead is never an error). Never blocks. |
+| `task_join` | `task_join of id` | Block until task `id` finishes, then return its deep-copied result — or re-raise its uncaught error (as the same `{kind, message, line}` it died with). Joining an already-finished task returns immediately; an unknown id (or self) returns null. All tasks blocked with none runnable is a `deadlock` error, not a hang — catchable by a `try`/`catch` around the join on the main task (`e.kind == "deadlock"`); terminal only if unhandled. Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
+| `task_send` | `task_send of [id, value]` | Append a deep-copied message to task `id`'s unbounded FIFO mailbox, waking it if it waits in `task_recv`. Returns `true` if delivered, `false` if `id` is finished/unknown (a silent drop — send-to-dead is never an error). Never blocks. Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
| `task_recv` | `task_recv of null` | Return the next message from this task's mailbox, or block cooperatively until one arrives. Forbidden inside a nested evaluation (raises `value`). |
-| `task_kill` | `task_kill of id` | Tear down task `id`: drop its mailbox, mark it dead, wake any joiner with an `interrupt` error. Returns `true` if killed, `false` for a finished/unknown/self target. |
-| `task_detach` | `task_detach of id` | Mark task `id` **fire-and-forget** (the pthread-detach precedent, #530): it is reaped the moment it finishes — or immediately if already finished — releasing its handle slot for reuse, so task-per-message workloads are bounded by *concurrent* tasks, not lifetime spawns. No join can ever see a detached task's failure, so its uncaught death prints its trace and **stops the program** (exit 1) at main's next scheduling point; detaching a task that already died of an uncaught error nothing joined stops the program at this call instead of discarding the error. Neither stop is catchable (#1736, [CONCURRENCY.md](CONCURRENCY.md#when-a-task-dies-of-an-uncaught-error-1736)). A reaped id reads as unknown afterwards (`task_join` null, `task_alive` `false`). A task may detach itself: `task_detach of (task_self of null)`. Returns 1, or 0 for main/unknown. |
+| `task_kill` | `task_kill of id` | Tear down task `id`: drop its mailbox, mark it dead, wake any joiner with an `interrupt` error. Returns `true` if killed, `false` for a finished/unknown/self target. Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
+| `task_detach` | `task_detach of id` | Mark task `id` **fire-and-forget** (the pthread-detach precedent, #530): it is reaped the moment it finishes — or immediately if already finished — releasing its handle slot for reuse, so task-per-message workloads are bounded by *concurrent* tasks, not lifetime spawns. No join can ever see a detached task's failure, so its uncaught death prints its trace and **stops the program** (exit 1) at main's next scheduling point; detaching a task that already died of an uncaught error nothing joined stops the program at this call instead of discarding the error. Neither stop is catchable (#1736, [CONCURRENCY.md](CONCURRENCY.md#when-a-task-dies-of-an-uncaught-error-1736)). A reaped id reads as unknown afterwards (`task_join` null, `task_alive` `false`). A task may detach itself: `task_detach of (task_self of null)`. Returns 1, or 0 for main/unknown. Handle ids and generations require exact integers (integral floats are accepted); fractions raise `value` before effects (N5). |
| `task_sleep` | `task_sleep of ticks` | Suspend this task until the **virtual clock** advances by `ticks`. The clock is logical (discrete-event): it only jumps forward — to the earliest sleeper — when nothing else is runnable, so sleeping stays deterministic, not wall-clock. A negative sleep is treated as 0. A no-op when no task has been spawned. |
| `task_sched_seed` | `task_sched_seed of n` | Install a scheduling **seed**: the scheduler switches from FIFO round-robin to picking the next ready task from a seeded, platform-independent PRNG. Same seed ⇒ same interleaving (byte-identical run + replay, zero tape nondeterminism); a different seed explores a different ordering — the lever a deterministic simulation tester uses to search interleavings. No seed ⇒ unchanged FIFO. Typically called once at program start. Returns null. |
@@ -931,8 +1095,8 @@ receiver.
| Name | Signature | Description |
|------|-----------|-------------|
-| `nearest_in_range` | `nearest_in_range of [entities, x, y, range, world_w, world_h]` | Position query: find the nearest active entity to `(x, y)` within `range` using torus (wrapping) distance. Because the query has no entity identity, no candidate is implicitly excluded. Returns `{"index", "dist", "dx", "dy"}` or `null`. Optional extra args: custom key names `[..., px_key, py_key, active_key]`. |
-| `nearest_in_range_all` | `nearest_in_range_all of [entities, range, world_w, world_h]` | For every entity `i`, return its nearest **other** active entity within `range` using torus distance, or `null`; entity `i` is never its own candidate. The result list has one entry per input entity. Optional trailing custom key names `[..., px_key, py_key, active_key]`. |
+| `nearest_in_range` | `nearest_in_range of [entities, x, y, range, world_w, world_h]` | Position query: find the nearest active entity to `(x, y)` within `range` using torus (wrapping) distance. Because the query has no entity identity, no candidate is implicitly excluded. Returns `{"index", "dist", "dx", "dy"}` or `null`. Optional extra args: custom key names `[..., px_key, py_key, active_key]`. The index field is an int; distance and displacements are floats. |
+| `nearest_in_range_all` | `nearest_in_range_all of [entities, range, world_w, world_h]` | For every entity `i`, return its nearest **other** active entity within `range` using torus distance, or `null`; entity `i` is never its own candidate. The result list has one entry per input entity. Optional trailing custom key names `[..., px_key, py_key, active_key]`. The index field is an int; distance and displacements are floats. |
The per-entity result and self-exclusion are executable here:
@@ -949,26 +1113,106 @@ print of [neighbors[0].index, neighbors[1].index, neighbors[2]]
| Name | Signature | Description |
|------|-----------|-------------|
-| `audio_open` | `audio_open of [freq, channels]` or `of null` | Open the mixer playback device. Defaults `[44100, 1]`. Returns the device id (`>= 2`), or `0` when SDL/audio is unavailable. Rates above **71,582,788 Hz** raise `value`: at every accepted rate, a generator's 30-second maximum has an `int`-sized sample count (#1821). Non-numeric `freq`/`channels` raise by default and answer `0` under `EIGS_STRICT=0` (#1007 — they used to be read without a type check, so a string opened the device against a garbage spec and still answered a real id, taking the device with it). A **short or non-list** argument also raises by default; with `EIGS_STRICT=0`, it opens at the defaults and returns the device id or `0` if unavailable (#1007). `of null` is still the defaults. |
-| `audio_sine` | `audio_sine of [freq, duration, amplitude]` | Generate a sine-wave sample list. `duration` is limited to 30 seconds; a longer duration raises `value`. |
-| `audio_square` | `audio_square of [freq, duration, amplitude]` | Generate a square-wave sample list. `duration` is limited to 30 seconds; a longer duration raises `value`. |
-| `audio_saw` | `audio_saw of [freq, duration, amplitude]` | Generate a sawtooth-wave sample list. `duration` is limited to 30 seconds; a longer duration raises `value`. |
-| `audio_noise` | `audio_noise of [duration, amplitude]` | Generate a white-noise sample list. The samples come from the stream behind `random`, so `seed_random` pins them, and they are trace-recorded: under `EIGS_REPLAY` the recorded samples are served (#1803). `duration` is limited to 30 seconds; a longer duration raises `value` and records nothing. |
-| `audio_sweep` | `audio_sweep of [freq_start, freq_end, duration, amplitude, waveform]` | Generate a frequency sweep with continuous phase. `waveform`: 0=sine, 1=sawtooth. Returns a sample list. `duration` is limited to 30 seconds; a longer duration raises `value`. |
-| `audio_play` | `audio_play of samples` | Play a clip once on a free mixer channel (oldest finite channel recycled when all 16 are busy). Returns the channel id, or `0` on bad args / closed device. A non-numeric element in `samples` raises a `type_mismatch` error (#1007 — it used to be coerced to 0, so a wrong-typed list played silence on a real channel id), and so does a `samples` that is not a list or buffer at all (#1007 — `audio_play of 42` answered the documented "nothing to play" `0`, indistinguishable from an empty clip). `of null` still plays nothing. |
-| `audio_play_loop` | `audio_play_loop of [samples, loops]` | Play `samples` `loops` times on one mixer channel; `loops == -1` loops forever (the mixer rewinds — no memory multiplication). Returns the channel id, or `0` on bad args / closed device. `loops` must be a number equal to `-1` or in `1..10000`; anything else raises by default and answers `0` under `EIGS_STRICT=0` (#1007), and so does a `samples` slot that is not a list or buffer. |
-| `audio_volume` | `audio_volume of [channel, vol]` | Live per-channel volume, `0.0`–`4.0`. Returns `1`, or `0` on a bad/inactive channel. A non-numeric `channel` or `vol` raises by default and answers `0` under `EIGS_STRICT=0` (#1007); an out-of-range channel is simply inactive and stays quiet. |
-| `audio_stop` | `audio_stop of channel` | Stop one mixer channel. Returns `1`, or `0` on a bad/inactive channel. A non-numeric `channel` raises by default and answers `0` under `EIGS_STRICT=0` (#1007). |
-| `audio_capture_open` | `audio_capture_open of [freq, channels]` | Open the recording (microphone) device and start capturing (#579). Defaults `[44100, 1]`; SDL converts to exactly the requested format. Returns the device id, or `0` when SDL/capture is unavailable. Non-numeric `freq`/`channels` raise by default and answer `0` under `EIGS_STRICT=0` (#1007), and so does a **short or non-list** argument, which used to open at the defaults and answer a real device id. `of null` is still the defaults. Re-opening closes the previous capture device. Trace-recorded — under `EIGS_REPLAY` no real device is opened. |
+| `audio_open` | `audio_open of [freq, channels]` or `of null` | Open the mixer playback device. Defaults `[44100, 1]`. `freq` follows N5 and the C int range. Rates above **71,582,788 Hz** raise `value`: at every accepted rate, a generator's 30-second maximum has an `int`-sized sample count (#1821). `channels` follows N5 and the domain `1..255` (R1.6e-D1). Integral floats are accepted; fractions and out-of-domain values raise `value` before device access or trace recording. Device ids and the unavailable result are ints. Returns the device id (`>= 2`), or `0` when SDL/audio is unavailable. Non-numeric `freq`/`channels` raise by default and answer `0` under `EIGS_STRICT=0` (#1007 — they used to be read without a type check, so a string opened the device against a garbage spec and still answered a real id, taking the device with it). A **short or non-list** argument also raises by default; with `EIGS_STRICT=0`, it opens at the defaults and returns the device id or `0` if unavailable (#1007). `of null` is still the defaults. |
+| `audio_pause` | `audio_pause of flag` or `of null` | Pause (`1`) or unpause (`0`) the mixer device. The flag follows N5 and the C int range; an integral float is accepted, while a fraction or unrepresentable value raises `value` even when no device is open. `null` defaults to pause. Returns null. |
+| `audio_queue_size` | `audio_queue_size of null` | Return the exact int count of unplayed bytes across finite mixer channels; channels looping forever are excluded. Returns int `0` when no device is open. |
+| `audio_music_play` | `audio_music_play of [path, loops]` | Play a music file through SDL_mixer. The optional loop count follows N5 and the C int range; `-1` loops forever, `0` plays once, and `N` plays `N+1` times. A fraction or unrepresentable loop count raises `value` before loading the file. Returns a bool indicating success. |
+| `audio_mix` | `audio_mix of [samples_a, samples_b]` | Add two lists of finite numeric samples, padding the shorter list with zeros, and clamp to `[-1, 1]`. Returns float samples; int inputs convert to float. |
+| `audio_gain` | `audio_gain of [samples, volume]` | Multiply a list of finite numeric samples by a finite numeric gain and clamp to `[-1, 1]`. Returns float samples; int inputs convert to float. |
+| `audio_sine` | `audio_sine of [freq, duration, amplitude]` | Generate a sine-wave list of float samples. Duration is limited to 30 seconds; a longer duration raises `value`. |
+| `audio_square` | `audio_square of [freq, duration, amplitude]` | Generate a square-wave list of float samples. Duration is limited to 30 seconds; a longer duration raises `value`. |
+| `audio_saw` | `audio_saw of [freq, duration, amplitude]` | Generate a sawtooth-wave list of float samples. Duration is limited to 30 seconds; a longer duration raises `value`. |
+| `audio_noise` | `audio_noise of [duration, amplitude]` | Generate a white-noise list of float samples. `seed_random` pins the samples; trace replay serves the recorded samples (#1803). Duration is limited to 30 seconds; a longer duration raises `value` and records nothing. |
+| `audio_sweep` | `audio_sweep of [freq_start, freq_end, duration, amplitude, waveform]` | Generate a frequency sweep as float samples with continuous phase. The waveform selector follows N5: zero selects sine; any other exact integer selects sawtooth. Duration is limited to 30 seconds; a longer duration raises `value`. |
+| `audio_envelope` | `audio_envelope of [samples, attack, decay, sustain_level, release]` | Return float samples with an ADSR envelope. Attack, decay and release times are continuous seconds: their sample counts round half away from zero and raise `value` outside the C int range. |
+| `audio_play` | `audio_play of samples` | Play a clip once on a free mixer channel (oldest finite channel recycled when all 16 are busy). Returns the channel id, or `0` on bad args / closed device. A non-numeric element in `samples` raises a `type_mismatch` error (#1007 — it used to be coerced to 0, so a wrong-typed list played silence on a real channel id), and so does a `samples` that is not a list or buffer at all (#1007 — `audio_play of 42` answered the documented "nothing to play" `0`, indistinguishable from an empty clip). `of null` still plays nothing. Numeric channel answers are ints. Samples cross the finite data boundary, clamp to [-1,1], then round half away from zero to PCM16; stored lenient NaN and every infinity raise `value` (N13). |
+| `audio_play_loop` | `audio_play_loop of [samples, loops]` | Play `samples` `loops` times on one mixer channel; `loops == -1` loops forever (the mixer rewinds — no memory multiplication). Returns the channel id, or `0` on bad args / closed device. `loops` must be a number equal to `-1` or in `1..10000`; anything else raises by default and answers `0` under `EIGS_STRICT=0` (#1007), and so does a `samples` slot that is not a list or buffer. Loop counts follow N5, then the C int range and the existing -1 or 1..10000 playback policy. Channel answers are ints; samples use the same finite/Q7 PCM conversion as `audio_play`. |
+| `audio_volume` | `audio_volume of [channel, vol]` | Live per-channel volume, `0.0`–`4.0`. Returns `1`, or `0` on a bad/inactive channel. A non-numeric `channel` or `vol` raises by default and answers `0` under `EIGS_STRICT=0` (#1007); an out-of-range channel is simply inactive and stays quiet. Channel numbers follow N5 and the C int range. Continuous gain clamps to [0,4], then rounds half away from zero; numeric status answers are ints. |
+| `audio_stop` | `audio_stop of channel` | Stop one mixer channel. Returns int `1`, or int `0` on an inactive channel. `channel` follows N5 and the C int range: integral floats are accepted; a fraction or unrepresentable value raises `value` before the device is consulted. A non-numeric `channel` raises by default and answers int `0` under `EIGS_STRICT=0` (#1007). |
+| `audio_capture_open` | `audio_capture_open of [freq, channels]` | Open the recording (microphone) device and start capturing (#579). Defaults `[44100, 1]`; SDL converts to exactly the requested format. `freq` follows N5 and the C int range; `channels` follows N5 and the domain `1..255` (R1.6e-D1). Integral floats are accepted; fractions and out-of-domain values raise `value` before device access or trace recording. Device ids and the unavailable result are ints. Returns the device id, or `0` when SDL/capture is unavailable. Non-numeric `freq`/`channels` raise by default and answer `0` under `EIGS_STRICT=0` (#1007). A **short or non-list** argument raises by default; with `EIGS_STRICT=0`, it opens at the defaults and returns the device id or `0` if unavailable (#1007). `of null` is still the defaults. Re-opening closes the previous capture device. Trace-recorded — under `EIGS_REPLAY` no real device is opened. |
| `audio_capture_read` | `audio_capture_read of null` | Drain samples accumulated since the last read as a **buffer** of floats in `[-1, 1]` (interleaved when `channels > 1`). At most 2048 samples per call — loop until the returned buffer is empty to drain fully (keeps each trace record replayable). Empty buffer = nothing new yet; `null` = no capture device open. Trace-recorded — replay serves the recorded samples, never a live microphone. |
| `audio_capture_close` | `audio_capture_close of null` | Stop and close the recording device, dropping undrained samples. Safe to call twice or with no device open. |
-| `audio_stream_open` | `audio_stream_open of [freq, channels]` | Open the live streaming playback device (queue mode, F-DS-17 — for on-the-fly synthesis like musical typing). Coexists with the `audio_open` mixer device. Defaults `[44100, 1]`. Returns the device id (`>= 2`), or `0` when SDL/audio is unavailable. Non-numeric `freq`/`channels` raise by default and answer `0` under `EIGS_STRICT=0` (#1007 — they used to skip the override, open at 44100/1 and answer a real id, so a caller that asked for 48000 was told it got it). A **short or non-list** argument raises for the same reason: `audio_stream_open of [48000]` answered device id 2 opened at 44100/1. `of null` is still the defaults. Re-opening closes the previous stream device. |
-| `audio_stream_push` | `audio_stream_push of samples` | Queue a block of float samples `[-1, 1]` (**list** or **buffer**) onto the live stream. Same size cap / clamp as `audio_play`. Pure output sink (not trace-recorded). Returns `true` on success, `false` on a closed device or bad shape; a `samples` that is not a list or buffer raises by default and answers `false` under `EIGS_STRICT=0` (#1007). |
-| `audio_stream_queued` | `audio_stream_queued of null` | Samples still buffered (not yet played) on the live stream — the refill pump pushes another block only while this stays under its latency target. Returns `0` when no stream is open. Trace-recorded (a live, timing-dependent value) — replay serves the recorded depth, keeping the session deterministic. |
+| `audio_stream_open` | `audio_stream_open of [freq, channels]` | Open the live streaming playback device (queue mode, F-DS-17 — for on-the-fly synthesis like musical typing). Coexists with the `audio_open` mixer device. Defaults `[44100, 1]`. `freq` follows N5 and the C int range; `channels` follows N5 and the domain `1..255` (R1.6e-D1). Integral floats are accepted; fractions and out-of-domain values raise `value` before device access or trace recording. Device ids and the unavailable result are ints. Returns the device id (`>= 2`), or `0` when SDL/audio is unavailable. Non-numeric `freq`/`channels` raise by default and answer `0` under `EIGS_STRICT=0` (#1007 — they used to skip the override, open at 44100/1 and answer a real id, so a caller that asked for 48000 was told it got it). A **short or non-list** argument raises for the same reason: `audio_stream_open of [48000]` answered device id 2 opened at 44100/1. `of null` is still the defaults. Re-opening closes the previous stream device. |
+| `audio_stream_push` | `audio_stream_push of samples` | Queue a block of float samples `[-1, 1]` (**list** or **buffer**) onto the live stream. Same size cap / clamp as `audio_play`. Pure output sink (not trace-recorded). Returns `true` on success, `false` on a closed device or bad shape; a `samples` that is not a list or buffer raises by default and answers `false` under `EIGS_STRICT=0` (#1007). Buffer samples follow the finite data boundary (N13); PCM clamps to [-1,1] and rounds half away from zero. |
+| `audio_stream_queued` | `audio_stream_queued of null` | Int sample count still buffered (not yet played) on the live stream — the refill pump pushes another block only while this stays under its latency target. Returns int `0` when no stream is open. Trace-recorded (a live, timing-dependent value) — replay serves the recorded depth, keeping the session deterministic. |
| `audio_stream_clear` | `audio_stream_clear of null` | Drop any buffered audio on the live stream (flush for a panic / all-notes-off). Safe with no device. |
| `audio_stream_close` | `audio_stream_close of null` | Stop and close the live stream device, dropping buffered audio. Safe to call twice or with no device open. |
+The sine, saw, square, noise and sweep generators reject non-finite durations
+and rounded sample counts outside the C int range. Their validated counts
+still truncate: at 44100 Hz, 0.00101 seconds produces 44 samples. The sample
+count rounding change is staged for R2.6 (RULINGS B5).
+
## Internal (sanitizer builds only)
| Name | Signature | Description |
|------|-----------|-------------|
+
+Buffer sizes, indices, offsets and counts follow N5. Byte inputs to
+`str_from_bytes`, `f64_from_bytes`, `inflate` and `buf_from_pcm16le` must be
+exact integers in 0..255; fractions and out-of-range bytes raise `value`.
+`buf_len`, `f64_to_bytes` and `inflate` return integer counts or bytes;
+buffer storage and scalar buffer reads are floats.
+
+Buffer construction and `reshape` check dimensions before narrowing to C integers.
+`buf_copy`, `buf_fill`, `buf_mix`, `buf_scale_range`, `buf_peak`, `buf_dot`,
+`buf_deinterleave` and `buf_resample_linear` use exact N5 windows/counts.
+`buf_set`, `buf_fill` and `buf_from_list` convert stored integers to float (Q2).
+`buf_to_pcm16le` retains its clamp and half-away-from-zero rounding.
+
+Model token ids and generation limits follow N5 and the C int range;
+fractional ids or token counts raise `value`. `eigen_generate` returns int
+token ids, while `eigen_eval_loss` returns a float. Temperature, top_p and
+training learning rates remain float quantities. Model JSON weights stay
+float data; the loader status strings are unchanged.
+
+`mean`, `norm`, `buf_get`, `buf_peak`, `buf_dot` and byte/PCM codec input
+readers refuse non-finite buffer cells with `value` in both strict modes (N13).
+
+`gfx_fb` palette values and `ppu_render_frame` memory bytes use the Q1 byte
+reader: exact integers in 0..255. Fractions and out-of-range values raise
+`value`; a stored NaN or infinity raises at the data boundary in both strict
+and lenient modes, without normalising the stored payload again.
+
+### Stored buffer data boundary
+
+Buffer kernels can retain non-finite data. Scalar indexing and iteration
+raise `value` when that data is read, in every strict mode (N13).
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+try:
+ print of p[0]
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Buffer equality
+
+Buffers compare their stored doubles with IEEE equality: NaN differs from
+everything, even itself; infinities compare by sign; signed zeros are equal.
+Equality never raises or changes math flags, including under strict mode.
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+print of (p == p)
+print of (p == g)
+sgd_update of [p, p, 1.0]
+print of (p == p)
+```
+```output
+true
+false
+false
+```
+
+N14 record and SQL writers use the same N6 float text and exact int64 digits.
+The executed cases are `tests/phase2/rows/N14.store-exact-digits.sh` and
+`tests/test_db_params.c` (#1762).
diff --git a/docs/CI.md b/docs/CI.md
index d72b092ca..80ab840c2 100644
--- a/docs/CI.md
+++ b/docs/CI.md
@@ -747,6 +747,7 @@ file:
| `Scorecard analysis` (`scorecard.yml`) | advisory | runs on push, schedule and `branch_protection_rule`, never on a PR; a posture score |
| `Analyze (python)`, `Analyze (javascript-typescript)` | advisory | CodeQL *default setup* (a GitHub app, not a workflow file) over the repo's non-C code; the C analysis that blocks is `Analyze C` |
| `issue-triage / …` (`issue-triage.yml`), `release.yml` jobs | advisory | not triggered by PRs or pushes to `main` |
+| `phase2 (MAP rows + lib differential)` (`phase2.yml`) | advisory | #1758's integration branch only: a push to `phase2-numbers` or `workflow_dispatch`, never a PR or the queue; `tools/phase2_ci.sh` checks the MAP rows of every landed round and the stdlib int differential |
## Coverage on pull requests
@@ -769,8 +770,10 @@ result pins; changing it or the driver selects every row.
The formatter suite uses `eigenscript --dump-tokens FILE` as its safe-mode
oracle. The command performs no execution and writes one tab-separated row
-per lexer token: numeric token type, value (strings/identifiers as hexadecimal
-bytes and numbers as hexadecimal floating point), and one-based source line.
+per lexer token: numeric token type, value, and one-based source line.
+Strings and identifiers use hexadecimal bytes. Numeric values carry their
+kind: `i` followed by exact signed decimal digits for an int, or `f` followed
+by C hexadecimal floating-point text for a float.
Columns are deliberately absent, while `INDENT` and `DEDENT` remain tokens;
this makes horizontal whitespace and comment-column changes immaterial but
detects changes to literal bytes, physical lines, or block structure. Lexer
@@ -804,7 +807,9 @@ need no suite run — plus changed-gate self-tests, exit 1 on any failure; the d
joins when a binary exists. Its gate list is the only copy: the
`gate-selftests` CI job runs the same script. Among them,
`tools/enrolment_check.sh` (also suite `[99ab]`) fails when a `tests/*.sh` or
-`tests/*.py` is invoked by no suite section, workflow step or enrolled script;
-exceptions live in `tests/enrolment_exemptions.txt` with a reason each.
+`tests/*.py` is invoked by no suite section, workflow step or enrolled script,
+and when a `tools/` entry point (a top-level `tools/*.sh` or `tools/*.py`, or a
+`tools//` package) is reached from no workflow step that way; exceptions
+live in `tests/enrolment_exemptions.txt` with a reason each.
Counts that grow with the suite (child-script sites, doc-claim populations)
are floors: adding a test needs no count edit, a drop is red.
diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md
index 5fd50bc79..aa861f137 100644
--- a/docs/COMPARISON.md
+++ b/docs/COMPARISON.md
@@ -1,5 +1,21 @@
# EigenScript compared to Python, JavaScript, Rust, and Lisp
+
+Width arithmetic uses explicit wrapping, checked and saturating builtins.
+N5 converts integral floats, then checks the named type range without masking.
+Width results are ints; checked overflow raises `value`. `u64` uses int64 bit
+patterns. `fround` returns a float after double-to-float32-to-double conversion
+(ints convert to double first). See [the width builtins](BUILTINS.md#width-arithmetic-n9).
+
+```eigenscript
+print of [i64_wrapping_add of [9223372036854775807, 1], u8_wrapping_add of [255, 1]]
+print of (fround of 0.1)
+```
+```output
+[-9223372036854775808, 0]
+0.10000000149011612
+```
+
A translation guide: each section shows the same program in a language
you already know, then in EigenScript. Every EigenScript block followed
by an `output` block is executed by the test suite
@@ -15,6 +31,44 @@ fundamental way: **every assignment is observed**, and the language has
first-class constructs (`converged`, `what is x`, `prev of x`) for
asking the running program about its own state and history.
+
+Bitwise operators and `bit_*` builtins return ints, including when an
+operand is an integral float. They use Lua 5.4's 64-bit shift rules: `>>`
+zero-fills; a negative count shifts in the opposite direction; a count of
+64 or more, or -64 or less, gives zero. Each operand must be an exact int64;
+a fractional or out-of-range float raises `value`.
+
+```eigenscript
+print of [1 << 64, 1 << -1, -1 >> 1, 8 >> -1, 1 << 63]
+print of [bit_shr of [-1, 1], bit_shl of [8, -1], num_type of (3.0 & 1)]
+```
+```output
+[0, 0, 9223372036854775807, 16, -9223372036854775808]
+[9223372036854775807, 4, "int"]
+```
+
+`//` floors the quotient; `%` has the divisor's sign. `//=` applies the same
+operation to its assignment target, like `/=`. Zero divisors raise `value`
+for int, float and mixed operands. Two ints produce an int for
+`//` and `%`; a float operand produces a float. `/` always produces a float.
+Float and mixed `//` use Python’s `divmod` quotient correction: `1.0 // 0.1`
+is `9.0`, and `1e-300 // -1e300` is `-1.0`. `//=` uses the same rule.
+
+```eigenscript
+x is -7
+x //= 2
+print of [x, 7 // -2, -7 % 3, 7 % -3, -7.5 % 2]
+print of [num_type of (7 // 2), num_type of (7.0 // 2)]
+y is 1.0
+y //= 0.1
+print of [1.0 // 0.1, 1 // 0.1, 1e-300 // -1e300, y, num_type of y]
+```
+```output
+[-4, -4, 2, -2, 0.5]
+["int", "float"]
+[9.0, 9.0, -1.0, 9.0, "float"]
+```
+
Like indexed nearest-neighbour APIs in Python's scientific ecosystem,
EigenScript's batched `nearest_in_range_all` does not count each query entity
as its own neighbour. Its scalar `nearest_in_range` takes an explicit position,
@@ -25,7 +79,7 @@ so it has no query-entity identity to exclude.
| | Python | JavaScript | Rust | Lisp | EigenScript |
|---|---|---|---|---|---|
| typing | dynamic | dynamic | static | dynamic | dynamic |
-| numbers | int + float | float | many | many | one: 64-bit float |
+| numbers | int + float | float | many | many | int64 + float64 |
| assignment | `x = 1` | `let x = 1` | `let x = 1;` | `(define x 1)` | `x is 1` |
| call | `f(x)` | `f(x)` | `f(x)` | `(f x)` | `f of x` |
| blocks | indentation | braces | braces | parens | indentation |
@@ -50,6 +104,54 @@ missing, unreadable, and directory paths. Empty files return empty values.
`vm_run_bytecode` raises a catchable `value` error naming a rejected chunk descriptor; a valid program may still return `null`. `sandbox_run` reports descriptor rejection in its structured `{ok: false, error: ...}` result. Its optional fourth limit, `max_work`, bounds cumulative bytecode instructions across function calls and callback re-entry (default 10,000,000), independently of loop and allocation limits. This meters VM work, not elapsed time: a blocking native callback must return before the sandbox can stop.
+The positions and counts of `char_at`, `substr`, `get_at`, `set_at`,
+`copy_into`, `fill` and the `list_*` mutation/slice builtins use exact conversion
+(N5). So do handle fields, bytecode ABI/counts/bytes, sandbox limits and numeric
+exit codes. Integral floats are accepted; fractions and values outside the
+receiving C type raise `value` before indexing or effects.
+`recv_timeout` rounds milliseconds half away from zero and raises beyond the
+C long range, with no one-year cap. `usleep` rounds microseconds and raises
+beyond the C int range. `record_history` returns its previous flag as an int;
+nonzero numeric flags still enable history. `task_detach` and sandbox error lines return ints.
+`read_bytes` produces int bytes; `write_bytes` requires exact bytes in 0..255
+and returns an int count. File caps and corpus/stream counts follow N5; an
+integral stream count outside its cap still raises `limit`. Subprocess exit
+statuses are ints, and timeout seconds round to representable poll milliseconds.
+
+```eigenscript
+print of (substr of ["hello", 1.0, 2.0])
+try:
+ print of (substr of ["hello", 1.5, 2])
+catch e:
+ print of e.kind
+print of (num_type of (record_history of 0.5))
+print of (usleep of 1.5)
+try:
+ usleep of 1e300
+catch e:
+ print of e.kind
+path is mktemp of null
+written is write_bytes of [path, [65.0, 66.0]]
+bytes is read_bytes of path
+print of [num_type of written, num_type of bytes[0], str_from_bytes of bytes]
+try:
+ write_bytes of [path, [65.5]]
+catch e:
+ print of e.kind
+remove_file of path
+```
+```output
+el
+value
+int
+null
+value
+["int", "int", "AB"]
+value
+```
+
+Runtime-published counts and indices are exact ints (S9-K1): `__loop_iterations__`, live and historical `when` assignment counts, aggregate `what` lengths, trajectory `window`, error `line`, and scheduler trace `seq` and task ids. Scheduler `tick` remains a continuous virtual time.
+
Sandbox execution does not update shared temporal history: assignment values, names, counts and observer snapshots stay outside that history even when recording is armed. Ordinary tape assignment records still emit. Host and trusted descriptor history recording resumes normally outside the sandbox; sandbox temporal reads remain refused.
`lib/eigen.eigs` snapshots the host values used by its tokenizer, parser, evaluator, import helpers, and fresh meta environments when it loads (#1386). Later host builtin rebinding does not change those dependencies, including entropy's `log`/`divide` calls. For `load_file`, the snapshots use values visible during initialization; loaded code still shares the host scope. String conversion still uses the pristine reserved f-string bridge. Explicit custom environments, debug hooks, and rebinding the interpreter's own helper names remain caller-controlled; captured caller-defined functions retain their own binding behavior.
@@ -75,14 +177,20 @@ print(7 / 2) # 3.5
print(7 // 2) # 3 (floor division operator)
```
-EigenScript — one number type, true division; floor explicitly:
+EigenScript — signed int64 and finite float64 share the `num` type;
+`num_type of` distinguishes their kind. Decimal integer literals fitting
+int64 are exact; a point, exponent or out-of-range decimal literal gives
+a float. Hex integer literals wrap modulo 2^64. Int `+ - *` raise `value`
+on overflow; unary minus and `abs` raise at INT64_MIN. Mixed arithmetic
+converts ints to floats, while mixed-kind comparison is exact, including
+beyond 2^53. `/` returns a float; `//` floors the quotient:
```eigenscript
x is 42
x += 1
print of x
print of (7 / 2)
-print of (floor of (7 / 2))
+print of (7 // 2)
```
```output
43
@@ -90,6 +198,117 @@ print of (floor of (7 / 2))
3
```
+
+Empty `min`/`max` raises `value` in both call forms (R1.6b-D2); empty
+`sum` remains int zero (C2). `sign_extend` reads exact int64 arguments and
+raises `value` if its existing subtraction underflows (R1.6b-D1).
+
+Counts and indices are ints. `floor`, `ceil` and half-away-from-zero `round`
+return an int when the result fits int64. `min`/`max` return the winning
+argument with its kind; the first argument wins a tie. `num` requires a
+complete numeric-literal string (N10). JSON integer text fitting int64
+decodes exactly (N14).
+
+The [C embedding API](EMBEDDING.md#value-handles) follows Lua’s numeric
+accessors: `eigs_value_new_int` constructs an exact int64, `eigs_value_is_int`
+tests the stored kind (`lua_isinteger`), and `eigs_value_as_int` reports
+whether a number converts exactly to int64 (`lua_tointegerx`).
+`eigs_value_as_num` converts either kind to double (`lua_tonumber`);
+`EIGS_TYPE_NUM` covers both.
+
+LSP hover on a numeric literal reports `int` or `float` from the literal’s
+kind: `3.0` remains a float even though its value is integral.
+
+```eigenscript
+print of [type of 3, num_type of 3, num_type of 3.0]
+print of [9007199254740993, 3.0, -0.0]
+print of [num_type of (len of "abc"), num_type of (floor of -2.5)]
+print of [num of "9007199254740993", num_type of (num of "3.0")]
+print of [num_type of (min of [3, 3.0]), num_type of (min of [3.0, 3])]
+print of (json_path of ["{\"n\":9223372036854775807}", "n"])
+try:
+ min of []
+catch e:
+ print of [e.kind, (index_of of [e.message, "min"]) >= 0]
+try:
+ print of (num of "12abc")
+catch e:
+ print of e.kind
+```
+```output
+["num", "int", "float"]
+[9007199254740993, 3.0, -0.0]
+["int", "int"]
+[9007199254740993, "float"]
+["int", "float"]
+9223372036854775807
+["value", true]
+value
+```
+
+`add`, `subtract` and `multiply` preserve exact ints for scalar operands and
+for each pair of list elements, with `value` on int64 overflow. `negative`
+preserves int scalar and list elements and raises at INT64_MIN. Buffer
+arithmetic remains float. A buffer kernel stops at its first raised result,
+releases its partial output and does not read later elements (#1417).
+An all-int `sum` returns an exact int, while a
+float anywhere in a nested input selects float addition for the entire
+reduction. A buffer selects float addition too. An empty list's sum is int
+zero, in both call forms. `sum` refuses non-finite buffer elements with a
+`value` error in every strict mode (N13). In strict mode, it stops at the first NaN with a "not a number" diagnostic and sets `math_flags.invalid`; later elements are not read (#1417).
+
+```eigenscript
+exact is sum of [9007199254740993, 1]
+print of [exact, num_type of exact]
+mixed is sum of [9223372036854775807, 1, 0.5]
+print of [num_type of mixed, mixed == 9223372036854775808.0]
+print of [num_type of (sum of []), num_type of (sum of ([]))]
+print of (add of [[9007199254740993, 2], [1, 3]])
+try:
+ negative of (-9223372036854775807 - 1)
+catch e:
+ print of e.kind
+```
+```output
+[9007199254740994, "int"]
+["float", true]
+["int", "int"]
+[9007199254740994, 5]
+value
+```
+
+PostgreSQL integer columns (`smallint`, `integer`, `bigint`, `oid`) return
+exact int64 values through `db_query_value`; `db_query_json` retains their
+exact decimal digits (N14, Q-DB253). A `bigint` beyond 2^53 is an int,
+including both int64 boundaries. `numeric` remains text, and finite real
+columns returned directly remain floats. Float parameters use N6 shortest round-trip text, preserving `.0` and
+signed zero. Store records use the same float writer and exact int64 digits
+(N14, #1762). The executable DB examples and boundary assertions
+in `tests/test_db.eigs` run on the server-db profile with PostgreSQL.
+
+TCP handles, ports and sent-byte counts are integers. Numeric socket handles, ports, receive counts and outgoing bytes require exact integers (integral floats convert); bytes must be in 0..255. `net_send` validates the entire payload before sending any byte. Poll timeouts are continuous milliseconds rounded to the C int range; an unrepresentable duration raises `value`.
+
+`http_early_bind` and `http_serve` require an exact integer port in 0..65535; integral floats convert and port zero requests an ephemeral port. Fractional or out-of-range ports raise `value` before binding. `shared_delete` and `shared_size` return integer counts.
+
+N6 prints each float with Python's shortest round-trip spelling: integral
+floats retain `.0`, negative zero is `-0.0`, and scientific notation starts
+below `1e-4` or at `1e16`. Ints keep their exact decimal digits. `print`,
+`write`, `str`, f-strings and JSON share the number writer. The executed
+examples are `tests/phase2/rows/N6.float-repr.eigs` and
+`tests/phase2/rows/N6.str-fstring.eigs`.
+
+The HTTP shared counter `shared_incr` follows numeric `+`: two int operands
+return an exact int and int64 overflow raises `value` without changing the
+stored counter (A). A missing counter starts at int zero. A float operand
+produces a float. The stored operand is its JSON-decoded value; N6 preserves an
+integer-valued float as a float by retaining its `.0`. Executable shared-counter examples in
+`tests/test_http.eigs` cover exact values, both overflow directions and the
+next operation after a raise.
+
+`http_post` also preserves every digit of JSON integer header values within
+int64 before sending them as text (N14, B6). The executed loopback example
+is `tests/phase2/builtins/builtins/http_post.eigs`.
+
## Functions
Python:
@@ -171,6 +390,9 @@ print of (c of null)
2
```
+Builtin `sort` sorts a list in place; `sort_by` returns a new list using numeric keys.
+Both are stable on every platform: equal elements or keys keep their input order.
+
## Lists: map / filter / comprehension
Python:
@@ -253,13 +475,18 @@ the rest accept either, and hand back a buffer when every operand was one.
before adding it to the accumulator, in ascending inner-index order, on both
containers. They do not fuse the multiplication and addition.
Raw non-finite results may remain in a buffer's internal work area, but every
-scalar read applies EigenScript's numeric guard: infinity saturates at
-±`1e308`, and a NaN raises in strict mode (or becomes `0` and sets the invalid
-math flag under `EIGS_STRICT=0`). Structural buffer equality and scalar
-reductions normalize each input before comparison or arithmetic; the reduction
+ordinary indexed read and iteration step refuse stored NaN or infinity with
+`value` in every strict mode, matching `get_at` (N13). Strict NaN retains
+its first-NaN diagnostic and invalid flag. All execution tiers report the
+same error kind, message and source line.
+Structural buffer equality compares stored doubles with IEEE equality, without
+raises or math flags; NaN differs from everything, including itself, infinities
+compare by sign, and signed zeros are equal. The remaining scalar reductions
+retain their declared data boundaries; the reduction
association contracts remain unchanged. Mixed buffer/list tensor operations and
numeric byte/sample/device conversions also normalize reads and stop on the
-first error. Byte conversion truncates and wraps modulo 256 after normalization.
+first error. `write_bytes` requires exact bytes in 0..255 (N5/Q1); other byte
+converters retain truncation and wrapping at this stage.
Raw copies, typed serialization and buffer-only work areas preserve their data.
`type_mismatch` error by default; `EIGS_STRICT=0` retains numeric-zero
substitution. `str_from_bytes` stops at numeric NUL and does not inspect later
@@ -298,6 +525,19 @@ Ada
3
```
+Store handles carry integer `_store_id` and `_store_gen` fields. Every store operation requires exact, in-range integer handle fields; integral floats convert, while fractional, non-finite or out-of-range numeric fields raise `value` before store access (R1.6f-D1). `store_count` returns an integer.
+
+`store_get`, `store_delete`, and `store_update` accept string keys or exact
+int64 numeric keys. Integral floats convert; fractional or out-of-range floats
+raise `value` before lookup or mutation. Numeric keys retain all int64 digits.
+Stored decimal integer text within int64 decodes as an int; a decimal point or
+exponent selects a float. Buffer payloads remain floats. Store records preserve
+int64 digits and use shortest round-trip float text, including `.0` for
+integral floats and the sign of `-0.0`, so `3.0` reads back as a float.
+
+See the [stored-record example](SPEC.md#stored-records) for an exact key above
+2^53 and a fractional-key refusal.
+
## Pattern matching
Rust:
@@ -513,6 +753,17 @@ deviations are in issue #1990.
## What has no equivalent elsewhere: the observer
+For non-numeric bindings, `converged` and ordinarily `stable` compare
+bounded, detached assignment snapshots; finite in-place mutations are visible
+on reassignment. A refused or oversized snapshot leaves both predicates false.
+An accepted snapshot containing a NaN buffer is unequal to itself under raw
+IEEE comparison, so it cannot converge; its `stable` predicate instead uses
+the entropy-window rule after a full bounded comparison window (S10-D1).
+Refusal clears that eligibility even if the entropy window is already complete.
+The matmul scenario's equality / stability /
+convergence remains `false` / `true` / `false`, with no equality-driven data
+or math-flag changes. The other predicates and non-numeric `report` use entropy.
+
Every assignment (outside `unobserved` blocks) updates an observer
tracking the value's entropy and trend (for containers, one pass over the
value's own elements — the walk stops at a reference, so a nested container
@@ -777,3 +1028,88 @@ Routes and the static root are fixed once `http_serve` starts: `http_route`,
`http_route_authed` and `http_static` then raise, including from a `code`
route, and an uncaught error in a `code` route's source answers 500 with a
generic body; the error message goes to the server's stderr only (#1140).
+
+Tensor sizes and indices follow N5: an integral float is accepted and a
+fraction raises `value`. `gather` keeps the selected list cell's int/float
+kind; buffer elements and numeric tensor calculations are floats. `shape`
+returns integer dimension counts. `mean` and `norm` refuse non-finite buffer
+cells with `value` in both strict modes.
+
+```eigenscript
+g is gather of [[[1, 2], [3, 4]], [1.0, 0]]
+print of [g, num_type of g[0], num_type of (shape of g)[0]]
+try:
+ gather of [[10, 20], 0.5]
+catch e:
+ print of e.kind
+```
+```output
+[[2, 3], "int", "int"]
+value
+```
+
+Buffer sizes, indices, offsets and counts follow N5. Byte inputs to
+`str_from_bytes`, `f64_from_bytes`, `inflate` and `buf_from_pcm16le` must be
+exact integers in 0..255; fractions and out-of-range bytes raise `value`.
+`buf_len`, `f64_to_bytes` and `inflate` return integer counts or bytes;
+buffer storage and scalar buffer reads are floats. `buf_get`, `buf_peak`,
+`buf_dot` and the byte/PCM codecs refuse non-finite input cells with `value`.
+
+Model token ids and generation limits follow N5 and the C int range;
+fractional ids or token counts raise `value`. `eigen_generate` returns int
+token ids, while `eigen_eval_loss` returns a float. Temperature, top_p and
+training learning rates remain float quantities. Model JSON weights stay
+float data; the loader status strings are unchanged.
+
+Graphics and audio magnitudes round half away from zero and raise `value`
+when the result exceeds the C call's range. This refusal happens even before
+SDL is loaded:
+
+```eigenscript
+try:
+ gfx_delay of 1e300
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Stored buffer data boundary
+
+Buffer kernels can retain non-finite data. Scalar indexing and iteration
+raise `value` when that data is read, in every strict mode (N13).
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+try:
+ print of p[0]
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Buffer equality
+
+Buffers compare their stored doubles with IEEE equality: NaN differs from
+everything, even itself; infinities compare by sign; signed zeros are equal.
+Equality never raises or changes math flags, including under strict mode.
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+print of (p == p)
+print of (p == g)
+sgd_update of [p, p, 1.0]
+print of (p == p)
+```
+```output
+true
+false
+false
+```
diff --git a/docs/CONCURRENCY.md b/docs/CONCURRENCY.md
index 127aac11e..a7f832f04 100644
--- a/docs/CONCURRENCY.md
+++ b/docs/CONCURRENCY.md
@@ -110,7 +110,8 @@ buf_set of [b, 0, 1]
cb is channel of 1
send of [cb, b]
buf_set of [b, 0, 99]
-row of ["buffer ", "1", str of (buf_get of [(recv of cb), 0])]
+# N6: compare buffer contents numerically; float text retains .0.
+row of ["buffer ", 1.0, buf_get of [(recv of cb), 0]]
t is text_builder_new of null
text_builder_append of [t, "a"]
@@ -168,10 +169,12 @@ depth-64 tail SHARES
**The rows the table cannot construct in three lines, and why they behave the
way they do.** `chan_clone_rec` (src/eigenscript.c) switches on `ValType` with
no `default:`, so `-Werror=switch` forces every new type to choose a side; the
-switch handles each type explicitly: `VAL_NUM`, `VAL_NULL`, `VAL_STR`,
-`VAL_LIST`, `VAL_DICT`, `VAL_BUFFER`, and `VAL_TEXT_BUILDER` are rebuilt
+switch handles each type explicitly: `VAL_FLOAT`, `VAL_INT`, `VAL_NULL`, `VAL_STR`,
+`VAL_LIST`, `VAL_DICT`, `VAL_BUFFER`, `VAL_BOOL`, and `VAL_TEXT_BUILDER` are rebuilt
(copy), while `VAL_FN` and `VAL_BUILTIN` take a refcount
-(share).
+(share). An int is a fresh exact copy, `make_int` of the same value
+(src/eigenscript.c:2682), and like the other scalars it is copied at any depth
+(src/eigenscript.c:2678).
A new `ValType` must choose a side in the switch for the compiler to accept it.
The copy is recursive but not an identity-preserving graph copy: two references
to one list or dict become two independent copies. At recursion depth 65
diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md
index 86b7bb323..a6f6bf332 100644
--- a/docs/EMBEDDING.md
+++ b/docs/EMBEDDING.md
@@ -88,6 +88,13 @@ caller environment. Thus, an API call made re-entrantly by a host callback
runs in the default environment even if that callback temporarily changed its
rounding mode, and the callback's mode is back when the inner call returns.
+The exact float-to-int conversion in `eigs_value_as_int` also saves and
+restores this environment, so a host's DAZ setting cannot turn a nonzero
+subnormal into a successful integer zero conversion. `eigs_value_new_int`
+and `eigs_value_is_int` do no floating-point work and need no environment
+swap. These are the R1.8 additions to the public-boundary census; the
+existing code-running and number-text entry points keep their boundaries.
+
Host callbacks execute in EigenScript's default environment; the runtime does
not swap around them. A callback that needs a different mode must save, set,
and restore it itself before returning to EigenScript. Re-entrant API calls do
@@ -361,6 +368,7 @@ embed API refuses it too: `eigs_set_global`, `eigs_get_global` and
```c
EigsValue *eigs_value_new_num(double n);
+EigsValue *eigs_value_new_int(int64_t n);
EigsValue *eigs_value_new_string(const char *s);
EigsValue *eigs_value_new_null(void);
EigsValue *eigs_value_new_bool(int b); /* true/false (#1637) */
@@ -382,6 +390,8 @@ Inspection:
```c
EigsValueType eigs_value_type(EigsValue *v); /* EIGS_TYPE_NUM, _STR, _LIST, _DICT, _NULL, _FN, _BUFFER, _OTHER, _BOOL */
double eigs_value_as_num(EigsValue *v); /* wrong type: sentinel + error; bool sentinel is NaN */
+int64_t eigs_value_as_int(EigsValue *v, int *ok); /* ok may be NULL */
+int eigs_value_is_int(EigsValue *v); /* int kind only; pure predicate */
int eigs_value_as_bool(EigsValue *v); /* wrong type: 0 + error */
const char *eigs_value_as_string(EigsValue *v); /* NULL if wrong type; borrowed pointer */
int eigs_value_list_len(EigsValue *v); /* wrong type: 0 + error */
@@ -394,6 +404,27 @@ double eigs_value_buffer_get(EigsValue *v, int i); /* OOB: 0.0 + error
void eigs_value_buffer_set(EigsValue *v, int i, double x); /* OOB: no-op + error */
```
+`EIGS_TYPE_NUM` covers signed int64 and float, like Lua's `LUA_TNUMBER`.
+`eigs_value_new_int` constructs an exact signed int64, like `lua_pushinteger`;
+`eigs_value_new_num` constructs a float, like `lua_pushnumber`.
+`eigs_value_as_num` converts either kind to `double`,
+like `lua_tonumber`; an int above 2^53 can lose precision in that conversion.
+Use `eigs_value_as_int` for an exact integer result, like `lua_tointegerx`:
+
+| input | result | `*ok` | new error |
+|---|---:|---:|---|
+| int, including `9007199254740993` | exact int64 | 1 | none |
+| integral float in [-2^63, 2^63), including `3.0` and -2^63 | exact int64 | 1 | none |
+| fractional or out-of-range number, including `1.5` and float 2^63 | 0 | 0 | none |
+| non-number, including `NULL` | 0 | 0 | `type_mismatch` |
+
+The success pointer may be `NULL`. The accessor never clears a pending
+error, and a numeric refusal does not replace it. `eigs_value_is_int`, like
+`lua_isinteger`, tests the stored kind: it returns false for `3.0`, and
+returns false without setting an error for any non-number. All four numeric
+operations leave ownership unchanged; each constructed value still needs
+`eigs_value_release`.
+
The numeric, boolean, list-length, and buffer accessors leave a pending
`type_mismatch` error when `v` has the wrong type (including `NULL`); buffer
`get` and `set` leave `index_range` for a negative or past-the-end index. Their
@@ -574,7 +605,7 @@ through lifetime metadata, never through repeated numeric IDs.
An installed memory tape suspends the file replay context as a whole. Clearing
memory restores the file's cursor, queued values, parser buffer, bindings and
strictness; it neither rewinds nor takes data from the memory source. A refused
-memory replacement leaves the active context untouched. The version 6 grammar
+memory replacement leaves the active context untouched. The current tape grammar
and older-format refusal rule are documented in `docs/TRACE.md`.
Host builtins participate with the take/record pair, the same contract
@@ -715,3 +746,15 @@ CHANGELOG entry. The opaque types (`EigsState`, `EigsThread`,
`EigsValue`) intentionally hide their layout: those structs change
between releases and the only safe way to touch them is through the
documented accessors.
+
+### Stored non-finite buffer data
+
+`eigs_value_buffer_get` returns a valid element's stored double exactly,
+including NaN payloads, infinities and negative zero. A set/get round trip
+preserves bits; the getter neither normalizes nor writes math flags and never
+raises for non-finite data. The setter already stores doubles unchanged. C
+callers can use `isfinite()` when their application needs finite values.
+Wrong-type and out-of-range reports and their sentinels keep the API contract
+above. Script indexing and iteration instead enforce the finite data boundary.
+This follows Lua's `lua_tonumberx` and CPython's `PyFloat_AsDouble` C boundary
+(R1.6z-D1; design §2.6a).
diff --git a/docs/GRAMMAR.md b/docs/GRAMMAR.md
index 9c9604d77..eef374cf5 100644
--- a/docs/GRAMMAR.md
+++ b/docs/GRAMMAR.md
@@ -99,10 +99,10 @@ converged stable improving oscillating diverging equilibrium
### Operators and Punctuation
```
-+ - * / %
++ - * / // %
& | ^ << >> ~
< > <= >= == !=
-+= -= *= /= %= &= |= ^= <<= >>=
++= -= *= /= //= %= &= |= ^= <<= >>=
( ) [ ] { }
, : .
|> pipe (left-associative, desugars a |> b to b of a)
@@ -194,7 +194,7 @@ destructure_stmt = '[' IDENT { ',' IDENT } ']' 'is' expression
local_assign_stmt = 'local' IDENT 'is' expression
assign_stmt = IDENT assign_op expression
-assign_op = 'is' | '+=' | '-=' | '*=' | '/=' | '%='
+assign_op = 'is' | '+=' | '-=' | '*=' | '/=' | '//=' | '%='
| '&=' | '|=' | '^=' | '<<=' | '>>='
; compound forms desugar: x += e == x is x + e
@@ -228,7 +228,7 @@ shift_expr = addition { ( '<<' | '>>' ) addition }
addition = multiply { ( '+' | '-' ) multiply }
-multiply = unary { ( '*' | '/' | '%' ) unary }
+multiply = unary { ( '*' | '/' | '//' | '%' ) unary }
unary = '-' unary
| 'not' unary
@@ -322,7 +322,7 @@ From lowest to highest precedence:
| 7 | `&` | Left | Bitwise AND |
| 8 | `<<` `>>` | Left | Shifts |
| 9 | `+` `-` | Left | Addition, subtraction |
-| 10 | `*` `/` `%` | Left | Multiplication, division, modulo |
+| 10 | `*` `/` `//` `%` | Left | Multiplication, true/floor division, floor modulo |
| 11 | `-` (unary) `not` `~` | Right | Negation, logical NOT, bitwise NOT |
| 12 | `of` | Right | Function call / observation (`f of g of x` = `f of (g of x)`) |
| 13 | `[i]` `[a:b]` `.key` | Left | Index, slice, dot access |
diff --git a/docs/LANGUAGE_CONTRACT.md b/docs/LANGUAGE_CONTRACT.md
index bd928ffb4..ed64a39ad 100644
--- a/docs/LANGUAGE_CONTRACT.md
+++ b/docs/LANGUAGE_CONTRACT.md
@@ -138,11 +138,18 @@ examples (executed by the suite).
## Numbers
**Promise:**
-- One numeric type: IEEE-754 double. Integers are exact up to 2^53.
-- No NaN, no Infinity reaches a program. By default a NaN-producing
+- One surface type, `num`, with exact signed int64 and finite IEEE-754 double
+ kinds. `num_type of` gives `"int"` or `"float"`, or null for a non-number.
+ Decimal integer literals within int64 and wrapping hexadecimal literals are
+ ints; a point or exponent produces a float, as does decimal overflow.
+ Int `+ - *` raise `value` on overflow; unary minus and `abs` raise at
+ INT64_MIN. Mixed arithmetic converts to float; cross-kind comparison is exact.
+ Float integer exactness ends at 2^53.
+- Scalar numbers exposed to a program are finite; buffer kernels may retain
+ non-finite work data as described below. By default a NaN-producing
operation, an out-of-domain argument or a wrong-typed argument raises a
catchable error (strict is the default, #1361); division by zero raises
- in every mode. Overflow saturates at ±1e308. Under the per-run opt-out
+ in every mode. Float overflow saturates at ±1e308. Under the per-run opt-out
`EIGS_STRICT=0` the arithmetic is finite by construction instead: a NaN
collapses to 0 and domain functions substitute a stand-in.
- **Every clamp is recorded.** The finite invariant keeps a program
@@ -168,13 +175,17 @@ examples (executed by the suite).
`1e308`, so a data column containing either used to parse to a
plausible number with nothing to check; `pow` of a negative base with
a fractional exponent, `f64_from_bytes` of a NaN bit pattern,
- `matmul` reaching `inf - inf`, and `tensor_load` of a file carrying
- NaN bytes collapse the same way. A buffer may retain the raw kernel value
- internally, but every scalar read applies this same collapse (or raises in
- strict mode), including structural equality and scalar reductions. Both bits are sticky until
- FPU. In strict mode, the default, every one of those out-of-domain
- calls and NaN sources raises a catchable `value` error naming the
- builtin instead of substituting (SPEC.md, *Strict mode*).
+ and `tensor_load` of a file carrying NaN bytes collapse the same way.
+ A buffer kernel such as `matmul` reaching `inf - inf` may retain raw
+ non-finite data internally. Scalar buffer extraction and reductions refuse
+ stored non-finite cells with `value` in every strict mode (R1.6z-D1).
+ Structural equality, including observer snapshot equality, instead compares
+ the stored IEEE values: NaN is unequal to itself, without normalization,
+ mutation, an error or math-flag writes. The flags are sticky for the state's
+ lifetime. In strict mode, the default, those scalar NaN sources and
+ out-of-domain calls raise a catchable `value` error naming the builtin
+ instead of substituting (SPEC.md, *Strict mode*); the stored-buffer
+ boundary above applies in every mode.
- **Saturation is not associative, and that is not detectable from the
value alone.** `(1e300 * 1e300) / 1e300` is `1e8`; `1e300 * (1e300 /
1e300)` is `1e300`. The first overflowed and came back down, and
@@ -182,13 +193,15 @@ examples (executed by the suite).
are what the finite invariant requires — the `overflow` flag is how
you tell. Stated here because the trade should be visible rather than
discovered.
-- `str of` produces the shortest representation that round-trips back to
- the same double; `num of (str of x) == x`.
+- Ints print exact decimal digits. For floats, `str of` produces the shortest
+ representation that round-trips back to the same double. During the builtin
+ transition, `num of` and JSON decoders still produce floats; an integer above
+ 2^53 must not be round-tripped through those readers yet.
- **Every producer of number text obeys that same rule** — `str of`,
`json_encode`, `json_build`, `json_path`, and the SIGUSR1 observer dump
all share one implementation (`eigs_num_text`), so a JSON round-trip
returns the same double and `json_encode of x` equals `str of x` for
- every number (#875).
+ every float (#875).
- `%` follows the dividend's sign (C semantics): `-7 % 3 == -1`.
**Status:** Enforced — `tests/test_number_format.eigs`,
@@ -230,9 +243,10 @@ integers. Every operand, the shift count included, must be an **exact integer in
`[-2^63, 2^63)`**; a fractional operand (`1.5 | 0`, `1 << 0.5`) or one outside
that range (`1e30 | 0`, `9223372036854775808 | 0`) raises a catchable `value` error ("number
has no integer representation"). This is Lua 5.4's rule (reference manual
-§3.4.3); nothing is truncated or wrapped. `-9223372036854775808 | 0` is valid. A shift count
-that passes the check is masked to `[0,63]`, so large or negative shifts
-are defined, not UB (`1 << 64` == `1 << 0` == `1`; `1 << 100` == `1 << 36`). The
+§3.4.3); operand conversion never truncates or wraps. Results are exact ints.
+`-9223372036854775808 | 0` is valid. Shifts follow Lua 5.4: a count outside
+`(-64,64)` gives zero, a negative count shifts in the opposite direction,
+and right shifts zero-fill (`1 << 64` is `0`; `-1 >> 1` is `9223372036854775807`). The
infix operators and the `bit_*` builtins agree bit-for-bit on the same operands —
they were once a divergent `int32` implementation (`0xEDB88320`, CRC-32's
polynomial, was the first casualty). Non-numeric operands **raise** a runtime
@@ -242,9 +256,11 @@ operator … raises") hold for *every* operator with no exceptions.
**Status:** Enforced — `tests/test_bitwise.eigs` (builtin == infix parity across
the high-bit range and out-of-range shift counts; a `value` raise for every
-non-integer operand form, in the VM, the builtins and JIT-compiled hot loops),
+non-integer operand form, including hot functions and loops),
`num_to_i64_exact` in `eigenscript.h`, `INT_BINOP_R` / `CASE(BNOT)` in `vm.c`,
-`bit_*` builtins in `builtins.c`, and the exactness bail in `jit.c`'s bitwise arms.
+`bit_*` builtins in `builtins.c`, and `eigs_bit_shift` in `eigenscript.h`.
+The JIT scanner and leaf evaluator refuse bitwise opcodes during round 1;
+the interpreter executes them with these same rules.
## Truthiness
@@ -462,17 +478,19 @@ unary and `of` are right-associative.
- Out-of-range indices (including too-negative, e.g. `a[-(len+1)]`)
raise a runtime error.
- A non-integer index **raises** (`a[1.5]` → error). Integer-valued doubles
- are accepted (`a[2.0]` works), since EigenScript has a single number type;
+ are accepted (`a[2.0]` works), since ints and floats share the numeric surface type;
but a fractional value is never silently truncated. Because `/` always
yields a double, a division-derived index must be collapsed explicitly —
`a[floor of ((lo + hi) / 2)]` — which keeps the rounding decision in the
programmer's hands. A value that is fractional only through float drift
(`2.9999998`) also raises, surfacing the sloppy arithmetic rather than
mis-indexing.
-- An integer-valued number outside the runtime's C `int` index representation
- raises `index_range`, just like any other out-of-bounds index. This applies
- consistently to indexing, dispatch keys, and slice bounds; it is not
- misdiagnosed as a fractional value.
+- An exact int64 subscript or dispatch key outside the runtime's C `int`
+ index representation raises `index_range`, like other out-of-bounds indices;
+ it is not misdiagnosed as fractional. A float must first satisfy Phase 2's
+ exact int64 conversion rule. Slice-bound and trace-line readers retain their
+ explicit representability checks: an int outside their C `int` domain raises
+ `value` before any narrowing.
**Status:** Enforced — `tests/test_trycatch.eigs`; `vm_index_is_int` guards
every dynamic index site in `OP_INDEX_GET`/`OP_INDEX_SET` and
diff --git a/docs/OBSERVER.md b/docs/OBSERVER.md
index f5a60c329..fc6f3cb94 100644
--- a/docs/OBSERVER.md
+++ b/docs/OBSERVER.md
@@ -136,8 +136,11 @@ a built-in goal precisely so it does not choose the user's frame.
`report of x` and the bare predicates (`converged`, `improving`, …) route
by the binding's current type. **Numeric bindings use the value channel**: it
classifies relative steps `Δv / max(|v|, |v_prev|, scale)` and retained raw
-steps. **Non-numeric bindings use the entropy channel**: it classifies changes
-in `entropy(value)` against the entropy thresholds. The resulting vocabulary
+steps. **Non-numeric `report` and four predicates use the entropy channel**:
+it classifies changes in `entropy(value)` against the entropy thresholds.
+`converged` and ordinarily `stable` compare bounded assignment snapshots;
+the non-reflexive buffer exception below gives `stable` its entropy rule.
+The resulting vocabulary
is `converged`, `equilibrium`, `stable`, `oscillating`, `improving`, and
`diverging`; `report` can additionally answer `moving` when no band matches.
See [PREDICATES](PREDICATES.md#which-channel-answers-861) for the precise
@@ -164,8 +167,11 @@ variable first so there is a binding history to inspect. Dict fields such as
**Since #861 the predicate words and `report` ROUTE: numeric bindings
answer from the value channel described below. For non-numeric bindings,
-`converged` and `stable` use deep equality across the full window (#1992),
-while the other predicates and `report` answer from entropy. The explicit
+`converged` uses deep equality across the full window (#1992). `stable`
+uses the same test for snapshots that equal themselves; an accepted snapshot
+containing a NaN buffer instead uses the entropy stability rule, after a full
+bounded comparison window is available (S10-D1).
+The other predicates and `report` answer from entropy. The explicit
`classify of [t, "entropy"]` always answers from entropy.** The routing exists because of everything this section
documents. The CI oracle now reports a per-predicate comparison: run
`python3 tools/observer_oracle/score.py src/eigenscript` and read its
@@ -182,9 +188,23 @@ change; the window never retains aliases to the program's mutable object. To
bound observer overhead, the snapshot walk stops after **4,096 content units**
(value nodes plus string/key bytes, buffer elements, and text-builder bytes).
An assignment above that bound is a refusal to judge: it clears the equality
-run, and `converged` and `stable` remain false until a complete window of
-bounded assignments has subsequently been observed. Entropy diagnostics are
-still recorded normally. Observer equality is non-raising: unlike the `==`
+run and leaves both predicates false until a new full window of bounded
+comparisons has been observed. Entropy diagnostics are
+still recorded normally.
+
+Stored buffer cells compare with raw IEEE equality, including inside list or
+dict snapshots: NaN is unequal to itself, with no normalization, mutation,
+error or math-flag write. Such an accepted, bounded snapshot therefore cannot
+converge. Once a full bounded comparison window is available, its `stable`
+predicate uses the entropy-window stability criterion instead. A refused or
+oversized snapshot clears that eligibility; a fresh full bounded window is
+required even if the entropy window was already complete.
+The real `matmul` scenario in `tests/test_observer_snapshot_fix2.sh` consequently
+prints `false`, `true`, `false` for `x == x`, `stable of x`, and
+`converged of x`. Finite snapshots retain the equality test and its protection
+against in-place mutation through aliases.
+
+Observer equality is non-raising: unlike the `==`
operator, a number and boolean at corresponding positions are simply unequal.
Functions compare by identity through a non-owning token; a function nested in
a container is refused, because retaining a closure would add an invisible
@@ -267,13 +287,21 @@ lies past the loop's back-edge and `bked` is 0, so every iteration ran natively,
tier only the OSR thunk's last exit is recorded, so the witness covers the last iteration's body: OSR compiles at the first
back-edge, so `x_0` is always assigned interpreted, and an earlier iteration that bailed goes unwitnessed until #1768. A run
whose body stops computing `x` on an inf, NaN or clamped value (the JIT's designed bail) is reported as interpreted and
-passes, so its readings grade the interpreter's observer for those iterations; any other run that does not witness fails as
-vacuous. The predicate READS never run natively: the JIT compiles neither `PREDICATE_SLOT` nor `PREDICATE_NAME`.
+passes, so its readings grade the interpreter's observer for those iterations. The non-capability floor uses float
+scaffolding and float recurrence operands, preserving its binary64 inputs and native witnesses after integer literals
+arrive. Exact int64 capability rows retain int literals; until the R5 int fast paths, their JIT-enabled arms may instead
+report an interpreted int-opcode refusal, requiring `INT_*` before observing `x`, no function thunk, and either no OSR
+thunk or its recorded prefix exit exactly at that int opcode. Their readings remain scored in every arm. Every other run that does not witness fails as vacuous, and both
+native tiers must still witness a body. The predicate READS never run natively: the JIT compiles neither
+`PREDICATE_SLOT` nor `PREDICATE_NAME`.
A row's capability is derived from its values (IEEE inf/-inf/NaN; an int a double cannot hold exactly). Such a row runs
only on a binary whose probe shows the capability (one probe checks that `1e308 * 10` overflows to inf AND that inf × 0 is
NaN; a binary that does one without the other is refused), skips are printed, `EXPECT=ieee,int64` makes an absent one
-fail, and every verdict on them must be right. By default the run also applies the committed floor,
+fail, and every verdict on them must be right, or be declared wrong: Phase 2 passes `--declared
+tests/phase2/oracle_expect.txt` (#1830), whose rows name an arm, a `row@checkpoint` and a predicate with the round
+that fixes it. An undeclared wrong verdict fails, so does a declaration whose verdict is right now or was not examined,
+and the integration merge (#1776) requires that file empty. By default the run also applies the committed floor,
`tools/observer_oracle/floor.json` (#1764): it fails when it gets wrong any verdict the v0.46.0 tag got right. An empty
`FLOOR=`, on the command line or in the environment, skips the floor. Its anchor is the binary
`tools/observer_oracle/build_anchor.sh` builds from the tag into
diff --git a/docs/PREDICATES.md b/docs/PREDICATES.md
index 1dc3614d7..51ae2eaad 100644
--- a/docs/PREDICATES.md
+++ b/docs/PREDICATES.md
@@ -82,11 +82,15 @@ The predicate words and `report` are **routed**: a binding whose most
recent observed assignment is **numeric** answers from the **value
channel** — the classifier below, over relative steps
`rel = Δv / max(|v|, |v_prev|, scale)` (#1045); every
-other binding (strings, containers) uses deep equality of successive values
-for **`converged` and `stable`**: both are true exactly when every successive
-pair in the full window is equal under the language's `==`. This makes order
-changes visible and lets an unchanged non-empty value converge. Its other
-four predicates, and `report`, answer from the **entropy channel**, the
+other binding (strings, containers) uses deep equality of successive
+assignment snapshots for **`converged`** and ordinarily **`stable`**: every
+successive pair in the full window must be equal. Numeric cells compare
+exactly across int/float kinds; a bool/number pair is simply unequal, without
+raising. This makes finite content changes visible and lets an unchanged
+non-empty value converge. The bounded NaN-buffer exception below routes
+`stable` to its entropy rule after a full bounded comparison window. The other
+four predicates, and `report`, answer
+from the **entropy channel**, the
windowed formulas in "The six predicates". `report_value of x` is the
value-channel classifier by name (identical to the routed words on a
numeric binding — the two surfaces cannot disagree); `classify of
@@ -97,11 +101,25 @@ folds, the tape — is unchanged everywhere.
Each equality entry compares against a private content snapshot, not a
reference that can be changed through an alias. Snapshotting is bounded to
4,096 content units per assignment (value nodes plus string/key bytes, buffer
-elements, and text-builder bytes). A larger value is **not sampled**: both
-predicates refuse to judge (return false), reset the equality run, and require
-a new full window of bounded samples before either can become true. Thus the
-observer's additional comparison work is bounded independently of container
-size; the pre-existing entropy calculation remains size-dependent.
+elements, and text-builder bytes). A larger value is **not sampled**: both
+predicates refuse to judge (return false) and reset the equality run. Both
+remain ineligible until a new full window of bounded comparisons is available.
+
+An accepted snapshot containing a NaN buffer, directly or inside a list or
+dict, is unequal to itself: buffer cells use raw IEEE equality without
+normalizing data, raising or writing math flags. `converged` remains false.
+For that bounded, non-reflexive snapshot only, after a full bounded comparison
+window, `stable` uses the entropy-window formula below (S10-D1); it can be true
+even though equality is false. This
+preserves the bank's `false` / `true` / `false` result for the real matmul
+scenario's `x == x`, `stable of x`, and `converged of x`. A current refused or
+oversized snapshot gets no entropy fallback and clears its eligibility, even
+when the entropy window is already complete. Finite snapshots keep the
+comparison against detached contents, so mutating an aliased container before
+assigning it again cannot manufacture equality.
+
+The observer's additional comparison work is bounded independently of
+container size; the pre-existing entropy calculation remains size-dependent.
Why: entropy is a function of `|x|` alone, so any clause on it is a
clause on magnitude. The current CI corpus compares every predicate, not
@@ -113,7 +131,8 @@ cell from the corpus and the selected binary; no score is copied into this
document. The explicit entropy channel can certify a geometric runaway and
can fail to certify a finite nonzero limit because magnitude is a lossy proxy
for motion. Entropy and dH are still computed and exposed for non-numeric
-bindings; they simply do not decide those bindings' `converged` and `stable`.
+bindings. They do not decide equality-routed convergence or finite-snapshot
+stability; they do decide stability for the bounded NaN-buffer exception.
**The numeric definitions** (a fixed window of `N = 10` relative steps — see
[The window](#the-window-1044) — `rel = Δv / max(|v|, |v_prev|, scale)`, raw
@@ -227,7 +246,7 @@ and everything built on it, so these remain **elision-sensitive**:
| `trajectory of x` | the `dh` list, `dH`, `last_entropy` (`rel`/`raw`/`last_value` are complete) |
| `classify of [t, "entropy"]` | the explicit entropy channel |
| `report` / the four entropy predicates on a **non-numeric** binding | the entropy route — its `dh_window` is missing the elided steps |
-| `converged` / `stable` on a **non-numeric** binding | the equality window is missing the elided comparisons |
+| `converged` / `stable` on a **non-numeric** binding | the equality window is missing the elided comparisons; NaN-buffer snapshot stability also lacks the elided entropy samples |
| the loop stall backstop | reads the last observed slot's `dH` |
| a **bare** predicate | reads the last **observed** binding — the alias is deliberately not moved by an elided assignment, so scratch work inside the block cannot hijack a `loop while not converged` |
@@ -396,7 +415,8 @@ single-step rule that the windowed version superseded.
Since #861 the kinds dispatch by route: a numeric binding answers from
the value-channel definitions in "Which channel answers" above; the
windowed-entropy formulas in the next section serve non-numeric bindings
-except for equality-routed `converged`/`stable`, and serve the explicit
+except for equality-routed `converged` and ordinary `stable`; the bounded
+NaN-buffer stability exception does use them. They also serve the explicit
`classify of [t, "entropy"]` channel. `report_value of
x` (#294) is the value-channel classifier by name — on a numeric binding
it and the predicate words are one classifier and cannot disagree.
@@ -408,7 +428,8 @@ it and the predicate words are one classifier and cannot disagree.
they answer the four entropy-routed predicates and `report` for non-numeric
bindings (strings, containers), plus all six predicates on the explicit
entropy channel; numeric bindings use the value-channel definitions above,
-and routed non-numeric `converged`/`stable` use deep equality.** The traces and design notes are kept because the
+and routed non-numeric `converged`/`stable` use deep equality except for the
+bounded NaN-buffer stability case described above.** The traces and design notes are kept because the
mechanics still run — on the signal they were always sound for.
### `converged` (kind 0)
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 1b86f5fe5..f90051f8c 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -1,5 +1,21 @@
# EigenScript Language Specification
+
+Width arithmetic uses explicit wrapping, checked and saturating builtins.
+N5 converts integral floats, then checks the named type range without masking.
+Width results are ints; checked overflow raises `value`. `u64` uses int64 bit
+patterns. `fround` returns a float after double-to-float32-to-double conversion
+(ints convert to double first). See [the width builtins](BUILTINS.md#width-arithmetic-n9).
+
+```eigenscript
+print of [i64_wrapping_add of [9223372036854775807, 1], u8_wrapping_add of [255, 1]]
+print of (fround of 0.1)
+```
+```output
+[-9223372036854775808, 0]
+0.10000000149011612
+```
+
This is the canonical, executable specification of EigenScript. **Every**
`eigenscript` code block here is EXECUTED by the test suite
(`tests/test_doc_examples.py`, suite section [89]) — the gate is opt-OUT.
@@ -135,7 +151,7 @@ EigenScript is dynamically typed. The runtime types are:
| type label | description | literal |
|---|---|---|
-| `num` | 64-bit float (the only number type) | `42`, `3.14` |
+| `num` | signed int64 or 64-bit float | `42`, `3.14` |
| `str` | immutable byte string | `"text"` |
| `list` | mutable ordered sequence | `[1, 2, 3]` |
| `dict` | mutable string-keyed map | `{"k": 1}` |
@@ -193,7 +209,7 @@ inner
outer
```
-Compound assignment operators update in place: `+= -= *= /= %= &= |= ^=
+Compound assignment operators update in place: `+= -= *= /= //= %= &= |= ^=
<<= >>=`. They work on plain names, dict fields, and indexed elements.
```eigenscript
@@ -218,9 +234,51 @@ print of xs
## Numbers and arithmetic
-All numbers are 64-bit floats. Integer-valued numbers print without a
-decimal point. Division is true division. `%` is modulo. There is no
-exponent operator; use `pow of [base, exp]`.
+Numbers have two kinds: signed int64 and 64-bit float. `type of` reports
+`"num"` for both; `num_type of` reports `"int"` or `"float"`, and null
+for a non-number. Decimal integer literals within int64 are exact ints.
+A decimal point or exponent makes a float; an out-of-range decimal integer
+literal falls back to float. Hex integer literals wrap modulo 2^64.
+
+Int `+`, `-` and `*` preserve the int kind and raise `value` on int64 overflow.
+Unary minus and `abs` also raise on INT64_MIN. Mixed int/float arithmetic
+converts the int to float; comparison across kinds is exact, including
+above 2^53. `/` returns float; `//` floors (an int for two ints, a float otherwise).
+`%` is floor modulo for both kinds, with the sign of the divisor. Both int
+operators are exact, and `INT64_MIN // -1` raises `value` while
+`INT64_MIN % -1` is the int 0. Division, floor division and modulo by zero
+raise `value`, including float and mixed operands. Builtin result kinds
+are documented individually; int-preserving
+arithmetic and reductions use checked int64 operations.
+
+Floats use Python’s shortest round-trip spelling: integral floats retain
+`.0`, negative zero prints as `-0.0`, and ints print their exact decimal
+digits. `print`, `write`, `str`, f-strings, JSON and store records share
+this number writer. There is no exponent operator; use
+`pow of [base, exp]`.
+
+
+`//` floors the quotient; `%` has the divisor's sign. `//=` applies the same
+operation to its assignment target, like `/=`. Zero divisors raise `value`
+for int, float and mixed operands. Two ints produce an int for
+`//` and `%`; a float operand produces a float. `/` always produces a float.
+Float and mixed `//` use Python’s `divmod` quotient correction: `1.0 // 0.1`
+is `9.0`, and `1e-300 // -1e300` is `-1.0`. `//=` uses the same rule.
+
+```eigenscript
+x is -7
+x //= 2
+print of [x, 7 // -2, -7 % 3, 7 % -3, -7.5 % 2]
+print of [num_type of (7 // 2), num_type of (7.0 // 2)]
+y is 1.0
+y //= 0.1
+print of [1.0 // 0.1, 1 // 0.1, 1e-300 // -1e300, y, num_type of y]
+```
+```output
+[-4, -4, 2, -2, 0.5]
+["int", "float"]
+[9.0, 9.0, -1.0, 9.0, "float"]
+```
Numeric literals accept the usual decimal forms plus a leading or
trailing dot and scientific notation: `.5`, `1.`, `1e5`, `1E5` are all
@@ -246,7 +304,7 @@ print of (abs of -5)
3.5
1
0.3333333333333333
-1024
+1024.0
5
```
@@ -269,18 +327,17 @@ still running
### The numeric model (deliberate position)
-There is one number kind — the IEEE-754 f64 — and by design there is no
+The numeric kinds are signed int64 and IEEE-754 f64; there is no
bigint or decimal type. This is a chosen position, not an oversight; the
consequences are contracts you can rely on:
The IEEE 754 default floating-point environment is part of number semantics.
-- **Integer exactness ends at 2^53.** Whole numbers up to `9007199254740992`
+- **Float integer exactness ends at 2^53.** Whole floats up to `9007199254740992`
are exact; past that the gaps between representable values exceed 1, so
- `+ 1` can be invisible. Keep integer identifiers, counters, and money-in-cents
- below 2^53, or use the bitwise seam below for wider exact integer work
- (its operands must be exact integers below 2^63 in magnitude; past 2^53 only
- the representable ones qualify).
+ `+ 1` can be invisible. For exact identifiers, counters, and money-in-cents, keep values in
+ the int kind rather than converting them to float. Bitwise operations preserve all int64 bits and return ints; integral float
+ operands convert only when they are exactly representable as int64.
- **Finite by construction — no `NaN`, no `Infinity` ever reach your program.**
Overflow saturates at `±1e308` instead of becoming `Infinity`. Every
*defined* operation returns a usable finite number; an *undefined* one —
@@ -325,14 +382,13 @@ The IEEE 754 default floating-point environment is part of number semantics.
value: sqrt: argument out of domain (negative)
```
- Under `EIGS_STRICT=0` the same program prints `0` twice.
- A `0`, `""` or `null` that is a genuine *answer* is untouched
- in both modes: `try_parse` of invalid syntax still returns `0`, `task_alive`
- of an unknown id still returns `0`, `char_at` past the end is still `""`,
- and `num` still *coerces* (`num of ([1, 2])` is `0` — that is its documented
+ Under `EIGS_STRICT=0` the same program prints `0.0` twice.
+ A `false`, `0`, `""` or `null` that is a genuine *answer* is untouched
+ in both modes: `try_parse` of invalid syntax still returns `false`, `task_alive`
+ of an unknown id still returns `false`, `char_at` past the end is still `""`,
+ and `num` still *coerces* (`num of ([1, 2])` is float `0.0` — that is its documented
contract, not a guard).
- The distinction is not derivable from the code: `task_alive` has two
- `return make_num(0)` lines four apart, one a type guard and one the
+ A return expression alone does not distinguish a type guard from a
documented answer. `tools/strict_differential.sh` checks the distinction
that can be executed: a guard probe must raise in strict mode, the default
(with the variable unset or set to `1`), and a pinned documented answer must not.
@@ -342,7 +398,7 @@ The IEEE 754 default floating-point environment is part of number semantics.
- **The `NaN`→`0` collapse.** Under `EIGS_STRICT=0` a `NaN` still collapses to
`0` and sets `math_flags.invalid`. In strict mode every reachable `NaN`
source raises a catchable `value` error naming the builtin: `pow` of a
- negative base with a fractional exponent, `num of "nan"`,
+ negative base with a fractional exponent,
`f64_from_bytes` of a `NaN` bit pattern, `matmul` when its accumulation
reaches `inf - inf`, `tensor_load` of a file carrying `NaN` bytes — and
the elementwise `divide` by zero, which answers `0` under `EIGS_STRICT=0` where the
@@ -351,9 +407,11 @@ The IEEE 754 default floating-point environment is part of number semantics.
operand can hold an infinity), so any other source hits a backstop that
raises as `arithmetic`. The JIT bails to the interpreter on a non-finite
result, so both tiers raise from the same guard. Buffer storage may retain
- a raw non-finite kernel result internally, but every scalar read uses the
- same rule: infinity saturates at ±`1e308`, while under `EIGS_STRICT=0` a
- `NaN` becomes `0` and sets `math_flags.invalid` (strict mode raises).
+ a raw non-finite kernel result internally. Indexing and iteration, like
+ `get_at`, refuse a stored NaN or infinity with `value` in every strict
+ mode (N13). Strict NaN preserves the first-NaN diagnostic and sets
+ `math_flags.invalid`; other non-finite data refusals leave flags alone.
+ The interpreter, JIT and leaf fallback agree on kind, message and line.
- **JSON parse failure in `json_path`.** Under `EIGS_STRICT=0` a malformed
document is walked leniently and a parse failure answers the same `""`
an absent key does. Under strict `json_path` applies `json_decode`'s
@@ -366,7 +424,7 @@ The IEEE 754 default floating-point environment is part of number semantics.
`json_build`, `sort`, `random_int`, `random_hex`, `token_name`,
sentinel for a valid-but-absent input — `index_of` miss `-1`,
`file_exists` of a missing path `0` — is unchanged in both modes.
- Overflow saturation is the same in both modes. (Division and modulo by zero
+ Float overflow saturation is the same in both modes; int overflow raises. (Division and modulo by zero
raise in *both* modes — no defined value.)
`zlib_*` forms) likewise reject a nonnumeric list element with a
builtin-named `type_mismatch` error by default; `EIGS_STRICT=0` retains the
@@ -380,26 +438,22 @@ The IEEE 754 default floating-point environment is part of number semantics.
seam for checksums, hashing, and byte protocols (see docs/BUILTINS.md).
```eigenscript
-print of (9007199254740992 + 1) # +1 is invisible past 2^53
+print of (9007199254740992 + 1) # exact int64 addition
print of (9007199254740993 == 9007199254740992)
print of (1e308 * 10) # saturates, never Infinity
print of (bit_and of [0xFFFFFFFF, 0xFF]) # int64 bitwise
print of (bit_shl of [1, 40]) # exact past 2^32
```
```output
-9007199254740992
-true
+9007199254740993
+false
1e+308
255
1099511627776
```
-Bigint / decimal value kinds would be a VM representation change rippling through
-the JIT and the AOT (whose numeric speed rests on everything-being-a-double), so
-they are deferred until a consumer genuinely forces them — not adopted
-speculatively. Nothing above says "a number *is* an f64" any more than it must:
-the contracts are exactness-below-2^53, finiteness, and the int64 bitwise seam,
-which a future wider numeric kind could still honor.
+Ints are exact throughout int64; floats retain the finite model above. Bigint
+and decimal arithmetic remain library needs rather than additional core kinds.
## Strings
@@ -456,21 +510,75 @@ mine
```
Convert explicitly with `str of n` and `num of s`. `num of` accepts
-decimal and hex-integer strings (hex converts identically on every
-profile and stops at the first non-hex character); a string with no
-leading number converts to `0`:
+exactly the numeric-literal grammar, with an optional sign and surrounding
+ASCII whitespace (N10). Decimal integer text fitting int64 gives an int;
+hex integers wrap modulo 2^64. A point/exponent or decimal overflow gives
+a float. Partial or empty text, `_`, `0b`/`0o`, hex floats, `inf` and `nan`
+raise `value` in every strict mode. Unlike Lua's `tonumber`, hex floats are
+refused because the language lexer refuses them:
```eigenscript
print of ("value is " + (str of 42))
print of ((num of "10") + 5)
print of (num of "0xFF")
-print of (num of "zebra")
+try:
+ print of (num of "zebra")
+catch e:
+ print of e.kind
```
```output
value is 42
15
255
-0
+value
+```
+
+
+Empty `min`/`max` raises `value` in both call forms (R1.6b-D2); empty
+`sum` remains int zero (C2). `sign_extend` reads exact int64 arguments and
+raises `value` if its existing subtraction underflows (R1.6b-D1).
+
+Counts and indices are ints. `floor`, `ceil` and half-away-from-zero `round`
+return an int when the result fits int64. `min`/`max` return the winning
+argument with its kind; the first argument wins a tie. `num` requires a
+complete numeric-literal string (N10). JSON integer text fitting int64
+decodes exactly (N14).
+
+The [C embedding API](EMBEDDING.md#value-handles) follows Lua’s numeric
+accessors: `eigs_value_new_int` constructs an exact int64, `eigs_value_is_int`
+tests the stored kind (`lua_isinteger`), and `eigs_value_as_int` reports
+whether a number converts exactly to int64 (`lua_tointegerx`).
+`eigs_value_as_num` converts either kind to double (`lua_tonumber`);
+`EIGS_TYPE_NUM` covers both.
+
+LSP hover on a numeric literal reports `int` or `float` from the literal’s
+kind: `3.0` remains a float even though its value is integral.
+
+```eigenscript
+print of [type of 3, num_type of 3, num_type of 3.0]
+print of [9007199254740993, 3.0, -0.0]
+print of [num_type of (len of "abc"), num_type of (floor of -2.5)]
+print of [num of "9007199254740993", num_type of (num of "3.0")]
+print of [num_type of (min of [3, 3.0]), num_type of (min of [3.0, 3])]
+print of (json_path of ["{\"n\":9223372036854775807}", "n"])
+try:
+ min of []
+catch e:
+ print of [e.kind, (index_of of [e.message, "min"]) >= 0]
+try:
+ print of (num of "12abc")
+catch e:
+ print of e.kind
+```
+```output
+["num", "int", "float"]
+[9007199254740993, 3.0, -0.0]
+["int", "int"]
+[9007199254740993, "float"]
+["int", "float"]
+9223372036854775807
+["value", true]
+value
```
### Text and Unicode (bytes by definition)
@@ -673,8 +781,24 @@ print of (bit_shl of [1, 32])
Every operand, the shift count included, must be an **exact integer** in
`[-2^63, 2^63)`, Lua 5.4's rule. A fractional operand or one outside int64
has no integer representation, and the operation raises a catchable
-`value` error instead of truncating. A valid shift count is then masked
-to `[0, 63]`:
+`value` error instead of truncating.
+
+Bitwise operators and `bit_*` builtins return ints, including when an
+operand is an integral float. They use Lua 5.4's 64-bit shift rules: `>>`
+zero-fills; a negative count shifts in the opposite direction; a count of
+64 or more, or -64 or less, gives zero. Each operand must be an exact int64;
+a fractional or out-of-range float raises `value`.
+
+```eigenscript
+print of [1 << 64, 1 << -1, -1 >> 1, 8 >> -1, 1 << 63]
+print of [bit_shr of [-1, 1], bit_shl of [8, -1], num_type of (3.0 & 1)]
+```
+```output
+[0, 0, 9223372036854775807, 16, -9223372036854775808]
+[9223372036854775807, 4, "int"]
+```
+
+The operand checks also apply to shift counts:
```eigenscript
print of ((-9223372036854775808 | 0) == -9223372036854775808)
@@ -694,7 +818,7 @@ catch e:
```
```output
true
-1
+0
value
'<<': number 0.5 has no integer representation
value
@@ -885,6 +1009,74 @@ print of ev.loop
2
```
+### Stored records
+
+Store handles carry integer `_store_id` and `_store_gen` fields. Every store operation requires exact, in-range integer handle fields; integral floats convert, while fractional, non-finite or out-of-range numeric fields raise `value` before store access (R1.6f-D1). `store_count` returns an integer.
+
+`store_get`, `store_delete`, and `store_update` accept string keys or exact
+int64 numeric keys. Integral floats convert; fractional or out-of-range floats
+raise `value` before lookup or mutation. Numeric keys retain all int64 digits.
+Stored decimal integer text within int64 decodes as an int; a decimal point or
+exponent selects a float. Buffer payloads remain floats. Store records preserve
+int64 digits and use shortest round-trip float text, including `.0` for
+integral floats and the sign of `-0.0`, so `3.0` reads back as a float.
+
+```eigenscript
+path is "store-numbers-example.db"
+if file_exists of path:
+ remove_file of path
+s is store_open of path
+print of [num_type of s["_store_id"], num_type of s["_store_gen"]]
+store_put of [s, "c", {"_id": "9007199254740993", "n": 9007199254740993, "f": 3.0}]
+r is store_get of [s, "c", 9007199254740993]
+print of [r.n, num_type of r.n, num_type of r.f]
+try:
+ store_delete of [s, "c", 1.5]
+catch e:
+ print of e.kind
+print of ((store_get of [s, "c", 9007199254740993]) != null)
+store_close of s
+remove_file of path
+```
+```output
+["int", "int"]
+[9007199254740993, "int", "float"]
+value
+true
+```
+
+PostgreSQL integer columns (`smallint`, `integer`, `bigint`, `oid`) return
+exact int64 values through `db_query_value`; `db_query_json` retains their
+exact decimal digits (N14, Q-DB253). A `bigint` beyond 2^53 is an int,
+including both int64 boundaries. `numeric` remains text, and finite real
+columns returned directly remain floats. Float parameters use N6 shortest round-trip text, preserving `.0` and
+signed zero. Store records use the same float writer and exact int64 digits
+(N14, #1762). The executable DB examples and boundary assertions
+in `tests/test_db.eigs` run on the server-db profile with PostgreSQL.
+
+TCP handles, ports and sent-byte counts are integers. Numeric socket handles, ports, receive counts and outgoing bytes require exact integers (integral floats convert); bytes must be in 0..255. `net_send` validates the entire payload before sending any byte. Poll timeouts are continuous milliseconds rounded to the C int range; an unrepresentable duration raises `value`.
+
+`http_early_bind` and `http_serve` require an exact integer port in 0..65535; integral floats convert and port zero requests an ephemeral port. Fractional or out-of-range ports raise `value` before binding. `shared_delete` and `shared_size` return integer counts.
+
+N6 prints each float with Python's shortest round-trip spelling: integral
+floats retain `.0`, negative zero is `-0.0`, and scientific notation starts
+below `1e-4` or at `1e16`. Ints keep their exact decimal digits. `print`,
+`write`, `str`, f-strings and JSON share the number writer. The executed
+examples are `tests/phase2/rows/N6.float-repr.eigs` and
+`tests/phase2/rows/N6.str-fstring.eigs`.
+
+The HTTP shared counter `shared_incr` follows numeric `+`: two int operands
+return an exact int and int64 overflow raises `value` without changing the
+stored counter (A). A missing counter starts at int zero. A float operand
+produces a float. The stored operand is its JSON-decoded value; N6 preserves an
+integer-valued float as a float by retaining its `.0`. Executable shared-counter examples in
+`tests/test_http.eigs` cover exact values, both overflow directions and the
+next operation after a raise.
+
+`http_post` also preserves every digit of JSON integer header values within
+int64 before sending them as text (N14, B6). The executed loopback example
+is `tests/phase2/builtins/builtins/http_post.eigs`.
+
## Functions
`define name(params) as:` introduces a function. `return` exits with a
@@ -1146,6 +1338,9 @@ print of (apply of [add5, 10])
15
```
+`sort` sorts a list in place; `sort_by` returns a new list using numeric keys.
+Both are stable on every platform: equal elements or keys keep their input order.
+
`sort_by` is a builtin; `map` and `filter` come from the standard
library (`lib/list.eigs`):
@@ -1208,6 +1403,54 @@ expressions match too
`vm_run_bytecode` raises a catchable `value` error naming a rejected chunk descriptor; a valid program may still return `null`. `sandbox_run` reports descriptor rejection in its structured `{ok: false, error: ...}` result. Its optional fourth limit, `max_work`, bounds cumulative bytecode instructions across function calls and callback re-entry (default 10,000,000), independently of loop and allocation limits. This meters VM work, not elapsed time: a blocking native callback must return before the sandbox can stop.
+The positions and counts of `char_at`, `substr`, `get_at`, `set_at`,
+`copy_into`, `fill` and the `list_*` mutation/slice builtins use exact conversion
+(N5). So do handle fields, bytecode ABI/counts/bytes, sandbox limits and numeric
+exit codes. Integral floats are accepted; fractions and values outside the
+receiving C type raise `value` before indexing or effects.
+`recv_timeout` rounds milliseconds half away from zero and raises beyond the
+C long range, with no one-year cap. `usleep` rounds microseconds and raises
+beyond the C int range. `record_history` returns its previous flag as an int;
+nonzero numeric flags still enable history. `task_detach` and sandbox error lines return ints.
+`read_bytes` produces int bytes; `write_bytes` requires exact bytes in 0..255
+and returns an int count. File caps and corpus/stream counts follow N5; an
+integral stream count outside its cap still raises `limit`. Subprocess exit
+statuses are ints, and timeout seconds round to representable poll milliseconds.
+
+```eigenscript
+print of (substr of ["hello", 1.0, 2.0])
+try:
+ print of (substr of ["hello", 1.5, 2])
+catch e:
+ print of e.kind
+print of (num_type of (record_history of 0.5))
+print of (usleep of 1.5)
+try:
+ usleep of 1e300
+catch e:
+ print of e.kind
+path is mktemp of null
+written is write_bytes of [path, [65.0, 66.0]]
+bytes is read_bytes of path
+print of [num_type of written, num_type of bytes[0], str_from_bytes of bytes]
+try:
+ write_bytes of [path, [65.5]]
+catch e:
+ print of e.kind
+remove_file of path
+```
+```output
+el
+value
+int
+null
+value
+["int", "int", "AB"]
+value
+```
+
+Runtime-published counts and indices are exact ints (S9-K1): `__loop_iterations__`, live and historical `when` assignment counts, aggregate `what` lengths, trajectory `window`, error `line`, and scheduler trace `seq` and task ids. Scheduler `tick` remains a continuous virtual time.
+
Sandbox execution does not update shared temporal history: assignment values, names, counts and observer snapshots stay outside that history even when recording is armed. Ordinary tape assignment records still emit. Host and trusted descriptor history recording resumes normally outside the sandbox; sandbox temporal reads remain refused.
`try:` / `catch name:` captures runtime errors. A **built-in** runtime
@@ -1684,10 +1927,12 @@ reference**: a list or dict computes over its own elements, and an element
that is itself a container contributes only its size term `log2(count+1)`
rather than being entered. This is the rule buffers and text builders
have always followed, so a dict holding a 5-element list measures exactly
-as one holding a 5-element buffer. The cost of an observed assignment is
+as one holding a 5-element buffer. The cost of the entropy walk is
therefore proportional to the value's own size, never to everything it can
-reach, and cyclic or shared object graphs are well-defined because they are
-never traversed (see [OBSERVER.md](OBSERVER.md)). Six bare-keyword predicates query
+reach. Cyclic or shared graphs are well-defined for that measurement because
+the entropy walk never enters them. Non-numeric assignment snapshots add the
+bounded recursive comparison work described below (see
+[OBSERVER.md](OBSERVER.md)). Six bare-keyword predicates query
the most recently observed variable: `converged`, `stable`,
`improving`, `oscillating`, `diverging`, `equilibrium`. The canonical
use is a self-terminating loop:
@@ -1712,9 +1957,21 @@ and `dh_zero · scale` as the absolute floor. The runtime computes `scale`
as the floor in `max(|v|, |v_prev|, scale)`; above that floor, the
limit's magnitude and the **unit** the value is stored in do not matter.
A loop converging to `5`, `5000` or `0.005` certifies identically, and a
-bank angle reads the same in radians and degrees. Non-numeric bindings (strings,
-containers) classify their entropy trajectory as before; the entropy
-MEASUREMENT (`where is x`) is unchanged for everything.
+bank angle reads the same in radians and degrees. For non-numeric bindings
+(strings, containers), `converged` and ordinarily `stable` compare detached
+assignment snapshots across a full window, bounded to 4,096 content units per
+assignment. Finite in-place mutations remain visible when the binding is
+assigned again. A refused or oversized snapshot leaves both predicates false.
+For an accepted snapshot containing a NaN buffer, directly or nested,
+`converged` stays false because raw IEEE equality is non-reflexive, while
+`stable` uses the entropy-window criterion only after a full bounded comparison
+window (S10-D1). Refusal clears that eligibility, so a fresh full bounded
+window is required even if the entropy window is already complete.
+The real matmul scenario
+in `tests/test_observer_snapshot_fix2.sh` gives `false`, `true`, `false` for
+`x == x`, `stable of x`, and `converged of x`, without equality changing data
+or math flags. The other four predicates and non-numeric `report` use entropy;
+the entropy MEASUREMENT (`where is x`) is unchanged for everything.
**Convergence-halting is opt-in.** A `loop while` is auto-halted on a
quiet observer trajectory (~100 iterations without motion, at any
@@ -1761,9 +2018,11 @@ trajectory the predicates read (value channel for numerics, entropy
otherwise — #861), resolving `oscillating` → `diverging` → `improving` →
`converged` → `equilibrium` → `stable`. This arrow is report priority, not
an implication lattice: a converged one-direction tail is not `equilibrium`.
-At a full window it agrees with the
+For a numeric binding, at a full window it agrees with the
bare predicates by construction: it either names a band whose predicate is
-true, or — when a full window matches none of them — returns `moving`. The
+true, or — when a full window matches none of them — returns `moving`.
+For a non-numeric binding it reports the entropy classification and need not
+agree with the snapshot-based `converged` or `stable` test. The
bands are not exhaustive, and `moving` is the honest answer for the gap
(#735); only while the window is still filling may `report` fall back to an
instantaneous label the predicates don't yet confirm. See
@@ -1789,7 +2048,7 @@ print of why is d
```
```output
true
-0
+0.0
```
**Function values are `opaque`** (#708). A function has no content the
@@ -1951,7 +2210,7 @@ print of (why is x)
```output
moving
2
-0
+0.0
```
```eigenscript
@@ -2423,7 +2682,7 @@ buffer
2.5
list
-[[0, 0, 0], [0, 0, 0]]
+[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
buffer
```
@@ -2438,6 +2697,37 @@ a tensor builtin needs no change, because a buffer supports all of those.
`sum of a` returns the total of a buffer's (or tensor's) elements, and
`norm of a` returns the L2 (Euclidean) norm, `sqrt(sum_i a[i]*a[i])`.
+`add`, `subtract` and `multiply` preserve exact ints for scalar operands and
+for each pair of list elements, with `value` on int64 overflow. `negative`
+preserves int scalar and list elements and raises at INT64_MIN. Buffer
+arithmetic remains float. A buffer kernel stops at its first raised result,
+releases its partial output and does not read later elements (#1417).
+An all-int `sum` returns an exact int, while a
+float anywhere in a nested input selects float addition for the entire
+reduction. A buffer selects float addition too. An empty list's sum is int
+zero, in both call forms. `sum` refuses non-finite buffer elements with a
+`value` error in every strict mode (N13). In strict mode, it stops at the first NaN with a "not a number" diagnostic and sets `math_flags.invalid`; later elements are not read (#1417).
+
+```eigenscript
+exact is sum of [9007199254740993, 1]
+print of [exact, num_type of exact]
+mixed is sum of [9223372036854775807, 1, 0.5]
+print of [num_type of mixed, mixed == 9223372036854775808.0]
+print of [num_type of (sum of []), num_type of (sum of ([]))]
+print of (add of [[9007199254740993, 2], [1, 3]])
+try:
+ negative of (-9223372036854775807 - 1)
+catch e:
+ print of e.kind
+```
+```output
+[9007199254740994, "int"]
+["float", true]
+["int", "int"]
+[9007199254740994, 5]
+value
+```
+
```eigenscript
v is buffer of 4
v[0] is 1
@@ -2448,7 +2738,7 @@ print of (sum of v)
print of (norm of v)
```
```output
-10
+10.0
5.477225575051661
```
@@ -2469,15 +2759,16 @@ b[3] is 3.5
print of (dot of [a, b])
```
```output
-25
+25.0
```
-For all three reductions (`sum`, `norm`, `dot`) the summation **order
-(association) is unspecified**: callers must not depend on the exact low-bit
-rounding of the result. This is a deliberate opt-in — it licenses an
+For the floating forms of these reductions (`sum`, `norm`, `dot`), the
+summation **order (association) is unspecified**: callers must not depend on
+the exact low-bit rounding of the result. This is a deliberate opt-in — it licenses an
optimizing backend (such as the AOT native compiler) to reassociate the sum
across SIMD lanes, which a strict left-to-right `loop while` accumulation
-forbids. The no-NaN/no-Inf invariant still holds. Write the explicit loop when
+forbids. Integer `sum` checks int64 overflow and never silently rounds or
+wraps. The no-NaN/no-Inf invariant still holds. Write the explicit loop when
you need a fixed reduction order.
### Shaped buffers (tensors)
@@ -2487,15 +2778,18 @@ A buffer can carry a 2-D shape, making it a flat-backed matrix. `buffer of
[buf, rows, cols]` shapes an existing flat buffer (the element count must
match). `shape of buf` returns `[rows, cols]` for a shaped buffer, or `[count]`
when unshaped. Indexing stays flat (`buf[r*cols + c]`).
-Every element crossing the buffer/scalar boundary uses the numeric guard:
-infinity saturates at ±`1e308`, while a `NaN` raises in strict mode or becomes
-`0` and sets `math_flags.invalid` under `EIGS_STRICT=0`. Structural buffer
-equality and scalar reductions normalize each input before comparison or
-arithmetic. A strict read that raises inside a function retains that
+At the buffer/scalar boundary, `sum`, `get_at`, indexing, iteration, `dot` and `write_bytes` refuse
+non-finite elements in every strict mode (N13). Other consumers use the numeric guard: infinity saturates
+at ±`1e308`, while a `NaN` raises in strict mode or becomes `0` and sets
+`math_flags.invalid` under `EIGS_STRICT=0`. Structural buffer equality compares
+stored doubles directly with IEEE equality; list/dict self-equality follows
+that rule through nested and cyclic containers. The other scalar reductions
+normalize each input before arithmetic. A strict read that raises inside a function retains that
function's source location and call frame. Buffer-to-list tensor materialization
and numeric byte/sample/device conversions apply the same read rule, stopping
-at the first raised read. Numeric bytes truncate and wrap modulo 256 after
-normalization; audio samples then clamp to their documented sample range.
+at the first raised read. `write_bytes` requires exact bytes in 0..255 (N5/Q1);
+the remaining byte converters retain truncation and wrapping at this stage.
+Audio samples clamp to their documented sample range.
Raw buffer copies, typed serialization and internal buffer-only kernel work
areas retain their stored representation.
@@ -2511,9 +2805,11 @@ elementwise, with a `[cols]` buffer broadcast over the rows of a
`[rows × cols]` buffer and a number broadcast over every element; `relu`,
`leaky_relu`, `softmax`, `log_softmax`, `sum`, `mean`, `norm`, `gather`
compute on the shape, and `scatter_add` is `gather`'s in-place dual. The
-result is identical to the nested-list tensor form, so storing weights as
-shaped buffers is purely a performance choice — and it is the substrate the
-reverse-mode autograd tape in `lib/autograd.eigs` runs on.
+result for float inputs is identical to the nested-list tensor form. Int
+list arithmetic stays exact, while buffer storage converts ints to floats,
+so storing integer weights as shaped buffers can change their precision.
+Shaped buffers are the substrate the reverse-mode autograd tape in
+`lib/autograd.eigs` runs on.
### `gather` and an out-of-range index
@@ -2534,7 +2830,7 @@ catch e:
print of e["message"]
```
```output
-[3, 4]
+[3.0, 4.0]
index_range
gather: column index 3 out of range for row 1 (cols 3)
```
@@ -2586,8 +2882,8 @@ print of (type of (add of [b, l]))
print of (mean of b)
```
```output
-[1, 2, 3]
-3
+[1.0, 2.0, 3.0]
+3.0
buffer
list
4.666666666666667
@@ -2608,8 +2904,8 @@ print of y[1]
print of (shape of w)
```
```output
-4
-6
+4.0
+6.0
[2, 2]
```
@@ -2632,9 +2928,10 @@ The facts that govern every program, in one place:
4. **Scope**: `is` updates the nearest enclosing binding or creates a
local; `local` forces the current scope. Functions see and may
mutate their defining environment (closure capture by reference).
-5. **Values**: numbers are 64-bit floats; strings immutable; lists,
- dicts, and buffers mutable and passed by reference; `==` is
- structural.
+5. **Values**: numbers are signed int64 or 64-bit floats under the
+ shared `num` type; `num_type` distinguishes their kinds. Strings
+ are immutable; lists, dicts, and buffers are mutable and passed
+ by reference; `==` is structural.
6. **Truthiness**: `0`, `null`, `""`, `[]`, `{}` are falsy; everything
else is truthy. Comparisons yield `1`/`0`.
7. **Errors**: runtime errors unwind to the nearest `try`; uncaught
@@ -2679,3 +2976,93 @@ Routes and the static root are fixed once `http_serve` starts: `http_route`,
`http_route_authed` and `http_static` then raise, including from a `code`
route, and an uncaught error in a `code` route's source answers 500 with a
generic body; the error message goes to the server's stderr only (#1140).
+
+Tensor sizes and indices follow N5: an integral float is accepted and a
+fraction raises `value`. `gather` keeps the selected list cell's int/float
+kind; buffer elements and numeric tensor calculations are floats. `shape`
+returns integer dimension counts. `mean` and `norm` refuse non-finite buffer
+cells with `value` in both strict modes.
+
+```eigenscript
+g is gather of [[[1, 2], [3, 4]], [1.0, 0]]
+print of [g, num_type of g[0], num_type of (shape of g)[0]]
+try:
+ gather of [[10, 20], 0.5]
+catch e:
+ print of e.kind
+```
+```output
+[[2, 3], "int", "int"]
+value
+```
+
+Buffer sizes, indices, offsets and counts follow N5. Byte inputs to
+`str_from_bytes`, `f64_from_bytes`, `inflate` and `buf_from_pcm16le` must be
+exact integers in 0..255; fractions and out-of-range bytes raise `value`.
+`buf_len`, `f64_to_bytes` and `inflate` return integer counts or bytes;
+buffer storage and scalar buffer reads are floats. `buf_get`, `buf_peak`,
+`buf_dot` and the byte/PCM codecs refuse non-finite input cells with `value`.
+
+Model token ids and generation limits follow N5 and the C int range;
+fractional ids or token counts raise `value`. `eigen_generate` returns int
+token ids, while `eigen_eval_loss` returns a float. Temperature, top_p and
+training learning rates remain float quantities. Model JSON weights stay
+float data; the loader status strings are unchanged.
+
+Audio playback (`audio_play`, `audio_play_loop`, `audio_stream_push`) refuses
+stored non-finite buffer samples at the scalar boundary in both strict modes
+(N13). Strict NaN preserves the first diagnostic and INVALID flag. Finite
+samples clamp to [-1,1], then round PCM16 half away from zero (Q7/Q4).
+Channel indices and loop counts follow N5; continuous mixer gain stays
+fractional after clamping to [0,4].
+
+Q7 magnitude validation also applies before SDL is loaded:
+
+```eigenscript
+try:
+ gfx_delay of 1e300
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Stored buffer data boundary
+
+Buffer kernels can retain non-finite data. Scalar indexing and iteration
+raise `value` when that data is read, in every strict mode (N13).
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+try:
+ print of p[0]
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Buffer equality
+
+Buffers compare their stored doubles with IEEE equality: NaN differs from
+everything, even itself; infinities compare by sign; signed zeros are equal.
+Equality never raises or changes math flags, including under strict mode.
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+print of (p == p)
+print of (p == g)
+sgd_update of [p, p, 1.0]
+print of (p == p)
+```
+```output
+true
+false
+false
+```
diff --git a/docs/STDLIB.md b/docs/STDLIB.md
index c7d62d1e6..3ec472b04 100644
--- a/docs/STDLIB.md
+++ b/docs/STDLIB.md
@@ -374,9 +374,9 @@ print of g[2]
```
```output
3
-5
-7
-9
+5.0
+7.0
+9.0
```
`x` is `[[1, 2, 3], [4, 5, 6]]` and `s` scales each column, so the gradient
@@ -407,9 +407,9 @@ autograd.ag_sgd_step of [wn, 0.1]
print of w[0]
```
```output
-7
-4
-6
+7.0
+4.0
+6.0
0.6
```
@@ -1265,7 +1265,7 @@ print of (data.df_mean of [rows, "score"])
```output
85.4
88
-80
+80.0
```
### lib/data.eigs
diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md
index 4d640abee..0f4488f1a 100644
--- a/docs/SYNTAX.md
+++ b/docs/SYNTAX.md
@@ -512,7 +512,7 @@ print of (when is loss) # 3 — three assignments
```output
0.2
-0.26827341240613545
-0
+0.0
3
```
@@ -540,7 +540,7 @@ loss is 80.0
print of (prev of loss) # 100 — the value before the latest assign
```
```output
-100
+100.0
```
Returns `null` when `x` has been assigned fewer than two times. Note
diff --git a/docs/TRACE.md b/docs/TRACE.md
index bb1f237b9..161823e63 100644
--- a/docs/TRACE.md
+++ b/docs/TRACE.md
@@ -89,13 +89,13 @@ The tape is plain text, one record per line:
| Record | Meaning |
|--------|---------|
-| `V ` | Version header — always the first record (e.g. `V 8 0.47.0`). Stamped once per tape-open; a journal appended across sessions carries one per session. See [Format Versioning](#format-versioning-411). |
+| `V ` | Version header — always the first record (e.g. `V 9 0.47.0`). Stamped once per tape-open; a journal appended across sessions carries one per session. See [Format Versioning](#format-versioning-411). |
| `B ` | Stream association, before that stream's events. Describes state grouping and host/causal correspondence; folded as metadata by stepping, never a source-line stop. See below. |
| `L ` | Source-line event (from `OP_LINE`, and when native code that ran EigenScript — a builtin's callback, an embedder's eval — gets control back: the line it entered with, so an assignment it makes next is filed under the same line on the tape as in live history, #1434; v5 carries the stream ID shown here). Duplicate lines within the same stream with no intervening `A`/`N` in that stream are deduped — the compiler emits per-statement LINEs and bare repeats are noise. |
| `S ` | Scope transition (#539 v2): the `A` records that follow belong to this frame instance — `` is the chunk name (``, ``, or the function name), `` the 0-based frame depth, `` a per-thread monotonically increasing frame-instance id stamped at frame push. Emitted lazily with the same dedup discipline as `L`: only when the frame owning the next assignment differs from the last `S`, so the byte cost lands at call boundaries that actually assign. Two invocations of the same function carry different serials — their local streams never merge. Skipped on replay; folded by `--step`. |
| `A =` | Assignment delta: a binding changed. Fires at **every scope** — function locals included — and is scope-qualified by the preceding `S` record, so a function-local `i` and the top-level `i` are separate streams (`--step` resolves names innermost-first along the reconstructed call chain, with shadowing). |
| `N =` | Nondeterministic builtin return — the replay-determinism substrate. |
-| `C ` | Immediate continuation of an oversized `N` value (v8). See [Value serialization](#value-serialization). |
+| `C ` | Immediate continuation of an oversized `N` value (v9). See [Value serialization](#value-serialization). |
| `O cfg ` | Observer configuration in force (v3). Written whenever the state's observer knobs differ from what this stream last announced, before its next `L`/`A`/`N`/`O win` record. See [Observer Configuration](#observer-configuration-10441045). |
### Stream identity and correspondence (v5, #1286)
@@ -243,8 +243,12 @@ identity: `what is x at N`, `e.line` and error headers.
`N` records are written with full fidelity so they can be parsed back
into real values on replay:
-- Numbers and `null` are written verbatim; a bool is written `true` or
- `false` and read back as a bool (format v6, #1637).
+- Ints are exact signed decimal digits; floats retain a decimal point or
+ exponent (`3.0`, `-0.0`, `1e+300`) so replay preserves the numeric kind
+ (format v9, #1758). An out-of-range int digit token is malformed rather
+ than rounded through a double. Version 6 tapes are refused by the format
+ check; re-record them with this binary. `null` is written verbatim; a bool
+ is written `true` or `false` and read back as a bool (#1637).
- Strings are double-quoted; `\"`, `\\`, `\n`, `\r` are escaped, other
control/non-printable bytes become `\xNN`.
- Lists and dicts are emitted recursively: `[1, 2, 3]`,
@@ -291,7 +295,7 @@ perspective lands on the tape as an `N` record:
otherwise-impossible result shapes and re-raises the
identical error without live filesystem access. The fixed-cap readers
`read_text`, `read_bytes`, and `read_bytes_buf` instead record an over-cap
- file's observed size as a `VAL_NUM` `N` record and reconstruct the error
+ file's observed size as a `VAL_INT` `N` record and reconstruct the error
containing both size and active cap. Empty files remain ordinary successful
empty string/list/buffer records.
Mutating file builtins return their taped success value or re-raise their
@@ -378,7 +382,7 @@ perspective lands on the tape as an `N` record:
microphone; the recorded device id and sample buffers (the `b[…]`
encoding) are served instead. `audio_capture_read` retains its 2048-sample
per-call device-read bound; those values fit one `N` record without needing
- v8 continuation records. Drain loops
+ v9 continuation records. Drain loops
("read until empty") replay faithfully: one record per call, empties
included. (`audio_capture_close` is deterministic — always `null` —
and untraced; under replay it is a no-op because no device was
@@ -812,11 +816,18 @@ everywhere else in the runtime (version-and-reject, never migrate).
answer (`file_exists`, `is_dir`, `mkdir`, ...) as `1`/`0`, so replaying it
on a v6 binary would hand a number where the program now gets a bool; it is
refused instead (covered by the existing replay kind-validation cases in
- `tests/test_replay.sh`). **v7** (#1885) adds metadata plus bounded chunk records for `import` and
+ `tests/test_replay.sh`). **v7** (#1885) added metadata plus bounded chunk records for `import` and
`load_file` source, making module acquisition replayable without live reads.
- **v8** (#1816) adds length-framed `N` values and `C` continuation records so
- answers larger than 64 KiB round-trip without truncation.
- Within a v8 tape, replay also refuses a recorded value of a kind its builtin cannot
+ Main's **v8** (#1816) adds length-framed `N` values and `C` continuation
+ records for answers larger than 64 KiB. The Phase 2 bank independently
+ used v8 for exact int digit tokens, integral-float `.0` markers (#1758),
+ and chunked module-source records (#1885).
+ **v9** combines both v8 contracts: exact numeric kinds, replayable module
+ sources, and length-framed oversized answers. An old main v8 numeric
+ digit token denotes a float, whereas the Phase 2 reader gives it int kind,
+ so either old v8 format is refused rather than silently reinterpreted.
+ Every older format is refused by replay, stepping and DAP.
+ Within a current-format tape, replay also refuses a recorded value of a kind its builtin cannot
return, with exit 3 and a message naming the record, the builtin, the kinds
it returns and the tape version. Every taped builtin declares its return
kinds in one table (`k_tape_kinds` in `src/trace.c`; for example
diff --git a/docs/design/phase2-inventory.txt b/docs/design/phase2-inventory.txt
new file mode 100644
index 000000000..35386caa2
--- /dev/null
+++ b/docs/design/phase2-inventory.txt
@@ -0,0 +1,2923 @@
+# docs/design/phase2-inventory.txt -- generated by tools/phase2_inventory.py; do not edit.
+# Population: VOCAB token occurrences in src/*.c src/*.h, comments and literals stripped.
+examined=2206 classified=2206 unclassified=0
+files=67 tokens=117 role_rules=8 derived_accessors=21 wrappers=8
+
+# Retired (required absent): buf_num_arg -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): buffer_read_byte -- R1.6e: gfx_fb and ppu_render_frame use buffer_byte_at
+# Retired (required absent): buffer_read_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): dict_value_get_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): dict_value_rewrite_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): dict_value_set_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): eigs_elem_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): eigs_list_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): eigs_num_arg -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): eigs_num_arg_slow -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): eigs_opt_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): eigs_slot_elem_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): eigs_slot_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): eigs_slot_opt_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): finite_num_to_byte -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): list_append_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): list_get_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): list_rewrite_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): list_set_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): make_num -- R1.6z: bridge deleted; every caller migrated
+# Retired (required absent): make_num_permanent -- R1.6z: bridge deleted; every caller migrated
+## Per category
+(a) type tests 503 CASE=61 DATA=4 TEST=435 YIELD=3
+(b) payload reads 615 RAW=133 READ=482
+(c) producers 536 GUARD=79 PARSE=3 PRODUCE=428 TAGSET=9 WRITE=17
+(d) JIT emitters and guards 125 EMIT=87 GUARD=1 PRODUCE=6 RAW=27 TAGSET=1 TEST=2 WRITE=1
+(e) number formatting 23 CASE=2 FORMAT=4 PARSE=1 RAW=8 READ=3 TEST=5
+(f) equality, ordering and hashing 51 CASE=2 PRODUCE=4 RAW=15 READ=7 TEST=23
+(g) tape encode and decode 68 CASE=4 DATA=5 FORMAT=6 PARSE=7 PRODUCE=8 RAW=31 READ=2 TEST=5
+(h) JSON, store and db encoders 89 CASE=4 FORMAT=7 PARSE=5 PRODUCE=22 RAW=8 READ=24 TEST=19
+(i) observer numeric paths 112 CASE=6 FORMAT=1 GUARD=1 PARSE=1 PRODUCE=35 RAW=26 READ=6 TEST=36
+(j) embed API 84 CASE=2 DATA=2 PRODUCE=2 RAW=8 READ=60 TEST=6 WRITE=4
+
+## Per token
+make_int 229
+val_is_number 184
+make_float 181
+VAL_INT 163
+VAL_FLOAT 156
+SLOT_F64_RAW 75
+eigs_float_arg 73
+num_guard 64
+VAL_F64_RAW 62
+number 59
+eigs_value_as_num 57
+VAL_I64_RAW 56
+AST_NUM 51
+eigs_list_float 47
+slot_from_f64 42
+slot_is_f64 42
+eigs_int_arg_range 38
+eigs_int_arg 35
+slot_is_smi 32
+eigs_slot_float 27
+buffer_read_f64 26
+eigs_number_arg 24
+slot_f64_value 23
+eigs_list_int_range 21
+slot_from_int 19
+eigs_num_text 18
+strtod 16
+slot_smi_val 15
+list_set_f64 14
+eigs_list_int 13
+num_to_i64_exact 13
+slot_int_value 13
+EIGS_NUM_MAX 11
+eigs_magnitude_arg 11
+NUM_REUSE 10
+buffer_byte_at 10
+eigs_geom_shape 10
+eigs_slot_opt_float 9
+eigs_byte_arg 8
+eigs_bounded_arg 7
+eigs_list_bounded 6
+eigs_list_magnitude 6
+eigs_opt_int_range 6
+eigs_slot_int 6
+num_guard_named 6
+buffer_index_at 5
+eigs_slot_int_range 5
+eigs_slot_magnitude 5
+VAL_F64_OFFSET 4
+buf_float_arg 4
+dict_value_rewrite_f64 4
+eigs_elem_byte 4
+eigs_opt_magnitude 4
+eigs_elem_bounded 3
+eigs_elem_float 3
+eigs_elem_int 3
+eigs_elem_int_range 3
+eigs_elem_magnitude 3
+eigs_elem_number 3
+eigs_opt_bounded 3
+eigs_opt_byte 3
+eigs_opt_float 3
+eigs_opt_int 3
+eigs_opt_number 3
+eigs_slot_byte 3
+eigs_slot_elem_byte 3
+eigs_slot_elem_number 3
+list_get_f64 3
+list_rewrite_f64 3
+list_set_int 3
+dict_value_rewrite_int 2
+eigs_list_byte 2
+eigs_list_number 2
+eigs_slot_bounded 2
+eigs_slot_number 2
+eigs_slot_opt_int_range 2
+f64_ 2
+list_rewrite_int 2
+make_float_permanent 2
+atof 1
+d_ 1
+dict_value_get_f64 1
+dict_value_get_int 1
+dict_value_set_f64 1
+dict_value_set_int 1
+eigs_slot_elem_bounded 1
+eigs_slot_elem_float 1
+eigs_slot_elem_int 1
+eigs_slot_elem_int_range 1
+eigs_slot_elem_magnitude 1
+eigs_slot_opt_bounded 1
+eigs_slot_opt_byte 1
+eigs_slot_opt_int 1
+eigs_slot_opt_magnitude 1
+eigs_slot_opt_number 1
+i64_ 1
+list_append_f64 1
+list_append_int 1
+list_get_int 1
+buf_num_arg 0
+buffer_read_byte 0
+buffer_read_num 0
+dict_value_get_num 0
+dict_value_rewrite_num 0
+dict_value_set_num 0
+eigs_elem_num 0
+eigs_list_num 0
+eigs_num_arg 0
+eigs_num_arg_slow 0
+eigs_opt_num 0
+eigs_slot_elem_num 0
+eigs_slot_num 0
+eigs_slot_opt_num 0
+finite_num_to_byte 0
+list_append_num 0
+list_get_num 0
+list_rewrite_num 0
+list_set_num 0
+make_num 0
+make_num_permanent 0
+data.number (AST) 3
+
+## Per file (category counts)
+builtins.c 323 a=84 b=75 c=115 e=4 f=13 h=22 i=10
+vm.c 320 a=74 b=102 c=80 e=2 f=10 i=52
+ext_gfx.c 260 a=100 b=73 c=87
+eigenscript.c 207 a=29 b=83 c=25 e=7 f=16 i=47
+builtins_tensor.c 194 a=53 b=58 c=83
+container.h 122 a=25 b=60 c=37
+builtins_buf.c 105 a=33 b=33 c=39
+jit.c 94 d=94
+eigenscript.h 63 a=6 b=41 c=15 e=1
+trace.c 63 g=63
+embed_smoke.c 60 j=60
+ext_store.c 58 h=58
+builtins_host.c 54 a=22 b=19 c=13
+jit_smoke.c 31 d=31
+lint.c 29 a=29
+chunk.c 25 a=4 e=9 f=12
+compiler.c 24 a=12 b=8 c=2 i=2
+value_slot.h 23 a=8 b=11 c=4
+ext_net.c 22 a=4 b=11 c=7
+eigs_embed.c 17 j=17
+model_infer.c 16 a=2 b=6 c=8
+ext_http.c 14 a=3 b=4 c=7
+builtins_int.c 11 b=4 c=7
+lexer.c 11 b=10 c=1
+model_io.c 11 a=5 b=5 h=1
+ext_db.c 8 h=8
+model_train.c 8 a=2 b=4 c=1 i=1
+embed_concurrent.c 6 j=6
+task.c 6 a=1 b=1 c=4
+parser.c 5 a=2 b=2 c=1
+tape_read.c 5 g=5
+eigenlsp.c 3 a=2 b=1
+lint_host.c 3 a=3
+main.c 3 b=3
+eigs_embed.h 1 j=1
+vm.h 1 b=1
+
+## Executors (arms per opcode; roles: interp jit_scan jit_emit leaf_scan leaf_exec)
+OP_ADD interp jit_scan jit_emit leaf_scan leaf_exec
+OP_BAND interp - jit_emit - -
+OP_BINARY_LINE interp jit_scan jit_emit leaf_scan leaf_exec
+OP_BINARY_LINE_END interp jit_scan jit_emit leaf_scan leaf_exec
+OP_BNOT interp - jit_emit - -
+OP_BOR interp - jit_emit - -
+OP_BXOR interp - jit_emit - -
+OP_CALL interp jit_scan jit_emit - -
+OP_CONST interp jit_scan jit_emit leaf_scan leaf_exec
+OP_DIV interp jit_scan jit_emit - -
+OP_DOT_GET interp jit_scan jit_emit - -
+OP_DOT_SET interp jit_scan jit_emit - -
+OP_DUP interp jit_scan jit_emit - -
+OP_DUP2 interp jit_scan jit_emit - -
+OP_EQ interp jit_scan jit_emit - -
+OP_FALSE interp jit_scan jit_emit - -
+OP_GE interp jit_scan jit_emit - -
+OP_GET_LOCAL interp jit_scan jit_emit leaf_scan leaf_exec
+OP_GET_NAME interp jit_scan jit_emit - -
+OP_GT interp jit_scan jit_emit - -
+OP_INDEX_GET interp jit_scan jit_emit leaf_scan leaf_exec
+OP_INDEX_SET interp jit_scan jit_emit - -
+OP_ITER_NEXT interp jit_scan jit_emit - -
+OP_JUMP interp jit_scan jit_emit - -
+OP_JUMP_BACK interp jit_scan jit_emit - -
+OP_JUMP_IF_FALSE interp jit_scan jit_emit - -
+OP_JUMP_IF_FALSE_PEEK interp jit_scan jit_emit - -
+OP_JUMP_IF_TRUE interp jit_scan jit_emit - -
+OP_JUMP_IF_TRUE_PEEK interp jit_scan jit_emit - -
+OP_LE interp jit_scan jit_emit - -
+OP_LINE interp jit_scan - leaf_scan leaf_exec
+OP_LOCAL_DOT_GET interp jit_scan jit_emit leaf_scan leaf_exec
+OP_LOCAL_DOT_SET interp jit_scan jit_emit - -
+OP_LOCAL_IDX_DOT_GET interp jit_scan jit_emit - -
+OP_LOCAL_IDX_GET interp jit_scan jit_emit - -
+OP_LOOP_CAP_NAMED interp jit_scan jit_emit - -
+OP_LOOP_ENTER_NAMED interp jit_scan jit_emit - -
+OP_LOOP_EXIT_NAMED interp jit_scan jit_emit - -
+OP_LOOP_STALL_NAMED interp jit_scan jit_emit - -
+OP_LT interp jit_scan jit_emit - -
+OP_MOD interp - jit_emit - -
+OP_MUL interp jit_scan jit_emit leaf_scan leaf_exec
+OP_NE interp jit_scan jit_emit - -
+OP_NEG interp jit_scan jit_emit - -
+OP_NOT interp jit_scan jit_emit - -
+OP_NULL interp jit_scan jit_emit - -
+OP_NUM_ONE interp jit_scan jit_emit leaf_scan leaf_exec
+OP_NUM_ZERO interp jit_scan jit_emit leaf_scan leaf_exec
+OP_OBSERVE_ASSIGN interp jit_scan jit_emit - -
+OP_OBSERVE_ASSIGN_LOCAL interp jit_scan jit_emit - -
+OP_OBSERVE_NAME_POST interp jit_scan jit_emit - -
+OP_POP interp jit_scan jit_emit - -
+OP_REPORT_SLOT interp jit_scan jit_emit - -
+OP_RETURN interp jit_scan jit_emit leaf_scan leaf_exec
+OP_RETURN_NULL interp jit_scan jit_emit - -
+OP_SET_FN_NAME_LOCAL interp jit_scan jit_emit - -
+OP_SET_LOCAL interp jit_scan jit_emit - -
+OP_SET_NAME interp jit_scan jit_emit - -
+OP_SET_NAME_LOCAL interp jit_scan jit_emit - -
+OP_SHL interp - jit_emit - -
+OP_SHR interp - jit_emit - -
+OP_SUB interp jit_scan jit_emit leaf_scan leaf_exec
+OP_TRUE interp jit_scan jit_emit - -
+OP_UNOBSERVED_BEGIN interp jit_scan jit_emit - -
+OP_UNOBSERVED_END interp jit_scan jit_emit - -
+
+## Numeric arms (JIT emitter and leaf evaluator arms holding a numeric site)
+jit_emit OP_ADD OP_ADD OP_DIV OP_MUL OP_SUB
+jit_emit OP_BAND OP_BAND OP_BOR OP_BXOR OP_SHL OP_SHR
+jit_emit OP_BINARY_LINE OP_BINARY_LINE
+jit_emit OP_CONST OP_CONST
+jit_emit OP_EQ OP_EQ OP_GE OP_GT OP_LE OP_LT OP_NE
+jit_emit OP_INDEX_SET OP_INDEX_SET
+jit_emit OP_JUMP_IF_FALSE OP_JUMP_IF_FALSE OP_JUMP_IF_FALSE_PEEK OP_JUMP_IF_TRUE OP_JUMP_IF_TRUE_PEEK
+jit_emit OP_LOCAL_DOT_GET OP_LOCAL_DOT_GET
+jit_emit OP_LOCAL_DOT_SET OP_LOCAL_DOT_SET
+jit_emit OP_MOD OP_MOD
+jit_emit OP_NEG OP_BNOT OP_NEG OP_NOT
+jit_emit OP_NOT OP_NOT
+jit_emit OP_NULL OP_FALSE OP_NULL OP_NUM_ONE OP_NUM_ZERO OP_TRUE
+jit_emit OP_SET_LOCAL OP_SET_LOCAL
+jit_emit OP_SET_NAME OP_SET_FN_NAME_LOCAL OP_SET_NAME OP_SET_NAME_LOCAL
+leaf_exec OP_ADD|OP_SUB|OP_MUL OP_ADD OP_MUL OP_SUB
+leaf_exec OP_CONST OP_CONST
+leaf_exec OP_INDEX_GET OP_INDEX_GET
+leaf_exec OP_NUM_ONE OP_NUM_ONE
+leaf_exec OP_NUM_ZERO OP_NUM_ZERO
+
+## Slot classifiers (R1.1 worklist)
+D1=65 D2=19 D3=26 T=69 dispositions=110 missing=0 stale=0
+D1 container.h:dict_value_get_f64 guarded slot_f64_value=1
+D1 container.h:dict_value_get_int smi-arm slot_int_value=1
+D1 container.h:dict_value_get_ref smi-arm eigs_slot_ref=1 slot_is_f64=1 slot_is_smi=1
+D1 container.h:dict_value_get_return smi-arm eigs_slot_ref=1 slot_is_f64=1 slot_is_smi=1
+D1 container.h:eigs_slot_box smi-arm eigs_slot_ref=1 slot_is_f64=1 slot_is_smi=1
+D1 container.h:eigs_slot_store_borrow guarded slot_as_ptr=1 slot_is_f64=1 slot_is_heap=1
+D1 container.h:eigs_slot_store_owned guarded slot_as_ptr=1 slot_is_f64=1 slot_is_heap=1
+D1 container.h:list_get_f64 guarded slot_f64_value=1
+D1 container.h:list_get_int smi-arm slot_int_value=1
+D1 container.h:list_get_ref smi-arm eigs_slot_ref=1 slot_is_f64=1 slot_is_smi=1
+D1 container.h:list_get_return smi-arm eigs_slot_ref=1 slot_is_f64=1 slot_is_smi=1
+D1 container.h:slot_type smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_heap=1 slot_is_null=1 slot_is_smi=1 slot_type=1
+D1 container.h:slot_view smi-arm eigs_slot_ref=1 slot_f64_value=1 slot_is_f64=1 slot_is_smi=1 slot_smi_val=1 slot_type=1
+D1 eigenscript.c:ent_child_slot smi-arm eigs_slot_ref=1 slot_is_f64=1 slot_is_smi=1 slot_smi_val=1
+D1 eigenscript.c:module_ns_project guarded slot_is_f64=2
+D1 eigenscript.c:observer_entropy_now smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_ptr=1 slot_is_smi=1 slot_smi_val=1
+D1 eigenscript.c:slot_to_value smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_heap=1 slot_is_null=1 slot_is_smi=1 slot_smi_val=1
+D1 embed_smoke.c:main guarded slot_is_bool=1 slot_is_smi=1 slot_smi_val=1
+D1 ext_store.c:store_json_encode_slot smi-arm eigs_slot_ref=1 slot_is_f64=1 slot_is_smi=1 slot_smi_val=1
+D1 trace.c:trace_source_replay guarded list_elem_type=5 slot_f64_value=1 slot_int_value=1
+D1 trace.c:write_slot smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_heap=1 slot_is_null=1 slot_is_smi=1 slot_smi_val=1
+D1 value_slot.h:slot_f64_value guarded slot_as_ptr=1 slot_f64_value=1 slot_is_f64=1 slot_is_heap=1
+D1 value_slot.h:slot_int_value smi-arm slot_as_ptr=1 slot_int_value=1 slot_is_heap=1 slot_is_smi=1 slot_smi_val=1
+D1 value_slot.h:slot_is_f64 guarded SLOT_QNAN_MASK=2 slot_is_f64=1
+D1 value_slot.h:slot_is_smi smi-arm SLOT_TAG_MASK=1 TAG_SMI=1 slot_is_smi=1
+D1 value_slot.h:slot_smi_val smi-arm SLOT_PAYLOAD_MASK=1 SLOT_SMI_SIGN=2 slot_smi_val=1
+D1 value_slot.h:slot_truthy_immediate smi-arm SLOT_FALSE_BITS=1 SLOT_TRUE_BITS=1 slot_is_f64=1 slot_is_null=1 slot_is_smi=1 slot_smi_val=1
+D1 vm.c:#define ARITH_FAST guarded slot_as_ptr=2 slot_f64_value=2 slot_is_ptr=2
+D1 vm.c:#define INT_ARITH smi-arm slot_int_value=2
+D1 vm.c:#define INT_BINOP_R smi-arm slot_f64_value=1
+D1 vm.c:#define NUM_CMP smi-arm slot_as_ptr=2 slot_f64_value=2 slot_is_ptr=2
+D1 vm.c:#define READ_SLICE_BOUND smi-arm slot_as_ptr=1 slot_is_f64=1 slot_is_null=1 slot_is_ptr=1 slot_is_smi=1 slot_smi_val=1
+D1 vm.c:jit_helper_dot_set guarded slot_as_ptr=1 slot_is_f64=1 slot_is_ptr=1
+D1 vm.c:jit_helper_index_get guarded slot_as_ptr=1 slot_is_f64=1 slot_is_null=1 slot_is_ptr=1
+D1 vm.c:jit_helper_index_set guarded slot_as_ptr=1 slot_is_f64=3 slot_is_ptr=1
+D1 vm.c:jit_helper_iter_next guarded slot_as_ptr=1 slot_is_ptr=1 slot_is_smi=1
+D1 vm.c:jit_helper_local_dot_set guarded slot_is_f64=1
+D1 vm.c:loop_counter_load smi-arm slot_int_value=1
+D1 vm.c:loop_counter_store smi-arm SLOT_SMI_MAX=1 SLOT_SMI_MIN=1 slot_is_smi=1
+D1 vm.c:loop_iter_store smi-arm SLOT_SMI_MAX=1 SLOT_SMI_MIN=1 slot_is_smi=1
+D1 vm.c:obs_dump_scope smi-arm slot_as_ptr=3 slot_is_bool=1 slot_is_f64=1 slot_is_ptr=3 slot_is_smi=1 slot_smi_val=1
+D1 vm.c:obs_dump_value smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_null=1 slot_is_ptr=1 slot_is_smi=1 slot_smi_val=1
+D1 vm.c:obs_slot_num smi-arm slot_as_ptr=2 slot_is_f64=1 slot_is_heap=1 slot_is_smi=1 slot_smi_val=1
+D1 vm.c:obs_slot_observable refuse slot_is_bool=1 slot_is_f64=1 slot_is_null=1 slot_is_ptr=1
+D1 vm.c:slot_bridge_unwrap smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_null=1 slot_is_smi=1
+D1 vm.c:slot_truthy smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_heap=1 slot_is_null=1 slot_is_smi=1 slot_smi_val=1
+D1 vm.c:slot_type_name smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_null=1 slot_is_ptr=1 slot_is_smi=1
+D1 vm.c:vm_bit_operand smi-arm slot_f64_value=1 slot_int_value=1
+D1 vm.c:vm_iter_index smi-arm slot_f64_value=1 slot_int_value=1
+D1 vm.c:vm_iter_limit smi-arm slot_int_value=1
+D1 vm.c:vm_leaf_accessor_exec/OP_ADD|OP_SUB|OP_MUL guarded slot_f64_value=2
+D1 vm.c:vm_leaf_accessor_exec/OP_INDEX_GET guarded slot_as_ptr=1 slot_f64_value=1 slot_is_ptr=1
+D1 vm.c:vm_number_compare smi-arm slot_f64_value=2 slot_int_value=2
+D1 vm.c:vm_run_ex/CASE(BNOT) smi-arm slot_f64_value=1
+D1 vm.c:vm_run_ex/CASE(DOT_SET) guarded slot_as_ptr=1 slot_is_f64=1 slot_is_ptr=1
+D1 vm.c:vm_run_ex/CASE(EQ) guarded slot_f64_value=2 slot_is_smi=2
+D1 vm.c:vm_run_ex/CASE(INDEX_GET) guarded slot_as_ptr=1 slot_is_f64=1 slot_is_ptr=1
+D1 vm.c:vm_run_ex/CASE(INDEX_SET) guarded slot_as_ptr=1 slot_is_f64=3 slot_is_ptr=1
+D1 vm.c:vm_run_ex/CASE(ITER_NEXT) guarded slot_is_smi=1
+D1 vm.c:vm_run_ex/CASE(LOCAL_DOT_SET) guarded slot_is_f64=1
+D1 vm.c:vm_run_ex/CASE(LOCAL_IDX_DOT_SET) guarded slot_is_f64=1
+D1 vm.c:vm_run_ex/CASE(NE) guarded slot_f64_value=2 slot_is_smi=2
+D1 vm.c:vm_run_ex/CASE(NEG) smi-arm slot_as_ptr=1 slot_f64_value=1 slot_int_value=1 slot_is_ptr=1
+D1 vm.c:vm_slot_lift smi-arm slot_as_ptr=1 slot_is_bool=1 slot_is_f64=1 slot_is_null=1 slot_is_ptr=1 slot_is_smi=1
+D1 vm.c:vm_store_local_slot guarded slot_as_ptr=1 slot_is_f64=1 slot_is_heap=1
+D2 builtins.c:bool_gate_name guarded slot_as_ptr=1 slot_is_ptr=1
+D2 builtins.c:sort_cmp_str guarded slot_as_ptr=2
+D2 builtins_host.c:ls_entry_cmp guarded slot_as_ptr=2
+D2 container.h:eigs_slot_ref refuse eigs_slot_ref=1 slot_as_ptr=1 slot_is_bool=1 slot_is_heap=1 slot_is_null=1
+D2 eigenscript.c:#define GC_EDGE_TABLE guarded slot_as_ptr=6 slot_is_ptr=3
+D2 eigenscript.c:env_get_hashed guarded slot_as_ptr=1 slot_is_ptr=1
+D2 eigenscript.c:env_get_local_hashed guarded slot_as_ptr=1 slot_is_ptr=1
+D2 eigenscript.c:gc_collect_at_exit guarded slot_as_ptr=1 slot_is_ptr=1
+D2 value_slot.h:#define slot_as_heap refuse slot_as_heap=1 slot_as_ptr=1
+D2 value_slot.h:slot_as_ptr refuse SLOT_PAYLOAD_MASK=1 slot_as_ptr=1 slot_is_heap=1
+D2 vm.c:obs_slot_value guarded slot_as_ptr=1 slot_is_bool=1
+D2 vm.c:vm_borrow_scan guarded slot_as_ptr=2 slot_is_heap=2
+D2 vm.c:vm_builtin_name_for guarded slot_as_ptr=1 slot_is_heap=1
+D2 vm.c:vm_leaf_accessor_exec/OP_LOCAL_DOT_GET guarded slot_as_ptr=1 slot_is_null=1 slot_is_ptr=1
+D2 vm.c:vm_local_lift guarded slot_as_ptr=1 slot_is_null=1 slot_is_ptr=1
+D2 vm.c:vm_run_ex/CASE(DESTRUCTURE_UNPACK) guarded slot_as_ptr=2 slot_is_ptr=1
+D2 vm.c:vm_run_ex/CASE(DISPATCH) smi-arm slot_as_ptr=1 slot_is_ptr=1 slot_type=1
+D2 vm.c:vm_run_ex/CASE(SLICE_GET) guarded slot_as_ptr=1 slot_is_ptr=1
+D2 vm.c:vm_slot_value_opaque guarded slot_as_ptr=1 slot_is_ptr=1
+D3 container.h:eigs_container_slot not-a-classifier SLOT_SMI_MAX=1 SLOT_SMI_MIN=1
+D3 container.h:eigs_slot_adopt guarded slot_is_heap=1
+D3 eigenscript.c:slot_from_value not-a-classifier SLOT_SMI_MAX=1 SLOT_SMI_MIN=1
+D3 jit.c:jit_compile_to_thunk/OP_EQ not-a-classifier SLOT_FALSE_BITS=1 SLOT_TRUE_BITS=1
+D3 jit.c:jit_compile_to_thunk/OP_NEG not-a-classifier SLOT_FALSE_BITS=1 SLOT_TRUE_BITS=1
+D3 jit.c:jit_compile_to_thunk/OP_NULL not-a-classifier SLOT_FALSE_BITS=1 SLOT_TRUE_BITS=1
+D3 jit_smoke.c:run_bool_cases not-a-classifier SLOT_FALSE_BITS=2 SLOT_TRUE_BITS=3
+D3 jit_smoke.c:run_index_bail_cases guarded slot_is_null=1
+D3 value_slot.h:slot_decref guarded SLOT_PAYLOAD_MASK=1 SLOT_QNAN_MASK=2 SLOT_TAG_MASK=1 TAG_HEAP=1
+D3 value_slot.h:slot_false not-a-classifier SLOT_FALSE_BITS=1
+D3 value_slot.h:slot_from_bool not-a-classifier TAG_BOOL=1
+D3 value_slot.h:slot_from_heap guarded SLOT_NULL_BITS=1 SLOT_PAYLOAD_MASK=1 TAG_HEAP=1
+D3 value_slot.h:slot_from_int not-a-classifier SLOT_PAYLOAD_MASK=1 SLOT_SMI_MAX=1 SLOT_SMI_MIN=1 TAG_SMI=1
+D3 value_slot.h:slot_incref guarded SLOT_PAYLOAD_MASK=1 SLOT_QNAN_MASK=2 SLOT_TAG_MASK=1 TAG_HEAP=1
+D3 value_slot.h:slot_is_bool guarded SLOT_TAG_MASK=1 TAG_BOOL=1 slot_is_bool=1
+D3 value_slot.h:slot_is_heap guarded SLOT_TAG_MASK=1 TAG_HEAP=1 slot_is_heap=1
+D3 value_slot.h:slot_is_null guarded SLOT_NULL_BITS=1 slot_is_null=1
+D3 value_slot.h:slot_is_ptr guarded SLOT_TAG_MASK=1 TAG_HEAP=1 slot_is_ptr=1
+D3 value_slot.h:slot_null not-a-classifier SLOT_NULL_BITS=1
+D3 value_slot.h:slot_true not-a-classifier SLOT_TRUE_BITS=1
+D3 vm.c:jit_helper_dot_get guarded slot_is_null=1
+D3 vm.c:jit_helper_local_idx_dot_get guarded slot_is_null=1
+D3 vm.c:vm_local_is_null guarded slot_is_null=1
+D3 vm.c:vm_run_ex/CASE(DOT_GET) guarded slot_is_null=1
+D3 vm.c:vm_run_ex/CASE(INT_CONST) not-a-classifier SLOT_SMI_MAX=1 SLOT_SMI_MIN=1
+D3 vm.c:vm_run_ex/CASE(LOCAL_IDX_DOT_GET) guarded slot_is_null=1
+
+## Builtins
+registered=388 numeric=309 classified=309 unclassified=0 stale=0
+classes: float-producing=58 int-preserving=37 int-requiring=161 other=53
+LOSS (37): abs add ceil db_execute db_query_json db_query_value floor hex http_post json_build json_decode json_encode json_path max min monotonic_ns multiply negative num num_copy random_int round scan_int_tokens scan_ints seed_random shared_incr sign_extend sort sort_by store_delete store_get store_put store_query store_update subtract sum task_sched_seed
+Q7: geometry=9 bounded=13 magnitude=13
+abs builtin_abs int-preserving LOSS - eigs_number_arg,make_float,make_int,val_is_number
+acos builtin_acos float-producing - - eigs_float_arg,make_float,val_is_number
+add builtin_tensor_add int-preserving LOSS - eigs_float_arg,eigs_number_arg,make_float,make_int,num_guard,val_is_number
+append builtin_append other - - VAL_FLOAT
+asin builtin_asin float-producing - - eigs_float_arg,make_float,val_is_number
+assert builtin_assert other - - eigs_num_text
+atan builtin_atan float-producing - - eigs_float_arg,make_float,val_is_number
+atan2 builtin_atan2 float-producing - - eigs_float_arg,make_float,val_is_number
+audio_capture_open builtin_audio_capture_open int-requiring - - VAL_FLOAT,VAL_INT,eigs_list_int_range,make_int
+audio_envelope builtin_audio_envelope float-producing - magnitude VAL_FLOAT,VAL_INT,eigs_list_float,eigs_slot_magnitude,make_float,slot_from_f64
+audio_gain builtin_audio_gain float-producing - - VAL_FLOAT,VAL_INT,eigs_list_float,make_float
+audio_mix builtin_audio_mix float-producing - - VAL_FLOAT,VAL_INT,eigs_list_float,make_float
+audio_music_play builtin_audio_music_play int-requiring - - VAL_FLOAT,VAL_INT,eigs_list_int_range
+audio_music_volume builtin_audio_music_volume other - bounded VAL_FLOAT,VAL_INT,eigs_bounded_arg,eigs_list_bounded,val_is_number
+audio_noise builtin_audio_noise float-producing - magnitude VAL_FLOAT,VAL_INT,eigs_list_float,eigs_slot_magnitude,make_float,slot_from_f64
+audio_open builtin_audio_open int-requiring - - VAL_FLOAT,VAL_INT,eigs_list_int_range,make_int
+audio_pause builtin_audio_pause int-requiring - - eigs_int_arg_range,val_is_number
+audio_play builtin_audio_play other - bounded VAL_FLOAT,VAL_INT,buffer_read_f64,eigs_slot_bounded,eigs_slot_float,make_int,slot_from_f64
+audio_play_loop builtin_audio_play_loop int-requiring - bounded VAL_FLOAT,VAL_INT,buffer_read_f64,eigs_list_int_range,eigs_slot_bounded,eigs_slot_float,make_int,slot_from_f64
+audio_queue_size builtin_audio_queue_size int-preserving - - make_int
+audio_saw builtin_audio_saw float-producing - magnitude VAL_FLOAT,VAL_INT,eigs_list_float,eigs_slot_magnitude,make_float,slot_from_f64
+audio_sine builtin_audio_sine float-producing - magnitude VAL_FLOAT,VAL_INT,eigs_list_float,eigs_slot_magnitude,make_float,slot_from_f64
+audio_square builtin_audio_square float-producing - magnitude VAL_FLOAT,VAL_INT,eigs_list_float,eigs_slot_magnitude,make_float,slot_from_f64
+audio_stop builtin_audio_stop int-requiring - - eigs_int_arg_range,make_int,val_is_number
+audio_stream_open builtin_audio_stream_open int-requiring - - VAL_FLOAT,VAL_INT,eigs_list_int_range,make_int
+audio_stream_push builtin_audio_stream_push other - bounded VAL_FLOAT,VAL_INT,buffer_read_f64,eigs_slot_bounded,eigs_slot_float,slot_from_f64
+audio_stream_queued builtin_audio_stream_queued int-preserving - - make_int
+audio_sweep builtin_audio_sweep float-producing - magnitude VAL_FLOAT,VAL_INT,eigs_list_float,eigs_list_int,eigs_slot_magnitude,make_float,slot_from_f64
+audio_volume builtin_audio_volume int-requiring - bounded VAL_FLOAT,VAL_INT,eigs_list_bounded,eigs_list_int_range,make_int
+bit_and builtin_bit_and int-requiring - - eigs_int_arg,make_int,val_is_number
+bit_not builtin_bit_not int-requiring - - eigs_int_arg,make_int,val_is_number
+bit_or builtin_bit_or int-requiring - - eigs_int_arg,make_int,val_is_number
+bit_shl builtin_bit_shift_left int-requiring - - eigs_int_arg,make_int,val_is_number
+bit_shr builtin_bit_shift_right int-requiring - - eigs_int_arg,make_int,val_is_number
+bit_xor builtin_bit_xor int-requiring - - eigs_int_arg,make_int,val_is_number
+buf_copy builtin_buf_copy int-requiring - - VAL_FLOAT,VAL_INT,eigs_slot_int
+buf_deinterleave builtin_buf_deinterleave int-requiring - - VAL_FLOAT,VAL_INT,eigs_int_arg,eigs_slot_int,val_is_number
+buf_dot builtin_buf_dot float-producing - - VAL_FLOAT,VAL_INT,buffer_read_f64,eigs_slot_int,make_float,num_guard
+buf_fill builtin_buf_fill float-producing - - VAL_FLOAT,VAL_INT,buf_float_arg,eigs_slot_float,eigs_slot_int
+buf_from_list builtin_buf_from_list float-producing - - VAL_FLOAT,VAL_INT,eigs_slot_float
+buf_from_pcm16le builtin_buf_from_pcm16le int-requiring - - VAL_FLOAT,VAL_INT,buffer_byte_at,eigs_slot_int,num_guard
+buf_get builtin_buf_get int-requiring - - buffer_read_f64,eigs_list_int,make_float
+buf_len builtin_buf_len int-preserving - - make_int
+buf_mix builtin_buf_mix float-producing - - VAL_FLOAT,VAL_INT,buf_float_arg,eigs_slot_float,eigs_slot_int,num_guard
+buf_peak builtin_buf_peak float-producing - - VAL_FLOAT,VAL_INT,buffer_read_f64,eigs_slot_int,make_float
+buf_resample_linear builtin_buf_resample_linear float-producing - - VAL_FLOAT,VAL_INT,eigs_slot_int,num_guard
+buf_scale_range builtin_buf_scale_range float-producing - - VAL_FLOAT,VAL_INT,buf_float_arg,eigs_slot_float,eigs_slot_int,num_guard
+buf_set builtin_buf_set int-requiring - - VAL_FLOAT,VAL_INT,eigs_list_float,eigs_list_int
+buf_to_pcm16le builtin_buf_to_pcm16le other - bounded VAL_FLOAT,VAL_INT,buffer_read_f64,eigs_slot_int,num_guard
+buffer builtin_buffer int-requiring - - VAL_FLOAT,VAL_INT,eigs_int_arg_range,eigs_list_int_range,val_is_number
+build_corpus builtin_build_corpus int-requiring - - VAL_FLOAT,VAL_INT,eigs_int_arg_range,eigs_opt_int_range,make_int,number,val_is_number
+ceil builtin_ceil int-preserving LOSS - eigs_number_arg,make_float,make_int,num_to_i64_exact,val_is_number
+channel builtin_channel int-preserving - - make_int
+channel_closed builtin_channel_closed int-requiring - - eigs_int_arg_range,val_is_number
+char_at builtin_char_at int-requiring - - eigs_int_arg_range,val_is_number
+chr builtin_chr int-requiring - - eigs_int_arg,val_is_number
+clock_unix builtin_clock_unix float-producing - - make_float
+close_channel builtin_close_channel int-requiring - - eigs_int_arg_range,val_is_number
+copy_into builtin_copy_into int-requiring - - eigs_float_arg,eigs_int_arg_range,val_is_number
+cos builtin_cos float-producing - - eigs_float_arg,make_float,val_is_number
+db_execute builtin_db_execute other LOSS - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_float_arg,eigs_num_text
+db_query_json builtin_db_query_json other LOSS - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_float_arg,eigs_num_text
+db_query_value builtin_db_query_value other LOSS - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_float_arg,eigs_num_text,make_float,make_int,strtod
+dispatch builtin_dispatch int-requiring - - eigs_number_arg,num_to_i64_exact,val_is_number
+divide builtin_tensor_divide float-producing - - eigs_float_arg,eigs_number_arg,make_float,make_int,num_guard,val_is_number
+dot builtin_dot float-producing - - buffer_read_f64,make_float,num_guard
+eigen_eval_loss builtin_eigen_eval_loss float-producing - - VAL_FLOAT,VAL_INT,eigs_list_int_range,eigs_slot_int_range,make_float
+eigen_generate builtin_eigen_generate int-requiring - - eigs_slot_int_range,eigs_slot_opt_float,eigs_slot_opt_int_range,make_int
+eigen_model_load builtin_eigen_model_load other - - eigs_int_arg_range,strtod,val_is_number
+exec_capture builtin_exec_capture other - magnitude VAL_FLOAT,VAL_INT,eigs_list_float,eigs_magnitude_arg,make_float,make_int
+exit builtin_exit int-requiring - - eigs_int_arg_range,val_is_number
+exp builtin_tensor_exp float-producing - - eigs_number_arg,make_float,make_int,num_guard,val_is_number
+f64_from_bytes builtin_f64_from_bytes int-requiring - - buffer_byte_at,eigs_slot_elem_byte,make_float,num_guard_named
+f64_to_bytes builtin_f64_to_bytes other - - eigs_float_arg,make_int,val_is_number
+fill builtin_fill int-requiring - - eigs_int_arg_range
+floor builtin_floor int-preserving LOSS - eigs_number_arg,make_float,make_int,num_to_i64_exact,val_is_number
+fround builtin_fround float-producing - - eigs_float_arg,make_float
+gather builtin_tensor_gather int-requiring - - VAL_FLOAT,VAL_INT,buffer_index_at,buffer_read_f64,eigs_float_arg,eigs_int_arg,eigs_slot_elem_number,eigs_slot_int,make_float,make_int,val_is_number
+get_at builtin_get_at int-requiring - - buffer_read_f64,eigs_int_arg_range,make_float,val_is_number
+get_observer_thresholds builtin_get_observer_thresholds float-producing - - make_float
+gfx_circle builtin_gfx_circle other - geometry,bounded VAL_FLOAT,VAL_INT,eigs_geom_shape,eigs_list_bounded,slot_from_f64
+gfx_clear builtin_gfx_clear other - bounded VAL_FLOAT,VAL_INT,eigs_list_bounded
+gfx_clip builtin_gfx_clip other - geometry VAL_FLOAT,VAL_INT,eigs_geom_shape
+gfx_delay builtin_gfx_delay other - magnitude eigs_magnitude_arg,val_is_number
+gfx_fb builtin_gfx_fb int-requiring - geometry VAL_FLOAT,VAL_INT,buffer_byte_at,eigs_geom_shape,eigs_list_float,eigs_list_int_range,slot_from_f64
+gfx_line builtin_gfx_line other - geometry,bounded VAL_FLOAT,VAL_INT,eigs_geom_shape,eigs_list_bounded
+gfx_open builtin_gfx_open other - magnitude VAL_FLOAT,VAL_INT,eigs_list_magnitude
+gfx_point builtin_gfx_point other - geometry,bounded VAL_FLOAT,VAL_INT,eigs_geom_shape,eigs_list_bounded
+gfx_poll builtin_gfx_poll int-preserving - - make_int
+gfx_read builtin_gfx_read other - geometry VAL_FLOAT,VAL_INT,eigs_geom_shape,eigs_list_float,make_int
+gfx_rect builtin_gfx_rect other - geometry,bounded VAL_FLOAT,VAL_INT,eigs_geom_shape,eigs_list_bounded
+gfx_rrect builtin_gfx_rrect other - geometry,bounded VAL_FLOAT,VAL_INT,eigs_geom_shape,eigs_list_bounded,slot_from_f64
+gfx_text builtin_gfx_text other - geometry,bounded VAL_FLOAT,VAL_INT,eigs_geom_shape,eigs_list_bounded,eigs_list_float,slot_from_f64
+gfx_text_height builtin_gfx_text_height int-preserving - magnitude VAL_FLOAT,VAL_INT,eigs_list_magnitude,eigs_magnitude_arg,make_int,val_is_number
+gfx_text_width builtin_gfx_text_width int-preserving - magnitude VAL_FLOAT,VAL_INT,eigs_list_magnitude,make_int
+gfx_ticks builtin_gfx_ticks int-preserving - - make_int
+hex builtin_hex int-requiring LOSS - eigs_int_arg,eigs_list_int,val_is_number
+http_early_bind builtin_http_early_bind int-requiring - - eigs_int_arg_range,val_is_number
+http_post builtin_http_post other LOSS - eigs_num_text
+http_route builtin_http_route other - - eigs_num_text
+http_route_authed builtin_http_route_authed other - - eigs_num_text
+http_serve builtin_http_serve int-requiring - - eigs_int_arg_range,eigs_num_text
+http_static builtin_http_static other - - eigs_num_text
+i16_checked_add builtin_i16_checked_add int-requiring - - eigs_int_arg_range,make_int
+i16_checked_mul builtin_i16_checked_mul int-requiring - - eigs_int_arg_range,make_int
+i16_checked_sub builtin_i16_checked_sub int-requiring - - eigs_int_arg_range,make_int
+i16_saturating_add builtin_i16_saturating_add int-requiring - - eigs_int_arg_range,make_int
+i16_saturating_mul builtin_i16_saturating_mul int-requiring - - eigs_int_arg_range,make_int
+i16_saturating_sub builtin_i16_saturating_sub int-requiring - - eigs_int_arg_range,make_int
+i16_wrapping_add builtin_i16_wrapping_add int-requiring - - eigs_int_arg_range,make_int
+i16_wrapping_mul builtin_i16_wrapping_mul int-requiring - - eigs_int_arg_range,make_int
+i16_wrapping_sub builtin_i16_wrapping_sub int-requiring - - eigs_int_arg_range,make_int
+i32_checked_add builtin_i32_checked_add int-requiring - - eigs_int_arg_range,make_int
+i32_checked_mul builtin_i32_checked_mul int-requiring - - eigs_int_arg_range,make_int
+i32_checked_sub builtin_i32_checked_sub int-requiring - - eigs_int_arg_range,make_int
+i32_saturating_add builtin_i32_saturating_add int-requiring - - eigs_int_arg_range,make_int
+i32_saturating_mul builtin_i32_saturating_mul int-requiring - - eigs_int_arg_range,make_int
+i32_saturating_sub builtin_i32_saturating_sub int-requiring - - eigs_int_arg_range,make_int
+i32_wrapping_add builtin_i32_wrapping_add int-requiring - - eigs_int_arg_range,make_int
+i32_wrapping_mul builtin_i32_wrapping_mul int-requiring - - eigs_int_arg_range,make_int
+i32_wrapping_sub builtin_i32_wrapping_sub int-requiring - - eigs_int_arg_range,make_int
+i64_checked_add builtin_i64_checked_add int-requiring - - eigs_int_arg_range,make_int
+i64_checked_mul builtin_i64_checked_mul int-requiring - - eigs_int_arg_range,make_int
+i64_checked_sub builtin_i64_checked_sub int-requiring - - eigs_int_arg_range,make_int
+i64_saturating_add builtin_i64_saturating_add int-requiring - - eigs_int_arg_range,make_int
+i64_saturating_mul builtin_i64_saturating_mul int-requiring - - eigs_int_arg_range,make_int
+i64_saturating_sub builtin_i64_saturating_sub int-requiring - - eigs_int_arg_range,make_int
+i64_wrapping_add builtin_i64_wrapping_add int-requiring - - eigs_int_arg_range,make_int
+i64_wrapping_mul builtin_i64_wrapping_mul int-requiring - - eigs_int_arg_range,make_int
+i64_wrapping_sub builtin_i64_wrapping_sub int-requiring - - eigs_int_arg_range,make_int
+i8_checked_add builtin_i8_checked_add int-requiring - - eigs_int_arg_range,make_int
+i8_checked_mul builtin_i8_checked_mul int-requiring - - eigs_int_arg_range,make_int
+i8_checked_sub builtin_i8_checked_sub int-requiring - - eigs_int_arg_range,make_int
+i8_saturating_add builtin_i8_saturating_add int-requiring - - eigs_int_arg_range,make_int
+i8_saturating_mul builtin_i8_saturating_mul int-requiring - - eigs_int_arg_range,make_int
+i8_saturating_sub builtin_i8_saturating_sub int-requiring - - eigs_int_arg_range,make_int
+i8_wrapping_add builtin_i8_wrapping_add int-requiring - - eigs_int_arg_range,make_int
+i8_wrapping_mul builtin_i8_wrapping_mul int-requiring - - eigs_int_arg_range,make_int
+i8_wrapping_sub builtin_i8_wrapping_sub int-requiring - - eigs_int_arg_range,make_int
+index_of builtin_index_of int-preserving - - make_int
+inflate builtin_inflate int-requiring - - VAL_FLOAT,VAL_INT,buffer_byte_at,eigs_byte_arg,eigs_slot_byte,make_int,val_is_number
+join builtin_join other - - eigs_num_text
+json_build builtin_json_build other LOSS - VAL_I64_RAW,VAL_INT,eigs_float_arg,eigs_num_text,val_is_number
+json_decode builtin_json_decode other LOSS - make_float,make_int,strtod
+json_encode builtin_json_encode other LOSS - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_float_arg,eigs_num_text
+json_path builtin_json_path other LOSS - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_float_arg,eigs_num_text,make_float,make_int,strtod,val_is_number
+leaky_relu builtin_tensor_leaky_relu float-producing - - VAL_FLOAT,VAL_INT,eigs_float_arg,eigs_slot_float,make_float,val_is_number
+len builtin_len int-preserving - - make_int
+list_contains builtin_list_contains other - - values_equal
+list_index_of builtin_list_index_of int-preserving - - make_int,values_equal
+list_insert_at builtin_list_insert_at int-requiring - - eigs_int_arg_range,val_is_number
+list_remove_at builtin_list_remove_at int-requiring - - eigs_int_arg_range,val_is_number
+list_slice builtin_list_slice int-requiring - - eigs_int_arg_range,val_is_number
+list_truncate builtin_list_truncate int-requiring - - eigs_int_arg_range,val_is_number
+log builtin_tensor_log float-producing - - eigs_number_arg,make_float,make_int,num_guard,val_is_number
+log_softmax builtin_tensor_log_softmax float-producing - - VAL_FLOAT,VAL_INT,eigs_slot_float,make_float,val_is_number
+matmul builtin_tensor_matmul float-producing - - SLOT_F64_RAW,VAL_FLOAT,VAL_INT,eigs_slot_float,make_float,num_guard_named
+matmul_at builtin_tensor_matmul_at float-producing - - VAL_FLOAT,VAL_INT,eigs_slot_float,make_float
+matmul_bt builtin_tensor_matmul_bt float-producing - - VAL_FLOAT,VAL_INT,eigs_slot_float,make_float
+max builtin_max int-preserving LOSS - eigs_number_arg,make_float,make_int,val_is_number
+mean builtin_tensor_mean float-producing - - buffer_read_f64,eigs_float_arg,make_float,num_guard,val_is_number
+min builtin_min int-preserving LOSS - eigs_number_arg,make_float,make_int,val_is_number
+model_load_weights builtin_eigen_model_load other - - eigs_int_arg_range,strtod,val_is_number
+monotonic_ms builtin_monotonic_ms float-producing - - make_float
+monotonic_ns builtin_monotonic_ns int-preserving LOSS - make_int
+multiply builtin_tensor_multiply int-preserving LOSS - eigs_float_arg,eigs_number_arg,make_float,make_int,num_guard,val_is_number
+native_train_step_builtin builtin_native_train_step int-requiring - - VAL_FLOAT,VAL_INT,atof,eigs_list_float,eigs_slot_float,eigs_slot_int_range,strtod
+nearest_in_range builtin_nearest_in_range float-producing - - eigs_float_arg,eigs_list_float,make_float,make_int,val_is_number
+nearest_in_range_all builtin_nearest_in_range_all float-producing - - eigs_float_arg,eigs_list_float,make_float,make_int,val_is_number
+negative builtin_tensor_negative int-preserving LOSS - eigs_number_arg,make_float,make_int,val_is_number
+net_accept builtin_net_accept int-requiring - - eigs_int_arg,eigs_opt_magnitude,make_int,val_is_number
+net_close builtin_net_close int-requiring - - eigs_int_arg,val_is_number
+net_dial builtin_net_dial int-requiring - - eigs_list_int_range,eigs_opt_magnitude,make_int
+net_listen builtin_net_listen int-requiring - - eigs_int_arg_range,make_int,val_is_number
+net_port builtin_net_port int-requiring - - eigs_int_arg,make_int,val_is_number
+net_recv builtin_net_recv int-requiring - - eigs_int_arg,eigs_opt_int_range,eigs_opt_magnitude
+net_send builtin_net_send int-requiring - - buffer_byte_at,eigs_elem_byte,eigs_int_arg,make_int
+norm builtin_tensor_norm float-producing - - buffer_read_f64,eigs_float_arg,make_float,num_guard,val_is_number
+num builtin_num other LOSS - make_float,make_int,num_guard_named,strtod,val_is_number
+num_copy builtin_num_copy int-preserving LOSS - eigs_number_arg,make_float,make_int,val_is_number
+num_type builtin_num_type other - - VAL_FLOAT,VAL_INT
+numerical_grad builtin_numerical_grad float-producing - - VAL_FLOAT,VAL_INT,eigs_float_arg,eigs_slot_float,eigs_slot_opt_float,list_get_f64,list_set_f64,make_float,val_is_number
+numerical_grad_cols builtin_numerical_grad_cols float-producing - - VAL_FLOAT,VAL_INT,buffer_index_at,eigs_float_arg,eigs_slot_float,eigs_slot_int,eigs_slot_opt_float,list_get_f64,list_set_f64,make_float,val_is_number
+numerical_grad_rows builtin_numerical_grad_rows float-producing - - VAL_FLOAT,VAL_INT,buffer_index_at,eigs_float_arg,eigs_slot_float,eigs_slot_int,eigs_slot_opt_float,list_get_f64,list_set_f64,make_float,val_is_number
+observe builtin_observe other - - make_float
+ord builtin_ord int-preserving - - make_float,make_int
+pi builtin_pi float-producing - - make_float
+pow builtin_tensor_pow float-producing - - eigs_float_arg,eigs_number_arg,make_float,make_int,num_guard,num_guard_named,val_is_number
+ppu_render_frame builtin_ppu_render_frame int-requiring - - buffer_byte_at
+print builtin_print other - - eigs_num_text
+random builtin_random float-producing - - make_float
+random_hex builtin_random_hex int-requiring - - eigs_int_arg_range,val_is_number
+random_int builtin_random_int int-requiring LOSS - eigs_int_arg,make_float,make_int,val_is_number
+random_normal builtin_random_normal float-producing - - eigs_list_float,eigs_list_int_range,make_float
+range builtin_range int-preserving - - eigs_int_arg_range,make_int,val_is_number
+read_bytes builtin_read_bytes int-preserving - - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_int_arg,make_int,val_is_number
+read_bytes_buf builtin_read_bytes_buf int-requiring - - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_int_arg,make_int,val_is_number
+read_text builtin_read_text other - - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_int_arg,make_int,val_is_number
+record_history builtin_record_history other - - eigs_float_arg,make_int,val_is_number
+recv builtin_recv int-requiring - - eigs_int_arg_range,val_is_number
+recv_timeout builtin_recv_timeout int-requiring - magnitude eigs_int_arg_range,eigs_magnitude_arg,val_is_number
+relu builtin_tensor_relu float-producing - - VAL_FLOAT,VAL_INT,eigs_float_arg,eigs_slot_float,make_float,val_is_number
+reshape builtin_reshape int-requiring - - VAL_FLOAT,VAL_INT,eigs_list_int_range
+round builtin_round int-preserving LOSS - eigs_number_arg,make_float,make_int,num_to_i64_exact,val_is_number
+sandbox_run builtin_sandbox_run int-requiring - - VAL_FLOAT,VAL_INT,eigs_byte_arg,eigs_int_arg_range,eigs_list_int,make_int,val_is_number
+scan_int_tokens builtin_scan_int_tokens int-preserving LOSS - make_int
+scan_ints builtin_scan_ints int-preserving LOSS - make_int
+scatter_add builtin_tensor_scatter_add float-producing - - VAL_FLOAT,VAL_INT,buffer_index_at,eigs_float_arg,eigs_slot_float,eigs_slot_int,num_guard,val_is_number
+seed_random builtin_seed_random int-requiring LOSS - eigs_int_arg,val_is_number
+send builtin_send int-requiring - - eigs_int_arg_range,val_is_number
+set_at builtin_set_at int-requiring - - eigs_float_arg,eigs_int_arg_range,val_is_number
+set_exit_code builtin_set_exit_code int-requiring - - eigs_int_arg_range,val_is_number
+sgd_update builtin_sgd_update float-producing - - VAL_FLOAT,VAL_INT,eigs_slot_float,eigs_slot_opt_float,list_set_f64
+sgd_update_cols builtin_sgd_update_cols float-producing - - VAL_FLOAT,VAL_INT,buffer_index_at,eigs_slot_float,eigs_slot_int,eigs_slot_opt_float,list_set_f64
+sgd_update_rows builtin_sgd_update_rows float-producing - - VAL_FLOAT,VAL_INT,buffer_index_at,eigs_slot_float,eigs_slot_int,eigs_slot_opt_float,list_set_f64
+shape builtin_tensor_shape int-preserving - - make_int,val_is_number
+shared_delete builtin_shared_delete int-preserving - - make_int
+shared_incr builtin_shared_incr int-preserving LOSS - eigs_number_arg,make_float,make_int,val_is_number
+shared_size builtin_shared_size int-preserving - - make_int
+sign_extend builtin_sign_extend int-requiring LOSS - eigs_list_int,make_float,make_int,val_is_number
+sin builtin_sin float-producing - - eigs_float_arg,make_float,val_is_number
+softmax builtin_tensor_softmax float-producing - - VAL_FLOAT,VAL_INT,eigs_slot_float,make_float,val_is_number
+sort builtin_sort other LOSS - VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_float_arg,val_is_number
+sort_by builtin_sort_by other LOSS - VAL_I64_RAW,VAL_INT,eigs_float_arg,val_is_number
+spawn builtin_spawn int-preserving - - make_int
+sqrt builtin_tensor_sqrt float-producing - - eigs_number_arg,make_float,make_int,val_is_number
+state_at builtin_state_at int-requiring - - eigs_int_arg_range,val_is_number
+store_close builtin_store_close int-requiring - - SLOT_F64_RAW,VAL_F64_RAW,VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_int_arg_range,eigs_num_text,slot_is_f64,slot_is_smi,slot_smi_val,val_is_number
+store_collections builtin_store_collections int-requiring - - eigs_int_arg_range,val_is_number
+store_count builtin_store_count int-requiring - - eigs_int_arg_range,make_int,val_is_number
+store_delete builtin_store_delete int-requiring LOSS - eigs_int_arg,eigs_int_arg_range,val_is_number
+store_drop builtin_store_drop int-requiring - - SLOT_F64_RAW,VAL_F64_RAW,VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_int_arg_range,eigs_num_text,slot_is_f64,slot_is_smi,slot_smi_val,val_is_number
+store_get builtin_store_get int-requiring LOSS - eigs_float_arg,eigs_int_arg,eigs_int_arg_range,eigs_number_arg,make_float,make_int,num_to_i64_exact,strtod,val_is_number
+store_open builtin_store_open int-preserving - - eigs_int_arg_range,make_float,make_int,strtod,val_is_number
+store_put builtin_store_put other LOSS - SLOT_F64_RAW,VAL_F64_RAW,VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_int_arg_range,eigs_num_text,make_int,slot_is_f64,slot_is_smi,slot_smi_val,val_is_number
+store_query builtin_store_query other LOSS - eigs_float_arg,eigs_int_arg_range,eigs_number_arg,make_float,make_int,num_to_i64_exact,strtod,val_is_number
+store_update builtin_store_update other LOSS - SLOT_F64_RAW,VAL_F64_RAW,VAL_FLOAT,VAL_I64_RAW,VAL_INT,eigs_int_arg,eigs_int_arg_range,eigs_num_text,make_int,slot_is_f64,slot_is_smi,slot_smi_val,val_is_number
+str builtin_str other - - eigs_num_text
+str_from_bytes builtin_str_from_bytes int-requiring - - VAL_FLOAT,VAL_INT,buffer_byte_at,eigs_byte_arg,eigs_slot_byte,val_is_number
+stream_open builtin_stream_open int-requiring - - eigs_int_arg,val_is_number
+stream_write builtin_stream_write float-producing - - eigs_float_arg,val_is_number
+substr builtin_substr int-requiring - - eigs_int_arg_range,val_is_number
+subtract builtin_tensor_subtract int-preserving LOSS - eigs_float_arg,eigs_number_arg,make_float,make_int,num_guard,val_is_number
+sum builtin_tensor_sum int-preserving LOSS - VAL_FLOAT,buffer_read_f64,eigs_number_arg,make_float,make_int,num_guard,val_is_number
+tan builtin_tan float-producing - - eigs_float_arg,make_float,val_is_number
+task_alive builtin_task_alive int-requiring - - eigs_int_arg,val_is_number
+task_detach builtin_task_detach int-requiring - - eigs_int_arg,make_int,val_is_number
+task_join builtin_task_join int-requiring - - eigs_int_arg,val_is_number
+task_kill builtin_task_kill int-requiring - - eigs_int_arg,val_is_number
+task_sched_seed builtin_task_sched_seed int-requiring LOSS - eigs_int_arg,val_is_number
+task_self builtin_task_self int-preserving - - make_int
+task_send builtin_task_send int-requiring - - eigs_int_arg,val_is_number
+task_sleep builtin_task_sleep other - - eigs_float_arg,val_is_number
+task_spawn builtin_task_spawn int-preserving - - make_int
+tensor_load builtin_tensor_load float-producing - - VAL_INT,make_float,make_int,num_guard_named
+tensor_save builtin_tensor_save float-producing - - VAL_FLOAT,VAL_INT,eigs_slot_float
+text_builder_append builtin_text_builder_append other - - eigs_num_text
+text_builder_append_line builtin_text_builder_append_line other - - eigs_num_text
+thread_join builtin_thread_join int-requiring - - eigs_int_arg_range,val_is_number
+throw builtin_throw other - - eigs_num_text
+token_name builtin_token_name int-requiring - - eigs_int_arg_range,val_is_number
+try_recv builtin_try_recv int-requiring - - eigs_int_arg_range,val_is_number
+type builtin_type other - - VAL_FLOAT,VAL_INT
+u16_checked_add builtin_u16_checked_add int-requiring - - eigs_int_arg_range,make_int
+u16_checked_mul builtin_u16_checked_mul int-requiring - - eigs_int_arg_range,make_int
+u16_checked_sub builtin_u16_checked_sub int-requiring - - eigs_int_arg_range,make_int
+u16_saturating_add builtin_u16_saturating_add int-requiring - - eigs_int_arg_range,make_int
+u16_saturating_mul builtin_u16_saturating_mul int-requiring - - eigs_int_arg_range,make_int
+u16_saturating_sub builtin_u16_saturating_sub int-requiring - - eigs_int_arg_range,make_int
+u16_wrapping_add builtin_u16_wrapping_add int-requiring - - eigs_int_arg_range,make_int
+u16_wrapping_mul builtin_u16_wrapping_mul int-requiring - - eigs_int_arg_range,make_int
+u16_wrapping_sub builtin_u16_wrapping_sub int-requiring - - eigs_int_arg_range,make_int
+u32_checked_add builtin_u32_checked_add int-requiring - - eigs_int_arg_range,make_int
+u32_checked_mul builtin_u32_checked_mul int-requiring - - eigs_int_arg_range,make_int
+u32_checked_sub builtin_u32_checked_sub int-requiring - - eigs_int_arg_range,make_int
+u32_saturating_add builtin_u32_saturating_add int-requiring - - eigs_int_arg_range,make_int
+u32_saturating_mul builtin_u32_saturating_mul int-requiring - - eigs_int_arg_range,make_int
+u32_saturating_sub builtin_u32_saturating_sub int-requiring - - eigs_int_arg_range,make_int
+u32_wrapping_add builtin_u32_wrapping_add int-requiring - - eigs_int_arg_range,make_int
+u32_wrapping_mul builtin_u32_wrapping_mul int-requiring - - eigs_int_arg_range,make_int
+u32_wrapping_sub builtin_u32_wrapping_sub int-requiring - - eigs_int_arg_range,make_int
+u64_checked_add builtin_u64_checked_add int-requiring - - eigs_int_arg_range,make_int
+u64_checked_mul builtin_u64_checked_mul int-requiring - - eigs_int_arg_range,make_int
+u64_checked_sub builtin_u64_checked_sub int-requiring - - eigs_int_arg_range,make_int
+u64_compare builtin_u64_compare int-requiring - - eigs_int_arg_range,make_int
+u64_div builtin_u64_div int-requiring - - eigs_int_arg_range,make_int
+u64_rem builtin_u64_rem int-requiring - - eigs_int_arg_range,make_int
+u64_saturating_add builtin_u64_saturating_add int-requiring - - eigs_int_arg_range,make_int
+u64_saturating_mul builtin_u64_saturating_mul int-requiring - - eigs_int_arg_range,make_int
+u64_saturating_sub builtin_u64_saturating_sub int-requiring - - eigs_int_arg_range,make_int
+u64_to_string builtin_u64_to_string int-requiring - - eigs_int_arg
+u64_wrapping_add builtin_u64_wrapping_add int-requiring - - eigs_int_arg_range,make_int
+u64_wrapping_mul builtin_u64_wrapping_mul int-requiring - - eigs_int_arg_range,make_int
+u64_wrapping_sub builtin_u64_wrapping_sub int-requiring - - eigs_int_arg_range,make_int
+u8_checked_add builtin_u8_checked_add int-requiring - - eigs_int_arg_range,make_int
+u8_checked_mul builtin_u8_checked_mul int-requiring - - eigs_int_arg_range,make_int
+u8_checked_sub builtin_u8_checked_sub int-requiring - - eigs_int_arg_range,make_int
+u8_saturating_add builtin_u8_saturating_add int-requiring - - eigs_int_arg_range,make_int
+u8_saturating_mul builtin_u8_saturating_mul int-requiring - - eigs_int_arg_range,make_int
+u8_saturating_sub builtin_u8_saturating_sub int-requiring - - eigs_int_arg_range,make_int
+u8_wrapping_add builtin_u8_wrapping_add int-requiring - - eigs_int_arg_range,make_int
+u8_wrapping_mul builtin_u8_wrapping_mul int-requiring - - eigs_int_arg_range,make_int
+u8_wrapping_sub builtin_u8_wrapping_sub int-requiring - - eigs_int_arg_range,make_int
+usleep builtin_usleep other - magnitude eigs_magnitude_arg,val_is_number
+vm_run_bytecode builtin_vm_run_bytecode int-requiring - - VAL_FLOAT,VAL_INT,eigs_byte_arg,eigs_int_arg_range,val_is_number
+write builtin_write other - - eigs_num_text
+write_bytes builtin_write_bytes int-requiring - - VAL_FLOAT,VAL_INT,buffer_byte_at,eigs_float_arg,eigs_slot_elem_byte,make_int
+zeros builtin_tensor_zeros int-requiring - - VAL_FLOAT,VAL_INT,eigs_int_arg,eigs_list_int,make_float,val_is_number
+zeros_like builtin_tensor_zeros_like float-producing - - make_float,val_is_number
+
+## Sites (file:line scope token kind category)
+builtins.c:46 builtin_c_int eigs_int_arg_range READ b
+builtins.c:172 builtin_usleep val_is_number TEST a
+builtins.c:174 builtin_usleep eigs_magnitude_arg READ b
+builtins.c:346 bit_int eigs_int_arg READ b
+builtins.c:356 bit_pair val_is_number TEST a
+builtins.c:356 bit_pair val_is_number TEST a
+builtins.c:367 builtin_bit_and make_int PRODUCE c
+builtins.c:375 builtin_bit_or make_int PRODUCE c
+builtins.c:383 builtin_bit_xor make_int PRODUCE c
+builtins.c:387 builtin_bit_not val_is_number TEST a
+builtins.c:390 builtin_bit_not make_int PRODUCE c
+builtins.c:398 builtin_bit_shift_left make_int PRODUCE c
+builtins.c:406 builtin_bit_shift_right make_int PRODUCE c
+builtins.c:420 builtin_monotonic_ns make_int PRODUCE c
+builtins.c:428 builtin_monotonic_ms make_float PRODUCE c
+builtins.c:439 builtin_clock_unix make_float PRODUCE c
+builtins.c:444 builtin_len make_int PRODUCE c
+builtins.c:446 builtin_len make_int PRODUCE c
+builtins.c:449 builtin_len make_int PRODUCE c
+builtins.c:452 builtin_len make_int PRODUCE c
+builtins.c:454 builtin_len make_int PRODUCE c
+builtins.c:464 builtin_len make_int PRODUCE c
+builtins.c:502 builtin_number_value make_float PRODUCE c
+builtins.c:502 builtin_number_value make_int PRODUCE c
+builtins.c:513 builtin_num make_float PRODUCE c
+builtins.c:514 builtin_num val_is_number TEST a
+builtins.c:519 builtin_num make_float PRODUCE c
+builtins.c:538 builtin_num make_int PRODUCE c
+builtins.c:556 builtin_num make_int PRODUCE c
+builtins.c:562 builtin_num strtod PARSE c
+builtins.c:564 builtin_num num_guard_named GUARD c
+builtins.c:565 builtin_num make_float PRODUCE c
+builtins.c:587 builtin_append VAL_FLOAT YIELD a
+builtins.c:611 builtin_get_observer_thresholds make_float PRODUCE i
+builtins.c:612 builtin_get_observer_thresholds make_float PRODUCE i
+builtins.c:613 builtin_get_observer_thresholds make_float PRODUCE i
+builtins.c:640 builtin_exit val_is_number TEST a
+builtins.c:644 builtin_exit val_is_number TEST a
+builtins.c:659 builtin_set_exit_code val_is_number TEST a
+builtins.c:664 builtin_set_exit_code eigs_int_arg_range READ b
+builtins.c:799 builtin_observe make_float PRODUCE i
+builtins.c:800 builtin_observe make_float PRODUCE i
+builtins.c:801 builtin_observe make_float PRODUCE i
+builtins.c:883 builtin_type VAL_FLOAT CASE a
+builtins.c:884 builtin_type VAL_INT CASE a
+builtins.c:902 builtin_num_type VAL_INT TEST a
+builtins.c:903 builtin_num_type VAL_FLOAT TEST a
+builtins.c:939 eigs_json_encode_value VAL_FLOAT CASE h
+builtins.c:946 eigs_json_encode_value eigs_float_arg READ h
+builtins.c:946 eigs_json_encode_value eigs_num_text FORMAT h
+builtins.c:950 eigs_json_encode_value VAL_INT CASE h
+builtins.c:952 eigs_json_encode_value VAL_I64_RAW RAW h
+builtins.c:1317 eigs_json_parse_number make_float PRODUCE h
+builtins.c:1324 eigs_json_parse_number make_float PRODUCE h
+builtins.c:1333 eigs_json_parse_number make_float PRODUCE h
+builtins.c:1340 eigs_json_parse_number make_int PRODUCE h
+builtins.c:1343 eigs_json_parse_number strtod PARSE h
+builtins.c:1353 eigs_json_parse_number strtod PARSE h
+builtins.c:1357 eigs_json_parse_number make_float PRODUCE h
+builtins.c:1528 builtin_json_build val_is_number TEST h
+builtins.c:1530 builtin_json_build VAL_I64_RAW RAW h
+builtins.c:1530 builtin_json_build VAL_INT TEST h
+builtins.c:1531 builtin_json_build eigs_float_arg READ h
+builtins.c:1531 builtin_json_build eigs_num_text FORMAT h
+builtins.c:1701 builtin_scan_ints make_int PRODUCE c
+builtins.c:1796 builtin_scan_int_tokens make_int PRODUCE c
+builtins.c:1797 builtin_scan_int_tokens make_int PRODUCE c
+builtins.c:1798 builtin_scan_int_tokens make_int PRODUCE c
+builtins.c:1799 builtin_scan_int_tokens make_int PRODUCE c
+builtins.c:1800 builtin_scan_int_tokens make_int PRODUCE c
+builtins.c:1801 builtin_scan_int_tokens make_int PRODUCE c
+builtins.c:1904 builtin_char_at val_is_number TEST a
+builtins.c:1941 builtin_substr val_is_number TEST a
+builtins.c:1942 builtin_substr val_is_number TEST a
+builtins.c:1976 builtin_index_of make_int PRODUCE c
+builtins.c:1983 builtin_index_of make_int PRODUCE c
+builtins.c:1988 builtin_index_of make_int PRODUCE c
+builtins.c:1989 builtin_index_of make_int PRODUCE c
+builtins.c:1997 builtin_sin make_float PRODUCE c
+builtins.c:1997 builtin_sin val_is_number TEST a
+builtins.c:1998 builtin_sin eigs_float_arg READ b
+builtins.c:1998 builtin_sin make_float PRODUCE c
+builtins.c:2002 builtin_cos make_float PRODUCE c
+builtins.c:2002 builtin_cos val_is_number TEST a
+builtins.c:2003 builtin_cos eigs_float_arg READ b
+builtins.c:2003 builtin_cos make_float PRODUCE c
+builtins.c:2007 builtin_tan make_float PRODUCE c
+builtins.c:2007 builtin_tan val_is_number TEST a
+builtins.c:2008 builtin_tan eigs_float_arg READ b
+builtins.c:2008 builtin_tan make_float PRODUCE c
+builtins.c:2012 builtin_asin make_float PRODUCE c
+builtins.c:2012 builtin_asin val_is_number TEST a
+builtins.c:2013 builtin_asin eigs_float_arg READ b
+builtins.c:2018 builtin_asin make_float PRODUCE c
+builtins.c:2022 builtin_asin make_float PRODUCE c
+builtins.c:2026 builtin_acos make_float PRODUCE c
+builtins.c:2026 builtin_acos val_is_number TEST a
+builtins.c:2027 builtin_acos eigs_float_arg READ b
+builtins.c:2032 builtin_acos make_float PRODUCE c
+builtins.c:2036 builtin_acos make_float PRODUCE c
+builtins.c:2040 builtin_atan make_float PRODUCE c
+builtins.c:2040 builtin_atan val_is_number TEST a
+builtins.c:2041 builtin_atan eigs_float_arg READ b
+builtins.c:2041 builtin_atan make_float PRODUCE c
+builtins.c:2046 builtin_atan2 make_float PRODUCE c
+builtins.c:2049 builtin_atan2 make_float PRODUCE c
+builtins.c:2049 builtin_atan2 val_is_number TEST a
+builtins.c:2049 builtin_atan2 val_is_number TEST a
+builtins.c:2050 builtin_atan2 eigs_float_arg READ b
+builtins.c:2050 builtin_atan2 eigs_float_arg READ b
+builtins.c:2050 builtin_atan2 make_float PRODUCE c
+builtins.c:2054 builtin_floor make_float PRODUCE c
+builtins.c:2054 builtin_floor val_is_number TEST a
+builtins.c:2055 builtin_floor eigs_number_arg READ b
+builtins.c:2057 builtin_floor make_int PRODUCE c
+builtins.c:2060 builtin_floor make_float PRODUCE c
+builtins.c:2060 builtin_floor make_int PRODUCE c
+builtins.c:2060 builtin_floor num_to_i64_exact READ b
+builtins.c:2064 builtin_ceil make_float PRODUCE c
+builtins.c:2064 builtin_ceil val_is_number TEST a
+builtins.c:2065 builtin_ceil eigs_number_arg READ b
+builtins.c:2067 builtin_ceil make_int PRODUCE c
+builtins.c:2070 builtin_ceil make_float PRODUCE c
+builtins.c:2070 builtin_ceil make_int PRODUCE c
+builtins.c:2070 builtin_ceil num_to_i64_exact READ b
+builtins.c:2074 builtin_round make_float PRODUCE c
+builtins.c:2074 builtin_round val_is_number TEST a
+builtins.c:2075 builtin_round eigs_number_arg READ b
+builtins.c:2077 builtin_round make_int PRODUCE c
+builtins.c:2080 builtin_round make_float PRODUCE c
+builtins.c:2080 builtin_round make_int PRODUCE c
+builtins.c:2080 builtin_round num_to_i64_exact READ b
+builtins.c:2084 builtin_abs make_float PRODUCE c
+builtins.c:2084 builtin_abs val_is_number TEST a
+builtins.c:2085 builtin_abs eigs_number_arg READ b
+builtins.c:2092 builtin_abs make_int PRODUCE c
+builtins.c:2094 builtin_abs make_float PRODUCE c
+builtins.c:2104 minmax_reduce val_is_number TEST a
+builtins.c:2105 minmax_reduce eigs_number_arg READ b
+builtins.c:2110 minmax_reduce make_float PRODUCE c
+builtins.c:2112 minmax_reduce make_float PRODUCE c
+builtins.c:2125 minmax_reduce make_float PRODUCE c
+builtins.c:2125 minmax_reduce val_is_number TEST a
+builtins.c:2127 minmax_reduce make_float PRODUCE c
+builtins.c:2127 minmax_reduce val_is_number TEST a
+builtins.c:2129 minmax_reduce eigs_number_arg READ b
+builtins.c:2150 builtin_pi make_float PRODUCE c
+builtins.c:2206 builtin_random make_float PRODUCE c
+builtins.c:2215 builtin_random_int make_float PRODUCE c
+builtins.c:2218 builtin_random_int val_is_number TEST a
+builtins.c:2218 builtin_random_int val_is_number TEST a
+builtins.c:2219 builtin_random_int make_float PRODUCE c
+builtins.c:2220 builtin_random_int eigs_int_arg READ b
+builtins.c:2221 builtin_random_int eigs_int_arg READ b
+builtins.c:2223 builtin_random_int make_int PRODUCE c
+builtins.c:2232 builtin_random_int make_int PRODUCE c
+builtins.c:2246 builtin_seed_random val_is_number TEST a
+builtins.c:2247 builtin_seed_random eigs_int_arg READ b
+builtins.c:2475 builtin_json_path val_is_number TEST h
+builtins.c:2477 builtin_json_path VAL_I64_RAW RAW h
+builtins.c:2477 builtin_json_path VAL_INT TEST h
+builtins.c:2478 builtin_json_path eigs_float_arg READ h
+builtins.c:2478 builtin_json_path eigs_num_text FORMAT h
+builtins.c:2587 builtin_token_name val_is_number TEST a
+builtins.c:2625 builtin_state_at val_is_number TEST i
+builtins.c:2674 builtin_chr val_is_number TEST a
+builtins.c:2678 builtin_chr eigs_int_arg READ b
+builtins.c:2702 builtin_hex val_is_number TEST e
+builtins.c:2703 builtin_hex eigs_int_arg READ e
+builtins.c:2705 builtin_hex eigs_list_int READ e
+builtins.c:2706 builtin_hex eigs_list_int READ e
+builtins.c:2733 builtin_ord make_float PRODUCE c
+builtins.c:2736 builtin_ord make_int PRODUCE c
+builtins.c:2737 builtin_ord make_int PRODUCE c
+builtins.c:2854 vm_desc_abi_error val_is_number TEST a
+builtins.c:2965 desc_isolate_const VAL_FLOAT CASE a
+builtins.c:2966 desc_isolate_const VAL_INT CASE a
+builtins.c:3083 vm_build_chunk_desc_body val_is_number TEST a
+builtins.c:3088 vm_build_chunk_desc_body eigs_byte_arg READ b
+builtins.c:3146 vm_build_chunk_desc_body val_is_number TEST a
+builtins.c:3546 builtin_sandbox_run val_is_number TEST a
+builtins.c:3554 builtin_sandbox_run val_is_number TEST a
+builtins.c:3555 builtin_sandbox_run eigs_list_int READ b
+builtins.c:3571 builtin_sandbox_run val_is_number TEST a
+builtins.c:3572 builtin_sandbox_run eigs_list_int READ b
+builtins.c:3631 builtin_sandbox_run make_int PRODUCE c
+builtins.c:3894 builtin_sandbox_run make_int PRODUCE c
+builtins.c:3907 builtin_sandbox_run make_int PRODUCE c
+builtins.c:3922 builtin_sandbox_run make_int PRODUCE c
+builtins.c:3957 builtin_sandbox_run make_int PRODUCE c
+builtins.c:3986 builtin_record_history val_is_number TEST i
+builtins.c:3992 builtin_record_history eigs_float_arg READ i
+builtins.c:3999 builtin_record_history make_int PRODUCE i
+builtins.c:4027 builtin_copy_into val_is_number TEST a
+builtins.c:4045 builtin_copy_into val_is_number TEST a
+builtins.c:4050 builtin_copy_into eigs_float_arg READ b
+builtins.c:4085 builtin_list_slice val_is_number TEST a
+builtins.c:4085 builtin_list_slice val_is_number TEST a
+builtins.c:4112 builtin_num_copy val_is_number TEST a
+builtins.c:4113 builtin_num_copy eigs_number_arg READ b
+builtins.c:4145 range_bound eigs_int_arg_range READ b
+builtins.c:4156 builtin_range val_is_number TEST a
+builtins.c:4165 builtin_range val_is_number TEST a
+builtins.c:4227 builtin_range make_int PRODUCE c
+builtins.c:4265 at_index val_is_number TEST a
+builtins.c:4285 buf_at_index val_is_number TEST a
+builtins.c:4335 builtin_set_at val_is_number TEST a
+builtins.c:4340 builtin_set_at eigs_float_arg READ b
+builtins.c:4403 builtin_get_at buffer_read_f64 READ b
+builtins.c:4403 builtin_get_at make_float PRODUCE c
+builtins.c:4411 builtin_get_at buffer_read_f64 READ b
+builtins.c:4411 builtin_get_at make_float PRODUCE c
+builtins.c:4796 builtin_spawn make_int PRODUCE c
+builtins.c:4802 builtin_spawn make_int PRODUCE c
+builtins.c:4803 builtin_spawn make_int PRODUCE c
+builtins.c:4828 builtin_thread_join val_is_number TEST a
+builtins.c:4834 builtin_thread_join eigs_int_arg_range READ b
+builtins.c:4834 builtin_thread_join val_is_number TEST a
+builtins.c:4939 get_channel_why val_is_number TEST a
+builtins.c:4943 get_channel_why eigs_int_arg_range READ b
+builtins.c:4943 get_channel_why val_is_number TEST a
+builtins.c:5009 builtin_channel make_int PRODUCE c
+builtins.c:5010 builtin_channel make_int PRODUCE c
+builtins.c:5109 builtin_recv_timeout val_is_number TEST a
+builtins.c:5114 builtin_recv_timeout eigs_magnitude_arg READ b
+builtins.c:5332 builtin_task_spawn make_int PRODUCE c
+builtins.c:5342 task_handle_resolve val_is_number TEST a
+builtins.c:5343 task_handle_resolve eigs_int_arg READ b
+builtins.c:5397 builtin_task_join val_is_number TEST a
+builtins.c:5398 builtin_task_join eigs_int_arg READ b
+builtins.c:5444 builtin_task_send val_is_number TEST a
+builtins.c:5448 builtin_task_send eigs_int_arg READ b
+builtins.c:5476 builtin_task_kill val_is_number TEST a
+builtins.c:5479 builtin_task_kill eigs_int_arg READ b
+builtins.c:5489 builtin_task_alive val_is_number TEST a
+builtins.c:5511 builtin_task_sleep val_is_number TEST a
+builtins.c:5515 builtin_task_sleep eigs_float_arg READ b
+builtins.c:5550 builtin_task_self make_int PRODUCE c
+builtins.c:5564 builtin_task_detach val_is_number TEST a
+builtins.c:5569 builtin_task_detach make_int PRODUCE c
+builtins.c:5572 builtin_task_detach make_int PRODUCE c
+builtins.c:5583 builtin_task_sched_seed val_is_number TEST a
+builtins.c:5587 builtin_task_sched_seed eigs_int_arg READ b
+builtins.c:5738 builtin_nearest_in_range eigs_list_float READ b
+builtins.c:5739 builtin_nearest_in_range eigs_list_float READ b
+builtins.c:5740 builtin_nearest_in_range eigs_list_float READ b
+builtins.c:5741 builtin_nearest_in_range eigs_list_float READ b
+builtins.c:5742 builtin_nearest_in_range eigs_list_float READ b
+builtins.c:5799 builtin_nearest_in_range eigs_float_arg READ b
+builtins.c:5799 builtin_nearest_in_range val_is_number TEST a
+builtins.c:5823 builtin_nearest_in_range eigs_float_arg READ b
+builtins.c:5826 builtin_nearest_in_range val_is_number TEST a
+builtins.c:5826 builtin_nearest_in_range val_is_number TEST a
+builtins.c:5828 builtin_nearest_in_range eigs_float_arg READ b
+builtins.c:5829 builtin_nearest_in_range eigs_float_arg READ b
+builtins.c:5844 builtin_nearest_in_range make_int PRODUCE c
+builtins.c:5845 builtin_nearest_in_range make_float PRODUCE c
+builtins.c:5846 builtin_nearest_in_range make_float PRODUCE c
+builtins.c:5847 builtin_nearest_in_range make_float PRODUCE c
+builtins.c:5872 builtin_nearest_in_range_all eigs_list_float READ b
+builtins.c:5873 builtin_nearest_in_range_all eigs_list_float READ b
+builtins.c:5874 builtin_nearest_in_range_all eigs_list_float READ b
+builtins.c:5929 builtin_nearest_in_range_all eigs_float_arg READ b
+builtins.c:5929 builtin_nearest_in_range_all val_is_number TEST a
+builtins.c:5949 builtin_nearest_in_range_all eigs_float_arg READ b
+builtins.c:5952 builtin_nearest_in_range_all val_is_number TEST a
+builtins.c:5952 builtin_nearest_in_range_all val_is_number TEST a
+builtins.c:5956 builtin_nearest_in_range_all eigs_float_arg READ b
+builtins.c:5957 builtin_nearest_in_range_all eigs_float_arg READ b
+builtins.c:5999 builtin_nearest_in_range_all make_int PRODUCE c
+builtins.c:6000 builtin_nearest_in_range_all make_float PRODUCE c
+builtins.c:6001 builtin_nearest_in_range_all make_float PRODUCE c
+builtins.c:6002 builtin_nearest_in_range_all make_float PRODUCE c
+builtins.c:6025 builtin_sign_extend make_float PRODUCE c
+builtins.c:6027 builtin_sign_extend make_float PRODUCE c
+builtins.c:6027 builtin_sign_extend val_is_number TEST a
+builtins.c:6027 builtin_sign_extend val_is_number TEST a
+builtins.c:6028 builtin_sign_extend eigs_list_int READ b
+builtins.c:6029 builtin_sign_extend eigs_list_int READ b
+builtins.c:6031 builtin_sign_extend make_int PRODUCE c
+builtins.c:6038 builtin_sign_extend make_int PRODUCE c
+builtins.c:6040 builtin_sign_extend make_int PRODUCE c
+builtins.c:6057 builtin_list_truncate val_is_number TEST a
+builtins.c:6088 builtin_list_remove_at val_is_number TEST a
+builtins.c:6111 builtin_list_insert_at val_is_number TEST a
+builtins.c:6144 builtin_list_index_of make_int PRODUCE f
+builtins.c:6148 builtin_list_index_of make_int PRODUCE f
+builtins.c:6154 builtin_list_index_of make_int PRODUCE f
+builtins.c:6158 builtin_list_index_of make_int PRODUCE f
+builtins.c:6229 builtin_sort_by val_is_number TEST f
+builtins.c:6238 builtin_sort_by VAL_INT TEST f
+builtins.c:6239 builtin_sort_by VAL_I64_RAW RAW f
+builtins.c:6240 builtin_sort_by eigs_float_arg READ f
+builtins.c:6262 sort_cmp_num VAL_INT TEST a
+builtins.c:6262 sort_cmp_num VAL_INT TEST a
+builtins.c:6263 sort_cmp_num VAL_I64_RAW RAW b
+builtins.c:6263 sort_cmp_num VAL_I64_RAW RAW b
+builtins.c:6263 sort_cmp_num VAL_I64_RAW RAW b
+builtins.c:6263 sort_cmp_num VAL_I64_RAW RAW b
+builtins.c:6264 sort_cmp_num VAL_INT TEST a
+builtins.c:6265 sort_cmp_num VAL_I64_RAW RAW b
+builtins.c:6265 sort_cmp_num eigs_float_arg READ b
+builtins.c:6268 sort_cmp_num VAL_INT TEST a
+builtins.c:6269 sort_cmp_num VAL_I64_RAW RAW b
+builtins.c:6269 sort_cmp_num eigs_float_arg READ b
+builtins.c:6272 sort_cmp_num eigs_float_arg READ b
+builtins.c:6272 sort_cmp_num eigs_float_arg READ b
+builtins.c:6290 builtin_sort VAL_FLOAT TEST f
+builtins.c:6290 builtin_sort VAL_INT TEST f
+builtins.c:6291 builtin_sort val_is_number TEST f
+builtins.c:6298 builtin_sort VAL_FLOAT TEST f
+builtins.c:6298 builtin_sort VAL_INT TEST f
+builtins.c:6319 builtin_dispatch val_is_number TEST a
+builtins.c:6323 builtin_dispatch eigs_number_arg READ b
+builtins.c:6327 builtin_dispatch num_to_i64_exact READ b
+builtins.c:6358 builtin_dispatch val_is_number TEST a
+builtins.c:6440 builtin_dot make_float PRODUCE c
+builtins.c:6443 builtin_dot make_float PRODUCE c
+builtins.c:6448 builtin_dot buffer_read_f64 READ b
+builtins.c:6450 builtin_dot buffer_read_f64 READ b
+builtins.c:6452 builtin_dot num_guard GUARD c
+builtins.c:6452 builtin_dot num_guard GUARD c
+builtins.c:6454 builtin_dot make_float PRODUCE c
+builtins_buf.c:35 builtin_buffer VAL_FLOAT TEST a
+builtins_buf.c:35 builtin_buffer VAL_INT TEST a
+builtins_buf.c:36 builtin_buffer VAL_FLOAT TEST a
+builtins_buf.c:36 builtin_buffer VAL_INT TEST a
+builtins_buf.c:38 builtin_buffer eigs_list_int_range READ b
+builtins_buf.c:39 builtin_buffer eigs_list_int_range READ b
+builtins_buf.c:58 builtin_buffer val_is_number TEST a
+builtins_buf.c:59 builtin_buffer val_is_number TEST a
+builtins_buf.c:61 builtin_buffer eigs_int_arg_range READ b
+builtins_buf.c:84 builtin_reshape VAL_FLOAT TEST a
+builtins_buf.c:84 builtin_reshape VAL_INT TEST a
+builtins_buf.c:85 builtin_reshape VAL_FLOAT TEST a
+builtins_buf.c:85 builtin_reshape VAL_INT TEST a
+builtins_buf.c:87 builtin_reshape eigs_list_int_range READ b
+builtins_buf.c:88 builtin_reshape eigs_list_int_range READ b
+builtins_buf.c:113 builtin_buf_get make_float PRODUCE c
+builtins_buf.c:120 builtin_buf_get make_float PRODUCE c
+builtins_buf.c:122 builtin_buf_get eigs_list_int READ b
+builtins_buf.c:123 builtin_buf_get make_float PRODUCE c
+builtins_buf.c:128 builtin_buf_get make_float PRODUCE c
+builtins_buf.c:130 builtin_buf_get buffer_read_f64 READ b
+builtins_buf.c:130 builtin_buf_get make_float PRODUCE c
+builtins_buf.c:149 builtin_buf_set VAL_FLOAT TEST a
+builtins_buf.c:149 builtin_buf_set VAL_INT TEST a
+builtins_buf.c:153 builtin_buf_set VAL_FLOAT TEST a
+builtins_buf.c:153 builtin_buf_set VAL_INT TEST a
+builtins_buf.c:157 builtin_buf_set eigs_list_int READ b
+builtins_buf.c:158 builtin_buf_set eigs_list_float READ b
+builtins_buf.c:171 builtin_buf_len make_int PRODUCE c
+builtins_buf.c:172 builtin_buf_len make_int PRODUCE c
+builtins_buf.c:192 builtin_buf_from_list VAL_FLOAT TEST a
+builtins_buf.c:192 builtin_buf_from_list VAL_INT TEST a
+builtins_buf.c:193 builtin_buf_from_list eigs_slot_float READ b
+builtins_buf.c:215 strict_numeric_byte_list val_is_number TEST a
+builtins_buf.c:222 strict_numeric_byte_list val_is_number TEST a
+builtins_buf.c:223 strict_numeric_byte_list val_is_number TEST a
+builtins_buf.c:224 strict_numeric_byte_list eigs_byte_arg READ b
+builtins_buf.c:253 builtin_str_from_bytes buffer_byte_at READ b
+builtins_buf.c:254 builtin_str_from_bytes VAL_FLOAT TEST a
+builtins_buf.c:254 builtin_str_from_bytes VAL_INT TEST a
+builtins_buf.c:255 builtin_str_from_bytes eigs_slot_byte READ b
+builtins_buf.c:275 builtin_f64_to_bytes val_is_number TEST a
+builtins_buf.c:276 builtin_f64_to_bytes eigs_float_arg READ b
+builtins_buf.c:276 builtin_f64_to_bytes val_is_number TEST a
+builtins_buf.c:283 builtin_f64_to_bytes make_int PRODUCE c
+builtins_buf.c:299 builtin_f64_from_bytes eigs_slot_elem_byte READ b
+builtins_buf.c:309 builtin_f64_from_bytes buffer_byte_at READ b
+builtins_buf.c:313 builtin_f64_from_bytes make_float PRODUCE c
+builtins_buf.c:322 builtin_f64_from_bytes make_float PRODUCE c
+builtins_buf.c:322 builtin_f64_from_bytes num_guard_named GUARD c
+builtins_buf.c:374 zlib_bytes_arg buffer_byte_at READ b
+builtins_buf.c:375 zlib_bytes_arg VAL_FLOAT TEST a
+builtins_buf.c:375 zlib_bytes_arg VAL_INT TEST a
+builtins_buf.c:376 zlib_bytes_arg eigs_slot_byte READ b
+builtins_buf.c:400 zlib_bytes_result make_int PRODUCE c
+builtins_buf.c:518 buf_count_arg VAL_FLOAT TEST a
+builtins_buf.c:518 buf_count_arg VAL_INT TEST a
+builtins_buf.c:522 buf_count_arg eigs_slot_int READ b
+builtins_buf.c:533 buf_float_arg READ b
+builtins_buf.c:535 buf_float_arg VAL_FLOAT TEST a
+builtins_buf.c:535 buf_float_arg VAL_INT TEST a
+builtins_buf.c:539 buf_float_arg eigs_slot_float READ b
+builtins_buf.c:552 buf_window_arg VAL_FLOAT TEST a
+builtins_buf.c:552 buf_window_arg VAL_INT TEST a
+builtins_buf.c:556 buf_window_arg eigs_slot_int READ b
+builtins_buf.c:617 builtin_buf_mix buf_float_arg READ b
+builtins_buf.c:622 builtin_buf_mix num_guard GUARD c
+builtins_buf.c:622 builtin_buf_mix num_guard GUARD c
+builtins_buf.c:641 builtin_buf_scale_range buf_float_arg READ b
+builtins_buf.c:645 builtin_buf_scale_range num_guard GUARD c
+builtins_buf.c:664 builtin_buf_fill buf_float_arg READ b
+builtins_buf.c:679 builtin_buf_peak make_float PRODUCE c
+builtins_buf.c:687 builtin_buf_peak make_float PRODUCE c
+builtins_buf.c:690 builtin_buf_peak buffer_read_f64 READ b
+builtins_buf.c:695 builtin_buf_peak make_float PRODUCE c
+builtins_buf.c:709 builtin_buf_dot make_float PRODUCE c
+builtins_buf.c:720 builtin_buf_dot make_float PRODUCE c
+builtins_buf.c:723 builtin_buf_dot buffer_read_f64 READ b
+builtins_buf.c:725 builtin_buf_dot buffer_read_f64 READ b
+builtins_buf.c:727 builtin_buf_dot num_guard GUARD c
+builtins_buf.c:727 builtin_buf_dot num_guard GUARD c
+builtins_buf.c:729 builtin_buf_dot make_float PRODUCE c
+builtins_buf.c:782 builtin_buf_from_pcm16le buffer_byte_at READ b
+builtins_buf.c:783 builtin_buf_from_pcm16le buffer_byte_at READ b
+builtins_buf.c:786 builtin_buf_from_pcm16le num_guard GUARD c
+builtins_buf.c:786 builtin_buf_from_pcm16le num_guard GUARD c
+builtins_buf.c:788 builtin_buf_from_pcm16le num_guard GUARD c
+builtins_buf.c:789 builtin_buf_from_pcm16le num_guard GUARD c
+builtins_buf.c:822 builtin_buf_to_pcm16le buffer_read_f64 READ b
+builtins_buf.c:826 builtin_buf_to_pcm16le num_guard GUARD c
+builtins_buf.c:827 builtin_buf_to_pcm16le num_guard GUARD c
+builtins_buf.c:828 builtin_buf_to_pcm16le num_guard GUARD c
+builtins_buf.c:829 builtin_buf_to_pcm16le num_guard GUARD c
+builtins_buf.c:829 builtin_buf_to_pcm16le num_guard GUARD c
+builtins_buf.c:855 builtin_buf_deinterleave val_is_number TEST a
+builtins_buf.c:855 builtin_buf_deinterleave val_is_number TEST a
+builtins_buf.c:859 builtin_buf_deinterleave eigs_int_arg READ b
+builtins_buf.c:860 builtin_buf_deinterleave eigs_int_arg READ b
+builtins_buf.c:938 builtin_buf_resample_linear num_guard GUARD c
+builtins_buf.c:938 builtin_buf_resample_linear num_guard GUARD c
+builtins_buf.c:952 builtin_buf_resample_linear num_guard GUARD c
+builtins_buf.c:953 builtin_buf_resample_linear num_guard GUARD c
+builtins_buf.c:953 builtin_buf_resample_linear num_guard GUARD c
+builtins_buf.c:953 builtin_buf_resample_linear num_guard GUARD c
+builtins_buf.c:954 builtin_buf_resample_linear num_guard GUARD c
+builtins_host.c:41 builtin_host_c_int eigs_int_arg_range READ b
+builtins_host.c:226 builtin_stream_open val_is_number TEST a
+builtins_host.c:228 builtin_stream_open eigs_int_arg READ b
+builtins_host.c:273 builtin_stream_write val_is_number TEST a
+builtins_host.c:278 builtin_stream_write eigs_float_arg READ b
+builtins_host.c:442 builtin_build_corpus VAL_FLOAT TEST a
+builtins_host.c:442 builtin_build_corpus VAL_INT TEST a
+builtins_host.c:445 builtin_build_corpus VAL_FLOAT TEST a
+builtins_host.c:445 builtin_build_corpus VAL_INT TEST a
+builtins_host.c:447 builtin_build_corpus VAL_FLOAT TEST a
+builtins_host.c:447 builtin_build_corpus VAL_INT TEST a
+builtins_host.c:466 builtin_build_corpus val_is_number TEST a
+builtins_host.c:503 builtin_build_corpus eigs_opt_int_range READ b
+builtins_host.c:532 builtin_build_corpus eigs_opt_int_range READ b
+builtins_host.c:761 builtin_build_corpus number RAW b
+builtins_host.c:761 builtin_build_corpus number RAW b
+builtins_host.c:762 builtin_build_corpus number RAW b
+builtins_host.c:762 builtin_build_corpus number RAW b
+builtins_host.c:763 builtin_build_corpus number RAW b
+builtins_host.c:764 builtin_build_corpus number RAW b
+builtins_host.c:863 builtin_build_corpus make_int PRODUCE c
+builtins_host.c:864 builtin_build_corpus make_int PRODUCE c
+builtins_host.c:865 builtin_build_corpus make_int PRODUCE c
+builtins_host.c:1226 reader_cap_parse val_is_number TEST a
+builtins_host.c:1229 reader_cap_parse eigs_int_arg READ b
+builtins_host.c:1251 reader_cap_raise make_int PRODUCE c
+builtins_host.c:1303 replay_reader_size VAL_I64_RAW RAW b
+builtins_host.c:1303 replay_reader_size VAL_INT TEST a
+builtins_host.c:1309 replay_reader_size VAL_I64_RAW RAW b
+builtins_host.c:1324 builtin_read_bytes VAL_FLOAT TEST a
+builtins_host.c:1324 builtin_read_bytes VAL_INT TEST a
+builtins_host.c:1362 builtin_read_bytes make_int PRODUCE c
+builtins_host.c:1377 builtin_read_bytes make_int PRODUCE c
+builtins_host.c:1415 builtin_read_bytes_buf VAL_FLOAT TEST a
+builtins_host.c:1415 builtin_read_bytes_buf VAL_INT TEST a
+builtins_host.c:1490 builtin_read_text VAL_FLOAT TEST a
+builtins_host.c:1490 builtin_read_text VAL_INT TEST a
+builtins_host.c:1620 exec_capture_result make_int PRODUCE c
+builtins_host.c:1646 builtin_exec_capture VAL_FLOAT TEST a
+builtins_host.c:1646 builtin_exec_capture VAL_INT TEST a
+builtins_host.c:1648 builtin_exec_capture eigs_list_float READ b
+builtins_host.c:1653 builtin_exec_capture make_float PRODUCE c
+builtins_host.c:1654 builtin_exec_capture eigs_magnitude_arg READ b
+builtins_host.c:1802 builtin_random_hex val_is_number TEST a
+builtins_host.c:1830 builtin_write_bytes make_int PRODUCE c
+builtins_host.c:1836 builtin_write_bytes make_int PRODUCE c
+builtins_host.c:1844 builtin_write_bytes eigs_float_arg READ b
+builtins_host.c:1845 builtin_write_bytes make_int PRODUCE c
+builtins_host.c:1858 builtin_write_bytes make_int PRODUCE c
+builtins_host.c:1868 builtin_write_bytes VAL_FLOAT TEST a
+builtins_host.c:1868 builtin_write_bytes VAL_INT TEST a
+builtins_host.c:1875 builtin_write_bytes eigs_slot_elem_byte READ b
+builtins_host.c:1877 builtin_write_bytes buffer_byte_at READ b
+builtins_host.c:1899 builtin_write_bytes make_int PRODUCE c
+builtins_int.c:106 n9_pair eigs_int_arg_range READ b
+builtins_int.c:107 n9_pair eigs_int_arg_range READ b
+builtins_int.c:162 n9_width make_int PRODUCE c
+builtins_int.c:163 n9_width make_int PRODUCE c
+builtins_int.c:168 n9_width make_int PRODUCE c
+builtins_int.c:169 n9_width make_int PRODUCE c
+builtins_int.c:536 builtin_u64_compare make_int PRODUCE c
+builtins_int.c:546 n9_unsigned_div make_int PRODUCE c
+builtins_int.c:561 builtin_u64_to_string eigs_int_arg READ b
+builtins_int.c:572 builtin_fround eigs_float_arg READ b
+builtins_int.c:576 builtin_fround make_float PRODUCE c
+builtins_tensor.c:229 tensor_dims VAL_FLOAT TEST a
+builtins_tensor.c:229 tensor_dims VAL_INT TEST a
+builtins_tensor.c:257 #define FLAT_BAD VAL_FLOAT TEST a
+builtins_tensor.c:257 #define FLAT_BAD VAL_INT TEST a
+builtins_tensor.c:292 tensor_to_flat VAL_FLOAT TEST a
+builtins_tensor.c:292 tensor_to_flat VAL_INT TEST a
+builtins_tensor.c:292 tensor_to_flat eigs_slot_float READ b
+builtins_tensor.c:301 tensor_to_flat VAL_FLOAT TEST a
+builtins_tensor.c:301 tensor_to_flat VAL_INT TEST a
+builtins_tensor.c:301 tensor_to_flat eigs_slot_float READ b
+builtins_tensor.c:315 flat_to_tensor_2d make_float PRODUCE c
+builtins_tensor.c:331 flat_to_tensor_1d make_float PRODUCE c
+builtins_tensor.c:363 tensor_total val_is_number TEST a
+builtins_tensor.c:384 tensor_flatten_recursive eigs_float_arg READ b
+builtins_tensor.c:384 tensor_flatten_recursive val_is_number TEST a
+builtins_tensor.c:387 tensor_flatten_recursive buffer_read_f64 READ b
+builtins_tensor.c:403 op_add num_guard GUARD c
+builtins_tensor.c:404 op_sub num_guard GUARD c
+builtins_tensor.c:405 op_mul num_guard GUARD c
+builtins_tensor.c:412 op_div num_guard GUARD c
+builtins_tensor.c:416 op_pow num_guard_named GUARD c
+builtins_tensor.c:504 tensor_elementwise make_float PRODUCE c
+builtins_tensor.c:507 tensor_elementwise val_is_number TEST a
+builtins_tensor.c:507 tensor_elementwise val_is_number TEST a
+builtins_tensor.c:510 tensor_elementwise eigs_number_arg READ b
+builtins_tensor.c:512 tensor_elementwise eigs_number_arg READ b
+builtins_tensor.c:524 tensor_elementwise make_int PRODUCE c
+builtins_tensor.c:529 tensor_elementwise make_float PRODUCE c
+builtins_tensor.c:538 tensor_elementwise val_is_number TEST a
+builtins_tensor.c:539 tensor_elementwise eigs_float_arg READ b
+builtins_tensor.c:543 tensor_elementwise val_is_number TEST a
+builtins_tensor.c:544 tensor_elementwise eigs_float_arg READ b
+builtins_tensor.c:564 tensor_elementwise val_is_number TEST a
+builtins_tensor.c:575 tensor_elementwise val_is_number TEST a
+builtins_tensor.c:673 tensor_elementwise make_float PRODUCE c
+builtins_tensor.c:728 tensor_unary val_is_number TEST a
+builtins_tensor.c:729 tensor_unary eigs_number_arg READ b
+builtins_tensor.c:736 tensor_unary make_int PRODUCE c
+builtins_tensor.c:739 tensor_unary make_float PRODUCE c
+builtins_tensor.c:768 tensor_unary make_float PRODUCE c
+builtins_tensor.c:781 op_exp num_guard GUARD c
+builtins_tensor.c:795 op_log_safe num_guard GUARD c
+builtins_tensor.c:797 op_log_safe num_guard GUARD c
+builtins_tensor.c:860 builtin_tensor_matmul SLOT_F64_RAW RAW b
+builtins_tensor.c:891 builtin_tensor_matmul num_guard_named GUARD c
+builtins_tensor.c:1058 builtin_tensor_scatter_add val_is_number TEST a
+builtins_tensor.c:1090 builtin_tensor_scatter_add VAL_FLOAT TEST a
+builtins_tensor.c:1090 builtin_tensor_scatter_add VAL_INT TEST a
+builtins_tensor.c:1094 builtin_tensor_scatter_add eigs_slot_int READ b
+builtins_tensor.c:1099 builtin_tensor_scatter_add buffer_index_at READ b
+builtins_tensor.c:1104 builtin_tensor_scatter_add VAL_FLOAT TEST a
+builtins_tensor.c:1104 builtin_tensor_scatter_add VAL_INT TEST a
+builtins_tensor.c:1108 builtin_tensor_scatter_add eigs_slot_float READ b
+builtins_tensor.c:1112 builtin_tensor_scatter_add eigs_float_arg READ b
+builtins_tensor.c:1137 builtin_tensor_scatter_add num_guard GUARD c
+builtins_tensor.c:1140 builtin_tensor_scatter_add num_guard GUARD c
+builtins_tensor.c:1151 builtin_tensor_softmax make_float PRODUCE c
+builtins_tensor.c:1151 builtin_tensor_softmax val_is_number TEST a
+builtins_tensor.c:1185 builtin_tensor_log_softmax VAL_FLOAT TEST a
+builtins_tensor.c:1185 builtin_tensor_log_softmax VAL_INT TEST a
+builtins_tensor.c:1191 builtin_tensor_log_softmax make_float PRODUCE c
+builtins_tensor.c:1191 builtin_tensor_log_softmax val_is_number TEST a
+builtins_tensor.c:1225 builtin_tensor_relu val_is_number TEST a
+builtins_tensor.c:1226 builtin_tensor_relu eigs_float_arg READ b
+builtins_tensor.c:1227 builtin_tensor_relu make_float PRODUCE c
+builtins_tensor.c:1246 builtin_tensor_leaky_relu val_is_number TEST a
+builtins_tensor.c:1247 builtin_tensor_leaky_relu eigs_float_arg READ b
+builtins_tensor.c:1248 builtin_tensor_leaky_relu make_float PRODUCE c
+builtins_tensor.c:1279 builtin_tensor_mean make_float PRODUCE c
+builtins_tensor.c:1282 builtin_tensor_mean buffer_read_f64 READ b
+builtins_tensor.c:1284 builtin_tensor_mean num_guard GUARD c
+builtins_tensor.c:1286 builtin_tensor_mean make_float PRODUCE c
+builtins_tensor.c:1296 builtin_tensor_mean val_is_number TEST a
+builtins_tensor.c:1298 builtin_tensor_mean make_float PRODUCE c
+builtins_tensor.c:1303 builtin_tensor_mean make_float PRODUCE c
+builtins_tensor.c:1313 builtin_tensor_mean make_float PRODUCE c
+builtins_tensor.c:1328 tensor_sum_has_float VAL_FLOAT TEST a
+builtins_tensor.c:1340 tensor_sum_into val_is_number TEST a
+builtins_tensor.c:1341 tensor_sum_into eigs_number_arg READ b
+builtins_tensor.c:1352 tensor_sum_into num_guard GUARD c
+builtins_tensor.c:1357 tensor_sum_into buffer_read_f64 READ b
+builtins_tensor.c:1359 tensor_sum_into num_guard GUARD c
+builtins_tensor.c:1372 builtin_tensor_sum val_is_number TEST a
+builtins_tensor.c:1373 builtin_tensor_sum make_float PRODUCE c
+builtins_tensor.c:1380 builtin_tensor_sum make_float PRODUCE c
+builtins_tensor.c:1380 builtin_tensor_sum make_int PRODUCE c
+builtins_tensor.c:1389 builtin_tensor_norm buffer_read_f64 READ b
+builtins_tensor.c:1391 builtin_tensor_norm num_guard GUARD c
+builtins_tensor.c:1391 builtin_tensor_norm num_guard GUARD c
+builtins_tensor.c:1393 builtin_tensor_norm make_float PRODUCE c
+builtins_tensor.c:1393 builtin_tensor_norm num_guard GUARD c
+builtins_tensor.c:1403 builtin_tensor_norm val_is_number TEST a
+builtins_tensor.c:1405 builtin_tensor_norm make_float PRODUCE c
+builtins_tensor.c:1409 builtin_tensor_norm make_float PRODUCE c
+builtins_tensor.c:1417 builtin_tensor_norm num_guard GUARD c
+builtins_tensor.c:1417 builtin_tensor_norm num_guard GUARD c
+builtins_tensor.c:1419 builtin_tensor_norm make_float PRODUCE c
+builtins_tensor.c:1419 builtin_tensor_norm num_guard GUARD c
+builtins_tensor.c:1440 builtin_tensor_zeros val_is_number TEST a
+builtins_tensor.c:1441 builtin_tensor_zeros eigs_int_arg READ b
+builtins_tensor.c:1450 builtin_tensor_zeros VAL_FLOAT TEST a
+builtins_tensor.c:1450 builtin_tensor_zeros VAL_INT TEST a
+builtins_tensor.c:1451 builtin_tensor_zeros VAL_FLOAT TEST a
+builtins_tensor.c:1451 builtin_tensor_zeros VAL_INT TEST a
+builtins_tensor.c:1452 builtin_tensor_zeros eigs_list_int READ b
+builtins_tensor.c:1453 builtin_tensor_zeros eigs_list_int READ b
+builtins_tensor.c:1467 builtin_tensor_zeros make_float PRODUCE c
+builtins_tensor.c:1482 builtin_tensor_zeros_like make_float PRODUCE c
+builtins_tensor.c:1482 builtin_tensor_zeros_like val_is_number TEST a
+builtins_tensor.c:1499 builtin_tensor_zeros_like make_float PRODUCE c
+builtins_tensor.c:1515 flat_index_at eigs_slot_int READ b
+builtins_tensor.c:1517 flat_index_at buffer_index_at READ b
+builtins_tensor.c:1524 flat_index_is_num VAL_FLOAT TEST a
+builtins_tensor.c:1524 flat_index_is_num VAL_INT TEST a
+builtins_tensor.c:1619 builtin_tensor_gather val_is_number TEST a
+builtins_tensor.c:1620 builtin_tensor_gather eigs_int_arg READ b
+builtins_tensor.c:1627 builtin_tensor_gather buffer_read_f64 READ b
+builtins_tensor.c:1627 builtin_tensor_gather make_float PRODUCE c
+builtins_tensor.c:1670 builtin_tensor_gather eigs_float_arg READ b
+builtins_tensor.c:1673 builtin_tensor_gather make_float PRODUCE c
+builtins_tensor.c:1677 builtin_tensor_gather VAL_FLOAT TEST a
+builtins_tensor.c:1677 builtin_tensor_gather VAL_INT TEST a
+builtins_tensor.c:1683 builtin_tensor_gather eigs_slot_int READ b
+builtins_tensor.c:1694 builtin_tensor_gather eigs_slot_elem_number READ b
+builtins_tensor.c:1696 builtin_tensor_gather make_float PRODUCE c
+builtins_tensor.c:1696 builtin_tensor_gather make_int PRODUCE c
+builtins_tensor.c:1701 builtin_tensor_gather val_is_number TEST a
+builtins_tensor.c:1702 builtin_tensor_gather eigs_int_arg READ b
+builtins_tensor.c:1709 builtin_tensor_gather eigs_slot_elem_number READ b
+builtins_tensor.c:1711 builtin_tensor_gather make_float PRODUCE c
+builtins_tensor.c:1711 builtin_tensor_gather make_int PRODUCE c
+builtins_tensor.c:1720 builtin_tensor_gather make_float PRODUCE c
+builtins_tensor.c:1819 builtin_random_normal eigs_list_int_range READ b
+builtins_tensor.c:1821 builtin_random_normal eigs_list_int_range READ b
+builtins_tensor.c:1823 builtin_random_normal eigs_list_float READ b
+builtins_tensor.c:1837 builtin_random_normal make_float PRODUCE c
+builtins_tensor.c:1849 builtin_random_normal make_float PRODUCE c
+builtins_tensor.c:1860 builtin_tensor_shape val_is_number TEST a
+builtins_tensor.c:1868 builtin_tensor_shape make_int PRODUCE c
+builtins_tensor.c:1869 builtin_tensor_shape make_int PRODUCE c
+builtins_tensor.c:1873 builtin_tensor_shape make_int PRODUCE c
+builtins_tensor.c:1879 builtin_tensor_shape make_int PRODUCE c
+builtins_tensor.c:1887 builtin_tensor_shape make_int PRODUCE c
+builtins_tensor.c:1888 builtin_tensor_shape make_int PRODUCE c
+builtins_tensor.c:1893 builtin_tensor_shape make_int PRODUCE c
+builtins_tensor.c:1902 numerical_loss val_is_number TEST a
+builtins_tensor.c:1903 numerical_loss eigs_float_arg READ b
+builtins_tensor.c:1925 gradient_perturb list_set_f64 PRODUCE c
+builtins_tensor.c:1927 gradient_perturb list_get_f64 READ b
+builtins_tensor.c:1951 gradient_container_unchanged list_get_f64 READ b
+builtins_tensor.c:1952 gradient_container_unchanged list_set_f64 PRODUCE c
+builtins_tensor.c:1979 tensor_cells_numeric VAL_FLOAT TEST a
+builtins_tensor.c:1979 tensor_cells_numeric VAL_INT TEST a
+builtins_tensor.c:1980 tensor_cells_numeric eigs_slot_float READ b
+builtins_tensor.c:1983 tensor_cells_numeric VAL_FLOAT TEST a
+builtins_tensor.c:1983 tensor_cells_numeric VAL_INT TEST a
+builtins_tensor.c:1984 tensor_cells_numeric eigs_slot_float READ b
+builtins_tensor.c:2001 builtin_numerical_grad eigs_slot_opt_float READ b
+builtins_tensor.c:2037 builtin_numerical_grad eigs_slot_float READ b
+builtins_tensor.c:2046 builtin_numerical_grad list_set_f64 PRODUCE c
+builtins_tensor.c:2048 builtin_numerical_grad make_float PRODUCE c
+builtins_tensor.c:2070 builtin_numerical_grad eigs_slot_float READ b
+builtins_tensor.c:2082 builtin_numerical_grad list_set_f64 PRODUCE c
+builtins_tensor.c:2084 builtin_numerical_grad make_float PRODUCE c
+builtins_tensor.c:2111 builtin_sgd_update eigs_slot_opt_float READ b
+builtins_tensor.c:2132 builtin_sgd_update eigs_slot_float READ b
+builtins_tensor.c:2133 builtin_sgd_update eigs_slot_float READ b
+builtins_tensor.c:2134 builtin_sgd_update list_set_f64 PRODUCE c
+builtins_tensor.c:2147 builtin_sgd_update eigs_slot_float READ b
+builtins_tensor.c:2148 builtin_sgd_update eigs_slot_float READ b
+builtins_tensor.c:2149 builtin_sgd_update list_set_f64 PRODUCE c
+builtins_tensor.c:2173 builtin_numerical_grad_rows eigs_slot_opt_float READ b
+builtins_tensor.c:2226 builtin_numerical_grad_rows make_float PRODUCE c
+builtins_tensor.c:2247 builtin_numerical_grad_rows eigs_slot_float READ b
+builtins_tensor.c:2259 builtin_numerical_grad_rows list_set_f64 PRODUCE c
+builtins_tensor.c:2262 builtin_numerical_grad_rows list_set_f64 PRODUCE c
+builtins_tensor.c:2291 builtin_sgd_update_rows eigs_slot_opt_float READ b
+builtins_tensor.c:2335 builtin_sgd_update_rows eigs_slot_float READ b
+builtins_tensor.c:2336 builtin_sgd_update_rows eigs_slot_float READ b
+builtins_tensor.c:2337 builtin_sgd_update_rows list_set_f64 PRODUCE c
+builtins_tensor.c:2361 builtin_numerical_grad_cols eigs_slot_opt_float READ b
+builtins_tensor.c:2417 builtin_numerical_grad_cols make_float PRODUCE c
+builtins_tensor.c:2434 builtin_numerical_grad_cols eigs_slot_float READ b
+builtins_tensor.c:2449 builtin_numerical_grad_cols list_set_f64 PRODUCE c
+builtins_tensor.c:2453 builtin_numerical_grad_cols list_set_f64 PRODUCE c
+builtins_tensor.c:2482 builtin_sgd_update_cols eigs_slot_opt_float READ b
+builtins_tensor.c:2538 builtin_sgd_update_cols eigs_slot_float READ b
+builtins_tensor.c:2539 builtin_sgd_update_cols eigs_slot_float READ b
+builtins_tensor.c:2540 builtin_sgd_update_cols list_set_f64 PRODUCE c
+builtins_tensor.c:2661 tensor_load_record_verdict make_int PRODUCE c
+builtins_tensor.c:2662 tensor_load_record_verdict make_int PRODUCE c
+builtins_tensor.c:2681 builtin_tensor_load VAL_INT TEST a
+builtins_tensor.c:2682 builtin_tensor_load VAL_INT TEST a
+builtins_tensor.c:2756 builtin_tensor_load num_guard_named GUARD c
+chunk.c:196 const_hash_value VAL_FLOAT TEST f
+chunk.c:197 const_hash_value VAL_F64_RAW RAW f
+chunk.c:205 const_hash_value VAL_INT TEST f
+chunk.c:206 const_hash_value VAL_I64_RAW RAW f
+chunk.c:221 const_equal VAL_FLOAT TEST f
+chunk.c:221 const_equal VAL_FLOAT TEST f
+chunk.c:223 const_equal VAL_F64_RAW RAW f
+chunk.c:223 const_equal VAL_F64_RAW RAW f
+chunk.c:226 const_equal VAL_INT TEST f
+chunk.c:226 const_equal VAL_INT TEST f
+chunk.c:227 const_equal VAL_I64_RAW RAW f
+chunk.c:227 const_equal VAL_I64_RAW RAW f
+chunk.c:247 const_dedup_grow val_is_number TEST a
+chunk.c:256 chunk_add_constant_ex val_is_number TEST a
+chunk.c:422 chunk_disassemble VAL_FLOAT TEST e
+chunk.c:423 chunk_disassemble number RAW e
+chunk.c:424 chunk_disassemble VAL_F64_RAW RAW e
+chunk.c:424 chunk_disassemble eigs_num_text FORMAT e
+chunk.c:424 chunk_disassemble number RAW e
+chunk.c:424 chunk_disassemble number RAW e
+chunk.c:425 chunk_disassemble number RAW e
+chunk.c:427 chunk_disassemble VAL_INT TEST e
+chunk.c:428 chunk_disassemble VAL_I64_RAW RAW e
+chunk.c:732 chunk_verify_impl VAL_INT TEST a
+chunk.c:1038 chunk_scan_leaf_accessor/OP_CONST VAL_FLOAT TEST a
+compiler.c:643 add_num_constant make_float PRODUCE c
+compiler.c:650 add_int_constant make_int PRODUCE c
+compiler.c:659 literal_index_u16 data.number RAW b
+compiler.c:659 literal_index_u16 number RAW b
+compiler.c:876 collect_referenced_names_skip AST_NUM CASE a
+compiler.c:1020 collect_referenced_names AST_NUM CASE a
+compiler.c:1159 scan_for_captures AST_NUM CASE a
+compiler.c:1266 ast_has_closure AST_NUM CASE a
+compiler.c:1344 subtree_overwrite_safe AST_NUM CASE a
+compiler.c:1532 scan_for_interrogated AST_NUM CASE a
+compiler.c:1635 walk_scope_ast AST_NUM CASE a
+compiler.c:1881 scan_dispatch_rebind AST_NUM CASE a
+compiler.c:2113 cond_is_observer_based AST_NUM CASE i
+compiler.c:2201 compile_node_inner AST_NUM CASE a
+compiler.c:2202 compile_node_inner data.number RAW b
+compiler.c:2202 compile_node_inner number RAW b
+compiler.c:2202 compile_node_inner number RAW b
+compiler.c:2203 compile_node_inner number RAW b
+compiler.c:2204 compile_node_inner number RAW b
+compiler.c:2210 compile_node_inner number RAW b
+compiler.c:3123 compile_node_inner AST_NUM TEST a
+compiler.c:3178 compile_node_inner AST_NUM TEST a
+compiler.c:3222 compile_node_inner AST_NUM TEST a
+compiler.c:4063 obs_ast_scan_d AST_NUM CASE i
+container.h:27 eigs_container_slot VAL_F64_RAW RAW b
+container.h:27 eigs_container_slot VAL_FLOAT TEST a
+container.h:27 eigs_container_slot num_guard GUARD c
+container.h:27 eigs_container_slot slot_from_f64 PRODUCE c
+container.h:30 eigs_container_slot VAL_INT TEST a
+container.h:31 eigs_container_slot VAL_I64_RAW RAW b
+container.h:32 eigs_container_slot slot_from_int PRODUCE c
+container.h:50 eigs_slot_store_owned SLOT_F64_RAW RAW b
+container.h:50 eigs_slot_store_owned num_guard GUARD c
+container.h:50 eigs_slot_store_owned slot_from_f64 PRODUCE c
+container.h:50 eigs_slot_store_owned slot_is_f64 TEST a
+container.h:53 eigs_slot_store_owned VAL_FLOAT TEST a
+container.h:53 eigs_slot_store_owned VAL_INT TEST a
+container.h:59 eigs_slot_store_borrow SLOT_F64_RAW RAW b
+container.h:59 eigs_slot_store_borrow num_guard GUARD c
+container.h:59 eigs_slot_store_borrow slot_from_f64 PRODUCE c
+container.h:59 eigs_slot_store_borrow slot_is_f64 TEST a
+container.h:86 slot_type VAL_FLOAT YIELD a
+container.h:86 slot_type slot_is_f64 TEST a
+container.h:90 slot_type VAL_INT YIELD a
+container.h:90 slot_type slot_is_smi TEST a
+container.h:116 list_get_ref slot_is_f64 TEST a
+container.h:116 list_get_ref slot_is_smi TEST a
+container.h:120 dict_value_get_ref slot_is_f64 TEST a
+container.h:120 dict_value_get_ref slot_is_smi TEST a
+container.h:129 slot_view slot_is_f64 TEST a
+container.h:130 slot_view slot_is_smi TEST a
+container.h:131 slot_view VAL_INT TAGSET c
+container.h:132 slot_view VAL_I64_RAW WRITE c
+container.h:132 slot_view slot_smi_val RAW b
+container.h:137 slot_view VAL_FLOAT TEST a
+container.h:139 slot_view VAL_FLOAT TAGSET c
+container.h:142 slot_view VAL_F64_RAW RAW b
+container.h:142 slot_view slot_f64_value READ b
+container.h:155 eigs_view_check VAL_FLOAT TEST a
+container.h:155 eigs_view_check VAL_INT TEST a
+container.h:174 eigs_slot_number eigs_slot_number READ b
+container.h:175 eigs_slot_number eigs_number_arg READ b
+container.h:178 eigs_slot_elem_number eigs_slot_elem_number READ b
+container.h:179 eigs_slot_elem_number eigs_elem_number READ b
+container.h:182 eigs_slot_opt_number eigs_slot_opt_number READ b
+container.h:183 eigs_slot_opt_number eigs_opt_number READ b
+container.h:186 eigs_slot_float eigs_slot_float READ b
+container.h:187 eigs_slot_float eigs_float_arg READ b
+container.h:190 eigs_slot_elem_float eigs_slot_elem_float READ b
+container.h:191 eigs_slot_elem_float eigs_elem_float READ b
+container.h:194 eigs_slot_opt_float eigs_slot_opt_float READ b
+container.h:195 eigs_slot_opt_float eigs_opt_float READ b
+container.h:198 eigs_slot_int eigs_slot_int READ b
+container.h:199 eigs_slot_int eigs_int_arg READ b
+container.h:202 eigs_slot_elem_int eigs_slot_elem_int READ b
+container.h:203 eigs_slot_elem_int eigs_elem_int READ b
+container.h:206 eigs_slot_opt_int eigs_slot_opt_int READ b
+container.h:207 eigs_slot_opt_int eigs_opt_int READ b
+container.h:210 eigs_slot_byte eigs_slot_byte READ b
+container.h:211 eigs_slot_byte eigs_byte_arg READ b
+container.h:214 eigs_slot_elem_byte eigs_slot_elem_byte READ b
+container.h:215 eigs_slot_elem_byte eigs_elem_byte READ b
+container.h:218 eigs_slot_opt_byte eigs_slot_opt_byte READ b
+container.h:219 eigs_slot_opt_byte eigs_opt_byte READ b
+container.h:222 eigs_slot_int_range eigs_slot_int_range READ b
+container.h:223 eigs_slot_int_range eigs_int_arg_range READ b
+container.h:226 eigs_slot_elem_int_range eigs_slot_elem_int_range READ b
+container.h:227 eigs_slot_elem_int_range eigs_elem_int_range READ b
+container.h:230 eigs_slot_opt_int_range eigs_slot_opt_int_range READ b
+container.h:231 eigs_slot_opt_int_range eigs_opt_int_range READ b
+container.h:234 eigs_slot_bounded eigs_slot_bounded READ b
+container.h:235 eigs_slot_bounded eigs_bounded_arg READ b
+container.h:238 eigs_slot_elem_bounded eigs_slot_elem_bounded READ b
+container.h:239 eigs_slot_elem_bounded eigs_elem_bounded READ b
+container.h:242 eigs_slot_opt_bounded eigs_slot_opt_bounded READ b
+container.h:243 eigs_slot_opt_bounded eigs_opt_bounded READ b
+container.h:246 eigs_slot_magnitude eigs_slot_magnitude READ b
+container.h:247 eigs_slot_magnitude eigs_magnitude_arg READ b
+container.h:250 eigs_slot_elem_magnitude eigs_slot_elem_magnitude READ b
+container.h:251 eigs_slot_elem_magnitude eigs_elem_magnitude READ b
+container.h:254 eigs_slot_opt_magnitude eigs_slot_opt_magnitude READ b
+container.h:255 eigs_slot_opt_magnitude eigs_opt_magnitude READ b
+container.h:268 list_get_return slot_is_smi TEST a
+container.h:269 list_get_return SLOT_F64_RAW RAW b
+container.h:269 list_get_return make_float PRODUCE c
+container.h:269 list_get_return slot_is_f64 TEST a
+container.h:277 dict_value_get_return slot_is_smi TEST a
+container.h:278 dict_value_get_return SLOT_F64_RAW RAW b
+container.h:278 dict_value_get_return make_float PRODUCE c
+container.h:278 dict_value_get_return slot_is_f64 TEST a
+container.h:308 eigs_slot_box SLOT_F64_RAW RAW b
+container.h:308 eigs_slot_box make_float PRODUCE c
+container.h:308 eigs_slot_box num_guard GUARD c
+container.h:308 eigs_slot_box slot_is_f64 TEST a
+container.h:309 eigs_slot_box slot_is_smi TEST a
+container.h:334 list_rewrite_f64 list_rewrite_f64 PRODUCE c
+container.h:336 list_rewrite_f64 slot_from_f64 PRODUCE c
+container.h:340 list_set_f64 list_set_f64 PRODUCE c
+container.h:341 list_set_f64 list_rewrite_f64 PRODUCE c
+container.h:341 list_set_f64 num_guard GUARD c
+container.h:344 list_get_f64 list_get_f64 READ b
+container.h:345 list_get_f64 slot_f64_value READ b
+container.h:348 dict_value_rewrite_f64 dict_value_rewrite_f64 PRODUCE c
+container.h:350 dict_value_rewrite_f64 slot_from_f64 PRODUCE c
+container.h:354 dict_value_set_f64 dict_value_set_f64 PRODUCE c
+container.h:355 dict_value_set_f64 dict_value_rewrite_f64 PRODUCE c
+container.h:355 dict_value_set_f64 num_guard GUARD c
+container.h:358 dict_value_get_f64 dict_value_get_f64 READ b
+container.h:359 dict_value_get_f64 slot_f64_value READ b
+container.h:362 list_append_f64 list_append_f64 PRODUCE c
+container.h:363 list_append_f64 num_guard GUARD c
+container.h:365 list_append_f64 slot_from_f64 PRODUCE c
+container.h:368 list_rewrite_int list_rewrite_int PRODUCE c
+container.h:370 list_rewrite_int slot_from_int PRODUCE c
+container.h:374 list_set_int list_set_int PRODUCE c
+container.h:375 list_set_int list_rewrite_int PRODUCE c
+container.h:385 list_get_int list_get_int READ b
+container.h:386 list_get_int slot_int_value READ b
+container.h:389 dict_value_rewrite_int dict_value_rewrite_int PRODUCE c
+container.h:391 dict_value_rewrite_int slot_from_int PRODUCE c
+container.h:395 dict_value_set_int dict_value_set_int PRODUCE c
+container.h:396 dict_value_set_int dict_value_rewrite_int PRODUCE c
+container.h:399 dict_value_get_int dict_value_get_int READ b
+container.h:400 dict_value_get_int slot_int_value READ b
+container.h:403 list_append_int list_append_int PRODUCE c
+container.h:406 list_append_int slot_from_int PRODUCE c
+eigenlsp.c:500 walk_ast_symbols AST_NUM CASE a
+eigenlsp.c:640 collect_references AST_NUM CASE a
+eigenlsp.c:1162 handle_hover number RAW b
+eigenscript.c:330 val_type_name VAL_FLOAT CASE a
+eigenscript.c:331 val_type_name VAL_INT CASE a
+eigenscript.c:450 ent_child_slot SLOT_F64_RAW RAW i
+eigenscript.c:450 ent_child_slot slot_is_f64 TEST i
+eigenscript.c:452 ent_child_slot slot_is_smi TEST i
+eigenscript.c:452 ent_child_slot slot_smi_val RAW i
+eigenscript.c:460 compute_entropy_impl VAL_F64_RAW RAW i
+eigenscript.c:460 compute_entropy_impl VAL_FLOAT CASE i
+eigenscript.c:463 compute_entropy_impl VAL_I64_RAW RAW i
+eigenscript.c:463 compute_entropy_impl VAL_INT CASE i
+eigenscript.c:624 values_equal_for_observation VAL_FLOAT TEST a
+eigenscript.c:624 values_equal_for_observation VAL_INT TEST a
+eigenscript.c:625 values_equal_for_observation VAL_F64_RAW RAW b
+eigenscript.c:625 values_equal_for_observation VAL_I64_RAW RAW b
+eigenscript.c:626 values_equal_for_observation VAL_FLOAT TEST a
+eigenscript.c:626 values_equal_for_observation VAL_INT TEST a
+eigenscript.c:627 values_equal_for_observation VAL_F64_RAW RAW b
+eigenscript.c:627 values_equal_for_observation VAL_I64_RAW RAW b
+eigenscript.c:631 values_equal_for_observation VAL_F64_RAW RAW b
+eigenscript.c:631 values_equal_for_observation VAL_F64_RAW RAW b
+eigenscript.c:631 values_equal_for_observation VAL_FLOAT CASE a
+eigenscript.c:632 values_equal_for_observation VAL_I64_RAW RAW b
+eigenscript.c:632 values_equal_for_observation VAL_I64_RAW RAW b
+eigenscript.c:632 values_equal_for_observation VAL_INT CASE a
+eigenscript.c:1027 observer_slot_update_locked val_is_number TEST i
+eigenscript.c:1029 observer_slot_update_locked VAL_I64_RAW RAW i
+eigenscript.c:1029 observer_slot_update_locked VAL_INT TEST i
+eigenscript.c:1030 observer_slot_update_locked VAL_F64_RAW RAW i
+eigenscript.c:1090 observer_slot_update_value_num_shared VAL_I64_RAW RAW i
+eigenscript.c:1090 observer_slot_update_value_num_shared VAL_INT TEST i
+eigenscript.c:1091 observer_slot_update_value_num_shared VAL_F64_RAW RAW i
+eigenscript.c:1164 observer_slot_update val_is_number TEST i
+eigenscript.c:1233 observer_slot_sample_gated val_is_number TEST i
+eigenscript.c:1234 observer_slot_sample_gated VAL_I64_RAW RAW i
+eigenscript.c:1234 observer_slot_sample_gated VAL_INT TEST i
+eigenscript.c:1235 observer_slot_sample_gated VAL_F64_RAW RAW i
+eigenscript.c:1330 observer_slot_saturated EIGS_NUM_MAX GUARD i
+eigenscript.c:1818 observer_entropy_now slot_is_f64 TEST i
+eigenscript.c:1820 observer_entropy_now SLOT_F64_RAW RAW i
+eigenscript.c:1821 observer_entropy_now slot_is_smi TEST i
+eigenscript.c:1823 observer_entropy_now slot_smi_val RAW i
+eigenscript.c:1868 observer_slot_trajectory make_float PRODUCE i
+eigenscript.c:1869 observer_slot_trajectory make_float PRODUCE i
+eigenscript.c:1872 observer_slot_trajectory make_float PRODUCE i
+eigenscript.c:1876 observer_slot_trajectory make_float PRODUCE i
+eigenscript.c:1877 observer_slot_trajectory make_float PRODUCE i
+eigenscript.c:1878 observer_slot_trajectory make_float PRODUCE i
+eigenscript.c:1879 observer_slot_trajectory make_float PRODUCE i
+eigenscript.c:1885 observer_slot_trajectory make_int PRODUCE i
+eigenscript.c:1918 observer_slot_from_trajectory eigs_float_arg READ i
+eigenscript.c:1918 observer_slot_from_trajectory val_is_number TEST i
+eigenscript.c:1919 observer_slot_from_trajectory val_is_number TEST i
+eigenscript.c:1937 observer_slot_from_trajectory VAL_FLOAT TEST i
+eigenscript.c:1937 observer_slot_from_trajectory VAL_FLOAT TEST i
+eigenscript.c:1937 observer_slot_from_trajectory VAL_INT TEST i
+eigenscript.c:1937 observer_slot_from_trajectory VAL_INT TEST i
+eigenscript.c:1942 observer_slot_from_trajectory eigs_slot_float READ i
+eigenscript.c:1943 observer_slot_from_trajectory eigs_slot_float READ i
+eigenscript.c:1953 observer_slot_from_trajectory VAL_FLOAT TEST i
+eigenscript.c:1953 observer_slot_from_trajectory VAL_INT TEST i
+eigenscript.c:1958 observer_slot_from_trajectory eigs_slot_float READ i
+eigenscript.c:1971 observer_slot_from_trajectory val_is_number TEST i
+eigenscript.c:1976 observer_slot_from_trajectory eigs_float_arg READ i
+eigenscript.c:2079 free_value VAL_FLOAT TEST a
+eigenscript.c:2160 free_value VAL_FLOAT CASE a
+eigenscript.c:2161 free_value VAL_INT CASE a
+eigenscript.c:2172 make_float_pooled num_guard GUARD c
+eigenscript.c:2188 make_float_pooled VAL_FLOAT TAGSET c
+eigenscript.c:2189 make_float_pooled VAL_F64_RAW WRITE c
+eigenscript.c:2195 make_float make_float PRODUCE c
+eigenscript.c:2199 make_int make_int PRODUCE c
+eigenscript.c:2201 make_int VAL_INT TAGSET c
+eigenscript.c:2202 make_int VAL_I64_RAW WRITE c
+eigenscript.c:2208 recycle_intermediate VAL_FLOAT TEST a
+eigenscript.c:2230 make_float_permanent make_float_permanent PRODUCE c
+eigenscript.c:2231 make_float_permanent num_guard GUARD c
+eigenscript.c:2233 make_float_permanent VAL_FLOAT TAGSET c
+eigenscript.c:2234 make_float_permanent VAL_F64_RAW WRITE c
+eigenscript.c:2267 eigs_number_read VAL_INT TEST a
+eigenscript.c:2268 eigs_number_read VAL_I64_RAW RAW b
+eigenscript.c:2270 eigs_number_read VAL_FLOAT TEST a
+eigenscript.c:2271 eigs_number_read VAL_F64_RAW RAW b
+eigenscript.c:2279 eigs_number_arg eigs_number_arg READ b
+eigenscript.c:2290 eigs_float_arg eigs_float_arg READ b
+eigenscript.c:2291 eigs_float_arg eigs_number_arg READ b
+eigenscript.c:2295 eigs_int_arg eigs_int_arg READ b
+eigenscript.c:2300 eigs_int_arg num_to_i64_exact READ b
+eigenscript.c:2306 eigs_int_arg_range READ b
+eigenscript.c:2308 eigs_int_arg_range eigs_int_arg READ b
+eigenscript.c:2319 eigs_byte_arg eigs_byte_arg READ b
+eigenscript.c:2321 eigs_byte_arg eigs_int_arg_range READ b
+eigenscript.c:2485 eigs_geom_shape READ b
+eigenscript.c:2492 eigs_geom_shape eigs_list_float READ b
+eigenscript.c:2532 eigs_bounded_arg eigs_bounded_arg READ b
+eigenscript.c:2533 eigs_bounded_arg eigs_float_arg READ b
+eigenscript.c:2549 eigs_magnitude_arg READ b
+eigenscript.c:2551 eigs_magnitude_arg eigs_number_arg READ b
+eigenscript.c:2555 eigs_magnitude_arg num_to_i64_exact READ b
+eigenscript.c:2579 eigs_reader_elem_default val_is_number TEST a
+eigenscript.c:2582 eigs_list_number eigs_list_number READ b
+eigenscript.c:2585 eigs_list_number eigs_number_arg READ b
+eigenscript.c:2587 eigs_elem_number eigs_elem_number READ b
+eigenscript.c:2588 eigs_elem_number eigs_number_arg READ b
+eigenscript.c:2590 eigs_opt_number eigs_opt_number READ b
+eigenscript.c:2592 eigs_opt_number eigs_number_arg READ b
+eigenscript.c:2597 eigs_list_float eigs_list_float READ b
+eigenscript.c:2600 eigs_list_float eigs_float_arg READ b
+eigenscript.c:2602 eigs_elem_float eigs_elem_float READ b
+eigenscript.c:2603 eigs_elem_float eigs_float_arg READ b
+eigenscript.c:2605 eigs_opt_float eigs_opt_float READ b
+eigenscript.c:2607 eigs_opt_float eigs_float_arg READ b
+eigenscript.c:2612 eigs_list_int eigs_list_int READ b
+eigenscript.c:2615 eigs_list_int eigs_int_arg READ b
+eigenscript.c:2617 eigs_elem_int eigs_elem_int READ b
+eigenscript.c:2618 eigs_elem_int eigs_int_arg READ b
+eigenscript.c:2620 eigs_opt_int eigs_opt_int READ b
+eigenscript.c:2622 eigs_opt_int eigs_int_arg READ b
+eigenscript.c:2627 eigs_list_byte eigs_list_byte READ b
+eigenscript.c:2630 eigs_list_byte eigs_byte_arg READ b
+eigenscript.c:2632 eigs_elem_byte eigs_elem_byte READ b
+eigenscript.c:2633 eigs_elem_byte eigs_byte_arg READ b
+eigenscript.c:2635 eigs_opt_byte eigs_opt_byte READ b
+eigenscript.c:2637 eigs_opt_byte eigs_byte_arg READ b
+eigenscript.c:2642 eigs_list_int_range eigs_list_int_range READ b
+eigenscript.c:2645 eigs_list_int_range eigs_int_arg_range READ b
+eigenscript.c:2647 eigs_elem_int_range eigs_elem_int_range READ b
+eigenscript.c:2648 eigs_elem_int_range VAL_INT TAGSET c
+eigenscript.c:2649 eigs_elem_int_range eigs_int_arg_range READ b
+eigenscript.c:2651 eigs_opt_int_range eigs_opt_int_range READ b
+eigenscript.c:2653 eigs_opt_int_range eigs_int_arg_range READ b
+eigenscript.c:2656 eigs_list_bounded eigs_list_bounded READ b
+eigenscript.c:2659 eigs_list_bounded eigs_bounded_arg READ b
+eigenscript.c:2661 eigs_elem_bounded eigs_elem_bounded READ b
+eigenscript.c:2662 eigs_elem_bounded VAL_INT TAGSET c
+eigenscript.c:2663 eigs_elem_bounded eigs_bounded_arg READ b
+eigenscript.c:2665 eigs_opt_bounded eigs_opt_bounded READ b
+eigenscript.c:2667 eigs_opt_bounded eigs_bounded_arg READ b
+eigenscript.c:2670 eigs_list_magnitude eigs_list_magnitude READ b
+eigenscript.c:2673 eigs_list_magnitude eigs_magnitude_arg READ b
+eigenscript.c:2675 eigs_elem_magnitude eigs_elem_magnitude READ b
+eigenscript.c:2676 eigs_elem_magnitude VAL_INT TAGSET c
+eigenscript.c:2677 eigs_elem_magnitude eigs_magnitude_arg READ b
+eigenscript.c:2679 eigs_opt_magnitude eigs_opt_magnitude READ b
+eigenscript.c:2681 eigs_opt_magnitude eigs_magnitude_arg READ b
+eigenscript.c:2686 buffer_read_f64 buffer_read_f64 READ b
+eigenscript.c:2692 buffer_read_f64 num_guard GUARD c
+eigenscript.c:2698 buffer_read_f64 num_guard GUARD c
+eigenscript.c:2700 buffer_index_at buffer_index_at READ b
+eigenscript.c:2708 buffer_index_at buffer_read_f64 READ b
+eigenscript.c:2710 buffer_index_at VAL_FLOAT TAGSET c
+eigenscript.c:2711 buffer_index_at VAL_F64_RAW WRITE c
+eigenscript.c:2712 buffer_index_at eigs_int_arg READ b
+eigenscript.c:2716 buffer_byte_at buffer_byte_at READ b
+eigenscript.c:2718 buffer_byte_at buffer_index_at READ b
+eigenscript.c:2758 slot_from_value VAL_FLOAT TEST a
+eigenscript.c:2762 slot_from_value VAL_F64_RAW RAW b
+eigenscript.c:2764 slot_from_value slot_from_f64 PRODUCE c
+eigenscript.c:2768 slot_from_value VAL_INT TEST a
+eigenscript.c:2769 slot_from_value VAL_I64_RAW RAW b
+eigenscript.c:2772 slot_from_value slot_from_int PRODUCE c
+eigenscript.c:2792 slot_to_value SLOT_F64_RAW RAW b
+eigenscript.c:2792 slot_to_value make_float PRODUCE c
+eigenscript.c:2792 slot_to_value slot_is_f64 TEST a
+eigenscript.c:2800 slot_to_value make_int PRODUCE c
+eigenscript.c:2800 slot_to_value slot_is_smi TEST a
+eigenscript.c:2800 slot_to_value slot_smi_val RAW b
+eigenscript.c:3294 module_ns_project slot_is_f64 TEST a
+eigenscript.c:3294 module_ns_project slot_is_f64 TEST a
+eigenscript.c:3299 module_ns_project SLOT_F64_RAW RAW b
+eigenscript.c:3299 module_ns_project dict_value_rewrite_f64 PRODUCE c
+eigenscript.c:3559 chan_clone_rec VAL_FLOAT TEST a
+eigenscript.c:3559 chan_clone_rec VAL_INT TEST a
+eigenscript.c:3562 chan_clone_rec VAL_F64_RAW RAW b
+eigenscript.c:3562 chan_clone_rec VAL_FLOAT CASE a
+eigenscript.c:3562 chan_clone_rec make_float PRODUCE c
+eigenscript.c:3563 chan_clone_rec VAL_I64_RAW RAW b
+eigenscript.c:3563 chan_clone_rec VAL_INT CASE a
+eigenscript.c:3563 chan_clone_rec make_int PRODUCE c
+eigenscript.c:3766 is_truthy VAL_F64_RAW RAW b
+eigenscript.c:3766 is_truthy VAL_FLOAT CASE a
+eigenscript.c:3767 is_truthy VAL_I64_RAW RAW b
+eigenscript.c:3767 is_truthy VAL_INT CASE a
+eigenscript.c:3887 values_equal_impl VAL_FLOAT TEST f
+eigenscript.c:3887 values_equal_impl VAL_INT TEST f
+eigenscript.c:3888 values_equal_impl VAL_F64_RAW RAW f
+eigenscript.c:3888 values_equal_impl VAL_I64_RAW RAW f
+eigenscript.c:3889 values_equal_impl VAL_FLOAT TEST f
+eigenscript.c:3889 values_equal_impl VAL_INT TEST f
+eigenscript.c:3890 values_equal_impl VAL_F64_RAW RAW f
+eigenscript.c:3890 values_equal_impl VAL_I64_RAW RAW f
+eigenscript.c:3891 values_equal_impl val_is_number TEST f
+eigenscript.c:3892 values_equal_impl val_is_number TEST f
+eigenscript.c:3906 values_equal_impl VAL_F64_RAW RAW f
+eigenscript.c:3906 values_equal_impl VAL_F64_RAW RAW f
+eigenscript.c:3906 values_equal_impl VAL_FLOAT CASE f
+eigenscript.c:3907 values_equal_impl VAL_I64_RAW RAW f
+eigenscript.c:3907 values_equal_impl VAL_I64_RAW RAW f
+eigenscript.c:3907 values_equal_impl VAL_INT CASE f
+eigenscript.c:3977 eigs_num_text eigs_num_text FORMAT e
+eigenscript.c:4039 eigs_num_text strtod PARSE e
+eigenscript.c:4094 value_to_string VAL_FLOAT CASE e
+eigenscript.c:4095 value_to_string VAL_F64_RAW RAW e
+eigenscript.c:4095 value_to_string eigs_num_text FORMAT e
+eigenscript.c:4098 value_to_string VAL_INT CASE e
+eigenscript.c:4099 value_to_string VAL_I64_RAW RAW e
+eigenscript.c:5473 gc_value_is_node VAL_FLOAT CASE a
+eigenscript.c:5474 gc_value_is_node VAL_INT CASE a
+eigenscript.h:196 number RAW b
+eigenscript.h:226 AST_NUM DATA a
+eigenscript.h:252 number RAW b
+eigenscript.h:293 VAL_FLOAT DATA a
+eigenscript.h:302 VAL_INT DATA a
+eigenscript.h:310 #define val_is_number VAL_FLOAT TEST a
+eigenscript.h:310 #define val_is_number VAL_INT TEST a
+eigenscript.h:310 #define val_is_number val_is_number TEST a
+eigenscript.h:331 #define VAL_F64_RAW VAL_F64_RAW RAW b
+eigenscript.h:331 #define VAL_F64_RAW f64_ RAW b
+eigenscript.h:332 #define VAL_I64_RAW VAL_I64_RAW RAW b
+eigenscript.h:332 #define VAL_I64_RAW i64_ RAW b
+eigenscript.h:333 #define VAL_F64_OFFSET VAL_F64_OFFSET RAW b
+eigenscript.h:333 #define VAL_F64_OFFSET f64_ RAW b
+eigenscript.h:1440 eigs_number_arg READ b
+eigenscript.h:1441 eigs_list_number READ b
+eigenscript.h:1442 eigs_elem_number READ b
+eigenscript.h:1443 eigs_opt_number READ b
+eigenscript.h:1444 eigs_float_arg READ b
+eigenscript.h:1445 eigs_list_float READ b
+eigenscript.h:1446 eigs_elem_float READ b
+eigenscript.h:1447 eigs_opt_float READ b
+eigenscript.h:1448 eigs_int_arg READ b
+eigenscript.h:1449 eigs_list_int READ b
+eigenscript.h:1450 eigs_elem_int READ b
+eigenscript.h:1451 eigs_opt_int READ b
+eigenscript.h:1452 eigs_byte_arg READ b
+eigenscript.h:1453 eigs_list_byte READ b
+eigenscript.h:1454 eigs_elem_byte READ b
+eigenscript.h:1455 eigs_opt_byte READ b
+eigenscript.h:1456 eigs_int_arg_range READ b
+eigenscript.h:1457 eigs_list_int_range READ b
+eigenscript.h:1458 eigs_elem_int_range READ b
+eigenscript.h:1459 eigs_opt_int_range READ b
+eigenscript.h:1469 eigs_geom_shape READ b
+eigenscript.h:1471 eigs_bounded_arg READ b
+eigenscript.h:1472 eigs_list_bounded READ b
+eigenscript.h:1473 eigs_elem_bounded READ b
+eigenscript.h:1474 eigs_opt_bounded READ b
+eigenscript.h:1475 eigs_magnitude_arg READ b
+eigenscript.h:1476 eigs_list_magnitude READ b
+eigenscript.h:1477 eigs_elem_magnitude READ b
+eigenscript.h:1478 eigs_opt_magnitude READ b
+eigenscript.h:1853 make_float PRODUCE c
+eigenscript.h:1854 make_float_permanent PRODUCE c
+eigenscript.h:1855 make_int PRODUCE c
+eigenscript.h:1918 #define EIGS_NUM_MAX EIGS_NUM_MAX GUARD c
+eigenscript.h:1967 num_guard_value EIGS_NUM_MAX GUARD c
+eigenscript.h:1967 num_guard_value EIGS_NUM_MAX GUARD c
+eigenscript.h:1968 num_guard_value EIGS_NUM_MAX GUARD c
+eigenscript.h:1968 num_guard_value EIGS_NUM_MAX GUARD c
+eigenscript.h:1972 num_guard num_guard GUARD c
+eigenscript.h:1980 num_guard EIGS_NUM_MAX GUARD c
+eigenscript.h:1980 num_guard EIGS_NUM_MAX GUARD c
+eigenscript.h:1981 num_guard EIGS_NUM_MAX GUARD c
+eigenscript.h:1981 num_guard EIGS_NUM_MAX GUARD c
+eigenscript.h:1990 num_to_i64_exact num_to_i64_exact READ b
+eigenscript.h:2009 buffer_read_f64 READ b
+eigenscript.h:2010 buffer_byte_at READ b
+eigenscript.h:2011 buffer_index_at READ b
+eigenscript.h:2023 num_guard_named num_guard_named GUARD c
+eigenscript.h:2029 num_guard_named num_guard GUARD c
+eigenscript.h:2340 eigs_num_text FORMAT e
+eigs_embed.c:372 eigs_value_new_num make_float PRODUCE j
+eigs_embed.c:373 eigs_value_new_int make_int PRODUCE j
+eigs_embed.c:386 eigs_value_type VAL_FLOAT CASE j
+eigs_embed.c:387 eigs_value_type VAL_INT CASE j
+eigs_embed.c:431 eigs_value_as_num eigs_value_as_num READ j
+eigs_embed.c:436 eigs_value_as_num VAL_I64_RAW RAW j
+eigs_embed.c:436 eigs_value_as_num VAL_INT TEST j
+eigs_embed.c:437 eigs_value_as_num VAL_F64_RAW RAW j
+eigs_embed.c:437 eigs_value_as_num VAL_FLOAT TEST j
+eigs_embed.c:444 eigs_value_as_int VAL_INT TEST j
+eigs_embed.c:446 eigs_value_as_int VAL_I64_RAW RAW j
+eigs_embed.c:448 eigs_value_as_int VAL_FLOAT TEST j
+eigs_embed.c:454 eigs_value_as_int VAL_F64_RAW RAW j
+eigs_embed.c:454 eigs_value_as_int num_to_i64_exact READ j
+eigs_embed.c:464 eigs_value_is_int VAL_INT TEST j
+eigs_embed.c:666 eigs_trace_declare_kind VAL_FLOAT DATA j
+eigs_embed.c:666 eigs_trace_declare_kind VAL_INT DATA j
+eigs_embed.h:173 eigs_value_as_num READ j
+embed_concurrent.c:106 global_worker eigs_value_as_num READ j
+embed_concurrent.c:768 take_drain eigs_value_as_num READ j
+embed_concurrent.c:1268 test_sink_only_buffer_bounded eigs_value_as_num READ j
+embed_concurrent.c:1763 fu_worker eigs_value_as_num READ j
+embed_concurrent.c:1830 test_register_while_workers_read eigs_value_as_num READ j
+embed_concurrent.c:1834 test_register_while_workers_read eigs_value_as_num READ j
+embed_smoke.c:61 test_embed_int eigs_value_as_num READ j
+embed_smoke.c:100 test_embed_int VAL_F64_RAW WRITE j
+embed_smoke.c:494 test_worker_exit_lifecycle eigs_value_as_num READ j
+embed_smoke.c:502 test_worker_exit_lifecycle eigs_slot_float READ j
+embed_smoke.c:503 test_worker_exit_lifecycle eigs_slot_float READ j
+embed_smoke.c:518 test_worker_exit_lifecycle eigs_value_as_num READ j
+embed_smoke.c:604 host_add eigs_value_as_num READ j
+embed_smoke.c:604 host_add eigs_value_as_num READ j
+embed_smoke.c:640 unattached_buffer_probe eigs_value_as_num READ j
+embed_smoke.c:673 main SLOT_F64_RAW WRITE j
+embed_smoke.c:675 main SLOT_F64_RAW WRITE j
+embed_smoke.c:677 main SLOT_F64_RAW RAW j
+embed_smoke.c:680 main SLOT_F64_RAW RAW j
+embed_smoke.c:682 main SLOT_F64_RAW RAW j
+embed_smoke.c:685 main slot_is_smi TEST j
+embed_smoke.c:685 main slot_smi_val RAW j
+embed_smoke.c:718 main eigs_value_as_num READ j
+embed_smoke.c:726 main eigs_value_as_num READ j
+embed_smoke.c:736 main eigs_value_as_num READ j
+embed_smoke.c:761 main eigs_value_as_num READ j
+embed_smoke.c:769 main eigs_value_as_num READ j
+embed_smoke.c:794 main eigs_value_as_num READ j
+embed_smoke.c:820 main eigs_value_as_num READ j
+embed_smoke.c:831 main eigs_value_as_num READ j
+embed_smoke.c:836 main eigs_value_as_num READ j
+embed_smoke.c:844 main eigs_value_as_num READ j
+embed_smoke.c:913 main eigs_value_as_num READ j
+embed_smoke.c:931 main eigs_value_as_num READ j
+embed_smoke.c:940 main eigs_value_as_num READ j
+embed_smoke.c:1003 main eigs_value_as_num READ j
+embed_smoke.c:1289 main eigs_value_as_num READ j
+embed_smoke.c:1332 main eigs_value_as_num READ j
+embed_smoke.c:1338 main eigs_value_as_num READ j
+embed_smoke.c:1342 main eigs_value_as_num READ j
+embed_smoke.c:1376 main eigs_value_as_num READ j
+embed_smoke.c:1454 main eigs_value_as_num READ j
+embed_smoke.c:1465 main eigs_value_as_num READ j
+embed_smoke.c:1495 main SLOT_F64_RAW WRITE j
+embed_smoke.c:1526 main eigs_value_as_num READ j
+embed_smoke.c:1536 main eigs_value_as_num READ j
+embed_smoke.c:1548 main eigs_value_as_num READ j
+embed_smoke.c:1548 main eigs_value_as_num READ j
+embed_smoke.c:1560 main eigs_value_as_num READ j
+embed_smoke.c:1569 main eigs_value_as_num READ j
+embed_smoke.c:1587 main eigs_value_as_num READ j
+embed_smoke.c:1647 main eigs_value_as_num READ j
+embed_smoke.c:1659 main eigs_value_as_num READ j
+embed_smoke.c:1663 main eigs_value_as_num READ j
+embed_smoke.c:1668 main eigs_value_as_num READ j
+embed_smoke.c:1710 main eigs_value_as_num READ j
+embed_smoke.c:1715 main eigs_value_as_num READ j
+embed_smoke.c:1724 main eigs_value_as_num READ j
+embed_smoke.c:1743 main eigs_value_as_num READ j
+embed_smoke.c:1747 main eigs_value_as_num READ j
+embed_smoke.c:1756 main eigs_value_as_num READ j
+embed_smoke.c:1760 main eigs_value_as_num READ j
+embed_smoke.c:1767 main eigs_value_as_num READ j
+embed_smoke.c:1823 main eigs_value_as_num READ j
+embed_smoke.c:1847 main eigs_value_as_num READ j
+embed_smoke.c:1874 main eigs_value_as_num READ j
+ext_db.c:59 db_build_query VAL_INT TEST h
+ext_db.c:60 db_build_query VAL_I64_RAW RAW h
+ext_db.c:62 db_build_query VAL_FLOAT TEST h
+ext_db.c:64 db_build_query eigs_float_arg READ h
+ext_db.c:66 db_build_query eigs_num_text FORMAT h
+ext_db.c:286 builtin_db_query_value make_int PRODUCE h
+ext_db.c:298 builtin_db_query_value make_float PRODUCE h
+ext_db.c:298 builtin_db_query_value strtod PARSE h
+ext_gfx.c:512 gfx_nums VAL_FLOAT TEST a
+ext_gfx.c:512 gfx_nums VAL_INT TEST a
+ext_gfx.c:525 gfx_list_all_num VAL_FLOAT TEST a
+ext_gfx.c:525 gfx_list_all_num VAL_INT TEST a
+ext_gfx.c:560 builtin_gfx_open VAL_FLOAT TEST a
+ext_gfx.c:560 builtin_gfx_open VAL_INT TEST a
+ext_gfx.c:561 builtin_gfx_open VAL_FLOAT TEST a
+ext_gfx.c:561 builtin_gfx_open VAL_INT TEST a
+ext_gfx.c:564 builtin_gfx_open eigs_list_magnitude READ b
+ext_gfx.c:565 builtin_gfx_open eigs_list_magnitude READ b
+ext_gfx.c:638 gfx_colour eigs_list_bounded READ b
+ext_gfx.c:640 gfx_colour eigs_list_bounded READ b
+ext_gfx.c:656 gfx_edges_ld slot_from_f64 PRODUCE c
+ext_gfx.c:656 gfx_edges_ld slot_from_f64 PRODUCE c
+ext_gfx.c:657 gfx_edges_ld slot_from_f64 PRODUCE c
+ext_gfx.c:657 gfx_edges_ld slot_from_f64 PRODUCE c
+ext_gfx.c:659 gfx_edges_ld eigs_geom_shape READ b
+ext_gfx.c:718 builtin_gfx_rect eigs_geom_shape READ b
+ext_gfx.c:736 builtin_gfx_line eigs_geom_shape READ b
+ext_gfx.c:753 builtin_gfx_point eigs_geom_shape READ b
+ext_gfx.c:771 builtin_gfx_circle eigs_geom_shape READ b
+ext_gfx.c:804 builtin_gfx_rrect eigs_geom_shape READ b
+ext_gfx.c:835 builtin_gfx_clip eigs_geom_shape READ b
+ext_gfx.c:866 builtin_gfx_read eigs_list_float READ b
+ext_gfx.c:868 builtin_gfx_read eigs_list_float READ b
+ext_gfx.c:874 builtin_gfx_read eigs_geom_shape READ b
+ext_gfx.c:881 builtin_gfx_read make_int PRODUCE c
+ext_gfx.c:882 builtin_gfx_read make_int PRODUCE c
+ext_gfx.c:883 builtin_gfx_read make_int PRODUCE c
+ext_gfx.c:926 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:932 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:937 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:938 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:943 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:944 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:945 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:950 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:951 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:952 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:957 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:958 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:968 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:969 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:977 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:978 builtin_gfx_poll make_int PRODUCE c
+ext_gfx.c:1000 builtin_gfx_ticks make_int PRODUCE c
+ext_gfx.c:1005 builtin_gfx_delay val_is_number TEST a
+ext_gfx.c:1008 builtin_gfx_delay eigs_magnitude_arg READ b
+ext_gfx.c:1157 builtin_gfx_text eigs_list_float READ b
+ext_gfx.c:1159 builtin_gfx_text eigs_list_float READ b
+ext_gfx.c:1161 builtin_gfx_text eigs_list_float READ b
+ext_gfx.c:1242 builtin_gfx_text_width VAL_FLOAT TEST a
+ext_gfx.c:1242 builtin_gfx_text_width VAL_INT TEST a
+ext_gfx.c:1244 builtin_gfx_text_width eigs_list_magnitude READ b
+ext_gfx.c:1254 builtin_gfx_text_width make_int PRODUCE c
+ext_gfx.c:1262 builtin_gfx_text_width make_int PRODUCE c
+ext_gfx.c:1269 builtin_gfx_text_width make_int PRODUCE c
+ext_gfx.c:1276 builtin_gfx_text_width make_int PRODUCE c
+ext_gfx.c:1289 builtin_gfx_text_height val_is_number TEST a
+ext_gfx.c:1291 builtin_gfx_text_height VAL_FLOAT TEST a
+ext_gfx.c:1291 builtin_gfx_text_height VAL_INT TEST a
+ext_gfx.c:1293 builtin_gfx_text_height val_is_number TEST a
+ext_gfx.c:1295 builtin_gfx_text_height eigs_magnitude_arg READ b
+ext_gfx.c:1299 builtin_gfx_text_height VAL_FLOAT TEST a
+ext_gfx.c:1299 builtin_gfx_text_height VAL_INT TEST a
+ext_gfx.c:1301 builtin_gfx_text_height eigs_list_magnitude READ b
+ext_gfx.c:1308 builtin_gfx_text_height make_int PRODUCE c
+ext_gfx.c:1313 builtin_gfx_text_height make_int PRODUCE c
+ext_gfx.c:1315 builtin_gfx_text_height make_int PRODUCE c
+ext_gfx.c:1383 audio_convert_samples buffer_read_f64 READ b
+ext_gfx.c:1386 audio_convert_samples VAL_FLOAT TEST a
+ext_gfx.c:1386 audio_convert_samples VAL_INT TEST a
+ext_gfx.c:1390 audio_convert_samples eigs_slot_float READ b
+ext_gfx.c:1398 audio_convert_samples eigs_slot_bounded READ b
+ext_gfx.c:1398 audio_convert_samples slot_from_f64 PRODUCE c
+ext_gfx.c:1483 builtin_audio_open VAL_FLOAT TEST a
+ext_gfx.c:1483 builtin_audio_open VAL_INT TEST a
+ext_gfx.c:1484 builtin_audio_open VAL_FLOAT TEST a
+ext_gfx.c:1484 builtin_audio_open VAL_INT TEST a
+ext_gfx.c:1485 builtin_audio_open make_int PRODUCE c
+ext_gfx.c:1491 builtin_audio_open eigs_list_int_range READ b
+ext_gfx.c:1492 builtin_audio_open eigs_list_int_range READ b
+ext_gfx.c:1493 builtin_audio_open make_int PRODUCE c
+ext_gfx.c:1503 builtin_audio_open make_int PRODUCE c
+ext_gfx.c:1506 builtin_audio_open make_int PRODUCE c
+ext_gfx.c:1521 builtin_audio_open make_int PRODUCE c
+ext_gfx.c:1525 builtin_audio_open make_int PRODUCE c
+ext_gfx.c:1544 builtin_audio_pause val_is_number TEST a
+ext_gfx.c:1547 builtin_audio_pause val_is_number TEST a
+ext_gfx.c:1548 builtin_audio_pause eigs_int_arg_range READ b
+ext_gfx.c:1639 builtin_audio_capture_open VAL_FLOAT TEST a
+ext_gfx.c:1639 builtin_audio_capture_open VAL_INT TEST a
+ext_gfx.c:1640 builtin_audio_capture_open VAL_FLOAT TEST a
+ext_gfx.c:1640 builtin_audio_capture_open VAL_INT TEST a
+ext_gfx.c:1641 builtin_audio_capture_open make_int PRODUCE c
+ext_gfx.c:1647 builtin_audio_capture_open eigs_list_int_range READ b
+ext_gfx.c:1648 builtin_audio_capture_open eigs_list_int_range READ b
+ext_gfx.c:1649 builtin_audio_capture_open make_int PRODUCE c
+ext_gfx.c:1653 builtin_audio_capture_open make_int PRODUCE c
+ext_gfx.c:1657 builtin_audio_capture_open make_int PRODUCE c
+ext_gfx.c:1678 builtin_audio_capture_open make_int PRODUCE c
+ext_gfx.c:1681 builtin_audio_capture_open make_int PRODUCE c
+ext_gfx.c:1773 builtin_audio_stream_open VAL_FLOAT TEST a
+ext_gfx.c:1773 builtin_audio_stream_open VAL_INT TEST a
+ext_gfx.c:1774 builtin_audio_stream_open VAL_FLOAT TEST a
+ext_gfx.c:1774 builtin_audio_stream_open VAL_INT TEST a
+ext_gfx.c:1775 builtin_audio_stream_open make_int PRODUCE c
+ext_gfx.c:1781 builtin_audio_stream_open eigs_list_int_range READ b
+ext_gfx.c:1782 builtin_audio_stream_open eigs_list_int_range READ b
+ext_gfx.c:1783 builtin_audio_stream_open make_int PRODUCE c
+ext_gfx.c:1785 builtin_audio_stream_open make_int PRODUCE c
+ext_gfx.c:1788 builtin_audio_stream_open make_int PRODUCE c
+ext_gfx.c:1807 builtin_audio_stream_open make_int PRODUCE c
+ext_gfx.c:1811 builtin_audio_stream_open make_int PRODUCE c
+ext_gfx.c:1855 builtin_audio_stream_queued make_int PRODUCE c
+ext_gfx.c:1858 builtin_audio_stream_queued make_int PRODUCE c
+ext_gfx.c:1894 builtin_audio_music_play VAL_FLOAT TEST a
+ext_gfx.c:1894 builtin_audio_music_play VAL_INT TEST a
+ext_gfx.c:1898 builtin_audio_music_play VAL_FLOAT TEST a
+ext_gfx.c:1898 builtin_audio_music_play VAL_INT TEST a
+ext_gfx.c:1899 builtin_audio_music_play eigs_list_int_range READ b
+ext_gfx.c:1941 builtin_audio_music_volume val_is_number TEST a
+ext_gfx.c:1943 builtin_audio_music_volume VAL_FLOAT TEST a
+ext_gfx.c:1943 builtin_audio_music_volume VAL_INT TEST a
+ext_gfx.c:1946 builtin_audio_music_volume val_is_number TEST a
+ext_gfx.c:1947 builtin_audio_music_volume eigs_bounded_arg READ b
+ext_gfx.c:1949 builtin_audio_music_volume VAL_FLOAT TEST a
+ext_gfx.c:1949 builtin_audio_music_volume VAL_INT TEST a
+ext_gfx.c:1950 builtin_audio_music_volume eigs_list_bounded READ b
+ext_gfx.c:1973 builtin_audio_play make_int PRODUCE c
+ext_gfx.c:1978 builtin_audio_play make_int PRODUCE c
+ext_gfx.c:1980 builtin_audio_play make_int PRODUCE c
+ext_gfx.c:1982 builtin_audio_play make_int PRODUCE c
+ext_gfx.c:1996 builtin_audio_play_loop VAL_FLOAT TEST a
+ext_gfx.c:1996 builtin_audio_play_loop VAL_INT TEST a
+ext_gfx.c:1997 builtin_audio_play_loop make_int PRODUCE c
+ext_gfx.c:1999 builtin_audio_play_loop eigs_list_int_range READ b
+ext_gfx.c:1999 builtin_audio_play_loop make_int PRODUCE c
+ext_gfx.c:2002 builtin_audio_play_loop make_int PRODUCE c
+ext_gfx.c:2013 builtin_audio_play_loop make_int PRODUCE c
+ext_gfx.c:2018 builtin_audio_play_loop make_int PRODUCE c
+ext_gfx.c:2020 builtin_audio_play_loop make_int PRODUCE c
+ext_gfx.c:2022 builtin_audio_play_loop make_int PRODUCE c
+ext_gfx.c:2031 builtin_audio_volume VAL_FLOAT TEST a
+ext_gfx.c:2031 builtin_audio_volume VAL_INT TEST a
+ext_gfx.c:2032 builtin_audio_volume VAL_FLOAT TEST a
+ext_gfx.c:2032 builtin_audio_volume VAL_INT TEST a
+ext_gfx.c:2033 builtin_audio_volume make_int PRODUCE c
+ext_gfx.c:2035 builtin_audio_volume eigs_list_int_range READ b
+ext_gfx.c:2035 builtin_audio_volume make_int PRODUCE c
+ext_gfx.c:2037 builtin_audio_volume eigs_list_bounded READ b
+ext_gfx.c:2037 builtin_audio_volume make_int PRODUCE c
+ext_gfx.c:2038 builtin_audio_volume make_int PRODUCE c
+ext_gfx.c:2044 builtin_audio_volume make_int PRODUCE c
+ext_gfx.c:2051 builtin_audio_stop val_is_number TEST a
+ext_gfx.c:2052 builtin_audio_stop make_int PRODUCE c
+ext_gfx.c:2054 builtin_audio_stop eigs_int_arg_range READ b
+ext_gfx.c:2055 builtin_audio_stop make_int PRODUCE c
+ext_gfx.c:2056 builtin_audio_stop make_int PRODUCE c
+ext_gfx.c:2057 builtin_audio_stop make_int PRODUCE c
+ext_gfx.c:2063 builtin_audio_stop make_int PRODUCE c
+ext_gfx.c:2069 builtin_audio_queue_size make_int PRODUCE c
+ext_gfx.c:2083 builtin_audio_queue_size make_int PRODUCE c
+ext_gfx.c:2104 audio_generator_sample_count eigs_slot_magnitude READ b
+ext_gfx.c:2104 audio_generator_sample_count slot_from_f64 PRODUCE c
+ext_gfx.c:2139 builtin_audio_sine VAL_FLOAT TEST a
+ext_gfx.c:2139 builtin_audio_sine VAL_INT TEST a
+ext_gfx.c:2140 builtin_audio_sine VAL_FLOAT TEST a
+ext_gfx.c:2140 builtin_audio_sine VAL_INT TEST a
+ext_gfx.c:2141 builtin_audio_sine VAL_FLOAT TEST a
+ext_gfx.c:2141 builtin_audio_sine VAL_INT TEST a
+ext_gfx.c:2143 builtin_audio_sine eigs_list_float READ b
+ext_gfx.c:2144 builtin_audio_sine eigs_list_float READ b
+ext_gfx.c:2145 builtin_audio_sine eigs_list_float READ b
+ext_gfx.c:2156 builtin_audio_sine make_float PRODUCE c
+ext_gfx.c:2176 builtin_audio_saw VAL_FLOAT TEST a
+ext_gfx.c:2176 builtin_audio_saw VAL_INT TEST a
+ext_gfx.c:2177 builtin_audio_saw VAL_FLOAT TEST a
+ext_gfx.c:2177 builtin_audio_saw VAL_INT TEST a
+ext_gfx.c:2178 builtin_audio_saw VAL_FLOAT TEST a
+ext_gfx.c:2178 builtin_audio_saw VAL_INT TEST a
+ext_gfx.c:2180 builtin_audio_saw eigs_list_float READ b
+ext_gfx.c:2181 builtin_audio_saw eigs_list_float READ b
+ext_gfx.c:2182 builtin_audio_saw eigs_list_float READ b
+ext_gfx.c:2193 builtin_audio_saw make_float PRODUCE c
+ext_gfx.c:2213 builtin_audio_square VAL_FLOAT TEST a
+ext_gfx.c:2213 builtin_audio_square VAL_INT TEST a
+ext_gfx.c:2214 builtin_audio_square VAL_FLOAT TEST a
+ext_gfx.c:2214 builtin_audio_square VAL_INT TEST a
+ext_gfx.c:2215 builtin_audio_square VAL_FLOAT TEST a
+ext_gfx.c:2215 builtin_audio_square VAL_INT TEST a
+ext_gfx.c:2217 builtin_audio_square eigs_list_float READ b
+ext_gfx.c:2218 builtin_audio_square eigs_list_float READ b
+ext_gfx.c:2219 builtin_audio_square eigs_list_float READ b
+ext_gfx.c:2230 builtin_audio_square make_float PRODUCE c
+ext_gfx.c:2251 builtin_audio_sweep VAL_FLOAT TEST a
+ext_gfx.c:2251 builtin_audio_sweep VAL_INT TEST a
+ext_gfx.c:2252 builtin_audio_sweep VAL_FLOAT TEST a
+ext_gfx.c:2252 builtin_audio_sweep VAL_INT TEST a
+ext_gfx.c:2253 builtin_audio_sweep VAL_FLOAT TEST a
+ext_gfx.c:2253 builtin_audio_sweep VAL_INT TEST a
+ext_gfx.c:2254 builtin_audio_sweep VAL_FLOAT TEST a
+ext_gfx.c:2254 builtin_audio_sweep VAL_INT TEST a
+ext_gfx.c:2255 builtin_audio_sweep VAL_FLOAT TEST a
+ext_gfx.c:2255 builtin_audio_sweep VAL_INT TEST a
+ext_gfx.c:2257 builtin_audio_sweep eigs_list_float READ b
+ext_gfx.c:2258 builtin_audio_sweep eigs_list_float READ b
+ext_gfx.c:2259 builtin_audio_sweep eigs_list_float READ b
+ext_gfx.c:2260 builtin_audio_sweep eigs_list_float READ b
+ext_gfx.c:2261 builtin_audio_sweep eigs_list_int READ b
+ext_gfx.c:2280 builtin_audio_sweep make_float PRODUCE c
+ext_gfx.c:2364 builtin_audio_noise VAL_FLOAT TEST a
+ext_gfx.c:2364 builtin_audio_noise VAL_INT TEST a
+ext_gfx.c:2365 builtin_audio_noise VAL_FLOAT TEST a
+ext_gfx.c:2365 builtin_audio_noise VAL_INT TEST a
+ext_gfx.c:2367 builtin_audio_noise eigs_list_float READ b
+ext_gfx.c:2368 builtin_audio_noise eigs_list_float READ b
+ext_gfx.c:2400 builtin_audio_noise make_float PRODUCE c
+ext_gfx.c:2432 builtin_audio_mix VAL_FLOAT TEST a
+ext_gfx.c:2432 builtin_audio_mix VAL_INT TEST a
+ext_gfx.c:2432 builtin_audio_mix eigs_list_float READ b
+ext_gfx.c:2433 builtin_audio_mix VAL_FLOAT TEST a
+ext_gfx.c:2433 builtin_audio_mix VAL_INT TEST a
+ext_gfx.c:2433 builtin_audio_mix eigs_list_float READ b
+ext_gfx.c:2438 builtin_audio_mix make_float PRODUCE c
+ext_gfx.c:2464 builtin_audio_gain VAL_FLOAT TEST a
+ext_gfx.c:2464 builtin_audio_gain VAL_INT TEST a
+ext_gfx.c:2466 builtin_audio_gain eigs_list_float READ b
+ext_gfx.c:2472 builtin_audio_gain VAL_FLOAT TEST a
+ext_gfx.c:2472 builtin_audio_gain VAL_INT TEST a
+ext_gfx.c:2472 builtin_audio_gain eigs_list_float READ b
+ext_gfx.c:2477 builtin_audio_gain make_float PRODUCE c
+ext_gfx.c:2497 builtin_audio_envelope VAL_FLOAT TEST a
+ext_gfx.c:2497 builtin_audio_envelope VAL_INT TEST a
+ext_gfx.c:2498 builtin_audio_envelope VAL_FLOAT TEST a
+ext_gfx.c:2498 builtin_audio_envelope VAL_INT TEST a
+ext_gfx.c:2499 builtin_audio_envelope VAL_FLOAT TEST a
+ext_gfx.c:2499 builtin_audio_envelope VAL_INT TEST a
+ext_gfx.c:2500 builtin_audio_envelope VAL_FLOAT TEST a
+ext_gfx.c:2500 builtin_audio_envelope VAL_INT TEST a
+ext_gfx.c:2511 builtin_audio_envelope eigs_list_float READ b
+ext_gfx.c:2512 builtin_audio_envelope eigs_list_float READ b
+ext_gfx.c:2513 builtin_audio_envelope eigs_list_float READ b
+ext_gfx.c:2514 builtin_audio_envelope eigs_list_float READ b
+ext_gfx.c:2520 builtin_audio_envelope eigs_slot_magnitude READ b
+ext_gfx.c:2520 builtin_audio_envelope slot_from_f64 PRODUCE c
+ext_gfx.c:2522 builtin_audio_envelope eigs_slot_magnitude READ b
+ext_gfx.c:2522 builtin_audio_envelope slot_from_f64 PRODUCE c
+ext_gfx.c:2524 builtin_audio_envelope eigs_slot_magnitude READ b
+ext_gfx.c:2524 builtin_audio_envelope slot_from_f64 PRODUCE c
+ext_gfx.c:2550 builtin_audio_envelope VAL_FLOAT TEST a
+ext_gfx.c:2550 builtin_audio_envelope VAL_INT TEST a
+ext_gfx.c:2550 builtin_audio_envelope eigs_list_float READ b
+ext_gfx.c:2553 builtin_audio_envelope make_float PRODUCE c
+ext_gfx.c:2562 gfx_byte_at buffer_byte_at READ b
+ext_gfx.c:2584 builtin_gfx_fb eigs_list_int_range READ b
+ext_gfx.c:2585 builtin_gfx_fb eigs_list_int_range READ b
+ext_gfx.c:2586 builtin_gfx_fb eigs_list_float READ b
+ext_gfx.c:2588 builtin_gfx_fb eigs_list_float READ b
+ext_gfx.c:2590 builtin_gfx_fb eigs_list_float READ b
+ext_http.c:589 builtin_http_early_bind val_is_number TEST a
+ext_http.c:595 builtin_http_early_bind eigs_int_arg_range READ b
+ext_http.c:668 builtin_http_serve eigs_int_arg_range READ b
+ext_http.c:987 builtin_shared_incr val_is_number TEST a
+ext_http.c:991 builtin_shared_incr eigs_number_arg READ b
+ext_http.c:1004 builtin_shared_incr val_is_number TEST a
+ext_http.c:1005 builtin_shared_incr eigs_number_arg READ b
+ext_http.c:1020 builtin_shared_incr make_int PRODUCE c
+ext_http.c:1024 builtin_shared_incr make_float PRODUCE c
+ext_http.c:1096 builtin_shared_delete make_int PRODUCE c
+ext_http.c:1098 builtin_shared_delete make_int PRODUCE c
+ext_http.c:1114 builtin_shared_delete make_int PRODUCE c
+ext_http.c:1133 builtin_shared_size make_int PRODUCE c
+ext_http.c:1137 builtin_shared_size make_int PRODUCE c
+ext_net.c:145 net_count_at eigs_opt_int_range READ b
+ext_net.c:153 net_timeout_at eigs_opt_magnitude READ b
+ext_net.c:165 builtin_net_listen val_is_number TEST a
+ext_net.c:170 builtin_net_listen eigs_int_arg_range READ b
+ext_net.c:187 builtin_net_listen make_int PRODUCE c
+ext_net.c:194 builtin_net_port val_is_number TEST a
+ext_net.c:198 builtin_net_port eigs_int_arg READ b
+ext_net.c:207 builtin_net_port make_int PRODUCE c
+ext_net.c:223 builtin_net_accept val_is_number TEST a
+ext_net.c:227 builtin_net_accept eigs_int_arg READ b
+ext_net.c:239 builtin_net_accept make_int PRODUCE c
+ext_net.c:255 builtin_net_dial eigs_list_int_range READ b
+ext_net.c:292 builtin_net_dial make_int PRODUCE c
+ext_net.c:304 builtin_net_recv eigs_int_arg READ b
+ext_net.c:339 net_send_bytes make_int PRODUCE c
+ext_net.c:344 net_send_bytes make_int PRODUCE c
+ext_net.c:347 net_send_bytes make_int PRODUCE c
+ext_net.c:359 builtin_net_send eigs_int_arg READ b
+ext_net.c:375 builtin_net_send buffer_byte_at READ b
+ext_net.c:380 builtin_net_send eigs_elem_byte READ b
+ext_net.c:397 builtin_net_close val_is_number TEST a
+ext_net.c:403 builtin_net_close eigs_int_arg READ b
+ext_store.c:164 store_json_encode_buf_elem eigs_num_text FORMAT h
+ext_store.c:175 store_json_encode_slot slot_is_f64 TEST h
+ext_store.c:178 store_json_encode_slot SLOT_F64_RAW RAW h
+ext_store.c:180 store_json_encode_slot eigs_num_text FORMAT h
+ext_store.c:184 store_json_encode_slot slot_is_smi TEST h
+ext_store.c:187 store_json_encode_slot slot_smi_val RAW h
+ext_store.c:200 store_json_encode VAL_FLOAT CASE h
+ext_store.c:203 store_json_encode VAL_F64_RAW RAW h
+ext_store.c:205 store_json_encode eigs_num_text FORMAT h
+ext_store.c:209 store_json_encode VAL_INT CASE h
+ext_store.c:211 store_json_encode VAL_I64_RAW RAW h
+ext_store.c:334 store_json_parse_number strtod PARSE h
+ext_store.c:347 store_json_parse_number make_int PRODUCE h
+ext_store.c:348 store_json_parse_number make_float PRODUCE h
+ext_store.c:384 store_buffer_from_tag val_is_number TEST h
+ext_store.c:384 store_buffer_from_tag val_is_number TEST h
+ext_store.c:389 store_buffer_from_tag eigs_number_arg READ h
+ext_store.c:390 store_buffer_from_tag eigs_number_arg READ h
+ext_store.c:394 store_buffer_from_tag num_to_i64_exact READ h
+ext_store.c:396 store_buffer_from_tag num_to_i64_exact READ h
+ext_store.c:414 store_buffer_from_tag val_is_number TEST h
+ext_store.c:429 store_buffer_from_tag eigs_float_arg READ h
+ext_store.c:429 store_buffer_from_tag val_is_number TEST h
+ext_store.c:767 store_load_catalog val_is_number TEST h
+ext_store.c:767 store_load_catalog val_is_number TEST h
+ext_store.c:773 store_load_catalog eigs_int_arg_range READ h
+ext_store.c:846 get_store_why val_is_number TEST h
+ext_store.c:848 get_store_why eigs_int_arg_range READ h
+ext_store.c:851 get_store_why val_is_number TEST h
+ext_store.c:852 get_store_why eigs_int_arg_range READ h
+ext_store.c:991 builtin_store_open make_int PRODUCE h
+ext_store.c:992 builtin_store_open make_int PRODUCE h
+ext_store.c:1006 builtin_store_close eigs_int_arg_range READ h
+ext_store.c:1009 builtin_store_close eigs_int_arg_range READ h
+ext_store.c:1055 builtin_store_put make_int PRODUCE h
+ext_store.c:1056 builtin_store_put make_int PRODUCE h
+ext_store.c:1071 builtin_store_put eigs_int_arg_range READ h
+ext_store.c:1073 builtin_store_put eigs_int_arg_range READ h
+ext_store.c:1088 builtin_store_put make_int PRODUCE h
+ext_store.c:1212 builtin_store_get val_is_number TEST h
+ext_store.c:1213 builtin_store_get eigs_int_arg READ h
+ext_store.c:1225 builtin_store_get eigs_int_arg_range READ h
+ext_store.c:1285 store_locate eigs_int_arg_range READ h
+ext_store.c:1382 builtin_store_delete val_is_number TEST h
+ext_store.c:1383 builtin_store_delete eigs_int_arg READ h
+ext_store.c:1432 builtin_store_query eigs_int_arg_range READ h
+ext_store.c:1468 builtin_store_count make_int PRODUCE h
+ext_store.c:1476 builtin_store_count make_int PRODUCE h
+ext_store.c:1477 builtin_store_count make_int PRODUCE h
+ext_store.c:1480 builtin_store_count make_int PRODUCE h
+ext_store.c:1486 builtin_store_count make_int PRODUCE h
+ext_store.c:1490 builtin_store_count make_int PRODUCE h
+ext_store.c:1492 builtin_store_count eigs_int_arg_range READ h
+ext_store.c:1504 builtin_store_count make_int PRODUCE h
+ext_store.c:1510 builtin_store_count make_int PRODUCE h
+ext_store.c:1544 builtin_store_update val_is_number TEST h
+ext_store.c:1545 builtin_store_update eigs_int_arg READ h
+ext_store.c:1689 builtin_store_drop eigs_int_arg_range READ h
+jit.c:1303 emit_conditional_incref_rax TAG-IMM 0xFFFB EMIT d
+jit.c:1427 emit_addsd_xmm0_xmm1 emit_addsd_xmm0_xmm1 EMIT d
+jit.c:1431 emit_subsd_xmm0_xmm1 emit_subsd_xmm0_xmm1 EMIT d
+jit.c:1435 emit_mulsd_xmm0_xmm1 emit_mulsd_xmm0_xmm1 EMIT d
+jit.c:1439 emit_divsd_xmm0_xmm1 emit_divsd_xmm0_xmm1 EMIT d
+jit.c:1445 emit_cvttsd2si_xmm0_rax emit_cvttsd2si_xmm0_rax EMIT d
+jit.c:1449 emit_cvttsd2si_xmm1_rdx emit_cvttsd2si_xmm1_rdx EMIT d
+jit.c:1457 emit_cvtsi2sd_rax_xmm2 emit_cvtsi2sd_rax_xmm2 EMIT d
+jit.c:1461 emit_cvtsi2sd_rdx_xmm2 emit_cvtsi2sd_rdx_xmm2 EMIT d
+jit.c:1465 emit_ucomisd_xmm0_xmm2 emit_ucomisd_xmm0_xmm2 EMIT d
+jit.c:1469 emit_ucomisd_xmm1_xmm2 emit_ucomisd_xmm1_xmm2 EMIT d
+jit.c:1473 emit_cvtsi2sd_rdx_xmm0 emit_cvtsi2sd_rdx_xmm0 EMIT d
+jit.c:1520 emit_btr_63_rax emit_btr_63_rax EMIT d
+jit.c:1538 emit_cvtsi2sd_rax_xmm0 emit_cvtsi2sd_rax_xmm0 EMIT d
+jit.c:1552 emit_ucomisd_xmm0_xmm1 emit_ucomisd_xmm0_xmm1 EMIT d
+jit.c:1614 emit_btr_63_rdx emit_btr_63_rdx EMIT d
+jit.c:1619 emit_btr_63_rsi emit_btr_63_rsi EMIT d
+jit.c:1646 emit_immediate_num_check_rax emit_immediate_num_check_rax EMIT d
+jit.c:1649 emit_immediate_num_check_rax TAG-IMM 0xFFF8 EMIT d
+jit.c:1652 emit_immediate_num_check_rdx emit_immediate_num_check_rdx EMIT d
+jit.c:1655 emit_immediate_num_check_rdx TAG-IMM 0xFFF8 EMIT d
+jit.c:1661 emit_overflow_check_rax EMIT d
+jit.c:1664 emit_overflow_check_rax emit_btr_63_rdx EMIT d
+jit.c:1725 emit_cvttsd2si_xmm0_r8d emit_cvttsd2si_xmm0_r8d EMIT d
+jit.c:1730 emit_cvtsi2sd_r8d_xmm1 emit_cvtsi2sd_r8d_xmm1 EMIT d
+jit.c:1923 emit_jump_unless_bool_rax TAG-IMM 0xFFF9 EMIT d
+jit.c:2041 emit_conditional_decref_rsi_hook TAG-IMM 0xFFFB EMIT d
+jit.c:2187 emit_dict_cache_probe TAG-IMM 0xFFFB EMIT d
+jit.c:2352 emit_load_numeric_operand EMIT d
+jit.c:2357 emit_load_numeric_operand TAG-IMM 0xFFF8 EMIT d
+jit.c:2362 emit_load_numeric_operand TAG-IMM 0xFFFB EMIT d
+jit.c:2365 emit_load_numeric_operand TAG-IMM 0xFFFD EMIT d
+jit.c:2372 emit_load_numeric_operand VAL_FLOAT EMIT d
+jit.c:2376 emit_load_numeric_operand VAL_F64_OFFSET RAW d
+jit.c:2377 emit_load_numeric_operand VAL_F64_OFFSET RAW d
+jit.c:2406 emit_decref_binop_operands TAG-IMM 0xFFFB EMIT d
+jit.c:2434 emit_commit_binop TAG-IMM 0xFFFB EMIT d
+jit.c:2642 jit_compile_to_thunk max_normal_bits EMIT d
+jit.c:2643 jit_compile_to_thunk EIGS_NUM_MAX GUARD d
+jit.c:2643 jit_compile_to_thunk max_normal_bits EMIT d
+jit.c:2810 jit_compile_to_thunk/OP_NULL TAG-IMM 0xFFF8 EMIT d
+jit.c:2831 jit_compile_to_thunk/OP_CONST VAL_FLOAT TEST d
+jit.c:2834 jit_compile_to_thunk/OP_CONST VAL_F64_RAW RAW d
+jit.c:2837 jit_compile_to_thunk/OP_CONST TAG-IMM 0xFFFB EMIT d
+jit.c:3033 jit_compile_to_thunk/OP_SET_NAME TAG-IMM 0xFFFB EMIT d
+jit.c:3154 jit_compile_to_thunk/OP_LOCAL_DOT_GET emit_immediate_num_check_rax EMIT d
+jit.c:3280 jit_compile_to_thunk/OP_LOCAL_DOT_SET emit_immediate_num_check_rax EMIT d
+jit.c:3283 jit_compile_to_thunk/OP_LOCAL_DOT_SET emit_immediate_num_check_rdx EMIT d
+jit.c:3461 jit_compile_to_thunk/OP_SET_LOCAL TAG-IMM 0xFFF8 EMIT d
+jit.c:3466 jit_compile_to_thunk/OP_SET_LOCAL TAG-IMM 0xFFFB EMIT d
+jit.c:3468 jit_compile_to_thunk/OP_SET_LOCAL TAG-IMM 0xFFFD EMIT d
+jit.c:3474 jit_compile_to_thunk/OP_SET_LOCAL VAL_FLOAT EMIT d
+jit.c:3480 jit_compile_to_thunk/OP_SET_LOCAL VAL_F64_OFFSET RAW d
+jit.c:3539 jit_compile_to_thunk/OP_MOD emit_load_numeric_operand EMIT d
+jit.c:3543 jit_compile_to_thunk/OP_MOD emit_load_numeric_operand EMIT d
+jit.c:3553 jit_compile_to_thunk/OP_MOD emit_btr_63_rsi EMIT d
+jit.c:3567 jit_compile_to_thunk/OP_MOD fmod EMIT d
+jit.c:3573 jit_compile_to_thunk/OP_MOD emit_overflow_check_rax EMIT d
+jit.c:3573 jit_compile_to_thunk/OP_MOD max_normal_bits EMIT d
+jit.c:3621 jit_compile_to_thunk/OP_ADD emit_load_numeric_operand EMIT d
+jit.c:3625 jit_compile_to_thunk/OP_ADD emit_load_numeric_operand EMIT d
+jit.c:3633 jit_compile_to_thunk/OP_ADD emit_addsd_xmm0_xmm1 EMIT d
+jit.c:3634 jit_compile_to_thunk/OP_ADD emit_subsd_xmm0_xmm1 EMIT d
+jit.c:3635 jit_compile_to_thunk/OP_ADD emit_mulsd_xmm0_xmm1 EMIT d
+jit.c:3636 jit_compile_to_thunk/OP_ADD emit_divsd_xmm0_xmm1 EMIT d
+jit.c:3643 jit_compile_to_thunk/OP_ADD emit_overflow_check_rax EMIT d
+jit.c:3643 jit_compile_to_thunk/OP_ADD max_normal_bits EMIT d
+jit.c:3682 jit_compile_to_thunk/OP_EQ emit_load_numeric_operand EMIT d
+jit.c:3686 jit_compile_to_thunk/OP_EQ emit_load_numeric_operand EMIT d
+jit.c:3699 jit_compile_to_thunk/OP_EQ emit_ucomisd_xmm0_xmm1 EMIT d
+jit.c:3739 jit_compile_to_thunk/OP_BAND emit_immediate_num_check_rax EMIT d
+jit.c:3741 jit_compile_to_thunk/OP_BAND emit_immediate_num_check_rdx EMIT d
+jit.c:3747 jit_compile_to_thunk/OP_BAND emit_cvttsd2si_xmm0_rax EMIT d
+jit.c:3748 jit_compile_to_thunk/OP_BAND emit_cvtsi2sd_rax_xmm2 EMIT d
+jit.c:3749 jit_compile_to_thunk/OP_BAND emit_ucomisd_xmm0_xmm2 EMIT d
+jit.c:3752 jit_compile_to_thunk/OP_BAND emit_cvttsd2si_xmm1_rdx EMIT d
+jit.c:3753 jit_compile_to_thunk/OP_BAND emit_cvtsi2sd_rdx_xmm2 EMIT d
+jit.c:3754 jit_compile_to_thunk/OP_BAND emit_ucomisd_xmm1_xmm2 EMIT d
+jit.c:3774 jit_compile_to_thunk/OP_BAND emit_cvtsi2sd_rdx_xmm0 EMIT d
+jit.c:3804 jit_compile_to_thunk/OP_NOT emit_immediate_num_check_rax EMIT d
+jit.c:3817 jit_compile_to_thunk/OP_NEG emit_btr_63_rax EMIT d
+jit.c:3832 jit_compile_to_thunk/OP_NEG emit_cvttsd2si_xmm0_rax EMIT d
+jit.c:3833 jit_compile_to_thunk/OP_NEG emit_cvtsi2sd_rax_xmm2 EMIT d
+jit.c:3834 jit_compile_to_thunk/OP_NEG emit_ucomisd_xmm0_xmm2 EMIT d
+jit.c:3838 jit_compile_to_thunk/OP_NEG emit_cvtsi2sd_rax_xmm0 EMIT d
+jit.c:3957 jit_compile_to_thunk/OP_JUMP_IF_FALSE emit_immediate_num_check_rax EMIT d
+jit.c:3960 jit_compile_to_thunk/OP_JUMP_IF_FALSE emit_btr_63_rax EMIT d
+jit.c:4097 jit_compile_to_thunk/OP_INDEX_SET emit_immediate_num_check_rdx EMIT d
+jit.c:4102 jit_compile_to_thunk/OP_INDEX_SET TAG-IMM 0xFFFB EMIT d
+jit.c:4116 jit_compile_to_thunk/OP_INDEX_SET emit_cvttsd2si_xmm0_r8d EMIT d
+jit.c:4117 jit_compile_to_thunk/OP_INDEX_SET emit_cvtsi2sd_r8d_xmm1 EMIT d
+jit.c:4118 jit_compile_to_thunk/OP_INDEX_SET emit_ucomisd_xmm0_xmm1 EMIT d
+jit.c:4126 jit_compile_to_thunk/OP_INDEX_SET emit_immediate_num_check_rax EMIT d
+jit.c:4445 jit_compile_to_thunk/OP_BINARY_LINE VAL_FLOAT TEST d
+jit_smoke.c:103 jit_helper_local_idx_get slot_from_f64 PRODUCE d
+jit_smoke.c:138 jit_helper_iter_next SLOT_F64_RAW RAW d
+jit_smoke.c:142 jit_helper_iter_next slot_from_f64 PRODUCE d
+jit_smoke.c:149 jit_helper_index_get SLOT_F64_RAW RAW d
+jit_smoke.c:149 jit_helper_index_get SLOT_F64_RAW RAW d
+jit_smoke.c:153 jit_helper_index_get slot_from_f64 PRODUCE d
+jit_smoke.c:174 jit_helper_set_name SLOT_F64_RAW RAW d
+jit_smoke.c:174 jit_helper_set_name SLOT_F64_RAW RAW d
+jit_smoke.c:175 jit_helper_set_name SLOT_F64_RAW RAW d
+jit_smoke.c:178 jit_helper_set_name SLOT_F64_RAW RAW d
+jit_smoke.c:331 run_store_cases slot_from_f64 PRODUCE d
+jit_smoke.c:345 run_store_cases slot_from_f64 PRODUCE d
+jit_smoke.c:362 run_store_cases SLOT_F64_RAW RAW d
+jit_smoke.c:362 run_store_cases SLOT_F64_RAW RAW d
+jit_smoke.c:363 run_store_cases SLOT_F64_RAW RAW d
+jit_smoke.c:370 run_store_cases SLOT_F64_RAW RAW d
+jit_smoke.c:446 run_index_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:446 run_index_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:538 run_reader_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:539 run_reader_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:540 run_reader_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:540 run_reader_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:541 run_reader_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:542 run_reader_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:543 run_reader_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:543 run_reader_bail_cases SLOT_F64_RAW RAW d
+jit_smoke.c:603 run_exit_cases VAL_FLOAT TAGSET d
+jit_smoke.c:604 run_exit_cases VAL_F64_RAW WRITE d
+jit_smoke.c:630 run_exit_cases slot_from_f64 PRODUCE d
+jit_smoke.c:639 run_exit_cases SLOT_F64_RAW RAW d
+jit_smoke.c:644 run_exit_cases SLOT_F64_RAW RAW d
+lexer.c:35 tok_add number RAW b
+lexer.c:42 tok_add number RAW b
+lexer.c:42 tok_add number RAW b
+lexer.c:701 tokenize_at_line number RAW b
+lexer.c:792 tokenize_at_line number RAW b
+lexer.c:793 tokenize_at_line number RAW b
+lexer.c:806 tokenize_at_line strtod PARSE c
+lexer.c:807 tokenize_at_line number RAW b
+lexer.c:823 tokenize_at_line number RAW b
+lexer.c:824 tokenize_at_line number RAW b
+lexer.c:828 tokenize_at_line number RAW b
+lint.c:247 collect_refs AST_NUM CASE a
+lint.c:311 collect_assigns AST_NUM CASE a
+lint.c:369 cond_has_bare_predicate AST_NUM CASE a
+lint.c:431 collect_loop_assign_names AST_NUM CASE a
+lint.c:531 check_empty_blocks AST_NUM CASE a
+lint.c:640 check_dup_keys AST_NUM CASE a
+lint.c:740 check_builtin_shadow AST_NUM CASE a
+lint.c:870 check_disc_interrog AST_NUM CASE a
+lint.c:947 check_func_unreachable AST_NUM CASE a
+lint.c:1016 check_is_conditions AST_NUM CASE a
+lint.c:1118 check_unused_params AST_NUM CASE a
+lint.c:1229 w015_collect_locals AST_NUM CASE a
+lint.c:1297 w015_flag AST_NUM CASE a
+lint.c:1507 w023_walk AST_NUM CASE a
+lint.c:1566 w023_collect_binders AST_NUM CASE a
+lint.c:1727 w023_scan_sequence AST_NUM CASE a
+lint.c:1891 w016_scan AST_NUM CASE a
+lint.c:2050 w017_scan AST_NUM CASE a
+lint.c:2077 #define LINT_FOR_EACH_CHILD AST_NUM CASE a
+lint.c:2293 w022_collect_bindings AST_NUM CASE a
+lint.c:2419 w020_facts AST_NUM CASE a
+lint.c:2479 w020_facts AST_NUM CASE a
+lint.c:2553 w020_scan AST_NUM CASE a
+lint.c:2815 w018_scan AST_NUM CASE a
+lint.c:3096 w024_collect AST_NUM CASE a
+lint.c:3121 w024_index_varies AST_NUM CASE a
+lint.c:3151 w024_elem_walks AST_NUM CASE a
+lint.c:3187 w024_operand_inert AST_NUM TEST a
+lint.c:3538 w024_walk AST_NUM CASE a
+lint_host.c:545 w021_collect_imports AST_NUM CASE a
+lint_host.c:634 w021_walk AST_NUM CASE a
+lint_host.c:1100 e003_walk AST_NUM CASE a
+main.c:282 main number RAW b
+main.c:283 main number RAW b
+main.c:285 main number RAW b
+model_infer.c:550 builtin_eigen_generate eigs_slot_opt_float READ b
+model_infer.c:551 builtin_eigen_generate eigs_slot_opt_int_range READ b
+model_infer.c:552 builtin_eigen_generate eigs_slot_opt_float READ b
+model_infer.c:586 builtin_eigen_generate eigs_slot_int_range READ b
+model_infer.c:660 builtin_eigen_generate make_int PRODUCE c
+model_infer.c:691 builtin_eigen_eval_loss make_float PRODUCE c
+model_infer.c:697 builtin_eigen_eval_loss make_float PRODUCE c
+model_infer.c:699 builtin_eigen_eval_loss VAL_FLOAT TEST a
+model_infer.c:699 builtin_eigen_eval_loss VAL_INT TEST a
+model_infer.c:701 builtin_eigen_eval_loss make_float PRODUCE c
+model_infer.c:704 builtin_eigen_eval_loss eigs_list_int_range READ b
+model_infer.c:711 builtin_eigen_eval_loss make_float PRODUCE c
+model_infer.c:724 builtin_eigen_eval_loss make_float PRODUCE c
+model_infer.c:736 builtin_eigen_eval_loss eigs_slot_int_range READ b
+model_infer.c:765 builtin_eigen_eval_loss make_float PRODUCE c
+model_infer.c:766 builtin_eigen_eval_loss make_float PRODUCE c
+model_io.c:67 replay_model_load_banner val_is_number TEST a
+model_io.c:67 replay_model_load_banner val_is_number TEST a
+model_io.c:68 replay_model_load_banner val_is_number TEST a
+model_io.c:68 replay_model_load_banner val_is_number TEST a
+model_io.c:73 replay_model_load_banner eigs_int_arg_range READ b
+model_io.c:75 replay_model_load_banner eigs_int_arg_range READ b
+model_io.c:77 replay_model_load_banner eigs_int_arg_range READ b
+model_io.c:79 replay_model_load_banner eigs_int_arg_range READ b
+model_io.c:85 replay_model_load_banner val_is_number TEST a
+model_io.c:86 replay_model_load_banner eigs_int_arg_range READ b
+model_io.c:129 json_parse_number strtod PARSE h
+model_train.c:712 obs_drift_eps atof PARSE i
+model_train.c:1463 builtin_native_train_step eigs_slot_float READ b
+model_train.c:1464 builtin_native_train_step VAL_FLOAT TEST a
+model_train.c:1464 builtin_native_train_step VAL_INT TEST a
+model_train.c:1464 builtin_native_train_step eigs_list_float READ b
+model_train.c:1465 builtin_native_train_step strtod PARSE c
+model_train.c:1482 builtin_native_train_step eigs_slot_int_range READ b
+model_train.c:1496 builtin_native_train_step eigs_slot_int_range READ b
+parser.c:549 free_ast AST_NUM CASE a
+parser.c:832 parse_primary AST_NUM DATA a
+parser.c:833 parse_primary data.number WRITE c
+parser.c:833 parse_primary number RAW b
+parser.c:833 parse_primary number RAW b
+tape_read.c:368 tape_parse strtod PARSE g
+tape_read.c:387 tape_parse strtod PARSE g
+tape_read.c:388 tape_parse strtod PARSE g
+tape_read.c:389 tape_parse strtod PARSE g
+tape_read.c:391 tape_parse strtod PARSE g
+task.c:314 task_sched_trace_read make_int PRODUCE c
+task.c:315 task_sched_trace_read make_float PRODUCE c
+task.c:316 task_sched_trace_read make_int PRODUCE c
+task.c:584 task_do_kill make_int PRODUCE c
+task.c:689 task_error_summary eigs_float_arg READ b
+task.c:689 task_error_summary val_is_number TEST a
+trace.c:740 trace_query_at slot_from_int PRODUCE g
+trace.c:755 trace_query_at slot_from_f64 PRODUCE g
+trace.c:804 trace_query_when slot_from_int PRODUCE g
+trace.c:825 trace_query_when slot_from_f64 PRODUCE g
+trace.c:1587 obs_cfg_sync number RAW g
+trace.c:1588 obs_cfg_sync eigs_num_text FORMAT g
+trace.c:1588 obs_cfg_sync number RAW g
+trace.c:1588 obs_cfg_sync number RAW g
+trace.c:1589 obs_cfg_sync number RAW g
+trace.c:1590 obs_cfg_sync eigs_num_text FORMAT g
+trace.c:1590 obs_cfg_sync number RAW g
+trace.c:1590 obs_cfg_sync number RAW g
+trace.c:1591 obs_cfg_sync number RAW g
+trace.c:1592 obs_cfg_sync eigs_num_text FORMAT g
+trace.c:1592 obs_cfg_sync number RAW g
+trace.c:1592 obs_cfg_sync number RAW g
+trace.c:1593 obs_cfg_sync number RAW g
+trace.c:1595 obs_cfg_sync eigs_num_text FORMAT g
+trace.c:1595 obs_cfg_sync number RAW g
+trace.c:1595 obs_cfg_sync number RAW g
+trace.c:1596 obs_cfg_sync number RAW g
+trace.c:1939 #define TK_NUM_ VAL_FLOAT DATA g
+trace.c:1939 #define TK_NUM_ VAL_INT DATA g
+trace.c:2052 kinds_text VAL_FLOAT DATA g
+trace.c:2052 kinds_text VAL_INT DATA g
+trace.c:2053 kinds_text VAL_FLOAT DATA g
+trace.c:2362 parse_value_p strtod PARSE g
+trace.c:2468 parse_value_p make_int PRODUCE g
+trace.c:2471 parse_value_p strtod PARSE g
+trace.c:2481 parse_value_p make_float PRODUCE g
+trace.c:2827 trace_source_record make_int PRODUCE g
+trace.c:2836 trace_source_record make_int PRODUCE g
+trace.c:2860 trace_source_replay VAL_FLOAT TEST g
+trace.c:2860 trace_source_replay VAL_INT TEST g
+trace.c:2869 trace_source_replay VAL_INT TEST g
+trace.c:2871 trace_source_replay slot_f64_value READ g
+trace.c:2878 trace_source_replay slot_int_value READ g
+trace.c:3047 tape_float_text eigs_num_text FORMAT g
+trace.c:3067 write_value_ptr VAL_F64_RAW RAW g
+trace.c:3067 write_value_ptr VAL_FLOAT CASE g
+trace.c:3068 write_value_ptr VAL_I64_RAW RAW g
+trace.c:3068 write_value_ptr VAL_INT CASE g
+trace.c:3084 write_slot SLOT_F64_RAW RAW g
+trace.c:3084 write_slot slot_is_f64 TEST g
+trace.c:3088 write_slot slot_is_smi TEST g
+trace.c:3088 write_slot slot_smi_val RAW g
+trace.c:3211 write_value_ptr_full VAL_FLOAT CASE g
+trace.c:3212 write_value_ptr_full number RAW g
+trace.c:3213 write_value_ptr_full VAL_F64_RAW RAW g
+trace.c:3213 write_value_ptr_full number RAW g
+trace.c:3213 write_value_ptr_full number RAW g
+trace.c:3214 write_value_ptr_full number RAW g
+trace.c:3217 write_value_ptr_full VAL_INT CASE g
+trace.c:3218 write_value_ptr_full number RAW g
+trace.c:3219 write_value_ptr_full VAL_I64_RAW RAW g
+trace.c:3219 write_value_ptr_full number RAW g
+trace.c:3219 write_value_ptr_full number RAW g
+trace.c:3220 write_value_ptr_full number RAW g
+trace.c:3260 write_value_ptr_full number RAW g
+trace.c:3261 write_value_ptr_full eigs_num_text FORMAT g
+trace.c:3261 write_value_ptr_full number RAW g
+trace.c:3261 write_value_ptr_full number RAW g
+trace.c:3262 write_value_ptr_full number RAW g
+value_slot.h:84 slot_is_f64 slot_is_f64 TEST a
+value_slot.h:90 slot_is_smi slot_is_smi TEST a
+value_slot.h:101 #define SLOT_F64_RAW SLOT_F64_RAW RAW b
+value_slot.h:101 #define SLOT_F64_RAW d_ RAW b
+value_slot.h:113 slot_smi_val slot_smi_val RAW b
+value_slot.h:121 slot_from_f64 SLOT_F64_RAW WRITE c
+value_slot.h:121 slot_from_f64 slot_from_f64 PRODUCE c
+value_slot.h:125 slot_from_int slot_from_int PRODUCE c
+value_slot.h:127 slot_from_int make_int PRODUCE c
+value_slot.h:202 slot_truthy_immediate SLOT_F64_RAW RAW b
+value_slot.h:202 slot_truthy_immediate slot_is_f64 TEST a
+value_slot.h:205 slot_truthy_immediate slot_is_smi TEST a
+value_slot.h:205 slot_truthy_immediate slot_smi_val RAW b
+value_slot.h:219 slot_f64_value slot_f64_value READ b
+value_slot.h:220 slot_f64_value SLOT_F64_RAW RAW b
+value_slot.h:220 slot_f64_value slot_is_f64 TEST a
+value_slot.h:223 slot_f64_value VAL_F64_RAW RAW b
+value_slot.h:223 slot_f64_value VAL_FLOAT TEST a
+value_slot.h:229 slot_int_value slot_int_value READ b
+value_slot.h:230 slot_int_value slot_is_smi TEST a
+value_slot.h:230 slot_int_value slot_smi_val RAW b
+value_slot.h:233 slot_int_value VAL_I64_RAW RAW b
+value_slot.h:233 slot_int_value VAL_INT TEST a
+vm.c:117 obs_dump_num eigs_num_text FORMAT i
+vm.c:128 obs_dump_value SLOT_F64_RAW RAW i
+vm.c:128 obs_dump_value slot_is_f64 TEST i
+vm.c:129 obs_dump_value slot_is_smi TEST i
+vm.c:129 obs_dump_value slot_smi_val RAW i
+vm.c:135 obs_dump_value VAL_F64_RAW RAW i
+vm.c:135 obs_dump_value VAL_FLOAT CASE i
+vm.c:136 obs_dump_value VAL_I64_RAW RAW i
+vm.c:136 obs_dump_value VAL_INT CASE i
+vm.c:224 obs_dump_scope slot_is_f64 TEST i
+vm.c:225 obs_dump_scope SLOT_F64_RAW RAW i
+vm.c:226 obs_dump_scope slot_is_smi TEST i
+vm.c:227 obs_dump_scope slot_smi_val RAW i
+vm.c:731 dict_set_cached_immediate dict_value_rewrite_f64 PRODUCE c
+vm.c:811 slot_bridge_unwrap slot_is_f64 TEST a
+vm.c:813 slot_bridge_unwrap SLOT_F64_RAW RAW b
+vm.c:813 slot_bridge_unwrap make_float PRODUCE c
+vm.c:822 slot_bridge_unwrap slot_is_smi TEST a
+vm.c:858 vm_slot_lift SLOT_F64_RAW RAW b
+vm.c:858 vm_slot_lift make_float PRODUCE c
+vm.c:858 vm_slot_lift slot_is_f64 TEST a
+vm.c:863 vm_slot_lift slot_is_smi TEST a
+vm.c:895 slot_truthy SLOT_F64_RAW RAW b
+vm.c:895 slot_truthy slot_is_f64 TEST a
+vm.c:899 slot_truthy slot_is_smi TEST a
+vm.c:899 slot_truthy slot_smi_val RAW b
+vm.c:915 vm_int_arith slot_from_int PRODUCE c
+vm.c:923 vm_bit_operand slot_int_value READ b
+vm.c:924 vm_bit_operand num_to_i64_exact READ b
+vm.c:924 vm_bit_operand slot_f64_value READ b
+vm.c:932 vm_number_compare slot_int_value READ b
+vm.c:932 vm_number_compare slot_int_value READ b
+vm.c:934 vm_number_compare slot_f64_value READ b
+vm.c:935 vm_number_compare slot_f64_value READ b
+vm.c:947 vm_iter_index slot_int_value READ b
+vm.c:953 vm_iter_index slot_f64_value READ b
+vm.c:964 vm_iter_limit slot_int_value READ b
+vm.c:1012 make_iter_state make_int PRODUCE c
+vm.c:1025 make_iter_state make_int PRODUCE c
+vm.c:1250 vm_store_local_slot slot_is_f64 TEST a
+vm.c:1254 vm_store_local_slot VAL_FLOAT TEST a
+vm.c:1256 vm_store_local_slot SLOT_F64_RAW RAW b
+vm.c:1256 vm_store_local_slot VAL_F64_RAW WRITE c
+vm.c:1443 jit_helper_local_idx_get buffer_read_f64 READ b
+vm.c:1443 jit_helper_local_idx_get slot_from_f64 PRODUCE c
+vm.c:1604 jit_helper_dot_set slot_is_f64 TEST a
+vm.c:1607 jit_helper_dot_set SLOT_F64_RAW RAW b
+vm.c:1674 jit_helper_local_dot_set slot_is_f64 TEST a
+vm.c:1675 jit_helper_local_dot_set SLOT_F64_RAW RAW b
+vm.c:1725 obs_slot_observable slot_is_f64 TEST i
+vm.c:1732 obs_slot_num SLOT_F64_RAW RAW i
+vm.c:1732 obs_slot_num slot_is_f64 TEST i
+vm.c:1733 obs_slot_num slot_is_smi TEST i
+vm.c:1733 obs_slot_num slot_smi_val RAW i
+vm.c:1734 obs_slot_num VAL_INT TEST i
+vm.c:1735 obs_slot_num VAL_I64_RAW RAW i
+vm.c:1904 jit_helper_iter_next buffer_read_f64 READ b
+vm.c:1904 jit_helper_iter_next num_guard GUARD c
+vm.c:1904 jit_helper_iter_next slot_from_f64 PRODUCE c
+vm.c:1912 jit_helper_iter_next slot_is_smi TEST a
+vm.c:1915 jit_helper_iter_next list_set_int PRODUCE c
+vm.c:1970 vm_leaf_accessor_exec/OP_CONST VAL_F64_RAW RAW b
+vm.c:1970 vm_leaf_accessor_exec/OP_CONST slot_from_f64 PRODUCE c
+vm.c:1974 vm_leaf_accessor_exec/OP_NUM_ZERO slot_from_f64 PRODUCE c
+vm.c:1977 vm_leaf_accessor_exec/OP_NUM_ONE slot_from_f64 PRODUCE c
+vm.c:2016 vm_leaf_accessor_exec/OP_INDEX_GET slot_f64_value READ b
+vm.c:2031 vm_leaf_accessor_exec/OP_INDEX_GET buffer_read_f64 READ b
+vm.c:2031 vm_leaf_accessor_exec/OP_INDEX_GET slot_from_f64 PRODUCE c
+vm.c:2042 vm_leaf_accessor_exec/OP_ADD|OP_SUB|OP_MUL slot_f64_value READ b
+vm.c:2043 vm_leaf_accessor_exec/OP_ADD|OP_SUB|OP_MUL slot_f64_value READ b
+vm.c:2061 vm_leaf_accessor_exec/OP_ADD|OP_SUB|OP_MUL num_guard GUARD c
+vm.c:2061 vm_leaf_accessor_exec/OP_ADD|OP_SUB|OP_MUL slot_from_f64 PRODUCE c
+vm.c:2143 loop_iter_store make_int PRODUCE c
+vm.c:2150 loop_iter_store slot_is_smi TEST a
+vm.c:2152 loop_iter_store slot_from_int PRODUCE c
+vm.c:2157 loop_iter_store make_int PRODUCE c
+vm.c:2185 loop_counter_load slot_int_value READ b
+vm.c:2193 loop_counter_store slot_is_smi TEST a
+vm.c:2194 loop_counter_store slot_from_int PRODUCE c
+vm.c:2198 loop_counter_store slot_from_int PRODUCE c
+vm.c:2208 eigs_loop_enter_named slot_from_int PRODUCE c
+vm.c:2353 vm_value_index_is_int VAL_INT TEST a
+vm.c:2354 vm_value_index_is_int VAL_I64_RAW RAW b
+vm.c:2359 vm_value_index_is_int VAL_F64_RAW RAW b
+vm.c:2367 vm_subscript_index_is_int VAL_INT TEST a
+vm.c:2368 vm_subscript_index_is_int VAL_I64_RAW RAW b
+vm.c:2374 vm_subscript_index_is_int VAL_F64_RAW RAW b
+vm.c:2387 jit_helper_index_set slot_is_f64 TEST a
+vm.c:2392 jit_helper_index_set SLOT_F64_RAW RAW b
+vm.c:2393 jit_helper_index_set slot_is_f64 TEST a
+vm.c:2395 jit_helper_index_set SLOT_F64_RAW RAW b
+vm.c:2397 jit_helper_index_set SLOT_F64_RAW RAW b
+vm.c:2407 jit_helper_index_set slot_is_f64 TEST a
+vm.c:2409 jit_helper_index_set SLOT_F64_RAW RAW b
+vm.c:2410 jit_helper_index_set list_rewrite_f64 PRODUCE c
+vm.c:2412 jit_helper_index_set SLOT_F64_RAW RAW b
+vm.c:2424 jit_helper_index_set val_is_number TEST a
+vm.c:2427 jit_helper_index_set eigs_float_arg READ b
+vm.c:2434 jit_helper_index_set val_is_number TEST a
+vm.c:2437 jit_helper_index_set eigs_float_arg READ b
+vm.c:2440 jit_helper_index_set val_is_number TEST a
+vm.c:2441 jit_helper_index_set eigs_float_arg READ b
+vm.c:2461 jit_helper_index_get slot_is_f64 TEST a
+vm.c:2463 jit_helper_index_get SLOT_F64_RAW RAW b
+vm.c:2470 jit_helper_index_get SLOT_F64_RAW RAW b
+vm.c:2481 jit_helper_index_get buffer_read_f64 READ b
+vm.c:2483 jit_helper_index_get slot_from_f64 PRODUCE c
+vm.c:2485 jit_helper_index_get SLOT_F64_RAW RAW b
+vm.c:2501 jit_helper_index_get val_is_number TEST a
+vm.c:2504 jit_helper_index_get eigs_float_arg READ b
+vm.c:2516 jit_helper_index_get val_is_number TEST a
+vm.c:2519 jit_helper_index_get eigs_float_arg READ b
+vm.c:2528 jit_helper_index_get val_is_number TEST a
+vm.c:2531 jit_helper_index_get eigs_float_arg READ b
+vm.c:2533 jit_helper_index_get buffer_read_f64 READ b
+vm.c:2533 jit_helper_index_get make_float PRODUCE c
+vm.c:3099 vm_take_error_value make_int PRODUCE c
+vm.c:3225 slot_type_name slot_is_f64 TEST e
+vm.c:3226 slot_type_name slot_is_smi TEST e
+vm.c:3488 vm_run_ex/CASE(CONST) VAL_FLOAT TEST a
+vm.c:3489 vm_run_ex/CASE(CONST) VAL_F64_RAW RAW b
+vm.c:3489 vm_run_ex/CASE(CONST) slot_from_f64 PRODUCE c
+vm.c:3498 vm_run_ex/CASE(INT_ZERO) slot_from_int PRODUCE c
+vm.c:3502 vm_run_ex/CASE(INT_ONE) slot_from_int PRODUCE c
+vm.c:3508 vm_run_ex/CASE(INT_CONST) VAL_I64_RAW RAW b
+vm.c:3509 vm_run_ex/CASE(INT_CONST) slot_from_int PRODUCE c
+vm.c:3520 vm_run_ex/CASE(NUM_ZERO) slot_from_f64 PRODUCE c
+vm.c:3525 vm_run_ex/CASE(NUM_ONE) slot_from_f64 PRODUCE c
+vm.c:3545 #define NUM_REUSE NUM_REUSE TEST a
+vm.c:3545 #define NUM_REUSE VAL_FLOAT TEST a
+vm.c:3556 #define ARITH_FAST slot_f64_value READ b
+vm.c:3556 #define ARITH_FAST slot_f64_value READ b
+vm.c:3557 #define ARITH_FAST num_guard GUARD c
+vm.c:3562 #define ARITH_FAST NUM_REUSE TEST a
+vm.c:3563 #define ARITH_FAST VAL_F64_RAW WRITE c
+vm.c:3571 #define ARITH_FAST NUM_REUSE TEST a
+vm.c:3572 #define ARITH_FAST VAL_F64_RAW WRITE c
+vm.c:3580 #define ARITH_FAST slot_from_f64 PRODUCE c
+vm.c:3592 #define INT_ARITH slot_int_value READ b
+vm.c:3592 #define INT_ARITH slot_int_value READ b
+vm.c:3606 vm_run_ex/CASE(ADD) VAL_FLOAT TEST a
+vm.c:3606 vm_run_ex/CASE(ADD) VAL_FLOAT TEST a
+vm.c:3607 vm_run_ex/CASE(ADD) VAL_F64_RAW RAW b
+vm.c:3607 vm_run_ex/CASE(ADD) VAL_F64_RAW RAW b
+vm.c:3607 vm_run_ex/CASE(ADD) num_guard GUARD c
+vm.c:3608 vm_run_ex/CASE(ADD) NUM_REUSE TEST a
+vm.c:3608 vm_run_ex/CASE(ADD) VAL_F64_RAW WRITE c
+vm.c:3609 vm_run_ex/CASE(ADD) NUM_REUSE TEST a
+vm.c:3609 vm_run_ex/CASE(ADD) VAL_F64_RAW WRITE c
+vm.c:3610 vm_run_ex/CASE(ADD) make_float PRODUCE c
+vm.c:3611 vm_run_ex/CASE(ADD) val_is_number TEST a
+vm.c:3611 vm_run_ex/CASE(ADD) val_is_number TEST a
+vm.c:3612 vm_run_ex/CASE(ADD) eigs_float_arg READ b
+vm.c:3612 vm_run_ex/CASE(ADD) eigs_float_arg READ b
+vm.c:3612 vm_run_ex/CASE(ADD) make_float PRODUCE c
+vm.c:3612 vm_run_ex/CASE(ADD) num_guard GUARD c
+vm.c:3662 #define NUM_BINOP VAL_FLOAT TEST a
+vm.c:3662 #define NUM_BINOP VAL_FLOAT TEST a
+vm.c:3663 #define NUM_BINOP VAL_F64_RAW RAW b
+vm.c:3663 #define NUM_BINOP VAL_F64_RAW RAW b
+vm.c:3663 #define NUM_BINOP num_guard GUARD c
+vm.c:3664 #define NUM_BINOP VAL_F64_RAW RAW b
+vm.c:3664 #define NUM_BINOP VAL_F64_RAW RAW b
+vm.c:3666 #define NUM_BINOP NUM_REUSE TEST a
+vm.c:3666 #define NUM_BINOP VAL_F64_RAW WRITE c
+vm.c:3667 #define NUM_BINOP NUM_REUSE TEST a
+vm.c:3667 #define NUM_BINOP VAL_F64_RAW WRITE c
+vm.c:3668 #define NUM_BINOP make_float PRODUCE c
+vm.c:3669 #define NUM_BINOP val_is_number TEST a
+vm.c:3669 #define NUM_BINOP val_is_number TEST a
+vm.c:3670 #define NUM_BINOP eigs_float_arg READ b
+vm.c:3670 #define NUM_BINOP eigs_float_arg READ b
+vm.c:3671 #define NUM_BINOP num_guard GUARD c
+vm.c:3674 #define NUM_BINOP make_float PRODUCE c
+vm.c:3678 #define NUM_BINOP make_float PRODUCE c
+vm.c:3696 vm_run_ex/CASE(DIV) val_is_number TEST a
+vm.c:3696 vm_run_ex/CASE(DIV) val_is_number TEST a
+vm.c:3697 vm_run_ex/CASE(DIV) eigs_float_arg READ b
+vm.c:3699 vm_run_ex/CASE(DIV) make_float PRODUCE c
+vm.c:3701 vm_run_ex/CASE(DIV) eigs_float_arg READ b
+vm.c:3701 vm_run_ex/CASE(DIV) eigs_float_arg READ b
+vm.c:3701 vm_run_ex/CASE(DIV) num_guard GUARD c
+vm.c:3704 vm_run_ex/CASE(DIV) eigs_float_arg READ b
+vm.c:3706 vm_run_ex/CASE(DIV) NUM_REUSE TEST a
+vm.c:3706 vm_run_ex/CASE(DIV) VAL_F64_RAW WRITE c
+vm.c:3707 vm_run_ex/CASE(DIV) NUM_REUSE TEST a
+vm.c:3707 vm_run_ex/CASE(DIV) VAL_F64_RAW WRITE c
+vm.c:3708 vm_run_ex/CASE(DIV) make_float PRODUCE c
+vm.c:3713 vm_run_ex/CASE(DIV) make_float PRODUCE c
+vm.c:3721 vm_run_ex/CASE(IDIV) val_is_number TEST a
+vm.c:3721 vm_run_ex/CASE(IDIV) val_is_number TEST a
+vm.c:3722 vm_run_ex/CASE(IDIV) VAL_INT TEST a
+vm.c:3722 vm_run_ex/CASE(IDIV) VAL_INT TEST a
+vm.c:3723 vm_run_ex/CASE(IDIV) VAL_I64_RAW RAW b
+vm.c:3723 vm_run_ex/CASE(IDIV) VAL_I64_RAW RAW b
+vm.c:3726 vm_run_ex/CASE(IDIV) make_int PRODUCE c
+vm.c:3729 vm_run_ex/CASE(IDIV) make_int PRODUCE c
+vm.c:3735 vm_run_ex/CASE(IDIV) make_int PRODUCE c
+vm.c:3738 vm_run_ex/CASE(IDIV) eigs_float_arg READ b
+vm.c:3743 vm_run_ex/CASE(IDIV) make_float PRODUCE c
+vm.c:3748 vm_run_ex/CASE(IDIV) eigs_float_arg READ b
+vm.c:3760 vm_run_ex/CASE(IDIV) make_float PRODUCE c
+vm.c:3760 vm_run_ex/CASE(IDIV) num_guard GUARD c
+vm.c:3766 vm_run_ex/CASE(IDIV) make_int PRODUCE c
+vm.c:3776 vm_run_ex/CASE(MOD) val_is_number TEST a
+vm.c:3776 vm_run_ex/CASE(MOD) val_is_number TEST a
+vm.c:3777 vm_run_ex/CASE(MOD) VAL_INT TEST a
+vm.c:3777 vm_run_ex/CASE(MOD) VAL_INT TEST a
+vm.c:3778 vm_run_ex/CASE(MOD) VAL_I64_RAW RAW b
+vm.c:3778 vm_run_ex/CASE(MOD) VAL_I64_RAW RAW b
+vm.c:3781 vm_run_ex/CASE(MOD) make_int PRODUCE c
+vm.c:3787 vm_run_ex/CASE(MOD) make_int PRODUCE c
+vm.c:3790 vm_run_ex/CASE(MOD) eigs_float_arg READ b
+vm.c:3793 vm_run_ex/CASE(MOD) make_float PRODUCE c
+vm.c:3795 vm_run_ex/CASE(MOD) eigs_float_arg READ b
+vm.c:3795 vm_run_ex/CASE(MOD) make_float PRODUCE c
+vm.c:3795 vm_run_ex/CASE(MOD) num_guard GUARD c
+vm.c:3801 vm_run_ex/CASE(MOD) make_float PRODUCE c
+vm.c:3817 #define INT_BINOP_R slot_from_int PRODUCE c
+vm.c:3823 #define INT_BINOP_R slot_f64_value READ b
+vm.c:3843 vm_run_ex/CASE(NEG) slot_f64_value READ b
+vm.c:3846 vm_run_ex/CASE(NEG) NUM_REUSE TEST a
+vm.c:3846 vm_run_ex/CASE(NEG) VAL_F64_RAW WRITE c
+vm.c:3849 vm_run_ex/CASE(NEG) slot_from_f64 PRODUCE c
+vm.c:3853 vm_run_ex/CASE(NEG) slot_int_value READ b
+vm.c:3858 vm_run_ex/CASE(NEG) slot_from_int PRODUCE c
+vm.c:3863 vm_run_ex/CASE(NEG) slot_from_f64 PRODUCE c
+vm.c:3880 vm_run_ex/CASE(BNOT) slot_from_int PRODUCE c
+vm.c:3885 vm_run_ex/CASE(BNOT) slot_f64_value READ b
+vm.c:3898 vm_run_ex/CASE(EQ) slot_is_smi TEST f
+vm.c:3898 vm_run_ex/CASE(EQ) slot_is_smi TEST f
+vm.c:3904 vm_run_ex/CASE(EQ) slot_f64_value READ f
+vm.c:3904 vm_run_ex/CASE(EQ) slot_f64_value READ f
+vm.c:3924 vm_run_ex/CASE(NE) slot_is_smi TEST f
+vm.c:3924 vm_run_ex/CASE(NE) slot_is_smi TEST f
+vm.c:3930 vm_run_ex/CASE(NE) slot_f64_value READ f
+vm.c:3930 vm_run_ex/CASE(NE) slot_f64_value READ f
+vm.c:3952 #define NUM_CMP slot_f64_value READ f
+vm.c:3952 #define NUM_CMP slot_f64_value READ f
+vm.c:4903 vm_run_ex/CASE(INDEX_GET) slot_is_f64 TEST a
+vm.c:4905 vm_run_ex/CASE(INDEX_GET) SLOT_F64_RAW RAW b
+vm.c:4912 vm_run_ex/CASE(INDEX_GET) SLOT_F64_RAW RAW b
+vm.c:4922 vm_run_ex/CASE(INDEX_GET) buffer_read_f64 READ b
+vm.c:4924 vm_run_ex/CASE(INDEX_GET) slot_from_f64 PRODUCE c
+vm.c:4926 vm_run_ex/CASE(INDEX_GET) SLOT_F64_RAW RAW b
+vm.c:4948 vm_run_ex/CASE(INDEX_SET) slot_is_f64 TEST a
+vm.c:4950 vm_run_ex/CASE(INDEX_SET) SLOT_F64_RAW RAW b
+vm.c:4951 vm_run_ex/CASE(INDEX_SET) slot_is_f64 TEST a
+vm.c:4953 vm_run_ex/CASE(INDEX_SET) SLOT_F64_RAW RAW b
+vm.c:4955 vm_run_ex/CASE(INDEX_SET) SLOT_F64_RAW RAW b
+vm.c:4965 vm_run_ex/CASE(INDEX_SET) slot_is_f64 TEST a
+vm.c:4967 vm_run_ex/CASE(INDEX_SET) SLOT_F64_RAW RAW b
+vm.c:4967 vm_run_ex/CASE(INDEX_SET) list_set_f64 PRODUCE c
+vm.c:4969 vm_run_ex/CASE(INDEX_SET) SLOT_F64_RAW RAW b
+vm.c:4981 vm_run_ex/CASE(INDEX_SET) val_is_number TEST a
+vm.c:4984 vm_run_ex/CASE(INDEX_SET) eigs_float_arg READ b
+vm.c:4991 vm_run_ex/CASE(INDEX_SET) val_is_number TEST a
+vm.c:4994 vm_run_ex/CASE(INDEX_SET) eigs_float_arg READ b
+vm.c:4997 vm_run_ex/CASE(INDEX_SET) val_is_number TEST a
+vm.c:4998 vm_run_ex/CASE(INDEX_SET) eigs_float_arg READ b
+vm.c:5055 vm_run_ex/CASE(DOT_SET) slot_is_f64 TEST a
+vm.c:5058 vm_run_ex/CASE(DOT_SET) SLOT_F64_RAW RAW b
+vm.c:5119 vm_run_ex/CASE(LOCAL_DOT_SET) slot_is_f64 TEST a
+vm.c:5120 vm_run_ex/CASE(LOCAL_DOT_SET) SLOT_F64_RAW RAW b
+vm.c:5146 vm_run_ex/CASE(LOCAL_IDX_GET) buffer_read_f64 READ b
+vm.c:5146 vm_run_ex/CASE(LOCAL_IDX_GET) slot_from_f64 PRODUCE c
+vm.c:5252 vm_run_ex/CASE(LOCAL_IDX_DOT_SET) slot_is_f64 TEST a
+vm.c:5253 vm_run_ex/CASE(LOCAL_IDX_DOT_SET) SLOT_F64_RAW RAW b
+vm.c:5325 vm_run_ex/CASE(ITER_NEXT) slot_is_smi TEST a
+vm.c:5328 vm_run_ex/CASE(ITER_NEXT) list_set_int PRODUCE c
+vm.c:5332 vm_run_ex/CASE(ITER_NEXT) buffer_read_f64 READ b
+vm.c:5332 vm_run_ex/CASE(ITER_NEXT) slot_from_f64 PRODUCE c
+vm.c:5672 vm_run_ex/CASE(OBSERVE_VALUE_SLOT) make_float PRODUCE i
+vm.c:5673 vm_run_ex/CASE(OBSERVE_VALUE_SLOT) make_float PRODUCE i
+vm.c:5674 vm_run_ex/CASE(OBSERVE_VALUE_SLOT) make_float PRODUCE i
+vm.c:5708 vm_run_ex/CASE(OBSERVE_VALUE_NAME) make_float PRODUCE i
+vm.c:5709 vm_run_ex/CASE(OBSERVE_VALUE_NAME) make_float PRODUCE i
+vm.c:5710 vm_run_ex/CASE(OBSERVE_VALUE_NAME) make_float PRODUCE i
+vm.c:5774 vm_run_ex/CASE(INTERROGATE) VAL_INT TEST i
+vm.c:5775 vm_run_ex/CASE(INTERROGATE) VAL_F64_RAW RAW i
+vm.c:5775 vm_run_ex/CASE(INTERROGATE) VAL_FLOAT TEST i
+vm.c:5775 vm_run_ex/CASE(INTERROGATE) make_float PRODUCE i
+vm.c:5779 vm_run_ex/CASE(INTERROGATE) make_int PRODUCE i
+vm.c:5780 vm_run_ex/CASE(INTERROGATE) make_int PRODUCE i
+vm.c:5781 vm_run_ex/CASE(INTERROGATE) make_int PRODUCE i
+vm.c:5782 vm_run_ex/CASE(INTERROGATE) make_float PRODUCE i
+vm.c:5786 vm_run_ex/CASE(INTERROGATE) val_is_number TEST i
+vm.c:5800 vm_run_ex/CASE(INTERROGATE) make_int PRODUCE i
+vm.c:5801 vm_run_ex/CASE(INTERROGATE) make_float PRODUCE i
+vm.c:5802 vm_run_ex/CASE(INTERROGATE) make_float PRODUCE i
+vm.c:5803 vm_run_ex/CASE(INTERROGATE) make_float PRODUCE i
+vm.c:5823 vm_run_ex/CASE(INTERROGATE_NAMED) make_int PRODUCE i
+vm.c:5849 vm_run_ex/CASE(INTERROGATE_NAMED) make_float PRODUCE i
+vm.c:5852 vm_run_ex/CASE(INTERROGATE_NAMED) make_float PRODUCE i
+vm.c:5853 vm_run_ex/CASE(INTERROGATE_NAMED) make_float PRODUCE i
+vm.c:5856 vm_run_ex/CASE(INTERROGATE_NAMED) make_float PRODUCE i
+vm.c:5872 vm_run_ex/CASE(INTERROGATE_NAMED_AT) val_is_number TEST i
+vm.c:5880 vm_run_ex/CASE(INTERROGATE_NAMED_AT) VAL_INT TEST i
+vm.c:5883 vm_run_ex/CASE(INTERROGATE_NAMED_AT) VAL_I64_RAW RAW i
+vm.c:5888 vm_run_ex/CASE(INTERROGATE_NAMED_AT) VAL_F64_RAW RAW i
+vm.c:5908 vm_run_ex/CASE(INTERROGATE_NAMED_WHEN) val_is_number TEST i
+vm.c:5909 vm_run_ex/CASE(INTERROGATE_NAMED_WHEN) VAL_INT TEST i
+vm.c:5910 vm_run_ex/CASE(INTERROGATE_NAMED_WHEN) VAL_I64_RAW RAW i
+vm.c:5911 vm_run_ex/CASE(INTERROGATE_NAMED_WHEN) VAL_F64_RAW RAW i
+vm.c:6041 vm_run_ex/CASE(SLICE_GET) VAL_FLOAT CASE a
+vm.c:6041 vm_run_ex/CASE(SLICE_GET) VAL_INT CASE a
+vm.c:6058 #define READ_SLICE_BOUND slot_is_f64 TEST a
+vm.c:6059 #define READ_SLICE_BOUND SLOT_F64_RAW RAW b
+vm.c:6061 #define READ_SLICE_BOUND SLOT_F64_RAW RAW b
+vm.c:6068 #define READ_SLICE_BOUND VAL_INT TEST a
+vm.c:6072 #define READ_SLICE_BOUND VAL_I64_RAW RAW b
+vm.c:6075 #define READ_SLICE_BOUND VAL_FLOAT TEST a
+vm.c:6076 #define READ_SLICE_BOUND VAL_F64_RAW RAW b
+vm.c:6079 #define READ_SLICE_BOUND VAL_F64_RAW RAW b
+vm.c:6090 #define READ_SLICE_BOUND slot_is_smi TEST a
+vm.c:6093 #define READ_SLICE_BOUND slot_smi_val RAW b
+vm.c:6790 vm_run_ex/CASE(DISPATCH) VAL_FLOAT TEST a
+vm.c:6790 vm_run_ex/CASE(DISPATCH) VAL_INT TEST a
+vm.c:6796 vm_run_ex/CASE(DISPATCH) eigs_slot_number READ b
+vm.c:6801 vm_run_ex/CASE(DISPATCH) num_to_i64_exact READ b
+vm.h:841 vm_index_is_int num_to_i64_exact READ b
diff --git a/docs/design/phase2-numbers.md b/docs/design/phase2-numbers.md
new file mode 100644
index 000000000..6d70f75cf
--- /dev/null
+++ b/docs/design/phase2-numbers.md
@@ -0,0 +1,3265 @@
+# Phase 2 numbers (#1758): design (round 0, revised in design rounds 2 and 3)
+
+Status: design round 3, with R1.1/R1.2 implementation notes and R1.2
+fix-round verification in §12. The first program-visible int producer
+remains R1.3. Inventory and prototype measurements below describe the
+original design tree. Spec: issue #1758 (Revision 1), its owner decisions (A), (B), (C), the
+Q1-Q6 decisions
+([comment](https://github.com/InauguralSystems/EigenScript/issues/1758#issuecomment-6171744498))
+and the Q7-Q8 decisions
+([comment](https://github.com/InauguralSystems/EigenScript/issues/1758#issuecomment-6174302316)).
+Evidence base: issue #1746 and the `exp-ieee-p1` branch (canonical NaN, the
+single slot writer, the JIT bailing on NaN, refusal inside writers).
+
+Tree measured: `phase2-numbers` at `0740af03` (main, v0.46.0-8). The tag the
+Done-when means is `v0.46.0` = `4a3a5c19`.
+
+Companion artifacts:
+- `tools/phase2_inventory.py`: the site inventory, the executor matrix, the
+ slot-classifier derivation, and the builtin-table check. It writes
+ `docs/design/phase2-inventory.txt`.
+- `tests/phase2/MAP.md`: every N, O and T item mapped to planned rows, with
+ row shapes and tier witnesses.
+
+**What design round 2 changed** (after the blind critic's FAIL on round 0):
+- The third numeric evaluator, `vm_leaf_accessor_exec` (vm.c:1856, scanner
+ chunk.c:1072, #366), is in the inventory (§1), the JIT/leaf section (§4.5)
+ and the map (§7), with every arm, and it gets its own gate (§9.2).
+- Every meaning change now coincides with a token change (§2.7). Int
+ literals get new opcodes; `OP_NUM_ZERO`/`OP_NUM_ONE` keep their float
+ meaning; `slot_as_double` is deleted in R1.1.
+- The compile-forced count is re-measured with clang's `unavailable`
+ attribute under three configurations (§2.3a), and every site that does
+ not stop compiling is classified.
+- The slot classifiers are derived, not listed (§2.5), and raw union-member
+ reads are in the vocabulary.
+- The canonical-form invariant is corrected (§2.1): the lifts box on purpose.
+- The builtin table follows the decided Q1 (§6), with the LOSS rows #1762
+ found; §11 records Q1-Q6 as decided.
+- The test plan has row shapes and tier witnesses (§9).
+
+**What design round 3 changed** (after the critic's FAIL on round 2):
+- The `osr` witness proves the loop BODY ran natively: every OSR row is a
+ `loop while true:` whose only exit is a `return` right after the claimed
+ ops, and the witness requires the OSR record to reach the return sentinel
+ (§9.1). Every witness kind now has a negative plant, calibrated on main
+ (§9.2); the old form passed a plant with 0 native `INDEX_SET` calls.
+- R1.2 is behaviour-preserving and stays so: the builtin readers and
+ producers are a named BRIDGE that R1.6 deletes, so every builtin behaviour
+ change coincides with a token change, and a per-builtin behaviour table
+ gates R1.6 (§2.7, §8, §9.4). The first int value appears at R1.3, and
+ §8 lists every consumer converted in that commit.
+- `phase2_check.sh --through Rk` checks a round against exactly the rows that
+ have landed; R1.1 and R1.2 are mechanical rounds with named gates (§9.2).
+- The observer's boxed-int path is mapped and routed (§7, MAP
+ `O1.int-observed-numeric`).
+- `buffer_read_num` is renamed in R1.6, so the index reads (`gather`,
+ `scatter_add`) and the byte reads (`buf_from_pcm16le` and the byte
+ consumers) are re-decided there; the compile count is re-measured under the
+ Makefile's exact flags and attributed per commit (§2.3a).
+- Q7 and Q8 are decided and folded in: a validated `q7` column in the builtin
+ table, a geometry reader that clips the whole shape, and IEEE `totalOrder`
+ for sorting with NaN propagating through `min`/`max` (§2.6, §6, §11).
+
+Contents:
+1. The inventory
+2. Representation
+3. Measured prototype
+4. The JIT and the leaf evaluator
+5. N12 and SMI share the NaN space
+6. Builtin classification
+7. Semantics-to-sites map
+8. Round plan
+9. Test plan
+10. Lua differential tool
+11. Decided questions (Q1-Q8)
+12. R1.2 verification notes
+
+---
+
+## 1. The inventory
+
+`python3 tools/phase2_inventory.py` prints `examined=1798 classified=1798
+unclassified=0` on this tree and writes every site to
+`docs/design/phase2-inventory.txt` (file:line, enclosing function, token,
+syntactic kind, category).
+
+**Population.** A site is one occurrence, outside comments and string
+literals, of a token of the value model's numeric vocabulary in `src/*.c` and
+`src/*.h`. The vocabulary is:
+- the type tag `VAL_NUM` and the slot predicates;
+- the raw and checked payload accessors and the constructors;
+- the raw union members themselves: `.d_` / `->d_` (the slot's double) and
+ `.num_` / `->num_` (the Value's double). Inside their three accessor
+ macros (`SLOT_NUM_RAW`, `VAL_NUM_RAW`, `VAL_NUM_OFFSET`) they are RAW
+ sites; anywhere else they are a `BYPASS`, which the script leaves
+ unclassified, so the run fails. This closes the blind spot the critic's
+ plant `e_blind_member` (`static double p2_plant_blind(EigsSlot s) {
+ return s.d_; }` appended to vm.c) showed: the round-0 oracle stayed green
+ on it. The repo's own `tools/num_read_check.sh` (#1637) already refuses
+ such a bypass in CI; the inventory now sees it too;
+- the clamp (`num_guard`) and the number formatter;
+- the double parsers (`strtod`, `atof`);
+- the AST literal fields (`AST_NUM`, `data.num`, `num_val`);
+- in the JIT files only, the tag-range immediates (`0xFFF8`..`0xFFFF`) and the
+ emitters that assume a double (`emit_load_numeric_operand`, the `*sd`
+ arithmetic, `cvt*`, `fmod`, ...). The JIT's guards compare raw tag bits and
+ name no vocabulary token, so without these the JIT category would hold 40
+ sites instead of 127.
+
+**Attribution.** Each site carries its enclosing function. Inside the five
+opcode dispatchers it also carries its arm:
+- `vm_run_ex` (the interpreter): `CASE(X)`. A macro whose body spells
+ `CASE()` generates an arm at each invocation, closed
+ over chains (`INT_BINOP` -> `INT_BINOP_R` -> `CASE(NAME)`), so
+ `NUM_BINOP(SUB, ...)` and `NUM_CMP(LT, ...)` are interpreter arms too;
+- `jit_supported_prefix` and `jit_compile_to_thunk` (the JIT scanner and
+ emitter): the `if (op == OP_X || ...)` chain, named by its first opcode;
+- `chunk_scan_leaf_accessor` and `vm_leaf_accessor_exec` (the leaf scanner
+ and evaluator): the `case` labels at the dispatch switch's own depth,
+ grouped labels joined (`OP_ADD|OP_SUB|OP_MUL`).
+
+**The key and its blind spot.** The key is the token, so code that handles a
+number without naming one is invisible. Four derived checks bound that, and
+each can fail the run:
+1. Every function that returns or writes out a `double` and whose body
+ performs a payload read is a numeric accessor. It must be in the
+ vocabulary (its call sites then count as sites) or in a reviewed
+ `WRAPPERS` list with a reason. 17 such functions exist. 11 are in the
+ vocabulary and 6 are wrappers, for example `tensor_to_flat`, whose
+ per-element read is the site.
+2. Every `VAL_NUM`/`AST_NUM` must sit in a recognized syntactic position:
+ test, case, tag store, type yield, data, or emitter immediate. An
+ unrecognized position (for example `1u << VAL_NUM`) is unclassified.
+3. **The executor matrix** (`## Executors` in the inventory file). For every
+ opcode, the roles that implement it: `interp`, `jit_scan`, `jit_emit`,
+ `leaf_scan`, `leaf_exec`. The leaf scanner and the leaf evaluator must
+ admit exactly the same opcodes, in both directions (a scanner arm with no
+ evaluator arm, or the reverse, fails the run). Today both hold the same
+ 11: `LINE`, `CONST`, `NUM_ZERO`, `NUM_ONE`, `GET_LOCAL`, `LOCAL_DOT_GET`,
+ `INDEX_GET`, `ADD`, `SUB`, `MUL`, `RETURN`. So `OP_NUM_ZERO` and
+ `OP_NUM_ONE` have three executors (interpreter vm.c:3257, JIT emitter
+ jit.c:2797, leaf evaluator vm.c:1872) and two scanners; the round-0 draft
+ named two executors.
+4. **The slot classifiers** (`## Slot classifiers`). Every function, or
+ dispatcher arm, that tests, dereferences or types an `EigsSlot`, by the
+ tokens it names:
+ - D1: it names a number test (`slot_is_num`, `slot_is_numeric`,
+ `slot_as_double`). All three are renamed or deleted, so the compiler
+ forces every D1 entry (58 today);
+ - D2: it dereferences (`slot_as_ptr`, `slot_as_heap`, `eigs_slot_ref`)
+ without a number test (20);
+ - D3: it tests other tags or raw tag bits (23);
+ - T: it only reads a slot's `ValType` (`slot_type`, `list_elem_type`,
+ `dict_value_type`), 67. A comparison with `VAL_NUM` is compile-forced;
+ a comparison with another enumerator is unaffected by ints; a switch is
+ forced by `-Werror=switch`.
+
+ Every D2 and D3 entry needs a reviewed row in §2.5's table, with a
+ disposition from a fixed set. A missing row and a stale row each fail the
+ run. The three classifiers the critic found outside round 0's rule
+ (`eigs_slot_ref`, `obs_slot_observable`, `slot_type_name`) are D2/D3
+ entries of this derivation.
+
+Also outside the population:
+- `src/freestanding/` (`mini_strtod.c`, `mini_fmt.c`), which N1, N6 and N10
+ must also reach for the EigenOS profile; the round plan names it;
+- code that computes on a double after a counted read extracted it.
+
+**Categories** (one per site; the first matching rule wins). Role rules,
+keyed by file or function, come first; otherwise the token's own kind
+decides (a)/(b)/(c)/(e).
+
+| | category | sites | kinds |
+|---|---|---:|---|
+| a | type tests | 384 | TEST 327, CASE 52, DATA 3, YIELD 2 |
+| b | payload reads | 530 | READ 414, RAW 116 |
+| c | producers | 472 | PRODUCE 373, GUARD 71, WRITE 22, PARSE 3, TAGSET 3 |
+| d | JIT emitters and guards | 127 | EMIT 89, RAW 27, PRODUCE 6, TEST 2, GUARD 1, TAGSET 1, WRITE 1 |
+| e | number formatting | 14 | |
+| f | equality, ordering and hashing | 27 | |
+| g | tape encode and decode | 20 | |
+| h | JSON, store and db encoders | 70 | |
+| i | observer numeric paths | 90 | |
+| j | embed API | 64 | |
+| | **total** | **1798** | |
+
+`WRITE` is `VAL_NUM_RAW(v) = x`, the NUM_REUSE in-place writes the brief
+names as producers. Design round 2 added 25 sites: the raw members inside
+their three accessor macros (`RAW`, 3) and `buffer_read_byte`, the byte
+reader Q1 changes (`READ`, 22).
+
+Largest files: builtins.c 319, vm.c 304, ext_gfx.c 250, builtins_tensor.c
+184, builtins_buf.c 97, jit.c 96. The per-file and per-token tables are in
+the inventory file.
+
+**The brief's headline figures, re-measured.** The brief says ~420 `VAL_NUM`,
+366 `make_num` and 274 argument reads. Outside comments and literals this
+tree has:
+- `VAL_NUM` 329, plus `VAL_NUM_RAW` 92;
+- `make_num` 368;
+- argument and element reads 357 (`eigs_num_arg` 145, `eigs_list_num` 130,
+ `eigs_slot_num` 52, the optional and element forms 30).
+
+**Builtins.** The registration table is derived from the
+`env_set_local_owned(env, "name", make_builtin(fn))` lines plus the
+`ext_names.h` X-lists. It holds 388 names after S10-D2's `proc_read_buf`
+retirement (389 after R1.6g, 312 after sync #5). The initial 311-name population was
+calibrated against the runtime's own `eigenscript --api --json`
+(223 core + 88 extension = 311; equal in both directions). A builtin is numeric when a site is contained in
+its function, in a same-file function it calls or names as a value (a
+comparator, an op-table entry; closure depth 4), or in the shared formatter
+or equality it calls. The current population is 309 numeric builtins (R1.6g N9, minus S10-D2's explicit `proc_read_buf` retirement), and §6
+classifies every one. (The closure does not follow a call into another
+file, so a header helper that reads a number must itself be in the
+vocabulary for its callers to count: `buffer_read_byte` entered the
+vocabulary this round, which made `ppu_render_frame` numeric. A one-off scan
+of every header inline and every exported function that reads a number and
+returns something other than a double found no other such reader: the rest
+return a status (`jit_helper_*`, the observer), a slot (`slot_from_value`,
+the container writers) or a truth value (`is_truthy`,
+`slot_truthy_immediate`).) The script fails if a numeric builtin has no
+row, a row names no numeric builtin, a class or loss mark is not one of the
+allowed tokens, or the historical `LOSS` scope differs in either direction.
+`make_int` is a producer site too: an int-only builtin remains in the numeric
+population after its double bridge is removed.
+
+R1.6b aligns the `LOSS` check with §6's historical definition. The fixed set
+is generated from the approved pre-migration table at
+`0a73adb0612f8612d75393891ae2ba2e1c803663` (document blob
+`b2f583afd653e845887045795077823077388442`), and is kept in the inventory
+script so archive-based checks need no live git repository. R1.6b-D3
+(#1758 comment 6320170738) corrects the omitted historical `http_post`
+header loss, adding it to that approved scope. Both adding a
+mark outside that scope and removing one inside it fail. A successful
+migration therefore need not retain a lossy double path merely to satisfy
+the inventory; current builtin membership and classification checks remain.
+
+**Calibration: every check turns red on its plant.** Each plant runs
+against a scratch copy of `src/` and of this document (`--src`, `--doc`,
+`--no-write`), never the worktree; the unplanted copy passes first. The
+recorded round-0 calibration killed all 20 plants below, each by its own
+FAIL line. R1.6b replaces G's then-current double-path predicate with the
+historical-scope predicate described above; its one-time calibration removes
+the `make_int` vocabulary entry and adds/removes historical `LOSS` marks.
+
+| plant | mutation | the red line |
+|---|---|---|
+| A | `1u << VAL_NUM` appended to vm.c | `UNCLASSIFIED: ... VAL_NUM (VAL_NUM/AST_NUM in an unrecognized position)` |
+| B | a new function returning `VAL_NUM_RAW(v)` | `UNCLASSIFIED: ... p2_plant_read (reads a payload and returns/writes a double; not in VOCAB or WRAPPERS)` |
+| C | a registered numeric builtin with no row | ``numeric builtin `p2plant` ... has no classification row`` |
+| D | class `integer` | `` `abs` has unknown class 'integer' `` |
+| E | loss mark `maybe` | `` `type` has unknown loss mark 'maybe' `` |
+| F | a row for a non-numeric builtin | ``classification row `keys` names no numeric registered builtin`` |
+| G | `LOSS` on a builtin with no double path | `` `type` is marked LOSS but its closure has no double-path token `` |
+| H | a row without backticks | `unparsed table line: \| type \| other ...` |
+| I | the JIT files removed | `role rule 0 (d ...) matched no site` |
+| J | an empty source tree | `examined=0 classified=0` (fails) |
+| K | a VOCAB token that never occurs | `VOCAB token never occurs: p2_never_token` |
+| L | the `abs` row deleted | ``numeric builtin `abs` ... has no classification row`` |
+| M | the critic's `e_blind_member`: `static double p2_plant_blind(EigsSlot s) { return s.d_; }` | `UNCLASSIFIED: vm.c:6733 p2_plant_blind d_ (raw union member outside its accessor macro)` |
+| N | the leaf scanner admits `OP_DIV` | `leaf scanner admits OP_DIV but the leaf evaluator has no arm` |
+| O | a new function returning `slot_as_ptr(s)` | ``slot classifier `vm.c:p2_plant_deref` has no disposition row`` |
+| P | a disposition row for a function that does not exist | ``disposition row `vm.c:p2_gone` names no D2/D3 slot classifier`` |
+| Q | disposition `fine` | `` `vm.c:slot_type_name` has unknown disposition 'fine' `` |
+| R | `vm_leaf_accessor_exec` renamed | `dispatcher vm_leaf_accessor_exec (leaf_exec) attributed no opcode arm` |
+| S | `gfx_clear`'s `q7` column set to `size` | `` `gfx_clear` has a bad Q7 column 'size' (want `-` or an ordered subset of geometry,bounded,magnitude) `` |
+| T | `gfx_clear`'s `q7` column set to `-` (its note says continuous) | `` `gfx_clear`: Q7 column '-' disagrees with its note (a continuous parameter must be named in both) `` |
+
+M is the critic's plant verbatim; on the round-0 oracle it stayed green. (On
+its first run the plant driver reported T as SURVIVED although the gate
+exited 1 with the right FAIL line: the driver's expected-message pattern
+did not allow for the quotes around `-`. The pattern was fixed and T
+re-run; the gate was never green on it.)
+
+---
+
+## 2. Representation
+
+### 2.1 What every alternative shares: the slot
+
+`EigsSlot` stays a NaN-boxed 64-bit word (`src/value_slot.h`).
+
+| bits (high 16) | meaning |
+|---|---|
+| `< 0xFFF8` | a double (`slot_is_num` today) |
+| `0xFFF8` | null |
+| `0xFFF9` | bool |
+| `0xFFFA` | **SMI: an int in [-2^47, 2^47), 48-bit two's complement payload** (reserved today, unused) |
+| `0xFFFB` | heap pointer; **an int outside the SMI range is a heap-boxed int64 here** |
+| `0xFFFC`-`0xFFFE` | reserved |
+| `0xFFFF` | sentinel |
+
+- SMI encode: `s.u = 0xFFFA000000000000 | ((uint64_t)i & 0x0000FFFFFFFFFFFF)`.
+- SMI decode, sign-extending bit 47 portably:
+ `(int64_t)((s.u & PAYLOAD) ^ 0x800000000000) - 0x800000000000`.
+ The `(int64_t)(u << 16) >> 16` form relies on implementation-defined right
+ shifts.
+- Adding or subtracting two SMIs cannot overflow int64 (|a ± b| < 2^48), so
+ round 1 computes those in int64 and range-checks the result into SMI or
+ boxed. Multiplication can overflow int64 (|a·b| < 2^94), so it always uses
+ `__builtin_mul_overflow`, as do boxed operands for every operator. An int64
+ overflow raises `value` ((A)).
+
+**Canonical form (all alternatives), corrected in design round 2.** The
+round-0 draft said an int in [-2^47, 2^47) is never boxed in any slot, with
+`slot_from_heap` normalizing a small box. That cannot hold: the VM boxes
+numbers on purpose today, and must keep doing it for ints.
+- `vm_slot_lift` (vm.c:826, the store at 834) and `vm_local_lift` (vm.c:717,
+ store at 723) materialize an immediate into a heap Value and **overwrite
+ the slot with it**, because the caller keeps using that pointer.
+ `env_get_hashed` and `env_get_local_hashed` (eigenscript.c:4986-5026) do
+ the same through `slot_to_value` plus `slot_from_heap`.
+- `vm_push` goes through `slot_bridge_wrap` (vm.c:768), which by contract
+ "NEVER emits an immediate": a builtin's boxed result stays a heap slot.
+- So a boxed number on the stack or in an env slot is a legal state today
+ (the JIT's operand loader already accepts a heap `VAL_NUM` with refcount
+ >= 2, jit.c:2343). A boxed small int is the same state.
+
+The invariant that does hold, and that round 1 keeps:
+- **Container storage is canonical.** A list item or dict value is an
+ immediate number, null, bool, or a heap pointer to a Value that is not a
+ number (container.h:4-16). After round 1: a small int is stored as an
+ SMI, and a `VAL_INT` box only for an int outside [-2^47, 2^47). It is
+ enforced inside the four container writers that already unbox a
+ `VAL_NUM` (`eigs_container_slot`, `eigs_slot_adopt`,
+ `eigs_slot_store_owned`, `eigs_slot_store_borrow`), not beside their
+ callers (the #1746 lesson).
+- **The int writers produce the SMI.** `slot_from_int(int64_t)` (new, the
+ only int constructor) and `slot_from_value` (an owned Value*, which
+ already releases a `VAL_NUM` box into an immediate, eigenscript.c:1862)
+ produce an SMI for a small int and a box outside the range.
+- **Everything that compares compares by value whenever a box is
+ involved.** The only bit-level fast path is "both operands SMI" (equal
+ bits iff equal ints). An SMI against a box, or two boxes, compares the
+ int64 values; an int against a float uses N2's exact rule.
+ `values_equal_impl`, the EQ/NE/compare arms, the sort comparators and
+ every hash of a number follow this.
+
+Consequences:
+- A writer that skips the small-int rule produces a box where an SMI was
+ due. No program can see that: equality, ordering, printing and the tape
+ all go by value. The only witness is the C slot test
+ (`tests/phase2/slot_layer.c`, §8 R1.1), which drives each writer above
+ with every boundary value (±2^47, ±(2^47 - 1), 0, INT64_MIN, INT64_MAX)
+ and asserts the resulting tag. That is plant 1's named check (§9.3); the
+ round-0 claim that a program row (`N1.canon-eq`) would see it was wrong.
+- The JIT `OP_CONST` emitter never meets an int: int literals use new
+ opcodes (§2.7, §4.2).
+
+### 2.2 The four alternatives
+
+**Alt 1: `VAL_INT` beside `VAL_NUM`.**
+- Value: a new `ValType VAL_INT` with an `int64_t` payload, appended after
+ `VAL_BOOL`. `VAL_NUM` keeps meaning a double.
+- Slot: SMI plus boxed `VAL_INT`.
+- `type of` still answers `"num"` for both ((B)): `val_type_name(VAL_INT)`
+ returns `"num"`. `num_type of x` reads the tag.
+
+**Alt 1R (chosen): Alt 1, and the double vocabulary renamed, the ambiguous
+accessors deleted.**
+- `VAL_NUM` becomes `VAL_FLOAT`, `num_` becomes `f64_`, `VAL_NUM_RAW` and
+ `SLOT_NUM_RAW` become `*_F64_RAW`, `slot_is_num` becomes `slot_is_f64`,
+ `make_num` becomes `make_float`, `slot_from_num` becomes `slot_from_f64`.
+- `eigs_num_arg`, `eigs_list_num`, `eigs_opt_num` and the slot forms are
+ deleted. Replacements state intent:
+ - `eigs_float_arg` converts an int to a double. It is legal only for
+ float-producing builtins.
+ - `eigs_int_arg` applies N5's exact rule and raises otherwise.
+ - `eigs_number_arg` returns `EigsNum { kind, i, f }` for int-preserving
+ builtins.
+ - `make_int`.
+- Design round 2 extends the list (measured in §2.3a): the AST and token
+ literal payloads, `slot_as_double` (deleted), the container f64 helpers,
+ `make_num_permanent`, `buf_num_arg`, `finite_num_to_byte` and
+ `buffer_read_byte`.
+- This is the repo's own precedent: #1637 round 4 renamed the union member to
+ `num_` "so no unchecked read compiles" (eigenscript.h:281).
+
+**Alt 2: one `VAL_NUM` with an int/float subtype (Lua 5.4's `TValue`
+model).**
+- Value: `VAL_NUM` plus a `numkind` field. The payload union holds `num_`
+ (double) or `i64_`.
+- Slot: SMI plus boxed `VAL_NUM`/INT.
+- (B) falls out of the type layer for free: `type of`, `who is` and every
+ `== VAL_NUM` test already say "number".
+
+**Alt 3: double-plus-int shadow.**
+- As Alt 2, but an int Value also keeps `num_ = (double)i64_`, so every
+ existing double read keeps working.
+- Slots still need the SMI tag, because a slot has no room for a shadow, so
+ the slot work equals Alt 1's.
+
+### 2.3 How many sites change, and what a missed site does
+
+The counts come from the inventory (§1); the token-by-token split was
+computed from the site list.
+
+| | sites that must be revisited | how the revisit is forced | what a MISSED site does |
+|---|---|---|---|
+| **Alt 1** | the 382 TEST and 63 CASE sites (all categories) that should admit ints; reads after them; every producer that should yield an int; JIT producers | CASE only: 62 of the 63 `case VAL_NUM:`/`case AST_NUM:` switches have no `default:`, so `-Werror=switch` names them. TEST sites are not forced. | The int fails `== VAL_NUM` and takes the non-number branch. That raises a type error where the branch raises (loud, acceptable), but returns a **soft default** where the branch is soft. Examples: `what is 5` answers 0 and `who is 5` answers "unknown" (vm.c:5465-5484); `json_encode of 5` writes `null` (builtins.c:998, the one switch with a `default:`). Silently wrong, but never a lossy double. |
+| **Alt 1R** | the same sites | **Measured (§2.3a), under the Makefile's exact flags: 1,488 of the 1,798 sites stop compiling (1,450 non-JIT, 38 in the JIT files)**, each attributed to the commit that first forces it (R1.1 223, R1.2 464, R1.3 11, R1.6 743, R2.2 47), plus 53 inside a deleted definition. The other 257 keep a token on purpose, each class with its reason in §2.3a: the AST kind (52), the decided embed accessor (48), the libc parsers (15), the exact-conversion and formatter helpers (9), definitions (44), 86 JIT emitter immediates and helpers reviewed per guard (§4), and 3 `offsetof` uses that a real member rename does reach (measured). | **A compile error.** It cannot exist in code that names the vocabulary. The meaning changes that carry no token are listed with the gate that catches a miss (§2.7); the last `default:` that could absorb `VAL_INT` (builtins.c:998) is removed first in R1.2a. |
+| **Alt 2** | payload reads and producers (enforced only if `num_` is renamed); the type tests keep compiling and now admit ints | none for the 382+63 type tests and the 484 checked reads | **A silent wrong number.** After `== VAL_NUM`, a `VAL_NUM_RAW` read of an int reads its int64 bits as a double (5 reads as 2.5e-323). An accessor that converts loses precision above 2^53 silently. In the JIT, three emitters misread with no compiler to catch them: `emit_load_numeric_operand`'s heap arm (jit.c:2343) loads `data.num_` of a boxed int; the `OP_SET_LOCAL` in-place branch (jit.c:3410) writes double bits into an int Value; the `OP_CONST` emitter (jit.c:2816) pushes `VAL_NUM_RAW` bits of an int constant. Constant dedup `const_equal` (chunk.c:212) merges `3` and `3.0`. |
+| **Alt 3** | only the sites that need exactness (N2, N3, N6, N8, N14, tape) | none: every existing read compiles and works below 2^53 | **A silently lossy double**, exact below 2^53, so ordinary tests cannot see it. This is precisely the class the brief says to design out. |
+
+### 2.3a The compile measurement (design rounds 2 and 3)
+
+Round 0 claimed 1,449 non-JIT sites stop compiling; that was a token count,
+and the critic's compile measured 1,331. Round 2 extended the rename list
+and measured 1,416 of 1,798, under three hand-written configurations; the
+one it called `server-db` was really every TU with every extension, ZLIB
+included (the critic reproduced the count only with ZLIB). Round 3 does two
+things: it adds the two token events the plan still lacked (`buffer_read_num`,
+renamed in R1.6 so every buffer element read is re-decided, and the clamp,
+deleted in R2.2), and it measures under the Makefile's exact flags.
+
+**Method.**
+- A scratch copy of `src/`, never the worktree. Each token the plan renames
+ or deletes gets a declaration carrying clang's
+ `__attribute__((unavailable("P2 ...")))`, so every USE is a compile error
+ and the definition stays legal. The macros `SLOT_NUM_RAW` and
+ `EIGS_NUM_MAX` are routed through an unavailable `extern`; `VAL_NUM_RAW`
+ expands to the unavailable member `num_`.
+- The tokens, by the commit that removes them (§2.7):
+
+ | commit | tokens |
+ |---|---|
+ | R1.1 | `slot_is_num`, `slot_is_numeric`, `SLOT_NUM_RAW`, `slot_from_num`, `slot_as_double` |
+ | R1.2 | `VAL_NUM`, the member `num_` (so `VAL_NUM_RAW`), `VAL_NUM_OFFSET` |
+ | R1.3 | the AST literal payload `num`, the token payload `num_val` |
+ | R1.6 | the bridge (§2.7): `eigs_num_arg`, `eigs_num_arg_slow`, `eigs_list_num`, `eigs_elem_num`, `eigs_opt_num`, `eigs_slot_num`, `eigs_slot_elem_num`, `eigs_slot_opt_num`, `buf_num_arg`, `make_num`, `make_num_permanent`, the container helpers `list_set_num`, `list_append_num`, `list_rewrite_num`, `dict_value_set_num`, `dict_value_rewrite_num`, `list_get_num`, `dict_value_get_num`; `finite_num_to_byte`, `buffer_read_byte`, `buffer_read_num` |
+ | R2.2 | `num_guard`, `num_guard_named`, `EIGS_NUM_MAX` |
+- clang 18.1.3, `-fsyntax-only -ferror-limit=0`, one translation unit at a
+ time, five configurations. Three are the Makefile's own build variants,
+ with flags and TU lists read at run time (`make print-FLAGS_`,
+ `make print-SRC_V_`); `freestanding` takes `tools/freestanding_check.sh`'s
+ flags; `all` adds the TUs no build variant compiles (`eigenlsp.c`,
+ `eigsdap.c`, the embed and JIT smoke programs), with every extension on:
+
+ ```
+ release (29 TUs) -Wall -Wextra -Werror=implicit-function-declaration -Werror=switch -Werror=comment
+ -Werror=misleading-indentation -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE
+ -DEIGENSCRIPT_EXT_HTTP=0 -DEIGENSCRIPT_EXT_MODEL=0 -DEIGENSCRIPT_EXT_DB=0
+ -DEIGENSCRIPT_EXT_GFX=1 -DEIGENSCRIPT_VERSION="0.46.0"
+ server-db (35 TUs) the same CFLAGS, -I/usr/include/postgresql -DEIGENSCRIPT_EXT_HTTP=1
+ -DEIGENSCRIPT_EXT_MODEL=1 -DEIGENSCRIPT_EXT_DB=1 -DEIGENSCRIPT_EXT_NET=1
+ -DEIGENSCRIPT_EXT_GFX=1 (no ZLIB: the Makefile's FLAGS_server-db omits it)
+ zlib (29 TUs) release's flags plus -DEIGENSCRIPT_EXT_ZLIB=1
+ freestanding (23 TUs) -O2 -ffreestanding -fno-stack-protector -U_FORTIFY_SOURCE
+ -Werror=implicit-function-declaration -Werror=switch -Werror=comment
+ -Werror=misleading-indentation -DEIGENSCRIPT_FREESTANDING=1
+ -DEIGENSCRIPT_EXT_HTTP=0 -DEIGENSCRIPT_EXT_MODEL=0 -DEIGENSCRIPT_EXT_DB=0
+ all (41 TUs) CFLAGS plus every extension, -DEIGENSCRIPT_EXT_ZLIB=1 included
+ ```
+- The generated `lsp_stdlib_index.h` is produced into the copy
+ (`tools/gen_lsp_stdlib_index.sh /lsp_stdlib_index.h`).
+- An error line and each macro-expansion note under it are mapped back to
+ the unmutated copy's line (difflib). A site is **flagged** when its
+ file:line carries an error. A flagged site is attributed to the FIRST
+ commit whose token errors on its line: that is when a migrator must first
+ visit it.
+
+**Results** (the unmutated copy first, as the control):
+
+```
+base: cfg=release tus=29 errors=0 cfg=server-db tus=35 errors=0 cfg=zlib tus=29 errors=0
+ cfg=freestanding tus=23 errors=0 cfg=all tus=41 errors=0 (unparsed_failures=0 everywhere)
+R3: cfg=release tus=29 errors=2501 cfg=server-db tus=35 errors=2780 cfg=zlib tus=29 errors=2506
+ cfg=freestanding tus=23 errors=1920 cfg=all tus=41 errors=3011 (unparsed_failures=0 everywhere)
+inventory sites=1798 (non-JIT 1671, JIT-file 127)
+cfg=release distinct_flagged_lines=1209 sites_flagged=1384 (non-JIT 1377, JIT-file 7)
+cfg=server-db distinct_flagged_lines=1265 sites_flagged=1442 (non-JIT 1435, JIT-file 7)
+cfg=zlib distinct_flagged_lines=1213 sites_flagged=1389 (non-JIT 1382, JIT-file 7)
+cfg=freestanding distinct_flagged_lines=866 sites_flagged=1024 (non-JIT 1024, JIT-file 0)
+cfg=all distinct_flagged_lines=1307 sites_flagged=1488 (non-JIT 1450, JIT-file 38)
+UNION of the four build configs: sites_flagged=1447; all-TU config adds 41
+UNION: flagged=1488 (non-JIT 1450, JIT-file 38) inside-deleted-definitions=53 not-flagged=257 (non-JIT 168, JIT-file 89)
+first forced in: R1.1=223 R1.2=464 R1.3=11 R1.6=743 R2.2=47
+errors not naming a planned token: {"cannot take the address of an rvalue of type 'double'": 5}
+```
+
+The error totals count a header's error once per TU that includes it; the
+distinct lines and the sites are what matter. The five errors that name no
+token are an artifact of the `SLOT_NUM_RAW` reroute (it takes an address,
+and five uses pass an rvalue); every one sits on a line that also carries
+an `unavailable` error, so no site depends on them. Every flagged site has a
+first commit.
+
+**The 257 sites that never stop compiling, every one classified:**
+
+| class | sites | why it keeps compiling, and what forces it instead |
+|---|---:|---|
+| JIT emitter immediates and helpers | 86 | Tag compares (`0xFFF8`/`0xFFF9`/`0xFFFB`/`0xFFFD`) and the `emit_*` helpers. Reviewed per guard in §4.1: an SMI or a `VAL_INT` box fails every consuming guard and bails; the producer emitters keep their float meaning (§4.2). |
+| `AST_NUM` | 52 | Under (B) the node kind still means "number literal". Its payload `num` is renamed in R1.3, so all 7 reads of the literal's value are flagged; a site that only tests the kind is right for both kinds. The critic made the same call. |
+| `eigs_value_as_num` | 48 | The embed accessor: 40 call sites in `embed_smoke.c` and 6 in `embed_concurrent.c` (the embed test harnesses), plus its definition (`eigs_embed.c`) and declaration (`eigs_embed.h`). Q3 decided it converts an int as `lua_tonumber` does, so "this value as a double" is unchanged; exact ints get `eigs_value_as_int`/`eigs_value_is_int` (R1.8). |
+| definitions | 44 | Declarations and bodies of the renamed and deleted names themselves. |
+| `strtod` 14, `atof` 1 | 15 | The libc parsers. The lexer's result reaches the renamed `num_val` through `tok_add`, whose store is flagged; the literal's kind changes `tok_add`'s parameter, which forces the call. The JSON, store and trace scalar parsers return `make_num(d)` a few lines on (flagged, R1.6); trace.c:2324 fills a buffer's f64 storage (Q2). The rest parse model hyperparameters (float), the tape's observer config, and `tape_read.c:368`, which feeds the observer replay a double (`tape_read.c:635`); round 1 observes ints as doubles live and on replay alike (§8 risks), and round 3's change to `observer_slot_record_value`'s parameter forces this line. |
+| `num_to_i64_exact` 6, `eigs_num_text` 3 | 9 | Pure helpers over a double, unchanged in meaning; every input reaches them through a renamed reader. N6 changes the formatter's output on purpose in R1.7, a token-less change listed in §2.7 with its rows. |
+| `VAL_NUM_OFFSET` in jit.c | 3 | `offsetof` uses, which the attribute does not reach. A real rename does: renaming `num_` to `f64_` (and the macro) in a further scratch copy gives exactly 3 errors, jit.c:2367, 2368 and 3471. |
+| **total** | **257** | |
+
+**What round 2 got wrong here, and round 3 fixes.** Round 2 kept
+`buffer_read_num` (26 unflagged sites) as "a float by contract". Ten of its
+38 sites read a value that is NOT a float: byte values (`str_from_bytes`
+builtins_buf.c:240, `f64_from_bytes` :284, the zlib input :351,
+`buf_from_pcm16le` :754 and :756, `write_bytes` builtins_host.c:1548,
+`net_send` ext_net.c:361, and inside `buffer_read_byte` eigenscript.h:1920)
+and indices (`scatter_add` builtins_tensor.c:1036, `flat_index_at` :1436, the
+`gather` path the critic probed: an index of 1.5 truncates to 1 today). With
+`buffer_read_num` renamed in R1.6, all 38 sites are flagged in that commit,
+and each is re-decided: `buffer_read_f64` for data, the Q1 byte reader for a
+byte, the N5 index reader for an index (§2.6). Without the rename, nine of
+the ten lines stayed unflagged in round 2; most sit in a function another
+token forces anyway (the `finite_num_to_byte` call a line later), but
+`buf_from_pcm16le` names no other token before R2.2's clamp deletion, so
+nothing would have made R1.6 visit it.
+
+So the headline is: **1,488 of 1,798 sites stop compiling across the
+plan** (first forced: R1.1 223, R1.2 464, R1.3 11, R1.6 743, R2.2 47), 53
+more sit inside deleted definitions, and 257 keep a token for the reasons
+above. None of the 257 changes meaning without a token event: the ones that
+change meaning later do so in a commit listed in §2.7.
+
+### 2.4 The choice: Alt 1R
+
+1. **It is the only alternative whose missed site is loud by construction.**
+ Measured: 1,488 of 1,798 sites stop compiling until a person decides,
+ site by site, whether the site admits ints and with what semantics
+ (§2.3a). Alt 2 and Alt 3 leave 445 type tests and cases plus 484 checked
+ reads compiling with a widened or lossy meaning.
+2. **The JIT argues for it independently.** All three of Alt 2's emitted
+ misreads exist because the emitter checks `type == VAL_NUM` and then loads
+ at a fixed offset (`VAL_NUM_OFFSET`). Under Alt 1R a boxed int has another
+ type, so the same unchanged code bails (§4).
+3. **(B) does not require Alt 2.** (B) is a language-surface decision.
+ `val_type_name(VAL_INT)` returns `"num"`, and the `who` interrogative
+ answers "number". Lua itself keeps two variant tags under one basic type
+ (`LUA_VNUMINT` / `LUA_VNUMFLT` of `LUA_TNUMBER`) and tests the variant
+ wherever arithmetic cares, which is Alt 1's shape at the C level.
+4. **No measurement favours another alternative** (§3). The slot cost is
+ identical for all four; the Value-level type test is one compare (Alt 1)
+ against two (Alt 2).
+
+The cost of Alt 1R is the size of the round-1 diff. §8 stages it with
+deprecation warnings, so the compiler's warning count is the progress meter
+and the final commit deletes the old names. A mechanical `sed` from old name
+to new would reintroduce the silent class (`len` returning
+`make_float(count)`), so the migration is per site, with the builtin table
+(§6) deciding each builtin's accessor and the per-builtin behaviour table
+(§9.4) checking it.
+
+### 2.5 The slot classifiers, derived
+
+Nothing produces `0xFFFA` yet. The first SMI producer meets every function
+that classifies a slot, and today those guess: "not a number, null or bool"
+is taken to mean heap. What an SMI would do, once one exists:
+- **pointer misread**: `slot_type` (container.h:92), `eigs_slot_ref`
+ (container.h:98), `slot_bridge_unwrap` (vm.c:784), `slot_truthy`
+ (vm.c:862) dereference the payload as a `Value *`;
+- **silent null**: `vm_slot_lift` (vm.c:826) and `slot_to_value`
+ (eigenscript.c:1885) turn the int into null;
+- **silently unobserved**: `obs_slot_observable` (vm.c:1630) answers
+ "pointer or bool", so the binding answers from no data;
+- **unreplayable**: `write_slot` (trace.c:2836) writes ``;
+- **slow path into a misread**: `slot_as_double` (vm.c:872) returns 0;
+- **right by accident**: `slot_type_name` (vm.c:3000) answers `"num"`.
+
+Round 0 listed these by hand and gave R1.1 a rule ("every function holding
+both `slot_is_num` and `slot_as_ptr`") that the critic showed misses three of
+them: `eigs_slot_ref`, `obs_slot_observable` and `slot_type_name`. Round 2
+derives the population instead (§1, check 4, `## Slot classifiers` in the
+inventory file):
+- **D1 (58)** names `slot_is_num`, `slot_is_numeric` or `slot_as_double`.
+ All three are renamed or deleted in R1.1, so the compiler stops at every
+ D1 entry. `slot_type`, `slot_bridge_unwrap`, `slot_truthy`,
+ `vm_slot_lift`, `slot_to_value`, `write_slot` and every arithmetic and
+ compare arm are D1.
+- **D2 (20) and D3 (23)** name no number test, so nothing forces them. Each
+ has the reviewed row below. The inventory fails on a D2/D3 entry with no
+ row and on a row with no entry, and accepts only these dispositions:
+ - `guarded`: an SMI can reach it and is handled correctly as written (it
+ skips non-pointers, or tests one exact tag);
+ - `smi-arm`: R1.1 adds an explicit SMI arm;
+ - `refuse`: R1.1 makes it abort on an SMI, and on any tag it does not
+ handle, so a caller missed later fails loudly instead of misreading;
+ - `not-a-classifier`: it writes bits (a constructor, a bool result) and
+ classifies nothing.
+- **T (67)** only reads a slot's `ValType` (§1).
+
+The three the critic named are now derived entries: `eigs_slot_ref` (D2,
+refuse), `obs_slot_observable` (D3, refuse) and `slot_type_name` (D2,
+smi-arm).
+
+**R1.1's C slot test** (`tests/phase2/slot_layer.c`) reaches the static
+classifiers by including the single-file amalgamation (`make amalgamation`,
+#397, which already leaves out the CLI's `main`). Its population is the
+inventory file's D1/D2/D3 function entries (the file lists them; the test's
+generator reads each signature from the source and keeps those whose first
+parameter is an `EigsSlot`); it asserts `examined` equals that count and is
+above zero. Each
+entry is called under ASan with hand-built SMI slots at the boundary values
+(0, ±1, ±(2^47 - 1), -2^47): a `refuse` entry must abort (in a forked
+child), and every other entry must return without a sanitizer report, with
+the int's Value, type name or truthiness where it returns one. This runs in
+R1.1, before any program can produce an SMI.
+
+**As implemented in R1.1 (reported as design deviations in the R1.1
+result).**
+- **The table covers D1 too.** The rename made the compiler stop at every D1
+ entry, but a rename can be typed without a decision, so every D1, D2 and
+ D3 entry now has a reviewed row below (98 after R1.1), and the inventory
+ fails on a slot classifier of any class without one.
+- **A fifth disposition, `unwired`.** Some D1 arms need what R1.1 does not
+ have: an int Value (`make_int`, R1.3), the int ValType (R1.2a), or tape v7
+ (R1.3). Each calls `eigs_int_unwired(who, bits)`, which aborts naming the
+ reader. R1.2 completed `slot_type` and `slot_view` when `VAL_INT`
+ became available; it also completed `slot_int_value`'s boxed reader.
+ R1.3 deletes `eigs_int_unwired`, so the remaining arms in this group stop
+ compiling in the commit that makes ints reachable (2.7): `slot_to_value` (the one
+ place that boxes an SMI: the lifts, `slot_bridge_unwrap`, `eigs_slot_box`
+ and the `*_get_return` readers route there), `write_slot`, and `slot_from_int`'s out-of-range arm (the `VAL_INT` box).
+- **The permanent refusals** (`refuse`) go through `eigs_slot_refuse(who,
+ slot)`, which prints the bits and aborts; `slot_as_ptr` refuses a non-heap
+ slot under ASan and `-DEIGS_SLOT_CHECKED`. Readers that used to turn an
+ unknown tag into null or `` (`slot_to_value`, `vm_slot_lift`,
+ `write_slot`) refuse it now; no slot carries such a tag today.
+- **The C slot test's population** is
+ `tools/phase2_inventory.py --slot-population` (33 entries after R1.1);
+ `tests/phase2/slot_layer.sh` compares it with the entries the test
+ examined, both ways. The test also runs a tag matrix (a double, the
+ canonical NaN, -0.0, null, both bools, an SMI, a heap pointer, the
+ reserved tags and the sentinel against the predicates and the deciding
+ readers) and plant 1's named check (`slot_from_int` over the boundary
+ values). Its ABORT rows run in forked children and must name the reader.
+
+**As corrected in the R1.1 fix round (the R1.1 critic's FAIL).**
+- **The four type-error arms are `unwired`, not `guarded`.** `NUM_CMP`
+ (`< > <= >=`), `INT_BINOP_R` (`& | ^ << >>`), `CASE(NEG)` and
+ `CASE(BNOT)` met an SMI with the type error naming `num`; inside a `try`
+ that error became a wrong value (`r is xs[0] < 6` with an SMI 5 gave
+ `false`, the float control `true`). "Handled correctly as written" was
+ false. Each now refuses through `eigs_int_unwired`, so R1.3, which deletes
+ the token, cannot compile until each has its int arm. All four are tied to
+ **R1.3**, bitwise included: R1.3's int literals reach `&` at once
+ (`tests/bench_dmg_shape.eigs:21`, `c.a is c.a & 255`), and §8 already
+ gives bitwise operators exact int64 operands at R1.3, with R1.5 changing
+ only the shift rule.
+- **The tag matrix covers every entry.** Each of the 33 population entries
+ meets all twelve kinds (a double, the canonical NaN, -0.0, null, false,
+ true, an SMI, a heap pointer, the three reserved tags and the sentinel),
+ each kind with its expected outcome: a value, a refusal naming the reader,
+ or a raise. The runner requires the MATRIX set to equal the population.
+- **Part D drives the operator arms through the VM.** A host builtin
+ (`phase2_poke`) writes an SMI into a list element, and a program then
+ applies each refusing operator to it, plain and inside a `try`: each must
+ abort naming its arm, where the float control computes the value.
+- **The slot test builds at -O0** (about 41 s instead of 162 s, and well
+ under -O2's 602 MB peak), after the warnings #1775 listed were fixed:
+ `builtin_tensor_gather`'s missing return, and the maybe-uninitialized
+ locals behind `num_to_i64_exact` and `vm_index_resolve`. The first fix
+ round initialised those locals at all nine declarations, which cost
+ +2.07% Ir on DMG and +4.17% on a JIT-off float loop (the second R1.1
+ critic). Moving the write into the readers instead (`slot_f64_value`
+ writing 0.0, or the slot's bits, when it answers 0; `vm_index_is_int`
+ writing on both paths) cost the same: measured with callgrind, any of
+ them changed `vm_run_ex`'s code, +3.8% (+0.44% for `vm_index_is_int`
+ alone) on the float loop, which executes neither failure path. What
+ stays is the initialisation at exactly the three declarations GCC
+ flags at -O1 (`INT_BINOP_R`'s `_ad`/`_bd`, the `i` of `CASE(INDEX_SET)`
+ and of `jit_helper_index_set`): against R1.1, -0.0001% on the JIT-off
+ float loop, +0.0001% with the JIT, -0.08% on the DMG canary.
+- **A build warning fails the slot test under gcc only.** gcc is the
+ reference toolchain for #1775's class (-Wreturn-type,
+ -Wmaybe-uninitialized at -O0/-O1). R1.1's macOS lane (clang) printed 103
+ warnings the gcc build does not have (-Wdefault-const-init-field-unsafe
+ on `EigsView`), which the main clang build prints too. Under any compiler
+ other than gcc (asked through its predefined macros: `__GNUC__` without
+ `__clang__`) the slot test follows the main build's policy on that
+ platform: warnings print, and only the -Werror set fails.
+- **Its runner reads each tool on its own channel.** The scan and the test
+ each have a grammar; a line outside it, or any stderr, is a FAIL that
+ prints the line (the R1.1 critic: Python 3.13's DeprecationWarning on the
+ scan's stderr was read as two classifiers). The Python runs with `-W
+ error`.
+- **"The int's Value"** cannot be checked in R1.1: no int Value kind exists,
+ so allocating readers were ABORT rows. R1.2 turned `slot_type` and `slot_view` into RETURN rows as scheduled
+ with the Value type (the R1.2i follow-up completed them). Their boxed
+ companion `slot_int_value` was completed in the same follow-up.
+ `slot_to_value` and the readers that allocate an int Value remain ABORT
+ rows until R1.3 introduces `make_int`.
+
+
+| name | disposition | what an int (an SMI) does there, as of R1.1 |
+|---|---|---|
+| `builtins.c:bool_gate_name` | guarded | skips every non-pointer slot (`!slot_is_ptr`); an SMI is never a builtin (D2) |
+| `builtins.c:sort_cmp_str` | guarded | reached only after `builtin_sort` proved every element a string (its homogeneity test names `VAL_NUM`, compile-forced) (D2) |
+| `builtins_host.c:ls_entry_cmp` | guarded | sorts the string entries `ls` built itself (D2) |
+| `container.h:dict_value_get_f64` | guarded | final float-storage reader: an SMI returns 0 without writing the output; no allocation or ownership transfer (D1) |
+| `container.h:dict_value_get_int` | smi-arm | final int-storage reader: exact SMI or boxed int payload; no allocation or ownership transfer (D1) |
+| `container.h:dict_value_get_ref` | smi-arm | NULL for an SMI or a double (container.h:137); since R1.3 a boxed int (\|i\| >= 2^47) is a heap slot and comes back as its non-NULL `VAL_INT` Value, so the caller types the element before reading it (D1) |
+| `container.h:dict_value_get_return` | smi-arm | an int element returns a fresh int Value through `slot_to_value`, the one SMI boxer (unwired until R1.3) (D1) |
+| `container.h:eigs_container_slot` | not-a-classifier | the canonicalizing Value-to-slot writer (R1.2): it reads a Value, not a slot, and writes the bits; a `VAL_INT` box becomes an SMI inside [-2^47, 2^47) and stays a heap slot outside it (2.1) (D3) |
+| `container.h:eigs_slot_adopt` | guarded | decides ownership from what `eigs_container_slot` returned (compile-forced); a small int comes back as an SMI and its box is released (D3) |
+| `container.h:eigs_slot_box` | smi-arm | an SMI is boxed as an owned int through `slot_to_value`; floats retain `make_num` (R1.3) (D1) |
+| `container.h:eigs_slot_ref` | refuse | an immediate double or an SMI was dereferenced as a pointer; every caller tests for a number first. R1.1 aborts on any slot that is not a pointer, null or bool, so a caller missed later fails loudly (D2) |
+| `container.h:eigs_slot_store_borrow` | guarded | an SMI is canonical storage and comes back as it is, with no reference to take (D1) |
+| `container.h:eigs_slot_store_owned` | guarded | an SMI is canonical storage and comes back as it is, with no reference to release (D1) |
+| `container.h:list_get_f64` | guarded | final float-storage reader: an SMI returns 0 without writing the output; no allocation or ownership transfer (D1) |
+| `container.h:list_get_int` | smi-arm | final int-storage reader: exact SMI or boxed int payload; no allocation or ownership transfer (D1) |
+| `container.h:list_get_ref` | smi-arm | NULL for an SMI or a double (container.h:133); since R1.3 a boxed int (\|i\| >= 2^47) is a heap slot and comes back as its non-NULL `VAL_INT` Value (D1) |
+| `container.h:list_get_return` | smi-arm | an int element returns a fresh int Value through `slot_to_value` (unwired until R1.3) (D1) |
+| `container.h:slot_type` | smi-arm | an SMI's type is `VAL_INT` (R1.2); any other unknown tag refuses instead of being read as a pointer (D1) |
+| `container.h:slot_view` | smi-arm | an SMI's facade is an exact `VAL_INT` view (R1.2) (D1) |
+| `eigenscript.c:#define GC_EDGE_TABLE` | guarded | a GC edge exists only for `slot_is_ptr`; an SMI has no edge (D2) |
+| `eigenscript.c:ent_child_slot` | smi-arm | an int's entropy is its double's: the numeric channel until round 3 (design 7) (D1) |
+| `eigenscript.c:env_get_hashed` | guarded | a non-pointer slot goes through `slot_to_value` (compile-forced) and comes back boxed: the lift boxes on purpose (§2.1) (D2) |
+| `eigenscript.c:env_get_local_hashed` | guarded | as `env_get_hashed` (D2) |
+| `eigenscript.c:gc_collect_at_exit` | guarded | skips every non-pointer slot (D2) |
+| `eigenscript.c:module_ns_project` | guarded | an int skips the float-over-float rewrite: `slot_to_value` boxes it and the dict store re-canonicalizes it (D1) |
+| `eigenscript.c:observer_entropy_now` | smi-arm | an int is the numeric kind, as its double (D1) |
+| `eigenscript.c:slot_from_value` | not-a-classifier | the adopting Value-to-slot writer (R1.2): it reads a Value, not a slot, and writes the bits; a `VAL_INT` box becomes an SMI inside [-2^47, 2^47) (its box released) and stays a heap slot outside it (2.1) (D3) |
+| `eigenscript.c:slot_to_value` | smi-arm | the one SMI boxer: a fresh owned `make_int` preserves the exact value (R1.3); an unknown tag refuses (D1) |
+| `ext_store.c:store_json_encode_slot` | smi-arm | an int is written as its exact digits (design 7) (D1) |
+| `jit.c:jit_compile_to_thunk/OP_EQ` | not-a-classifier | writes the bool result bits (D3) |
+| `jit.c:jit_compile_to_thunk/OP_NEG` | not-a-classifier | the NOT arm writes bool result bits (D3) |
+| `jit.c:jit_compile_to_thunk/OP_NULL` | not-a-classifier | the constant arm writes null and bool bits (D3) |
+| `embed_smoke.c:main` | guarded | C smoke fixture checks a known result-list slot with slot_is_bool before reading its bool payload (R1.6z-D1) |
+| `jit_smoke.c:run_bool_cases` | not-a-classifier | test harness: expected bool bits (D3) |
+| `jit_smoke.c:run_index_bail_cases` | guarded | test harness: an exact null test of a result (D3) |
+| `trace.c:trace_source_replay` | guarded | source-size metadata requires `VAL_INT` before the exact int reader; a rejected float is read only for main's unchanged diagnostic (sync #5, #1885) (D1) |
+| `trace.c:write_slot` | smi-arm | v9 retains R1.3's exact int digits and float point/exponent markers; an unknown tag refuses (D1) |
+| `value_slot.h:#define slot_as_heap` | refuse | alias of `slot_as_ptr` (D2) |
+| `value_slot.h:slot_as_ptr` | refuse | the raw dereference: R1.1 refuses a non-heap slot in ASan builds and under `-DEIGS_SLOT_CHECKED` (the C slot test) (D2) |
+| `value_slot.h:slot_decref` | guarded | acts only on tag `0xFFFB`; an SMI has no refcount (D3) |
+| `value_slot.h:slot_f64_value` | guarded | the float reader (was vm.c's `slot_as_double`): answers 0 for an int, so each caller decided where an int goes (D1) |
+| `value_slot.h:slot_false` | not-a-classifier | constructor (D3) |
+| `value_slot.h:slot_from_bool` | not-a-classifier | constructor (D3) |
+| `value_slot.h:slot_from_heap` | guarded | maps null and bool Values to their immediates and every other Value to a heap slot, so a `VAL_INT` box stays a box: the lifts depend on that (§2.1) (D3) |
+| `value_slot.h:slot_from_int` | not-a-classifier | the one int writer: an SMI inside [-2^47, 2^47), otherwise an owned `VAL_INT` box with all int64 bits (R1.3) (D3) |
+| `value_slot.h:slot_incref` | guarded | as `slot_decref` (D3) |
+| `value_slot.h:slot_int_value` | smi-arm | an SMI's int, or a heap `VAL_INT`'s exact int64 (R1.2) (D1) |
+| `value_slot.h:slot_is_bool` | guarded | exact tag test (D3) |
+| `value_slot.h:slot_is_f64` | guarded | the double test (was `slot_is_num`): an SMI answers 0 (D1) |
+| `value_slot.h:slot_is_heap` | guarded | exact tag test (D3) |
+| `value_slot.h:slot_is_null` | guarded | exact bit test (D3) |
+| `value_slot.h:slot_is_ptr` | guarded | exact tag test (D3) |
+| `value_slot.h:slot_is_smi` | smi-arm | the SMI tag test (D1) |
+| `value_slot.h:slot_null` | not-a-classifier | constructor (D3) |
+| `value_slot.h:slot_smi_val` | smi-arm | an SMI's int, sign-extending bit 47 without a right shift (2.1) (D1) |
+| `value_slot.h:slot_true` | not-a-classifier | constructor (D3) |
+| `value_slot.h:slot_truthy_immediate` | smi-arm | an int is truthy when it is not 0, like a double (D1) |
+| `vm.c:#define ARITH_FAST` | guarded | an int is not a float (`slot_f64_value`): the arm's slow path pops it through `slot_bridge_unwrap`; R1.3 gives that path int arithmetic (D1) |
+| `vm.c:#define INT_ARITH` | smi-arm | two exact integer slots use checked int64 arithmetic; the result is an SMI when it fits and an owned int box otherwise, and overflow raises (R1.3) (D1) |
+| `vm.c:#define INT_BINOP_R` | smi-arm | both operands use `vm_bit_operand`; R1.5 returns the int kind and uses `eigs_bit_shift` for Lua counts and logical right shift (D1) |
+| `vm.c:#define NUM_CMP` | smi-arm | the float fast path remains; integer and cross-kind operands use exact N2 comparison (R1.3) (D1) |
+| `vm.c:#define READ_SLICE_BOUND` | smi-arm | an int bound is its value when a C `int` holds it, else a value error (D1) |
+| `vm.c:jit_helper_dot_get` | guarded | a non-null field is pushed as its bits; an SMI is a valid stack slot (D3) |
+| `vm.c:jit_helper_dot_set` | guarded | an int value skips the immediate store: the slow path pops it through `slot_bridge_unwrap` (D1) |
+| `vm.c:jit_helper_index_get` | guarded | an int index skips the fast path: the slow path lifts it through `slot_to_value` (D1) |
+| `vm.c:jit_helper_index_set` | guarded | an int index or value skips the fast path: the slow path pops it through `slot_bridge_unwrap` (D1) |
+| `vm.c:jit_helper_iter_next` | guarded | state must be a pointer; index and snapshot metadata use the exact checked iterator readers, matching CASE(ITER_NEXT) (R1.3) (D1) |
+| `vm.c:jit_helper_local_dot_set` | guarded | an int value skips the immediate store: `vm_slot_lift` boxes it (D1) |
+| `vm.c:jit_helper_local_idx_dot_get` | guarded | as `jit_helper_dot_get` (D3) |
+| `vm.c:loop_counter_load` | smi-arm | private stack counters use slot_int_value: an SMI or a boxed int is exact; any non-int is an internal error (sync 10 #1906) (D1) |
+| `vm.c:loop_counter_store` | smi-arm | exact int64 counter updates use slot_from_int; SMI writes need no ownership work, otherwise the prior slot is released and the new SMI or int box is owned (sync 10 #1906) (D1) |
+| `vm.c:loop_iter_store` | smi-arm | public loop counts are exact ints; a same-kind in-range SMI update avoids boxing, while other counts publish make_int through the environment setter, with shared writes locked (S9-K1/sync 10 #1906) (D1) |
+| `vm.c:obs_dump_scope` | smi-arm | an int's entropy, as its double (D1) |
+| `vm.c:obs_dump_value` | smi-arm | an int prints as its exact digits (D1) |
+| `vm.c:obs_slot_num` | smi-arm | the observer's numeric channel: a float, or an int as its exact double until round 3 (design 7) (D1) |
+| `vm.c:obs_slot_observable` | refuse | answered "pointer or bool", so an SMI was silently unobserved. R1.1 aborts on an SMI or a reserved tag; every caller routes a number to `obs_slot_num` first. A BOXED int is a pointer and goes to `observer_slot_update(Value *)`, whose number arm (eigenscript.c:785) R1.2's `VAL_NUM` rename forces (§7, O map) (D1 since R1.1: it names `slot_is_f64`) |
+| `vm.c:obs_slot_value` | guarded | called only after `obs_slot_observable` answered true (pointer or bool) (D2) |
+| `vm.c:slot_bridge_unwrap` | smi-arm | an SMI materializes as an owned exact int through `slot_to_value` (R1.3) (D1) |
+| `vm.c:vm_bit_operand` | smi-arm | an int64 operand is read exactly; float operands keep the #1740 conversion rule (R1.3) (D1) |
+| `vm.c:vm_number_compare` | smi-arm | int and cross-kind comparisons use exact N2 order, without double rounding (R1.3) (D1) |
+| `vm.c:vm_iter_index` | smi-arm | index metadata accepts an exact int in the C-int range, otherwise exhausts without narrowing (R1.3) (D1) |
+| `vm.c:vm_run_ex/CASE(ITER_NEXT)` | guarded | S9-K1-C: a proven SMI position is overwritten in place with its next exact int; a float or heap slot uses the owning writer, preserving raw-bytecode support and releasing the replaced reference (D3) |
+| `vm.c:vm_iter_limit` | smi-arm | int snapshot metadata clamps the live length; negative snapshots exhaust and oversized positive snapshots keep the live length (R1.3) (D1) |
+| `vm.c:slot_truthy` | smi-arm | an int is truthy when it is not 0; a reserved tag refuses instead of being dereferenced (D1) |
+| `vm.c:slot_type_name` | smi-arm | an SMI reached the final `return "num"` by accident. R1.1 adds the explicit SMI arm ("num", (B)) and aborts on a reserved tag (D1 since R1.1) |
+| `vm.c:vm_borrow_scan` | guarded | compares heap slots with the result pointer; an SMI is not heap (D2) |
+| `vm.c:vm_builtin_name_for` | guarded | skips every non-heap slot (D2) |
+| `vm.c:vm_leaf_accessor_exec/OP_ADD|OP_SUB|OP_MUL` | guarded | bails unless both operands are floats, so the generic call computes an int or mixed operation (4.5) (D1) |
+| `vm.c:vm_leaf_accessor_exec/OP_INDEX_GET` | guarded | bails on an int index; the generic call indexes (4.5) (D1) |
+| `vm.c:vm_leaf_accessor_exec/OP_LOCAL_DOT_GET` | guarded | a non-pointer target bails (`return 0`); a non-null field is borrowed as its bits, an SMI included (D2) |
+| `vm.c:vm_local_is_null` | guarded | exact null test (D3) |
+| `vm.c:vm_local_lift` | guarded | a non-pointer, non-null slot goes through `slot_to_value` (compile-forced) and is boxed on purpose (D2) |
+| `vm.c:vm_run_ex/CASE(BNOT)` | smi-arm | an int64 operand is read exactly; R1.5 returns its exact complement as the int kind (D1) |
+| `vm.c:vm_run_ex/CASE(DESTRUCTURE_UNPACK)` | guarded | a non-pointer source takes the error path, which names the type through `slot_type_name` (D2) |
+| `vm.c:vm_run_ex/CASE(DISPATCH)` | smi-arm | an int key is its exact value; a key outside C `int` raises `index_range` under main #1849/#2032, after N5's exact int64 check; the result returns through `slot_bridge_unwrap` (D1) |
+| `vm.c:vm_run_ex/CASE(DOT_GET)` | guarded | exact null test of a field (D3) |
+| `vm.c:vm_run_ex/CASE(DOT_SET)` | guarded | an int value skips the immediate store: the slow path pops it (D1) |
+| `vm.c:vm_run_ex/CASE(EQ)` | guarded | an int is not a float: the slow path (`values_equal_op` on the lifted Values); R1.3 adds N2 (D1) |
+| `vm.c:vm_run_ex/CASE(INT_CONST)` | not-a-classifier | int literal producer: a constant in the SMI range is emitted through `slot_from_int`; an out-of-range int borrows its constant-pool box into the stack with an owned reference (R1.3) (D3) |
+| `vm.c:vm_run_ex/CASE(INDEX_GET)` | guarded | an int index skips the fast path: the slow path lifts it through `slot_to_value` (D1) |
+| `vm.c:vm_run_ex/CASE(INDEX_SET)` | guarded | an int index or value skips the fast path: the slow path pops it (D1) |
+| `vm.c:vm_run_ex/CASE(LOCAL_DOT_SET)` | guarded | an int value: `vm_slot_lift` boxes it (D1) |
+| `vm.c:vm_run_ex/CASE(LOCAL_IDX_DOT_GET)` | guarded | exact null test of a field (D3) |
+| `vm.c:vm_run_ex/CASE(LOCAL_IDX_DOT_SET)` | guarded | an int value: `vm_slot_lift` boxes it (D1) |
+| `vm.c:vm_run_ex/CASE(NE)` | guarded | an int is not a float: the slow path; R1.3 adds N2 (D1) |
+| `vm.c:vm_run_ex/CASE(NEG)` | smi-arm | an int is negated exactly and INT64_MIN raises value; floats keep their sign semantics (R1.3) (D1) |
+| `vm.c:vm_run_ex/CASE(SLICE_GET)` | guarded | as DESTRUCTURE_UNPACK (D2) |
+| `vm.c:vm_slot_lift` | smi-arm | an SMI is boxed through `slot_to_value`; a reserved tag refuses (R1.3) (D1) |
+| `vm.c:vm_slot_value_opaque` | guarded | only a pointer can be a fn or builtin; an SMI answers "not opaque", which is right (D2) |
+| `vm.c:vm_store_local_slot` | guarded | an int skips the in-place float store; the generic swap stores the SMI (D1) |
+
+
+### 2.6 The accessor API after round 1 (Alt 1R)
+
+```
+/* slots (R1.1: readers and the SMI encoding; no int VALUE can exist yet) */
+int slot_is_f64(EigsSlot); /* high16 < 0xFFF8 */
+int slot_is_smi(EigsSlot); /* high16 == 0xFFFA */
+int slot_is_int(EigsSlot); /* SMI or heap VAL_INT */
+int slot_is_number(EigsSlot); /* f64 or int */
+EigsSlot slot_from_f64(double); /* the ONE double writer; round 2: canonical NaN */
+EigsSlot slot_from_int(int64_t); /* the ONE int writer: SMI in range, else a box.
+ Until R1.3 its only callers convert an
+ existing VAL_INT (§8, the int-source check);
+ R1.1's box arm is eigs_int_unwired */
+void eigs_slot_refuse(const char *, EigsSlot); /* R1.1: a broken contract */
+void eigs_int_unwired(const char *, uint64_t); /* R1.1; R1.3 deletes it (2.5) */
+int slot_f64_value(EigsSlot, double *); /* an immediate double or a VAL_FLOAT
+ box; 0 for anything else, ints
+ included. Replaces slot_as_double. */
+int slot_int_value(EigsSlot, int64_t *); /* an SMI or a VAL_INT box */
+#define SLOT_F64_RAW(s) /* was SLOT_NUM_RAW */
+
+/* Values (R1.2a; make_int lands in R1.3, the first int producer) */
+Value *make_float(double); Value *make_float_permanent(double);
+Value *make_int(int64_t);
+#define VAL_F64_RAW(v) /* member f64_; were VAL_NUM_RAW and num_ */
+#define VAL_F64_OFFSET /* was VAL_NUM_OFFSET (the JIT's fixed-offset load) */
+#define VAL_I64_RAW(v) /* member i64_ of a VAL_INT */
+
+/* the final readers, which replace the bridge in R1.6 (§2.7) */
+EigsNum eigs_number_arg(const Value *, const char *who); /* int-preserving */
+double eigs_float_arg(const Value *, const char *who); /* float-producing: int converts */
+int64_t eigs_int_arg(const Value *, const char *who); /* int-requiring: N5 exact rule */
+int eigs_int_arg_range(const Value *, int64_t lo, int64_t hi,
+ int64_t *out, const char *who); /* N5, then the C range */
+uint8_t eigs_byte_arg(const Value *, const char *who); /* Q1: N5, then 0..255 */
+/* buffer elements (buffer_read_num renamed in R1.6; every site re-decided) */
+double buffer_read_f64(const Value *buf, int64_t i); /* data: an f64 element (Q2) */
+int buffer_byte_at(const Value *buf, int64_t i, uint8_t *out, const char *who);
+ /* a byte value: N5, then 0..255 (Q1) */
+int buffer_index_at(const Value *buf, int64_t i, int64_t *out, const char *who);
+ /* an index: N5 (gather, scatter_add) */
+/* the container f64/int helpers (R1.6): list_set_f64, list_append_f64,
+ list_get_f64, dict_value_set_f64, ... and their _int twins (were *_num) */
+
+/* Q7 (decided): continuous parameters, by the quantity they represent */
+int eigs_geom_shape(const Value *args, int first, EigsShapeKind kind,
+ const EigsRect *drawable, EigsShape *out, const char *who);
+int eigs_bounded_arg(const Value *, double lo, double hi, int *out, const char *who);
+int eigs_magnitude_arg(const Value *, int64_t lo, int64_t hi, int64_t *out,
+ const char *who);
+
+/* literals (R1.3) */
+OP_INT_ZERO, OP_INT_ONE, OP_INT_CONST /* appended to OpCode; §2.7 */
+/* AST literal payload `num` and the token's `num_val` become an EigsNum */
+```
+
+Each `_arg` reader also has the list, slot and optional forms of today's
+checked accessors.
+
+**The Q7 readers** (decided in
+[#1758 comment 6174302316](https://github.com/InauguralSystems/EigenScript/issues/1758#issuecomment-6174302316);
+the builtin table's `q7` column, §6, names each builtin's classes):
+- **Geometry** (coordinates and sizes): clip the WHOLE shape in float
+ space against the drawable area, then round to nearest. A per-argument
+ reader cannot do that: saturating each endpoint of
+ `gfx_rect of [-3e9, 0, 6e9, 10]` separately covers half the screen
+ instead of all of it, so the reader takes the shape. `eigs_geom_shape`
+ reads all of a shape's numbers as doubles (a non-finite one raises
+ `value`, N13), then clips by kind and only then converts:
+ - `POINT` (x, y): outside the drawable rect, the shape is empty;
+ - `RECT` (x, y, w, h; also the clip rect, the framebuffer's destination
+ `dx, dy, w*scale, h*scale`, and a glyph cell of `gfx_text`): intersect
+ with the drawable rect in float space; an empty intersection draws
+ nothing;
+ - `LINE` (x1, y1, x2, y2): Liang-Barsky clipping of the segment in float
+ space, so the visible part keeps its slope;
+ - `CIRCLE` (cx, cy, r) and `RRECT` (x, y, w, h, r): reject when the
+ bounding box misses the drawable rect; otherwise clamp the span loop to
+ the drawable rows and columns before converting, so a radius of `3e9`
+ costs the visible rows, not 6e9 iterations;
+ - every surviving coordinate is then rounded half away from zero (Q4's
+ rule) into a C `int`, which the clip guarantees holds it.
+ `EigsShape.empty` tells the caller to draw nothing; that is the only
+ outcome a finite out-of-range shape has. The implementation retains the original
+ continuous coordinates beside clipped integer bounds for curved spans and
+ framebuffer source mapping; intermediate edge arithmetic must not overflow
+ merely because finite coordinates extend beyond the drawable area. The line
+ reader retains exact binary64 product sums through clip-edge and half-tie
+ decisions, without relying on a wider host long-double format.
+- **Bounded** (colour channels, gain, volume, PCM samples): clamp to the
+ domain, then round (`eigs_bounded_arg`); a NaN raises `value` (N13).
+- **Magnitude** (durations, sleeps, timeouts, sample counts, window sizes, a
+ measurement's scale): round, and raise `value` naming the builtin when the
+ result does not fit what the C call can honour (`eigs_magnitude_arg`).
+
+### 2.6a Stored non-finite buffers (R1.6z-D1)
+
+Buffer equality compares the stored doubles element by element with C `==`
+and requires equal counts. NaN equals nothing, including itself; +inf equals
+only +inf, -inf only -inf, and -0.0 equals +0.0. Equality is total: it never
+raises and never writes math flags, in either strict mode. Buffers stay out
+of the identity shortcut (#1417). This applies N2/N11 scalar equality to
+unboxed elements and avoids fabricated finite answers (Principles §2).
+
+Measured prior art in the ruling: Python `array.array('d')` and `memoryview`
+compare a NaN-containing buffer unequal to itself, matching NumPy
+`array_equal` with its default `equal_nan=False`. Python list identity
+shortcuts are deliberately rejected. Refusal would make equality partial;
+bit comparison would incorrectly distinguish signed zeros.
+
+**R1.6z-D2 (ruling #1758 comment 6385903088): structural all the way down.**
+A list or dict compared with itself is equal exactly when every element pair
+it reaches is equal. A container reaching a NaN-holding buffer is unequal to
+itself, including through nested or cyclic containers. This also governs
+`!=`, `list_contains` and `list_index_of`. Measured prior art in D2:
+
+| language | `l == l`, `l = [nan-buffer]` | `d == d` | `a == a` (buffer) |
+|---|---|---|---|
+| Python 3 (`array('d')` in a list/dict) | `True` | `True` | `False` |
+| Rust std (`Vec>`, `HashMap`) | `false` | `false` | `false` |
+
+Python's element identity shortcut produces the inconsistency rejected here
+(Principles §2); Rust's `PartialEq` is consistent. Identity remains a fast
+path only after a visited-set scan proves no reachable non-reflexive element.
+Before R2.2 that element is a NaN-holding buffer; R2.2 extends the predicate to
+NaN floats. The scan is linear in distinct containers and their stored edges
+and elements, terminates on cycles, never raises and never writes
+`g_math_flags`. Distinct containers retain today's pairwise traversal and
+the #1665 depth cap unchanged. `N11.container-self-equal-ieee` pre-registers
+the structural cases and a 40-level `[x, x]` finite-buffer DAG cost witness.
+
+The public C getter `eigs_value_buffer_get` returns the stored double exactly,
+including NaN payloads, infinities and negative zero. It never normalizes,
+raises for a valid element, or writes math flags. It mirrors the setter's
+bit-preserving store and Lua `lua_tonumberx`/CPython `PyFloat_AsDouble`,
+which also hand C the raw double. Clamping fabricates a value; a new status
+channel would change the ABI for a property C can test with `isfinite`.
+Wrong-type and out-of-range reports remain the separately decided API contract.
+The getter migration and public API docs land in the embed file group.
+
+VM indexing and iteration use the finite data boundary, matching `get_at`
+(R1.6d-D1): lenient NaN and every infinity raise `value`; strict NaN keeps
+its first diagnostic and INVALID flag. Speculative leaf evaluation bails
+before raising, preserving the callee's error kind, message and source line.
+
+### 2.7 Every meaning change coincides with a token change
+
+Round 0's order changed meanings with no token change: R1.1 widened
+`slot_as_double` with an SMI arm, and R1.3 re-meant `OP_NUM_ZERO`/`OP_NUM_ONE`
+as ints. Neither produces a compile error, so a missed caller or a missed
+executor (the critic found the leaf evaluator's `OP_NUM_ZERO`/`OP_NUM_ONE`
+arms, vm.c:1872-1877) would keep compiling with the old meaning. Round 2's
+order had a second flaw the critic found: R1.2 deleted the builtin readers
+(a compile error) while claiming to preserve behaviour, and R1.6 then changed
+about 114 builtin rows with no token event at all.
+
+The rule: **a meaning change lands in the same commit as the token change
+that stops every affected site**, or it is listed below with the reason no
+token exists and the gate that catches a miss instead.
+
+**The bridge.** The builtin-facing readers and producers are kept, under
+their current names, until R1.6, and they are the bridge: `eigs_num_arg` and
+its family, `buf_num_arg`, `make_num`/`make_num_permanent`, the container
+`*_num` helpers, `finite_num_to_byte`, `buffer_read_byte` and
+`buffer_read_num`. Through R1.5 they keep exactly today's meaning: a number
+read is a double (an int converts, as every number already is on the tag), a
+number produced is a float. R1.6 deletes the bridge, so every builtin site
+stops compiling in the commit that gives it its §6 semantics. R1.2 changes no reachable builtin behaviour: no int exists yet. Its exact
+sort and JSON text reader arms are the explicit exceptions to the bridge
+policy below; they retain final int semantics instead of converting to double.
+
+| commit | meaning change | token change in that commit | a missed site |
+|---|---|---|---|
+| R1.1 | a slot may hold an SMI (no producer yet) | `slot_is_num` -> `slot_is_f64`, `SLOT_NUM_RAW` -> `SLOT_F64_RAW`, `slot_from_num` -> `slot_from_f64`; `slot_is_numeric` and **`slot_as_double`** deleted | compile error: 223 sites first forced here (§2.3a), the 58 D1 classifiers among them |
+| R1.1 | a slot classifier without a number test meets an SMI | none can exist (they name no number token) | the derived D2/D3 table (§2.5): `refuse` entries abort, `smi-arm` entries answer; the inventory fails on an unreviewed entry |
+| R1.2a | a Value number may be an int (`VAL_INT`; no producer yet) | `VAL_NUM` -> `VAL_FLOAT`, `num_` -> `f64_`, `VAL_NUM_RAW`/`VAL_NUM_OFFSET` renamed; `VAL_INT` appended, so `-Werror=switch` stops every default-less ValType switch; the one `default:` (builtins.c:998) removed in the same commit | compile error: 464 sites first forced here. Each forced site gets its final int arm, live from R1.3; a builtin's number test admits an int and reads it through the bridge |
+| R1.3 | **the first int values**: int literals, and int arithmetic on them | the AST payload `num` and the token's `num_val` become an `EigsNum`; **new opcodes `OP_INT_ZERO`, `OP_INT_ONE`, `OP_INT_CONST`, appended** (the opcode-ABI rule; no `EIGS_BYTECODE_ABI` bump); `make_int` introduced; `eigs_int_unwired` deleted (the R1.1 placeholders, 2.5) | compile error at the 11 payload sites, at each `eigs_int_unwired` caller, and the four exhaustive OpCode switches (`op_name` chunk.c:337, `op_verify_operands` chunk.c:466, `op_verify_stack_effect` chunk.c:615, `op_stack_effect` compiler.c:305; `tools/obs_marker_check.sh` requires each new enumerator's `/*obs:NONE*/`); the JIT and leaf scanners refuse an opcode they do not know (their `default:`); the interpreter's dispatch table is the one executor the compiler cannot check, and a missing entry is a NULL jump that crashes the first int-literal row (loud) |
+| R1.3 | `OP_NUM_ZERO`, `OP_NUM_ONE`, `OP_CONST` | **no meaning change**: they keep their float meaning in all three executors (interpreter vm.c:3257, JIT emitter jit.c:2797, leaf evaluator vm.c:1872); an int is never an `OP_CONST` operand | n/a |
+| R1.4 | `//` | `OP_IDIV` appended, `TOK_SLASH_SLASH` appended (and `//=`, Q6) | compile error at the exhaustive switches |
+| R1.6 | **every builtin's §6 semantics** (N5 raises, int results, Q1 byte and Q7 continuous readers, the index readers of `gather`/`scatter_add`) | **the bridge is deleted** (21 tokens, §2.3a) | compile error: 743 sites first forced here; the per-builtin behaviour table (§9.4) checks every row's result against §6, on the tag and the branch |
+| R1.8 | the embed API gains ints | `eigs_value_new_int`, `eigs_value_as_int`, `eigs_value_is_int` added; `eigs_value_as_num` keeps its name **by decision** (Q3: it converts, like `lua_tonumber`) | the embed gate row (MAP `gate.N7.embed-int`) |
+| R2.2 | NaN and inf become reachable | `num_guard`, `num_guard_named`, `EIGS_NUM_MAX` deleted | compile error: 47 sites first forced here (the rest of the clamp's 74 sites were forced earlier) |
+
+**The meaning changes that cannot carry a token**, each with the gate that
+catches a miss:
+- **R1.3, the int arms written in R1.1 and R1.2 go live.** They were forced
+ then and written with their final semantics while no int could reach them.
+ Gate: R1.3's 44 MAP rows and the C slot test; and the int-source check
+ (§8) proves nothing before R1.3 could produce an int, so nothing earlier
+ could have exercised an unfinished arm. The R1.1 arms that could not be
+ final (`unwired`, 2.5) are token-forced instead: R1.3 deletes
+ `eigs_int_unwired`. Since the R1.1 fix round the four operator arms that
+ raised a type error for an SMI (`NUM_CMP`, `INT_BINOP_R`, `NEG`, `BNOT`)
+ are among them.
+- **R1.3, the tape parser.** `parse_value_p` (trace.c:2284) reads digits as
+ an int from v7 on; the version bump refuses an older tape. Gate: the T1
+ rows (`T1.twice-int`, `T1.v6-refused`).
+- **R1.3, the leaf evaluator vs the generic path.** The leaf has no frame and
+ no `OP_LINE`, so nothing marks it when the generic path's semantics change.
+ Gate: the `trace` tier of §9.2, which runs every row with the leaf off and
+ requires byte-identical output.
+- **R1.4 `%` and R1.5 the shift rules.** Each has one live executor in round
+ 1: the interpreter (`CASE(MOD)`, `INT_BINOP_R`). The JIT scanner refuses
+ MOD and the bitwise ops from R1.3, the leaf admits neither, and nothing
+ folds constants (checked: the compiler and parser evaluate no operator at
+ compile time). Gate: the N4 and N8 rows, including their `F` shapes.
+- **R1.7, the float text.** N6 changes `eigs_num_text`, the one shared
+ float formatter, and in the same commit the writers that format a float on
+ their own: the JSON encoder, `store_json_encode` and `db_build_query`
+ (#1762), and trace.c's `%.17g` writers (2817, 2836, 2950). (The decoders'
+ int kinds for integer text, `json_decode`, `json_path`, `store_query`,
+ `db_query_value` and `num of`, land in R1.6, where deleting `make_num`
+ forces them.) Gate: one row per writer (`N6.*`, `N14.encode-kinds`,
+ `N14.store-exact-digits`, `gate.N14.db-param-exact`, `T1.twice-float`).
+- **R2.2, consumers that assumed a finite double.** No token marks code that
+ merely receives a NaN or an inf once the clamp is gone:
+ - `values_equal_impl`'s identity shortcut (`if (a == b && ...) return
+ 1;`) would call a NaN box equal to itself. No program path reaches it
+ with one float box on both sides today (EQ compares two floats in its
+ fast path, and container comparisons pass fresh element views; read,
+ not probed, since no NaN exists yet), so R2.2 excludes floats from the
+ shortcut as #1417 excluded buffers, and extends the container reflexivity
+ scan to NaN floats (see §2.6a, R1.6z-D1/D2). The C slot test calls it with
+ one NaN box on both sides;
+ - C casts of a double to an integer type (undefined on NaN or inf): every
+ builtin argument goes through a §2.6 reader, which refuses a non-finite
+ value (N13); the internal casts left are the #1746 residue (R2.6);
+ - the sort comparators and `min`/`max` (Q8, decided): `sort` and `sort_by`
+ order floats by IEEE 754 `totalOrder`, the canonical NaN after +inf;
+ `min` and `max` propagate NaN. Gate: `N11.sort-total-order`,
+ `N11.min-max-nan`.
+
+---
+
+## 3. Measured prototype
+
+**Question.** What does an SMI arm cost in the VM's ADD fast path, in which
+order, and does inline SMI beat boxing every int?
+
+**Probe.** A 155-line standalone C file
+(`/tmp/claude-1000/.../p2r0/bench/p2bench.c`, not committed). It mirrors
+`ARITH_FAST` (vm.c:3290) on a two-slot stack. Each variant is one `noinline`
+handler; the accumulator round-trips through memory as on the VM stack. It
+is built with the runtime's flags: `-O2 -fstack-protector-strong
+-D_FORTIFY_SOURCE=2 -fPIE`, gcc 13.3. All nine runs agree on the checksum
+(2984375 at 1e6 iterations).
+
+**Instrument.** `perf` is unavailable (perf_event_paranoid=4). Instruction
+counts come from callgrind at 1e6 and 2e6 iterations. The difference divided
+by 1e6 cancels startup cost, and both runs scale exactly, so the counts are
+deterministic. Wall time is the interleaved median of 7 runs at 2e7
+iterations. The box (Celeron N3350) carried load 1.2-3.8 from another job,
+so read wall time as secondary.
+
+| variant | data | Ir/op | wall median ns/op |
+|---|---|---:|---:|
+| A: today, double-only + `num_guard` | float | **38** | 8.38 |
+| B: float first, then SMI (range check) | float | 40 (+2) | 8.88 |
+| C: SMI first, then float | float | 44 (+6) | 10.17 |
+| D: float first, SMI via shifted add + overflow flag | float | 40 (+2) | 9.04 |
+| E: no SMI, every int a boxed int64 (freelist hit) | float | 49 (+11: harness prologue, see below) | 8.76 |
+| B | int | 50 | 10.97 |
+| C | int | **42** | 10.46 |
+| D | int | **44** | 10.45 |
+| E | int | **77** | 17.24 |
+
+**Reading the disassembly.**
+- B's +2 on floats is two register copies the compiler keeps live for the
+ SMI arm (`movq %xmm0,%rdx; mov %rdx,%rcx`). That is a real cost of sharing
+ the operand bits.
+- E's +11 on floats is four callee-saved push/pop pairs in the per-variant
+ function prologue. That is an artifact of the harness: in the VM the
+ handler is inside `vm_run_ex`, which saves those registers once per call.
+
+**What it decides.**
+1. **Check order: float first.** It costs the float path +2 Ir (5% of the
+ isolated op). SMI-first costs floats +6. Float-first keeps every existing
+ float program within 2 Ir per arithmetic op.
+2. **Overflow check: the shifted add (D)** is 6 Ir cheaper on the int path
+ than a range check on the sum (B). Put both payloads in the top 48 bits
+ and let the hardware overflow flag fire exactly when the sum leaves 48
+ bits. This is a round-5 fast-path choice, recorded here because it
+ decides the int arm's shape.
+3. **Inline SMI against boxing every int: +33 Ir (+75%) and +65% wall per
+ add for boxing, even when every allocation hits a freelist.** This
+ confirms #1758's SMI tag decision with a number.
+
+**Scale.** The real interpreter spends 685 Ir per iteration of `loop while i
+< N: i is i + 1` (callgrind on the existing main binary, `EIGS_JIT_OFF=1`,
+1e5 against 2e5 iterations: 69,804,271 and 138,304,271). The SMI arm's +2
+on the float path is 0.3% of that.
+
+**What it does not decide.** Alt 1R against Alt 2. Their slot cost is
+identical, and at the Value level the difference is one compare against two.
+The choice rests on §2.3, not on speed.
+
+**A measured risk it exposes.** The same loop under the default JIT costs 402
+Ir per iteration (42,924,355 against 83,124,355; `[jit] compiled=1`). After
+N1, `i` is an int: the JIT scanner stops at the new `OP_INT_ONE` (§2.7), and
+the LT and ADD guards bail on the SMI (§4). Expect at least the
+interpreter's 685 (+70%) on int loops until round 5's fast paths.
+
+The leaf evaluator (#366, §4.5) loses int helpers the same way: an int
+literal makes its scanner refuse the chunk, and an int argument bails at the
+first arithmetic or index op. `define inc(x) as: return x + 1` then takes the
+generic call, the round trip #366 measured at about 200 ns per call
+(vm.c:1836-1842), until round 5 gives the leaf int arms.
+
+The CodSpeed lane's two-sided Ir gate (#1220) will fire on its JIT-tier
+targets with int loops: `scalar_loop`, `bench_perf`, `bench_dmg_shape`,
+`bench_idxset` and the real DMG `cpu_instrs` row (codspeed.yml). The lane's
+own measurement bounds the size: "JIT removes 48% of the instructions on
+bench_dmg_shape, 11.8% on DMG itself" (codspeed.yml:65-67). Losing the JIT
+on int code costs up to about 1.9x Ir there, and about 1.13x on DMG. That
+must be accepted up front, not discovered.
+
+---
+
+## 4. The JIT and the leaf evaluator
+
+Three evaluators compute numbers: the interpreter (`vm_run_ex`), the JIT
+(`jit_compile_to_thunk`, admitted by `jit_supported_prefix`) and the leaf
+evaluator (`vm_leaf_accessor_exec`, admitted by `chunk_scan_leaf_accessor`;
+§4.5). The inventory's executor matrix (§1, check 3) lists which of them
+implements each opcode.
+
+Round 1 needs correctness by bailing; fast paths are round 5. Every
+consuming guard was read for how it treats a slot whose high 16 bits are
+`0xFFFA` (SMI), and how it treats a heap slot whose `Value` is a boxed int.
+Line numbers are jit.c at `0740af03`.
+
+### 4.1 Consuming guards: all bail on SMI
+
+| guard | the tag-range test | SMI (`0xFFFA`) | boxed int, Alt 1R (`VAL_INT`) |
+|---|---|---|---|
+| `emit_immediate_num_check_rax/rdx` (1625-1636) | `shr $48; cmp $0xFFF8; jae bail` | 0xFFFA >= 0xFFF8: **bail** | 0xFFFB >= 0xFFF8: **bail** |
+| `emit_load_numeric_operand` (2343-2378) | `< 0xFFF8` is a double; `< 0xFFFB` bails (the comment reads "Tag 0xFFF8-0xFFFA (null/bool/SMI-reserved): not numeric"); `>= 0xFFFD` bails; else a heap Value must have `type == VAL_NUM && refcount >= 2` | **bail** | type is `VAL_INT`, not `VAL_NUM`: **bail** |
+| `emit_jump_unless_bool_rax` (1911-1916) | `cmp $0xFFF9; jnz` | not a bool, falls to the num check: **bail** | not a bool: **bail** |
+| `emit_dict_cache_probe` (2172-2185) | `cmp $0xFFFB; jne slow` | **slow path** (helper) | heap, then type `!= VAL_DICT`: **slow** |
+| `OP_SET_LOCAL` in-place branch (3410-3475) | TOS `>= 0xFFF8` goes to the generic swap; the old slot must be a heap `VAL_NUM` with rc 1 | TOS SMI: **generic swap** (correct) | old slot `VAL_INT`: **generic swap** |
+| `OP_INDEX_SET` inline (4067-4130) | index and value use `emit_immediate_num_check_*`; target `== 0xFFFB && type == VAL_BUFFER` | **helper** (C, the interpreter's semantics) | **helper** |
+| `OP_LOCAL_DOT_GET` inline (3108-3170) | the field uses `emit_immediate_num_check_rax` | **helper** | **helper** |
+
+The refcount paths treat an SMI as an immediate, which is correct (it has no
+refcount):
+- `emit_conditional_incref_rax` (1279) and `emit_conditional_decref_rsi_hook`
+ (2028) test `cmp $0xFFFB; jb skip`, and 0xFFFA < 0xFFFB.
+- The binop commit's OR-of-tags exit (2391, 2421) already lists "bool|smi
+ tags OR-ing up to 0xFFFB" as taking the per-slot path, which handles every
+ tag.
+
+The opcodes that consume through these guards are:
+- ADD, SUB, MUL, DIV, MOD, EQ, NE, LT, GT, LE and GE through
+ `emit_load_numeric_operand`;
+- BAND, BOR, BXOR, SHL, SHR, NEG, NOT, BNOT and JUMP_IF_* through
+ `emit_immediate_num_check_*`.
+
+ITER_NEXT, INDEX_GET and LOCAL_IDX_GET are C helper calls (`jit_helper_*` in
+vm.c). They are migrated with the C code, and the inventory counts their
+sites (for example `jit_helper_index_get` 17).
+
+**Finding: no consuming guard reads SMI tag bits as a double.** Under Alt 2
+three would misread a boxed int (§2.3). Under Alt 1R none does.
+
+### 4.2 Producer emitters: these do not bail
+
+The emitter writes bits directly, so a guard cannot protect these. Round 0
+re-meant two of them; round 2 changes none of their meanings, so none can be
+missed (§2.7):
+
+| emitter | today | round 1 |
+|---|---|---|
+| `OP_NUM_ZERO`/`OP_NUM_ONE` (2797-2814) | pushes the double bits of 0.0 and 1.0 | **Unchanged: they keep meaning float 0.0 and 1.0** in all three executors (this emitter, the interpreter vm.c:3257, the leaf vm.c:1872-1877). Today the compiler (compiler.c:2073-2074) emits them for any literal `== 0.0` or `== 1.0`, so `0` and `0.0` share an opcode; from R1.3 the compiler emits them only for a float literal, and `0`/`1` become the new `OP_INT_ZERO`/`OP_INT_ONE`. |
+| `OP_CONST` (2816-2838) | `VAL_NUM`: the bits of `num_`; else pushes a `TAG_HEAP` pointer plus an incref | **Unchanged**: an int is never an `OP_CONST` operand. Its `VAL_NUM` test is renamed `VAL_FLOAT` (forced), and `last_imm` (4375) stays 1 exactly for a float constant. |
+| `OP_INT_ZERO`/`OP_INT_ONE`/`OP_INT_CONST` (new, R1.3) | none | **Refused by the scanner** (its `default:`), so an int literal ends the compiled prefix and runs interpreted. Round 5 emits them: SMI bits computed by `slot_from_int` at compile time, or a `TAG_HEAP` box for an int64 outside the SMI range; `last_imm` is then 1 exactly when the bits are immediate (the jit skill's lockstep rule). |
+| BAND/BOR/BXOR/SHL/SHR (3716-3768), BNOT (3819-3830) | double operands, cvttsd2si, op, cvtsi2sd: a **double** result | N8 makes bitwise results ints. Round 1: **the scanner refuses these opcodes** (the op interprets) until round 5 emits SMI results. |
+| MOD (3511-3585) | `call fmod`, which truncates toward zero | N4 makes `%` floor modulo (sign of the divisor), so fmod is wrong for mixed-sign floats (`-7.5 % 2` gives -1.5; Lua gives 0.5). Round 1: **the scanner refuses MOD** (§8 R1.3; the MAP's `N4.fn-floor-mod` witness expects it); round 5 may emit the floor correction. |
+
+### 4.3 For round 2 (N11/N12)
+
+The experiment already did each of these and the branch carries the code:
+- `max_normal_bits` (2634) becomes inf's bits, so a NaN result bails;
+- NEG gains a NaN bail, because a flipped canonical NaN spells
+ `SLOT_NULL_BITS`;
+- comparisons gain a `jp` bail;
+- CONST canonicalizes a NaN constant.
+
+### 4.4 For round 5 (recorded so it is not rediscovered)
+
+An int fast path must bail when its result leaves the SMI range. A boxed
+result is a heap push, and `last_imm = 1` on ADD (4366) would then skip its
+decref at the following POP: one leak per iteration (the #231 shape).
+`jit_smoke` needs SMI rows for every guard above, each with a value that
+differs only in the tag bits, so a guard that compared the wrong bits would
+fail. The jit skill's wide-bit rule applies.
+
+### 4.5 The leaf evaluator (`vm_leaf_accessor_exec`, #366)
+
+**What it is.** `chunk_scan_leaf_accessor` (chunk.c:1072-1134) runs once per
+compiled function (compiler.c:2703 and 2767). It flags a chunk `leaf_accessor` when the body is one
+side-effect-free expression over the parameters ending in `RETURN`: at most
+128 bytes of code, at most 8 parameters, stack depth at most 8, no
+defaulted parameters, and only
+the 11 opcodes `LINE`, `CONST` (a `VAL_NUM` constant only), `NUM_ZERO`,
+`NUM_ONE`, `GET_LOCAL` (a parameter), `LOCAL_DOT_GET`, `INDEX_GET`, `ADD`,
+`SUB`, `MUL`, `RETURN`; its `default:` refuses everything else. An
+exactly-fed call to such a chunk is then computed by
+`vm_leaf_accessor_exec` (vm.c:1856-1971) against the caller's stack, with no
+frame, no env, no `OP_LINE` and no dispatch. Its contract: return 1 with the
+generic call's stack effect, or return 0 having touched nothing, and the
+generic path re-executes.
+
+**Where it runs.** Both call paths try it before the generic call:
+`CASE(CALL)` (vm.c:4253) and `jit_helper_call` (vm.c:2503, the JIT's calls).
+The gate is `!g_sandbox_active && leaf_accessor && argc == param_count &&
+!g_trace_hist`. So it computes `define sq(x) as: return x * x` and
+`one of 7` in **every** tier, `EIGS_JIT_OFF=1` included. Probed on the main
+binary: 200 calls of a leaf `sq` leave its chunk with no entries at all in
+`EIGS_JIT_HOT` under all three tiers (the leaf path never counts an entry;
+both generic paths do, vm.c:2617 and vm.c:4353).
+
+**It is off under `EIGS_TRACE`.** `g_trace_hist` is set while recording, so a
+recorded run computes every leaf call on the generic path and a live run
+computes it in the leaf. Probed: the same program records `sq` with 200
+generic entries (`jit yes`, `adv RET`). A recorded run and a live run
+therefore execute different evaluators. That is correct only while the two
+agree on every value, so it gets a gate: the `trace` tier of §9.2 runs every
+row with the leaf off and requires byte-identical output. Round 1 keeps the
+leaf off under trace (the tape records every line, and the leaf skips
+`OP_LINE`).
+
+**Every arm** (vm.c line, today, round 1):
+
+| arm | today | round 1 |
+|---|---|---|
+| `LINE` (1863) | skips the operand | unchanged |
+| `CONST` (1866) | `slot_from_num(VAL_NUM_RAW(constant))`; the scanner admitted only a `VAL_NUM` constant (chunk.c:1090) | renamed `slot_from_f64(VAL_F64_RAW(...))`; the scanner's test becomes `!= VAL_FLOAT`, so an int constant is refused (it is an `OP_INT_CONST` anyway) |
+| `NUM_ZERO` (1872), `NUM_ONE` (1875) | `slot_from_num(0.0)` / `(1.0)` | renamed `slot_from_f64`; **meaning unchanged** (float 0.0/1.0) |
+| `OP_INT_ZERO/ONE/CONST` | do not exist | **not admitted**: the scanner's `default:` refuses the chunk, so an int literal sends every call down the generic path. Round 5 adds scanner and evaluator arms together (the inventory requires both directions) |
+| `GET_LOCAL` (1878) | borrows the argument slot | unchanged (an SMI argument is a valid slot) |
+| `LOCAL_DOT_GET` (1883) | the target must be a pointer to a dict, else bail; a missing field is null; a field is borrowed as its bits | unchanged: an SMI field is borrowed as an SMI (§2.5 row `guarded`) |
+| `INDEX_GET` (1907) | the index through `slot_as_double` (an immediate or a boxed `VAL_NUM`) then `vm_index_is_int`; a list element is borrowed; a buffer element is `slot_from_num(buffer_read_num(...))` | `slot_as_double` is deleted (R1.1), so this arm stops compiling. It reads the index with `slot_f64_value` and **bails on an int index**; the buffer element is `slot_from_f64` (Q2: f64 storage), read through `buffer_read_f64` once R1.6 renames `buffer_read_num`. Round 5 adds the SMI index |
+| `ADD`/`SUB`/`MUL` (1933) | both operands through `slot_as_double`; `r = a op b`; the underflow flag for MUL (#1079); `slot_from_num(num_guard(r))` | stops compiling (R1.1). Reads with `slot_f64_value` and **bails unless both operands are floats**, so an int or mixed operation runs on the generic path with N2/N3/(A) semantics. Round 5 adds int arms whose overflow bails (never raises inside the leaf: the generic path raises with the caller's line). R2.2 deletes `num_guard` here with the others |
+| `RETURN` (1957) | increfs the result, pops args and fn, pushes the result | unchanged (`slot_incref` ignores an SMI) |
+| `default` (1966) | unreachable; returns 0 | unchanged |
+
+**Its witness.** No counter exists for the leaf. The design uses the hot-chunk
+table instead of adding one (§9.2): both generic call paths count an entry
+and the leaf never does, so "the callee has no entries in the normal run, and
+K entries in the leaf-off (`trace`) run" proves that all K calls were
+computed by the leaf. Probed on main: `sq` absent with the leaf on, `sq
+200` with it off.
+
+---
+
+## 5. N12 and SMI share the NaN space
+
+- **The canonical quiet NaN is `0x7FF8000000000000`**, with the sign bit
+ clear, as in #1746 and the `exp-ieee-p1` `SLOT_CANON_NAN`.
+- **Every tag has the sign bit set** (high 16 bits `0xFFF8`-`0xFFFF`). The
+ SMI tag is `0xFFFA`.
+- So canonical NaN (high 16 = `0x7FF8`) and every tag, SMI included, differ
+ in bit 63. **They cannot collide.**
+- The patterns that *can* collide are NaNs with the sign set and the quiet
+ bit set (high 16 `0xFFF8`-`0xFFFF`). This includes x86's default NaN
+ `0xFFF8000000000000`, which **is** `SLOT_NULL_BITS`, and any NaN built
+ from bytes (`f64_from_bits of 0xFFFA000000000005` spells SMI 5).
+- `slot_from_f64` is the single double-to-slot writer, and the JIT bails
+ before storing a NaN result, so no such pattern reaches a slot. Every NaN
+ is rewritten to `0x7FF8000000000000`. The other negative NaNs (high 16
+ `0xFFF0`-`0xFFF7` with a nonzero mantissa: the signalling ones) do not
+ collide but are canonicalized too: one NaN.
+- `-0.0` (`0x8000...`) and `-inf` (`0xFFF0000000000000`) are below `0xFFF8`,
+ so they are ordinary doubles.
+
+**`f64_bits of x`** returns the IEEE bits as an int, exact (int64
+reinterpretation, two's complement). Cases:
+- for a float held in a slot, the stored bits; for NaN that is always
+ `0x7FF8000000000000` = 9221120237041090560, which is outside the SMI
+ range, so a boxed int;
+- `f64_bits of -0.0` = INT64_MIN (boxed);
+- `f64_bits of 1.0` = 4607182418800017408 (boxed).
+
+**`f64_from_bits of i`** builds the double from `i`'s 64 bits. If they spell
+a NaN, the result enters a slot as the canonical NaN. Its payload is gone
+from the float but still held by the int `i`. So `f64_bits of (f64_from_bits
+of i) == i` for every `i` that is not a non-canonical NaN pattern, and
+payload fidelity goes through ints (N12: "Ints are never NaN-canonicalized").
+
+**Buffers** hold raw doubles and are not slots, so a buffer element may hold
+any payload. Reading it as a float (`buf_get`) puts it in a slot, so it is
+canonicalized. Reading it through the new bits accessor (N12: "Buffer
+elements are also readable and writable as bits") returns the stored bits
+exactly as an int. That is the path the bar critic found missing: payload
+`0x7FF0...01` came back canonical through every route on the prototype.
+
+---
+
+## 6. Builtin classification
+
+**Classes.** The reference for counterparts is Lua 5.4 (manual §6.7,
+`math`; §3.4.1-3.4.3, operators and coercions; §6.4, `string`; §6.6,
+`table`).
+
+- **int-preserving:** returns an exact int.
+ - Where the inputs are ints: `abs`, `min`, `max`, `sum`, `floor`, `ceil`,
+ `round`. These follow §6.7's "integer/float" annotation: "integer
+ results for integer arguments". `math.min`/`math.max` return the winning
+ argument.
+ - Where the result is a count, code or index by definition: `len`,
+ `range`, `ord`, `index_of`, handles. These follow Lua's `#` and
+ `string.byte`.
+ - Rounding (§6.7): "math.ceil, math.floor, and math.modf return an integer
+ when the result fits in the range of an integer, or a float otherwise".
+ `round` has no Lua counterpart and follows floor and ceil (Q4,
+ decided: half away from zero, an int when it fits int64).
+- **float-producing:** ints convert to float and the result is a float.
+ Examples: Lua's `math.sin`, `sqrt`, `log`, `exp`; `/` and `^`, which
+ "always convert integer operands to floats" (§3.4.1); and the float-tensor
+ and audio-sample builtins (buffer storage is float).
+- **int-requiring:** an argument is integer-valued, so it converts by N5's
+ exact rule, then the C type's range: an index, count, size, code, bit
+ pattern, byte value, handle, port or seed (Q1, decided: the parameter's
+ meaning decides, not the C type). Examples: Lua's string positions,
+ `table.insert`/`table.remove` positions, bitwise operands (§3.4.2),
+ `math.random(m, n)` bounds, `math.randomseed`, and `string.char`, which
+ raises on `65.5` and on `256` (probed, 5.4.9).
+- **other:** everything else that takes or returns numbers:
+ - formatting (N6);
+ - JSON, store and db codecs (N13, N14);
+ - parsing (`num`, N10);
+ - `type` ((B));
+ - continuous parameters (Q1, decided): gfx coordinates and sizes, colour
+ channels, durations and audio gain accept an int or a float. Where the
+ C API needs an int the value rounds to nearest (half away from zero, as
+ `round`), replacing today's truncation toward zero, which gets negative
+ coordinates wrong. What a finite value outside the C range does follows
+ the quantity (Q7, decided): geometry is clipped as a whole shape in
+ float space, a bounded quantity is clamped, a magnitude raises `value`
+ (§2.6). (Byte consumers, which truncated and wrapped modulo 256 here in
+ round 0, are int-requiring under Q1.)
+
+**Exact readers completed in R1.2.** `sort` and `sort_by` compare int
+values and numeric keys exactly, and `json_build` and `json_path` format an
+int as exact digits. These final semantics are retained: rounding through
+the double bridge would lose information above 2^53 (principle 2, No silent
+wrong answers). The sort comparators and `json_build` become reachable with
+R1.3's first int literals, so MAP `N2.sort-exact` is R1.3. `json_path`'s
+reader is ready too; its parser still starts producing integer values in
+R1.6. Float formatting stays on its existing path until R1.7, and the
+remaining builtin readers keep the bridge until R1.6.
+
+**The `loss` column.** This records the original inventory tree's risk,
+rather than remaining work in R1.2; the completed readers above retain their
+historical marks. `LOSS` marks a builtin whose inventoried body reads its
+argument through a double accessor, or builds its result from a computed
+double, on a path where its class needs an exact int. An int beyond 2^53
+would then come out silently wrong if the body were kept and its accessor
+re-pointed to int-to-float conversion. Under Alt 1R the old accessors no
+longer compile, so every row is revisited anyway; `LOSS` rows are where the
+mechanical re-point would be wrong. Builtins that only call the shared
+formatter or equality (`print`, `str`, `join`, `list_contains`) are `-`,
+because their exactness change happens once, in `eigs_num_text` or
+`values_equal_impl`.
+
+Two consequences of the definition, made explicit in design round 2:
+- **The Q1 rows are not LOSS.** A byte, a count, a handle, a port or a C
+ `int` is range-checked after the exact rule, and every int beyond 2^53 is
+ outside every such range, so the double path cannot return a wrong
+ in-range value: it raises either way. What WAS lossy beyond 2^53 is the
+ modulo-256 wrap the byte consumers did, and Q1 deletes it. Seeds are the
+ exception (a seed's range is all of int64), so `seed_random` and
+ `task_sched_seed` stay LOSS.
+- **A writer that loses digits of a double is LOSS even without ints.**
+ `store_put`/`store_update` (the record encoder's `%.15g`) and
+ `db_execute`/`db_query_json`/`db_query_value` (the parameter binder's
+ `%g`) lose digits on main and on the tag today, filed as
+ [#1762](https://github.com/InauguralSystems/EigenScript/issues/1762).
+ Round 0 missed them; the critic found them. R1.7, N14's int half, fixes
+ both writers by routing them through the one N6 number writer:
+ `store_json_encode` (ext_store.c:184-202) writes an int as exact digits
+ and a float as its shortest round-trip repr, replacing the int32 `%d` arm
+ and `%.15g`; `db_build_query` (ext_db.c:57-58) binds the same text
+ instead of `"%g"`. N13's refusal of a non-finite value stays inside the
+ encoder (R2.3). If #1762 lands on main first (it does not depend on ints),
+ R1.7 inherits the fix and only adds the int arm. Its MAP entries are the
+ row `N14.store-exact-digits` and the gate `gate.N14.db-param-exact`.
+
+**The `q7` column** (design round 3) names the Q7 class of each continuous
+parameter a builtin converts: an ordered subset of `geometry`, `bounded`,
+`magnitude`, or `-`. The inventory refuses any other token, and checks the
+column against the note both ways: a row's note says "continuous" exactly
+when its `q7` column is not `-`. Counts: geometry 9, bounded 13, magnitude 13.
+
+**Counts** (from the table below, checked by `tools/phase2_inventory.py`,
+which prints them): 309 numeric builtins (including N9, R1.6g; S10-D2 retires
+`proc_read_buf`). By class: 37 int-preserving, 58 float-producing,
+161 int-requiring, 53 other. The decided Q1 moved 16 rows
+from other to int-requiring (counts, byte values, handles, flags and
+selectors); `ppu_render_frame` is a new int-requiring row; design round 3
+moved `buf_from_pcm16le` from float-producing to int-requiring (its inputs are
+byte values, #1758 comment 6174964306) and corrected `gather` (its indices
+truncate today). The current table has 37 LOSS rows:
+- arithmetic: `abs`, `add`, `subtract`, `multiply`, `negative`, `sum`,
+ `num_copy`;
+- min/max and rounding: `min`, `max`, `floor`, `ceil`, `round`;
+- bitwise: `sign_extend`, `hex`;
+- clock, RNG and seeds: `monotonic_ns`, `random_int`, `seed_random`,
+ `task_sched_seed`;
+- shared counters and HTTP header decoding: `shared_incr`, `http_post` (R1.6b-D3);
+- integer scanners: `scan_ints`, `scan_int_tokens`;
+- ordering: `sort`, `sort_by`;
+- store keys: `store_get`, `store_delete`, `store_update`, plus
+ `store_query` and `db_query_value`, which decode integer text with
+ `strtod`;
+- writers that drop digits today (#1762): `store_put` (and `store_update`)
+ through `%.15g`, `db_execute` and `db_query_json` (and `db_query_value`)
+ through `%g`;
+- JSON and parsing: `json_decode`, `json_path`, `num`, `json_encode`,
+ `json_build`.
+
+Probes on the main binary (0.46.0, built 2026-10-13; not the tag) show the
+current state behind several rows:
+- `str of 3.0` prints `3`.
+- `json_encode of [3.0, 0.1]` gives `[3,0.1]`.
+- `9007199254740993 == 9007199254740992` is `true`: the lexer rounds.
+- `store_get of [s, "c", 1.9]` finds record `"1"` (a silent truncating key).
+- `store_get of [s, "c", 4294967297]` returns `null`, because the `(int)`
+ cast is out of range (a pre-existing defect, filed as
+ [#1760](https://github.com/InauguralSystems/EigenScript/issues/1760); R1.6's
+ `eigs_int_arg` for the key closes it if it is still open then).
+
+
+| builtin | class | loss | q7 | note |
+|---|---|---|---|---|
+| `abs` | int-preserving | LOSS | - | Lua math.abs (integer/float); abs of INT64_MIN raises (N4b) |
+| `acos` | float-producing | - | - | Lua math.acos |
+| `add` | int-preserving | LOSS | - | scalar arm mirrors `+` (overflow raises, (A)); buffer arms stay float |
+| `append` | other | - | - | appends any value; VAL_NUM appears only as a diagnostic default |
+| `asin` | float-producing | - | - | Lua math.asin |
+| `assert` | other | - | - | message via value_to_string (N6) |
+| `atan` | float-producing | - | - | Lua math.atan |
+| `atan2` | float-producing | - | - | Lua math.atan(y, x) |
+| `audio_capture_open` | int-requiring | - | - | freq (samples per second): N5, then the C int range; channels: N5, then the domain 1..255 (Q1; R1.6e-D1), checked before SDL/device/tape operations; returns an int device id |
+| `audio_envelope` | float-producing | - | magnitude | float samples; the attack and release times are continuous durations: a sample count the C int cannot hold raises (Q7 magnitude) |
+| `audio_gain` | float-producing | - | - | float samples |
+| `audio_mix` | float-producing | - | - | float samples |
+| `audio_music_play` | int-requiring | - | - | loop count: N5, then the C int range (Q1) |
+| `audio_music_volume` | other | - | bounded | the volume is a continuous gain: clamped to SDL_mixer's 0..128, then rounded (Q7 bounded) |
+| `audio_noise` | float-producing | - | magnitude | float samples; the duration is continuous: a sample count the C int cannot hold raises (Q7 magnitude) |
+| `audio_open` | int-requiring | - | - | freq (samples per second): N5, then the C int range; channels: N5, then the domain 1..255 (Q1; R1.6e-D1), checked before SDL/device/tape operations; returns an int device id |
+| `audio_pause` | int-requiring | - | - | the pause flag is discrete: N5 (Q1; today `(int)0.5` silently un-pauses) |
+| `audio_play` | other | - | bounded | the samples are continuous: clamped to [-1, 1], then rounded to int16 (Q7 bounded; today they truncate); returns a channel number |
+| `audio_play_loop` | int-requiring | - | bounded | loop count: N5, then the C int range (Q1); the samples are continuous: clamped and rounded to int16 (Q7 bounded); returns a channel |
+| `audio_queue_size` | int-preserving | - | - | byte count (computed in a double, below 2^53) |
+| `audio_saw` | float-producing | - | magnitude | float samples; the duration is continuous: a sample count the C int cannot hold raises (Q7 magnitude) |
+| `audio_sine` | float-producing | - | magnitude | float samples; the duration is continuous: a sample count the C int cannot hold raises (Q7 magnitude) |
+| `audio_square` | float-producing | - | magnitude | float samples; the duration is continuous: a sample count the C int cannot hold raises (Q7 magnitude) |
+| `audio_stop` | int-requiring | - | - | channel number |
+| `audio_stream_open` | int-requiring | - | - | freq (samples per second): N5, then the C int range; channels: N5, then the domain 1..255 (Q1; R1.6e-D1), checked before SDL/device/tape operations; returns an int device id |
+| `audio_stream_push` | other | - | bounded | the samples are continuous: clamped to [-1, 1], then rounded to int16 (Q7 bounded) |
+| `audio_stream_queued` | int-preserving | - | - | sample count |
+| `audio_sweep` | float-producing | - | magnitude | float samples; frequencies and amplitude stay floats; the duration is continuous: a sample count the C int cannot hold raises (Q7 magnitude); the wave selector is integer-valued: N5 (Q1) |
+| `audio_volume` | int-requiring | - | bounded | channel index: N5 (Q1); the volume is a continuous gain: clamped to the mixer's range (Q7 bounded) |
+| `bit_and` | int-requiring | - | - | Lua `&` (§3.4.2): exact int64 operands, exact int64-pattern result (N8, R1.5) |
+| `bit_not` | int-requiring | - | - | Lua unary `~` |
+| `bit_or` | int-requiring | - | - | Lua `\|` |
+| `bit_shl` | int-requiring | - | - | Lua `<<` (R1.5): count >=64 or <=-64 gives 0; a negative count shifts logically right |
+| `bit_shr` | int-requiring | - | - | Lua `>>` (N8, R1.5) is logical; count >=64 or <=-64 gives 0; a negative count shifts left |
+| `bit_xor` | int-requiring | - | - | Lua binary `~` |
+| `buf_copy` | int-requiring | - | - | offsets and counts |
+| `buf_deinterleave` | int-requiring | - | - | channel count and index |
+| `buf_dot` | float-producing | - | - | buffer dot product |
+| `buf_fill` | float-producing | - | - | fills float storage; offsets by N5 |
+| `buf_from_list` | float-producing | - | - | buffer storage is float: an int element converts (Q2) |
+| `buf_from_pcm16le` | int-requiring | - | - | its input elements are byte values: each is read through the Q1 byte reader (N5, then 0..255), so a byte of 300 or 1.5 raises `value` (#1758 comment 6174964306; today 300 and 1.5 decode silently); the decoded samples are floats |
+| `buf_get` | int-requiring | - | - | index; returns the float element |
+| `buf_len` | int-preserving | - | - | element count |
+| `buf_mix` | float-producing | - | - | float storage |
+| `buf_peak` | float-producing | - | - | float max abs |
+| `buf_resample_linear` | float-producing | - | - | float storage |
+| `buf_scale_range` | float-producing | - | - | float storage |
+| `buf_set` | int-requiring | - | - | index; the value converts to float storage (Q2) |
+| `buf_to_pcm16le` | other | - | bounded | the samples are continuous: clamped to [-1, 1] and rounded (Q7 bounded, today's behaviour); the result is byte values |
+| `buffer` | int-requiring | - | - | size or [rows, cols] |
+| `build_corpus` | int-requiring | - | - | counts; returns counts |
+| `ceil` | int-preserving | LOSS | - | Lua math.ceil: an int when the result fits int64, else a float |
+| `channel` | int-preserving | - | - | handle dict with int id/gen fields |
+| `channel_closed` | int-requiring | - | - | channel handle ids |
+| `char_at` | int-requiring | - | - | string position (Lua string.sub) |
+| `chr` | int-requiring | - | - | byte 1..255, already exactness-checked (Lua string.char) |
+| `clock_unix` | float-producing | - | - | fractional seconds |
+| `close_channel` | int-requiring | - | - | channel handle ids |
+| `copy_into` | int-requiring | - | - | offset; values convert to float storage |
+| `cos` | float-producing | - | - | Lua math.cos |
+| `db_execute` | other | LOSS | - | binds every numeric parameter with `%g`, 6 significant digits (ext_db.c:57-58, #1762). R1.7: the N6/N14 writer |
+| `db_query_json` | other | LOSS | - | binds parameters with `%g` (#1762); encodes rows (N14) |
+| `db_query_value` | other | LOSS | - | binds parameters with `%g` (#1762); decodes a numeric column with strtod, so integer text beyond 2^53 rounds |
+| `dispatch` | int-requiring | - | - | integer table key, already exactness-checked |
+| `divide` | float-producing | - | - | mirrors `/` (Lua §3.4.1: always float) |
+| `dot` | float-producing | - | - | buffer dot product |
+| `eigen_eval_loss` | float-producing | - | - | token ids are indices; the loss is a float |
+| `eigen_generate` | int-requiring | - | - | token ids and max_tokens are integer-valued (N5, Q1); temperature and top_p are floats |
+| `eigen_model_load` | other | - | - | parses model JSON numbers with strtod |
+| `exec_capture` | other | - | magnitude | the timeout is a continuous duration in seconds: a poll interval the C int cannot hold raises (Q7 magnitude); returns exit status ints |
+| `exit` | int-requiring | - | - | exit code: N5, then the C int range (Q1) |
+| `exp` | float-producing | - | - | Lua math.exp |
+| `f64_from_bytes` | int-requiring | - | - | 8 byte values: N5, then 0..255 (Q1; the modulo-256 wrap is gone); the float result's NaN is canonical (N12) |
+| `f64_to_bytes` | other | - | - | float to 8 byte ints; f64_bits (N12) is the int-valued sibling |
+| `fill` | int-requiring | - | - | count |
+| `floor` | int-preserving | LOSS | - | Lua math.floor: an int when the result fits int64, else a float |
+| `gather` | int-requiring | - | - | each index, from a list or an index buffer, is integer-valued: N5 raises on 1.5 (today `flat_index_at` truncates: `gather of [t, ib]` with `ib[0]` = 1.5 returns `[11, 22]`, probed); returns elements unchanged |
+| `get_at` | int-requiring | - | - | index, row and col; returns the element |
+| `get_observer_thresholds` | float-producing | - | - | observer thresholds (O3 drops h_low) |
+| `gfx_circle` | other | - | geometry,bounded | centre and radius are continuous geometry: the circle is clipped in float space against the drawable area, then rounded; the colour channels are continuous and bounded: clamped to 0..255, then rounded (Q7) |
+| `gfx_clear` | other | - | bounded | the colour channels are continuous and bounded: clamped to 0..255, then rounded (Q7) |
+| `gfx_clip` | other | - | geometry | the clip rect is continuous geometry: intersected with the drawable area in float space, then rounded (Q7) |
+| `gfx_delay` | other | - | magnitude | the milliseconds are a continuous duration: rounded; a delay Uint32 cannot hold raises (Q7 magnitude) |
+| `gfx_fb` | int-requiring | - | geometry | w and h lay out the pixel buffer (integer-valued: N5, Q1); its elements are palette bytes read through the Q1 byte reader; dx, dy and scale are continuous geometry: the destination rect is clipped in float space, then rounded (Q7) |
+| `gfx_line` | other | - | geometry,bounded | the endpoints are continuous geometry: the segment is clipped in float space against the drawable area, then rounded; the colour is bounded (Q7) |
+| `gfx_open` | other | - | magnitude | window width and height are continuous sizes with no drawable area yet to clip against: rounded, and a size the C int cannot hold raises (Q7 magnitude) |
+| `gfx_point` | other | - | geometry,bounded | the position is continuous geometry: a point outside the drawable area draws nothing; the colour is bounded (Q7) |
+| `gfx_poll` | int-preserving | - | - | event fields x, y, scancode, button are ints |
+| `gfx_read` | other | - | geometry | the origin is continuous geometry: rounded, and a point outside the drawable area reads nothing (Q7); returns the channel values as ints |
+| `gfx_rect` | other | - | geometry,bounded | the rect is continuous geometry: intersected with the drawable area in float space, then rounded; the colour is bounded (Q7) |
+| `gfx_rrect` | other | - | geometry,bounded | the rect and radius are continuous geometry: clipped in float space, then rounded; the colour is bounded (Q7) |
+| `gfx_text` | other | - | geometry,bounded | the position and scale are continuous geometry: each glyph cell is clipped in float space, then rounded; the colour is bounded (Q7) |
+| `gfx_text_height` | int-preserving | - | magnitude | pixel height, an int; the scale is continuous, and a measurement has no drawable area to clip against: rounded, and a scale the C int cannot hold raises (Q7 magnitude) |
+| `gfx_text_width` | int-preserving | - | magnitude | pixel width, an int; the scale is continuous, and a measurement has no drawable area to clip against: rounded, and a scale the C int cannot hold raises (Q7 magnitude) |
+| `gfx_ticks` | int-preserving | - | - | SDL ticks (Uint32 ms) |
+| `hex` | int-requiring | LOSS | - | refuses above 2^53 by a double compare, so int 2^53+1 reads as 2^53 and passes silently |
+| `http_early_bind` | int-requiring | - | - | port |
+| `http_post` | other | LOSS | - | header JSON ints decode exactly (N14, B6); lossy before R1.6 |
+| `http_route` | other | - | - | formats via value_to_string |
+| `http_route_authed` | other | - | - | formats via value_to_string |
+| `http_serve` | int-requiring | - | - | port |
+| `http_static` | other | - | - | formats via value_to_string |
+| `index_of` | int-preserving | - | - | byte index or -1 |
+| `inflate` | int-requiring | - | - | byte values: N5, then 0..255 (Q1; the modulo-256 wrap is gone) |
+| `join` | other | - | - | elements via value_to_string (N6) |
+| `json_build` | other | LOSS | - | N14 encoder; exact int text arm written in R1.2, live from R1.3; float text stays on the bridge until R1.7 |
+| `json_decode` | other | LOSS | - | N14 decoder; integer text decodes through strtod (a double) |
+| `json_encode` | other | LOSS | - | N13/N14 encoder; reads values through eigs_num_arg; its `default:` writes null (builtins.c:998) |
+| `json_path` | other | LOSS | - | exact int text reader written in R1.2; decoder int production remains R1.6, float text R1.7 (N14, N6) |
+| `leaky_relu` | float-producing | - | - | ML activation (float tensors; no Lua counterpart) |
+| `len` | int-preserving | - | - | count (Lua `#` is an integer) |
+| `list_contains` | other | - | - | equality via values_equal (N2) |
+| `list_index_of` | int-preserving | - | - | returns an index; equality via values_equal (N2) |
+| `list_insert_at` | int-requiring | - | - | position (Lua table.insert) |
+| `list_remove_at` | int-requiring | - | - | position (Lua table.remove) |
+| `list_slice` | int-requiring | - | - | start and end indices |
+| `list_truncate` | int-requiring | - | - | new length |
+| `log` | float-producing | - | - | Lua math.log; log of 0 is -inf (N11) |
+| `log_softmax` | float-producing | - | - | ML (float) |
+| `matmul` | float-producing | - | - | float tensors |
+| `matmul_at` | float-producing | - | - | float tensors |
+| `matmul_bt` | float-producing | - | - | float tensors |
+| `max` | int-preserving | LOSS | - | Lua math.max returns the winning argument with its own subtype; empty input raises `value` in both call forms and strict modes (R1.6b-D2, #1895); a NaN argument makes the result NaN (Q8: IEEE 754-2019 maximum) |
+| `mean` | float-producing | - | - | arithmetic mean |
+| `min` | int-preserving | LOSS | - | Lua math.min; empty input raises `value` in both call forms and strict modes (R1.6b-D2, #1895); a NaN argument makes the result NaN (Q8: IEEE 754-2019 minimum) |
+| `model_load_weights` | other | - | - | alias of eigen_model_load |
+| `monotonic_ms` | float-producing | - | - | fractional milliseconds |
+| `monotonic_ns` | int-preserving | LOSS | - | exact below 2^53 ns (104 days of uptime), a rounded double after; an exact int under N1 |
+| `multiply` | int-preserving | LOSS | - | scalar arm mirrors `*` (overflow raises, (A)); buffer arms float |
+| `native_train_step_builtin` | int-requiring | - | - | token ids are integer-valued (N5, Q1); lr is a float (a numeric string is accepted through strtod) |
+| `nearest_in_range` | float-producing | - | - | geometry floats; the `index` field is an int |
+| `nearest_in_range_all` | float-producing | - | - | geometry floats; the `index` field is an int |
+| `negative` | int-preserving | LOSS | - | mirrors unary `-`; negating INT64_MIN raises (N4b) |
+| `net_accept` | int-requiring | - | - | socket handle |
+| `net_close` | int-requiring | - | - | socket handle |
+| `net_dial` | int-requiring | - | - | port; returns a handle |
+| `net_listen` | int-requiring | - | - | port; returns a handle |
+| `net_port` | int-requiring | - | - | socket handle; returns a port |
+| `net_recv` | int-requiring | - | - | handle and max bytes |
+| `net_send` | int-requiring | - | - | the handle and the byte values: N5, bytes then 0..255 (Q1; the modulo-256 wrap is gone) |
+| `norm` | float-producing | - | - | L2 norm |
+| `num` | other | LOSS | - | N10 parser: integer text decodes through strtod today (a double) |
+| `num_copy` | int-preserving | LOSS | - | copies a number and must keep its subtype; today re-boxes through a double |
+| `numerical_grad` | float-producing | - | - | finite differences |
+| `numerical_grad_cols` | float-producing | - | - | finite differences; column indices int |
+| `numerical_grad_rows` | float-producing | - | - | finite differences; row indices int |
+| `observe` | other | - | - | observer readouts (O4) |
+| `ord` | int-preserving | - | - | byte code (Lua string.byte) |
+| `pi` | float-producing | - | - | Lua math.pi |
+| `pow` | float-producing | - | - | mirrors `^` (Lua §3.4.1: exponentiation is always float) |
+| `ppu_render_frame` | int-requiring | - | - | reads the emulated memory buffer through `buffer_read_byte`: byte values by N5, then 0..255 (Q1; the modulo-256 wrap is gone) |
+| `print` | other | - | - | N6 printing via value_to_string |
+| `random` | float-producing | - | - | Lua math.random() in [0,1) |
+| `random_hex` | int-requiring | - | - | digit count |
+| `random_int` | int-requiring | LOSS | - | Lua math.random(m, n) is an integer; bounds read as doubles today |
+| `random_normal` | float-producing | - | - | dims int; samples float |
+| `range` | int-preserving | - | - | bounds by N5; the elements are ints (floats today, inside int range) |
+| `read_bytes` | int-preserving | - | - | byte values 0..255 |
+| `read_bytes_buf` | int-requiring | - | - | cap and length; bytes stored as floats |
+| `read_text` | other | - | - | string result; numeric over-cap size is internal tape metadata (#1668), with no numeric argument or successful numeric result |
+| `record_history` | other | - | - | truthiness flag in; previous flag out |
+| `recv` | int-requiring | - | - | channel handle ids |
+| `recv_timeout` | int-requiring | - | magnitude | the channel handle: N5 (Q1); the milliseconds are a continuous duration: a timeout the C long cannot hold raises (Q7 magnitude) |
+| `relu` | float-producing | - | - | ML activation (float tensors; no Lua counterpart) |
+| `reshape` | int-requiring | - | - | rows and cols |
+| `round` | int-preserving | LOSS | - | no Lua counterpart; follows floor and ceil (Q4) |
+| `sandbox_run` | int-requiring | - | - | iteration, byte and work limits; returns line ints |
+| `scan_int_tokens` | int-preserving | LOSS | - | integers accumulate in a double |
+| `scan_ints` | int-preserving | LOSS | - | integers accumulate in a double |
+| `scatter_add` | float-producing | - | - | indices are exactness-checked today, but raise an index error for 1.5; R1.6 reads them through the N5 index reader (`value`); float accumulation |
+| `seed_random` | int-requiring | LOSS | - | `(long)` of a double; Lua randomseed takes integers |
+| `send` | int-requiring | - | - | channel handle ids |
+| `set_at` | int-requiring | - | - | index, row and col; a buffer value converts to float |
+| `set_exit_code` | int-requiring | - | - | graceful exit code: N5, then 0..255 (Q1); main #1909 already checks fractions and range; R1.6-S2 stages the bridge migration with exit in round c |
+| `sgd_update` | float-producing | - | - | float update |
+| `sgd_update_cols` | float-producing | - | - | column indices int |
+| `sgd_update_rows` | float-producing | - | - | row indices int |
+| `shape` | int-preserving | - | - | dimension counts |
+| `shared_delete` | int-preserving | - | - | removed count |
+| `shared_incr` | int-preserving | LOSS | - | shared counter: delta and stored value in doubles |
+| `shared_size` | int-preserving | - | - | entry count |
+| `sign_extend` | int-requiring | LOSS | - | N5 exact int64 arguments and result; existing subtraction is checked under (A), raising `value` naming sign_extend on underflow (R1.6b-D1, #1894); high-bit masking is separate (#1896) |
+| `sin` | float-producing | - | - | Lua math.sin |
+| `softmax` | float-producing | - | - | ML (float) |
+| `sort` | other | LOSS | - | exact int comparator written in R1.2, live from R1.3 (N2.sort-exact); from R2.2 floats sort by IEEE totalOrder, NaN after +inf (Q8) |
+| `sort_by` | other | LOSS | - | numeric key kind retained for exact comparison in R1.2, live from R1.3; from R2.2 by IEEE totalOrder, NaN after +inf (Q8) |
+| `spawn` | int-preserving | - | - | handle dict with int id/gen fields |
+| `sqrt` | float-producing | - | - | Lua math.sqrt; sqrt of -1 is NaN (N11) |
+| `state_at` | int-requiring | - | - | step index |
+| `store_close` | int-requiring | - | - | handle id/gen |
+| `store_collections` | int-requiring | - | - | handle id |
+| `store_count` | int-requiring | - | - | handle and page ids; returns a count |
+| `store_delete` | int-requiring | LOSS | - | key formatted `%d` of an `(int)` cast: 1.9 finds "1"; keys above INT_MAX are UB (probed) |
+| `store_drop` | int-requiring | - | - | handle and page ids |
+| `store_get` | int-requiring | LOSS | - | same key cast as store_delete; decodes records (N14) |
+| `store_open` | int-preserving | - | - | handle dict with int id/gen fields |
+| `store_put` | other | LOSS | - | the record encoder writes a non-int32 number with `%.15g`: 1234567890123456 is stored as 1234567890123460 and 0.1 + 0.2 as 0.3, on main and the tag (#1762). R1.7: the N6/N14 writer; R2.3: refusal inside the encoder |
+| `store_query` | other | LOSS | - | decodes records; integer text through strtod (N14) |
+| `store_update` | other | LOSS | - | same key cast as store_delete; re-puts through the `%.15g` encoder (#1762); N13/N14 |
+| `str` | other | - | - | N6 printing via value_to_string |
+| `str_from_bytes` | int-requiring | - | - | byte values: N5, then 0..255 (Q1; Lua string.char raises on 65.5 and on 256) |
+| `stream_open` | int-requiring | - | - | element count, already floor-checked |
+| `stream_write` | float-producing | - | - | writes a raw f64 |
+| `substr` | int-requiring | - | - | start and length (Lua string.sub) |
+| `subtract` | int-preserving | LOSS | - | scalar arm mirrors `-` (overflow raises, (A)) |
+| `sum` | int-preserving | LOSS | - | an all-int input sums exactly, and overflow raises (A); today a guarded double sum |
+| `tan` | float-producing | - | - | Lua math.tan |
+| `task_alive` | int-requiring | - | - | task handle (packed below 2^53, exactness-checked) |
+| `task_detach` | int-requiring | - | - | task handle |
+| `task_join` | int-requiring | - | - | task handle |
+| `task_kill` | int-requiring | - | - | task handle |
+| `task_sched_seed` | int-requiring | LOSS | - | `(int64_t)` of a double |
+| `task_self` | int-preserving | - | - | task handle |
+| `task_send` | int-requiring | - | - | task handle |
+| `task_sleep` | other | - | - | milliseconds of virtual time, kept as a float: no C integer conversion, so Q7 does not apply |
+| `task_spawn` | int-preserving | - | - | returns a task handle |
+| `tensor_load` | float-producing | - | - | float tensor; shape ints |
+| `tensor_save` | float-producing | - | - | writes float cells |
+| `text_builder_append` | other | - | - | formats via value_to_string (N6) |
+| `text_builder_append_line` | other | - | - | formats via value_to_string (N6) |
+| `thread_join` | int-requiring | - | - | thread handle id/gen |
+| `throw` | other | - | - | message via value_to_string |
+| `token_name` | int-requiring | - | - | token id |
+| `try_recv` | int-requiring | - | - | channel handle ids |
+| `num_type` | other | - | - | N7: int/float kind, null for a non-number |
+| `type` | other | - | - | `type of` an int or a float is "num" ((B)) |
+| `usleep` | other | - | magnitude | the microseconds are a continuous duration: rounded; a sleep the C int cannot hold raises (Q7 magnitude) |
+| `vm_run_bytecode` | int-requiring | - | - | opcode bytes and ABI numbers |
+| `write` | other | - | - | N6 printing |
+| `write_bytes` | int-requiring | - | - | byte values: N5, then 0..255 (Q1; the modulo-256 wrap is gone); returns bytes written |
+| `zeros` | int-requiring | - | - | size or [rows, cols]; elements are float 0.0 |
+| `zeros_like` | float-producing | - | - | float zeros |
+| `i8_wrapping_add` | int-requiring | - | - | N9: i8 wrapping add; N5 then type range without masking; exact int result (Rust std) |
+| `i8_wrapping_sub` | int-requiring | - | - | N9: i8 wrapping sub; N5 then type range without masking; exact int result (Rust std) |
+| `i8_wrapping_mul` | int-requiring | - | - | N9: i8 wrapping mul; N5 then type range without masking; exact int result (Rust std) |
+| `i8_checked_add` | int-requiring | - | - | N9: i8 checked add; N5 then type range without masking; exact int result (Rust std) |
+| `i8_checked_sub` | int-requiring | - | - | N9: i8 checked sub; N5 then type range without masking; exact int result (Rust std) |
+| `i8_checked_mul` | int-requiring | - | - | N9: i8 checked mul; N5 then type range without masking; exact int result (Rust std) |
+| `i8_saturating_add` | int-requiring | - | - | N9: i8 saturating add; N5 then type range without masking; exact int result (Rust std) |
+| `i8_saturating_sub` | int-requiring | - | - | N9: i8 saturating sub; N5 then type range without masking; exact int result (Rust std) |
+| `i8_saturating_mul` | int-requiring | - | - | N9: i8 saturating mul; N5 then type range without masking; exact int result (Rust std) |
+| `i16_wrapping_add` | int-requiring | - | - | N9: i16 wrapping add; N5 then type range without masking; exact int result (Rust std) |
+| `i16_wrapping_sub` | int-requiring | - | - | N9: i16 wrapping sub; N5 then type range without masking; exact int result (Rust std) |
+| `i16_wrapping_mul` | int-requiring | - | - | N9: i16 wrapping mul; N5 then type range without masking; exact int result (Rust std) |
+| `i16_checked_add` | int-requiring | - | - | N9: i16 checked add; N5 then type range without masking; exact int result (Rust std) |
+| `i16_checked_sub` | int-requiring | - | - | N9: i16 checked sub; N5 then type range without masking; exact int result (Rust std) |
+| `i16_checked_mul` | int-requiring | - | - | N9: i16 checked mul; N5 then type range without masking; exact int result (Rust std) |
+| `i16_saturating_add` | int-requiring | - | - | N9: i16 saturating add; N5 then type range without masking; exact int result (Rust std) |
+| `i16_saturating_sub` | int-requiring | - | - | N9: i16 saturating sub; N5 then type range without masking; exact int result (Rust std) |
+| `i16_saturating_mul` | int-requiring | - | - | N9: i16 saturating mul; N5 then type range without masking; exact int result (Rust std) |
+| `i32_wrapping_add` | int-requiring | - | - | N9: i32 wrapping add; N5 then type range without masking; exact int result (Rust std) |
+| `i32_wrapping_sub` | int-requiring | - | - | N9: i32 wrapping sub; N5 then type range without masking; exact int result (Rust std) |
+| `i32_wrapping_mul` | int-requiring | - | - | N9: i32 wrapping mul; N5 then type range without masking; exact int result (Rust std) |
+| `i32_checked_add` | int-requiring | - | - | N9: i32 checked add; N5 then type range without masking; exact int result (Rust std) |
+| `i32_checked_sub` | int-requiring | - | - | N9: i32 checked sub; N5 then type range without masking; exact int result (Rust std) |
+| `i32_checked_mul` | int-requiring | - | - | N9: i32 checked mul; N5 then type range without masking; exact int result (Rust std) |
+| `i32_saturating_add` | int-requiring | - | - | N9: i32 saturating add; N5 then type range without masking; exact int result (Rust std) |
+| `i32_saturating_sub` | int-requiring | - | - | N9: i32 saturating sub; N5 then type range without masking; exact int result (Rust std) |
+| `i32_saturating_mul` | int-requiring | - | - | N9: i32 saturating mul; N5 then type range without masking; exact int result (Rust std) |
+| `i64_wrapping_add` | int-requiring | - | - | N9: i64 wrapping add; N5 then type range without masking; exact int result (Rust std) |
+| `i64_wrapping_sub` | int-requiring | - | - | N9: i64 wrapping sub; N5 then type range without masking; exact int result (Rust std) |
+| `i64_wrapping_mul` | int-requiring | - | - | N9: i64 wrapping mul; N5 then type range without masking; exact int result (Rust std) |
+| `i64_checked_add` | int-requiring | - | - | N9: i64 checked add; N5 then type range without masking; exact int result (Rust std) |
+| `i64_checked_sub` | int-requiring | - | - | N9: i64 checked sub; N5 then type range without masking; exact int result (Rust std) |
+| `i64_checked_mul` | int-requiring | - | - | N9: i64 checked mul; N5 then type range without masking; exact int result (Rust std) |
+| `i64_saturating_add` | int-requiring | - | - | N9: i64 saturating add; N5 then type range without masking; exact int result (Rust std) |
+| `i64_saturating_sub` | int-requiring | - | - | N9: i64 saturating sub; N5 then type range without masking; exact int result (Rust std) |
+| `i64_saturating_mul` | int-requiring | - | - | N9: i64 saturating mul; N5 then type range without masking; exact int result (Rust std) |
+| `u8_wrapping_add` | int-requiring | - | - | N9: u8 wrapping add; N5 then type range without masking; exact int result (Rust std) |
+| `u8_wrapping_sub` | int-requiring | - | - | N9: u8 wrapping sub; N5 then type range without masking; exact int result (Rust std) |
+| `u8_wrapping_mul` | int-requiring | - | - | N9: u8 wrapping mul; N5 then type range without masking; exact int result (Rust std) |
+| `u8_checked_add` | int-requiring | - | - | N9: u8 checked add; N5 then type range without masking; exact int result (Rust std) |
+| `u8_checked_sub` | int-requiring | - | - | N9: u8 checked sub; N5 then type range without masking; exact int result (Rust std) |
+| `u8_checked_mul` | int-requiring | - | - | N9: u8 checked mul; N5 then type range without masking; exact int result (Rust std) |
+| `u8_saturating_add` | int-requiring | - | - | N9: u8 saturating add; N5 then type range without masking; exact int result (Rust std) |
+| `u8_saturating_sub` | int-requiring | - | - | N9: u8 saturating sub; N5 then type range without masking; exact int result (Rust std) |
+| `u8_saturating_mul` | int-requiring | - | - | N9: u8 saturating mul; N5 then type range without masking; exact int result (Rust std) |
+| `u16_wrapping_add` | int-requiring | - | - | N9: u16 wrapping add; N5 then type range without masking; exact int result (Rust std) |
+| `u16_wrapping_sub` | int-requiring | - | - | N9: u16 wrapping sub; N5 then type range without masking; exact int result (Rust std) |
+| `u16_wrapping_mul` | int-requiring | - | - | N9: u16 wrapping mul; N5 then type range without masking; exact int result (Rust std) |
+| `u16_checked_add` | int-requiring | - | - | N9: u16 checked add; N5 then type range without masking; exact int result (Rust std) |
+| `u16_checked_sub` | int-requiring | - | - | N9: u16 checked sub; N5 then type range without masking; exact int result (Rust std) |
+| `u16_checked_mul` | int-requiring | - | - | N9: u16 checked mul; N5 then type range without masking; exact int result (Rust std) |
+| `u16_saturating_add` | int-requiring | - | - | N9: u16 saturating add; N5 then type range without masking; exact int result (Rust std) |
+| `u16_saturating_sub` | int-requiring | - | - | N9: u16 saturating sub; N5 then type range without masking; exact int result (Rust std) |
+| `u16_saturating_mul` | int-requiring | - | - | N9: u16 saturating mul; N5 then type range without masking; exact int result (Rust std) |
+| `u32_wrapping_add` | int-requiring | - | - | N9: u32 wrapping add; N5 then type range without masking; exact int result (Rust std) |
+| `u32_wrapping_sub` | int-requiring | - | - | N9: u32 wrapping sub; N5 then type range without masking; exact int result (Rust std) |
+| `u32_wrapping_mul` | int-requiring | - | - | N9: u32 wrapping mul; N5 then type range without masking; exact int result (Rust std) |
+| `u32_checked_add` | int-requiring | - | - | N9: u32 checked add; N5 then type range without masking; exact int result (Rust std) |
+| `u32_checked_sub` | int-requiring | - | - | N9: u32 checked sub; N5 then type range without masking; exact int result (Rust std) |
+| `u32_checked_mul` | int-requiring | - | - | N9: u32 checked mul; N5 then type range without masking; exact int result (Rust std) |
+| `u32_saturating_add` | int-requiring | - | - | N9: u32 saturating add; N5 then type range without masking; exact int result (Rust std) |
+| `u32_saturating_sub` | int-requiring | - | - | N9: u32 saturating sub; N5 then type range without masking; exact int result (Rust std) |
+| `u32_saturating_mul` | int-requiring | - | - | N9: u32 saturating mul; N5 then type range without masking; exact int result (Rust std) |
+| `u64_wrapping_add` | int-requiring | - | - | N9: u64 wrapping add; N5 then type range without masking; exact int result (Rust std) |
+| `u64_wrapping_sub` | int-requiring | - | - | N9: u64 wrapping sub; N5 then type range without masking; exact int result (Rust std) |
+| `u64_wrapping_mul` | int-requiring | - | - | N9: u64 wrapping mul; N5 then type range without masking; exact int result (Rust std) |
+| `u64_checked_add` | int-requiring | - | - | N9: u64 checked add; N5 then type range without masking; exact int result (Rust std) |
+| `u64_checked_sub` | int-requiring | - | - | N9: u64 checked sub; N5 then type range without masking; exact int result (Rust std) |
+| `u64_checked_mul` | int-requiring | - | - | N9: u64 checked mul; N5 then type range without masking; exact int result (Rust std) |
+| `u64_saturating_add` | int-requiring | - | - | N9: u64 saturating add; N5 then type range without masking; exact int result (Rust std) |
+| `u64_saturating_sub` | int-requiring | - | - | N9: u64 saturating sub; N5 then type range without masking; exact int result (Rust std) |
+| `u64_saturating_mul` | int-requiring | - | - | N9: u64 saturating mul; N5 then type range without masking; exact int result (Rust std) |
+| `u64_compare` | int-requiring | - | - | N9: N5 int64 bit patterns; unsigned order gives int -1, 0 or 1 (Rust std) |
+| `u64_div` | int-requiring | - | - | N9: N5 int64 bit patterns; unsigned quotient as int64 bits; zero raises value (Rust std) |
+| `u64_rem` | int-requiring | - | - | N9: N5 int64 bit patterns; unsigned remainder as int64 bits; zero raises value (Rust std) |
+| `u64_to_string` | int-requiring | - | - | N9: N5 int64 bit pattern to unsigned decimal string (Rust std) |
+| `fround` | float-producing | - | - | N9: int converts to f64 first; f64 to f32 to f64, float result; current result guard until R2.2 |
+
+
+**Builtins to add**, outside the checked table until they exist:
+- `num_type` (N7);
+- `f64_bits` and `f64_from_bits`, plus buffer bits get/set (N12);
+
+**N9 width family (R1.6g, in the checked table above):**
+
+The decided width family ([#1758 comment 6304880619](https://github.com/InauguralSystems/EigenScript/issues/1758#issuecomment-6304880619)):
+ `__`, with types `i8 i16 i32 i64 u8 u16 u32 u64`, modes
+ `wrapping checked saturating`, and ops `add sub mul` (72 builtins, one
+ table-driven implementation in `builtins_int.c`). Examples:
+ `i64_wrapping_add`, `u8_checked_add`, `i8_saturating_sub`.
+ N5 accepts integral floats and refuses fractions; operands must then fit
+ the named type's range, without masking. `i64` and `u64` accept every
+ int64; u64 uses its int64 bit pattern (`-1` is u64 max). Results are ints:
+ wrapping reduces modulo the width, checked raises `value` naming the
+ builtin on overflow, and saturating clamps to the type's range.
+- `u64_compare` (returns -1, 0 or 1), `u64_div`, `u64_rem`, and unary
+ `u64_to_string`; a zero divisor raises. Rust's std is the oracle, with
+ `as u64` / `as i64` mapping the bit pattern. No width-family negation,
+ shifts, division, remainder or power are added.
+- `fround`: `(double)(float)x`, converting an int first and returning a
+ float (JavaScript's `Math.fround`). Together these are the 77 builtins added by R1.6g.
+
+---
+
+## 7. Semantics-to-sites map
+
+Each item lists the files and functions that implement it. The site lists
+inside each function are in `docs/design/phase2-inventory.txt`. All line
+numbers are at `0740af03`.
+
+- **N1 (int literals; decimal outside int64 is a float; hex wraps):**
+ - lexer.c:717-753 (number lexing, hex accumulates in a double today);
+ the token's `num_val` (eigenscript.h:188);
+ - parser.c:791 (`TOK_NUM` to `AST_NUM`, `data.num`);
+ - compiler.c:2071-2077 (`AST_NUM` emission, reading the renamed payload):
+ from R1.3 an int literal emits the new `OP_INT_ZERO`/`OP_INT_ONE`/
+ `OP_INT_CONST`, and a float literal keeps `OP_NUM_ZERO`/`OP_NUM_ONE`/
+ `OP_CONST` (§2.7); compiler.c:2929/2985/3030 (index fast paths read
+ `data.num`);
+ - vm.h `OpCode` (the three opcodes appended, each `/*obs:NONE*/`), and the
+ exhaustive switches that must name them: `op_name` (chunk.c:337),
+ `op_verify_operands` (chunk.c:466), `op_verify_stack_effect`
+ (chunk.c:615), `op_stack_effect` (compiler.c:305); the interpreter's
+ dispatch table (vm.c:3091) and three new CASEs; `tests/test_opcode_abi.c`;
+ - chunk.c:193-260 (constant pool dedup: `const_hash_value`, `const_equal`,
+ `chunk_add_constant_ex`, which must key on kind and bits);
+ - vm.c:3238 `CASE(CONST)` and 3257 `CASE(NUM_ZERO)`/`NUM_ONE`: renamed,
+ float meaning unchanged;
+ - the JIT: the scanner refuses the new opcodes in round 1 (round 5 emits
+ them); its `NUM_ZERO`/`NUM_ONE`/`CONST` emitters (jit.c:2797/2816) keep
+ their float meaning (§4.2);
+ - the leaf evaluator: its scanner refuses the new opcodes (chunk.c:1129,
+ the `default:`); its `CONST`, `NUM_ZERO` and `NUM_ONE` arms
+ (vm.c:1866-1877) keep their float meaning (§4.5);
+ - lint.c, lint_host.c and eigenlsp.c (the `AST_NUM` type knowledge, 34
+ sites);
+ - `src/freestanding/mini_strtod.c`;
+ - ouroboros `src/frontend.eigs` (the AOT's lexer).
+- **N2 (subtype visible; exact comparison across kinds):**
+ - vm.c:3579 `CASE(EQ)`, 3600 `CASE(NE)` and 3621 `NUM_CMP`;
+ - eigenscript.c:2849 `values_equal_impl`;
+ - builtins.c:5868 `sort_cmp_num`, and `sort_by`, `list_contains`,
+ `list_index_of`;
+ - JIT comparisons bail on SMI (§4.1).
+ - The exact int/float compare:
+ - if the float is NaN, the result is unordered (every comparison
+ false except `!=`);
+ - if `f >= 2^63`, the float is greater than every int; if `f < -2^63`,
+ it is smaller;
+ - otherwise compare `i` with `fl = (int64_t)floor(f)`: `i < fl` or
+ `i > fl` decides, and when `i == fl` the float is greater exactly when
+ `f != floor(f)`.
+ - Lua's `LTintfloat`/`EQintfloat` (lvm.c) reach the same answers by
+ converting whichever side converts exactly.
+- **N3 (`+ - *` on ints give an int; overflow raises):**
+ - vm.c:3290 `ARITH_FAST`, 3324 `CASE(ADD)` and 3377 `NUM_BINOP`
+ (SUB, MUL);
+ - builtins_tensor.c `op_add`, `op_sub`, `op_mul` and
+ `tensor_elementwise` (the scalar arms of `add`, `subtract`,
+ `multiply`), plus `sum` and `shared_incr`;
+ - JIT bails (§4.1);
+ - the leaf's `ADD`/`SUB`/`MUL` arm (vm.c:1933-1956): bails unless both
+ operands are floats in round 1, so int, mixed and overflowing operations
+ take the generic path (§4.5).
+- **N4 (`/` float; `//` new; `%` floor; division by zero):**
+ - vm.c:3407 `CASE(DIV)` (int/int gives a float) and 3432 `CASE(MOD)`
+ (fmod becomes floor-mod; int by zero raises);
+ - **new `OP_IDIV`**, appended at the end of `OpCode` (vm.h). The
+ extend-vm checklist applies: dispatch table, handler, `op_name`
+ (chunk.c), `op_stack_effect` (compiler.c), an `/*obs:NONE*/` marker
+ (`tools/obs_marker_check.sh`) and `tests/test_opcode_abi.c`. Appending
+ needs no `EIGS_BYTECODE_ABI` bump.
+ - Lexer: `//` lexes as two `TOK_SLASH` today, so a new `TOK_SLASH_SLASH`
+ is appended to the token enum, the X2 table (lexer.c:241) and the token
+ text (lexer.c:357). `//=` is appended too and uses the X3 table. Keep
+ both text homes (`tok_base_string` and `tok_type_name`) in lockstep;
+ `tok_base_string_id_count` follows the last appended token, since
+ `TOK_EOF` keeps its existing ID. Append-only compound tokens also need
+ the parser's compound predicate and conversion outside the old range.
+ - parser.c (multiplicative level), fmt.c, lint, the LSP and ouroboros
+ `frontend.eigs`;
+ - JIT MOD (§4.2).
+ - Float and mixed `//` follow CPython's `float_divmod` quotient correction
+ (#1882): derive `(a - fmod(a,b)) / b`, subtract one for an opposite-sign
+ nonzero remainder, then floor and snap upward when the fractional part
+ exceeds 0.5; a zero quotient takes the sign of `a/b`. This gives
+ `1.0 // 0.1 == 9.0` and `1e-300 // -1e300 == -1.0`, unlike `floor(a/b)`.
+ The VM's `CASE(IDIV)` is the only evaluator; the JIT and leaf refuse it.
+ - Float `x % 0` (NaN), `x / 0` (inf) and float/mixed `x // 0`
+ (`floor(x/0)`, ±inf or NaN) wait for N11 in round 2. All raise today;
+ the `//` rule is #1758 comment 6294926822.
+- **N4b (INT64_MIN edges):**
+ - vm.c:3522 `CASE(NEG)` (int arm raises on INT64_MIN);
+ - builtins.c `builtin_abs` (2056);
+ - the new `OP_IDIV` (`x // -1`);
+ - `CASE(MOD)`: `INT64_MIN % -1` is 0, computed without `idiv`, which
+ faults on x86.
+- **N5 (float-to-int by the exact rule wherever an int is needed):**
+ - eigenscript.h:1910 `num_to_i64_exact` (#1740);
+ - every int-requiring row of §6, through `eigs_int_arg` (with
+ `eigs_int_arg_range`, `eigs_byte_arg` for Q1's integer-valued
+ parameters; the Q7 readers `eigs_geom_shape`, `eigs_bounded_arg` and
+ `eigs_magnitude_arg` for its continuous ones);
+ - vm.c index paths (`jit_helper_index_get`/`set`, `CASE(INDEX_*)`) and
+ `range_bound` (builtins.c:3877);
+ - the JIT `OP_INDEX_SET` integrality guard (jit.c:4100-4112) stays
+ correct, because a non-exact index goes to the helper;
+ - the leaf's `INDEX_GET` arm (vm.c:1907-1931): in round 1 it bails on an
+ int index (the generic path applies N5), and a float index still goes
+ through `vm_index_is_int`;
+ - `buffer_read_byte` and `finite_num_to_byte` (Q1 byte values), deleted
+ in R1.6 (§2.7), and the byte reads `buffer_read_num` performs today:
+ `buf_from_pcm16le` (builtins_buf.c:754, :756; #1758 comment 6174964306),
+ `str_from_bytes` (:240), `f64_from_bytes` (:284), the zlib input (:351),
+ `write_bytes` (builtins_host.c:1548) and `net_send` (ext_net.c:361), each
+ through `buffer_byte_at` after R1.6 renames `buffer_read_num`;
+ - the index reads: `gather`'s `flat_index_at` (builtins_tensor.c:1428-1440,
+ both its list path and its buffer path truncate today) and
+ `scatter_add` (:1036, exactness-checked but raising an index error),
+ each through `buffer_index_at` or `eigs_int_arg` (N5, `value`);
+ - the Q7 readers (§2.6) for every continuous parameter the `q7` column
+ names: the gfx shapes (ext_gfx.c), the audio samples, gains and
+ durations, `usleep`, `gfx_delay`, `exec_capture`, `recv_timeout`.
+- **N6 (printing):**
+ - eigenscript.c:2938 `eigs_num_text` (today integral doubles below 2^53
+ print without `.0`) and 2957 `value_to_string`;
+ - the f-string conversion (`EIGS_FSTR_CONV_NAME`, builtins.c:6207);
+ - vm.c `obs_dump_num` (106);
+ - chunk.c `chunk_disassemble`;
+ - the JSON encoders (h);
+ - trace.c's `%.17g` writers (2817, 2836, 2950), whose tape text is §7 T1;
+ - `src/freestanding/mini_fmt.c`.
+ - Also chunk.c:196, the constant dedup folding `-0.0` into `+0.0`. Under
+ N6, `-0.0` prints `-0.0`, so the dedup must compare bits.
+- **N7 (B: `type of` is `"num"`; `num_type`):**
+ - builtins.c:864 `builtin_type`, unchanged;
+ - `val_type_name(VAL_INT)` returns `"num"`;
+ - vm.c:5476 `who`, which answers "number";
+ - new `num_type` (builtins.c, registration at 6184).
+- **N8 (bitwise on ints, Lua shift rules):**
+ - vm.c:3459 `INT_BINOP_R` (`& 63` becomes Lua's rule; `>>` becomes
+ logical) and 3548 `CASE(BNOT)`;
+ - builtins.c:349-414 `bit_*`;
+ - JIT bitwise is refused (§4.2).
+- **N9 (width builtins):** new `builtins_int.c`, with §6's decided names,
+ operand rules and Rust std oracle; `fround` (`(double)(float)x`).
+- **N10 (`num of`):** builtins.c:494 `builtin_num`; the decided grammar is
+ exactly the lexer's numeric-literal grammar, plus surrounding ASCII
+ whitespace and an optional leading sign (#1758 comment 6304880619).
+ Decimal integer text is an int when it fits int64 and a float otherwise;
+ a decimal point or exponent selects float. Hex `0x`/`0X` integer text
+ wraps as the literal does. Partial input (#1638), empty/blank text,
+ underscore separators, `0b`, `0o`, hex floats, `inf` and `nan` raise
+ `value`. `try_parse` stays the non-raising form. Lua 5.4.9's `tonumber`
+ is the oracle, with its `nil` corresponding to a raise; hex floats are
+ the declared divergence because Lua accepts them and this lexer does not.
+ MAP `N10.radix` pins both hex spellings and the two refused prefixes.
+- **N11 (IEEE doubles; strict becomes opt-in):**
+ - eigenscript.h:1876 `num_guard`, `num_guard_named`, `EIGS_NUM_MAX` and
+ `STRICT_DOMAIN`: the 74 clamp sites (`num_guard` 60, `num_guard_named`
+ 6, `EIGS_NUM_MAX` 8);
+ - vm.c `CASE(DIV)`, `CASE(MOD)` and the float/mixed arm of `CASE(IDIV)`:
+ all three zero-divisor checks raise through R1.4; N11 changes them together
+ to IEEE division, floor division (`floor(x/0)`, ±inf or NaN) and modulo
+ (NaN), per #1758 comment 6294926822. Int `// 0` and `% 0` still raise;
+ - builtins_tensor.c `op_sqrt` (711) and `op_log_safe` (729);
+ - builtins.c `asin`/`acos` domain;
+ - state.c:62 `st->strict` defaults to 1 and must become opt-in;
+ - jit.c:2634 `max_normal_bits`;
+ - the leaf's `num_guard(r)` (vm.c:1954), deleted with the others;
+ - the consumers no token marks (§2.7): `values_equal_impl`'s identity
+ shortcut (eigenscript.c, first line of the function) must not call a
+ NaN box equal to itself; the sort comparators (`sort_cmp_num`,
+ builtins.c:5868, and `sort_by`'s key comparison, 5815) order floats by
+ IEEE `totalOrder`, NaN after +inf; `minmax_reduce` (builtins.c:2086-2088,
+ `min`/`max`) propagates NaN (Q8, decided).
+- **N12 (canonical NaN; payload fidelity through ints):**
+ - value_slot.h:73 `slot_from_num`, becoming `slot_from_f64`: the single
+ writer, including the leaf's five calls (vm.c:1869, 1873, 1876, 1927,
+ 1954), so a NaN the leaf computes is canonical too;
+ - the JIT NaN bails (§4.3);
+ - new `f64_bits`/`f64_from_bits` and buffer bits accessors;
+ - builtins_buf.c `f64_from_bytes` and `f64_to_bytes`;
+ - eigs_embed.c:339 (the host boundary; #1746 round 3, item 4).
+- **N13 (refusal inside writers):**
+ - builtins.c:907 `eigs_json_encode_value`;
+ - ext_store.c's encoder: the #1746 residue is `store_nonfinite_scalar`
+ (ext_store.c:278 on the experiment), which must be deleted in favour of
+ refusal inside the encoder;
+ - the byte encoders (`eigs_byte_arg`, which replaces `finite_num_to_byte`
+ and `buffer_read_byte` in R1.6) and the `buf_to_pcm16le` quantizer;
+ - every int conversion (`eigs_int_arg`, `num_to_i64_exact` callers);
+ - the tape writers record, not refuse.
+- **N14 (JSON):**
+ - builtins.c:907 encode and 1406 `builtin_json_decode`, plus
+ `eigs_json_parse_value`, `json_path` and `json_build`;
+ - the ext_store.c record codec;
+ - ext_http.c request/response JSON;
+ - model_io.c `json_*` (model files).
+- **(A) (overflow raises, no wraparound):** the N3 and N4b sites; `sum`,
+ `shared_incr`, `sign_extend`'s subtraction (R1.6b-D1), the `add`/`subtract`/`multiply` scalar arms; the N9
+ builtins provide explicit wrapping.
+- **(B):** as N7; `type of` and the 54 consumer `type of ... == "num"` lines
+ keep working.
+- **T1 (tape v7):**
+ - trace.h:32 `TRACE_FORMAT_VERSION`, 6 becoming 7;
+ - trace.c:2817 `write_value_ptr`, 2836 `write_slot` and 2950
+ `write_value_ptr_full`: ints as digits, floats in a form that always
+ shows the kind (N6 repr), `inf`/`-inf`/`nan` in round 2;
+ - trace.c:2284 `parse_value_p`: digits only give an int, anything else
+ goes through strtod as a float;
+ - trace.c:2194 and tape_read.c:155 (version refusal);
+ - tape_read.c:368 (the step reader's `strtod`);
+ - eigsdap.c;
+ - docs/TRACE.md (bump plus the compat decision, #411);
+ - the `O cfg` record (trace.c:1577-1584 writes `h_low`; tape_read.c:389
+ reads it). O3 drops it in round 3;
+ - the leaf evaluator is off while recording (`g_trace_hist`, vm.c:2505
+ and vm.c:4255), so a tape always holds the generic path's values; the
+ `trace` tier (§9.2) gates that the live leaf agrees;
+ - the Done-when's "record twice and replay strict byte-identically" for
+ int, float, inf and NaN programs: rows `T1.twice-int` (R1.3),
+ `T1.twice-float` (R1.7, N6's text), `T1.twice-inf` and `T1.twice-nan`
+ (R2.5). Each records one deterministic program twice, requires
+ byte-identical tapes, and replays strict against the live (leaf-on)
+ output.
+- **O1-O4 (observer, round 3):**
+ - eigenscript.c:698 `observer_slot_record_value`, 804
+ `observer_slot_update_num`, the `obs_num_*` predicates (935-1193) and
+ `compute_entropy_impl` (435), from which entropy is removed for numerics;
+ - vm.c `obs_slot_observable` (1630);
+ - **the routing of an int into the numeric channel, from R1.3 (when the
+ first int exists), converted to a double for the distance until round
+ 3.** Two paths, both forced before R1.3 and both live in R1.3:
+ - **an SMI**: the observe sites test `slot_is_num` first (D1, forced in
+ R1.1): `CASE(OBSERVE_ASSIGN_LOCAL)` (vm.c:5189, its elided
+ `unobserved:` path at 5202), `CASE(OBSERVE_NAME_POST)` (5431),
+ `jit_helper_observe_assign_local` (1678-1684) and
+ `jit_helper_observe_name_post` (1730). Each gains `slot_is_smi(s)` ->
+ `observer_slot_update_num(e, slot, (double)smi)` (or the sample form);
+ `obs_slot_observable` aborts on an SMI that reaches it (§2.5);
+ - **a boxed int** (an int outside [-2^47, 2^47), or any int a lift
+ boxed): `obs_slot_observable` answers true for any pointer, so it goes
+ to `observer_slot_update(Value *)` (eigenscript.c:769), whose number
+ arm (`newval->type == VAL_NUM`, :785) feeds the value channel and whose
+ `else` ends the numeric trajectory (#861). R1.2's `VAL_NUM` rename
+ forces that arm; it gains `VAL_INT` ->
+ `observer_slot_record_value(s, (double)i)`. The same holds for the
+ elided `observer_slot_sample_gated` (:851, its arm at :852) and for
+ `compute_entropy_impl`'s exhaustive switch (:437), whose `VAL_INT` arm
+ gives an int the entropy of its double;
+ - the MAP row `O1.int-observed-numeric` (R1.3) pins both: an SMI and a
+ boxed int (2^47 and up) must answer every observer readout exactly as
+ their float twins do;
+ - vm.c:5460 `CASE(INTERROGATE)` (`where`/`why`/`how`);
+ - `builtin_observe`;
+ - the `O cfg` record;
+ - tools/observer_oracle/score.py (the IEEE mode and `--floor` from the
+ oracle PR).
+- **The leaf evaluator, arm by arm** (vm.c:1856-1971, scanner
+ chunk.c:1072-1134; every arm is in §4.5):
+ - `CONST` (1866), `NUM_ZERO` (1872), `NUM_ONE` (1875): N1, float
+ literals only; the renamed writer `slot_from_f64` (N12); int literals
+ are refused by the scanner (§2.7);
+ - `GET_LOCAL` (1878), `LOCAL_DOT_GET` (1883): borrow any slot, an SMI
+ included (§2.5, `guarded`);
+ - `INDEX_GET` (1907): N5 (index exactness), Q2 (a buffer element is an
+ f64); bails on an int index in round 1;
+ - `ADD`/`SUB`/`MUL` (1933): N3, (A), N2's float half, #1079's underflow
+ flag, N11 (`num_guard` goes in R2.2), N12 (canonical NaN through
+ `slot_from_f64`); bails unless both operands are floats in round 1;
+ - `RETURN` (1957): unchanged;
+ - the gate `!g_trace_hist` (vm.c:2505, vm.c:4255): T1, and the `trace`
+ tier of §9.2.
+
+---
+
+## 8. Round plan
+
+Every commit builds, passes the full suite, and carries its own MAP rows.
+"Unchanged" means a differential against the baseline binary captured
+before the commit (the verify-and-fix rule), not "the suite passed". Every
+meaning change lands in the commit that carries its token change, or is in
+§2.7's token-less list with its gate. `phase2_check.sh --through `
+checks each commit against exactly the rows that have landed (§9.2).
+
+### Where the first int value appears: R1.3
+
+No int VALUE exists before R1.3, and this is structural, not a promise:
+- `make_int` is introduced in R1.3, so nothing earlier can call it;
+- `slot_from_int` exists from R1.1 (the C slot test needs it), but until R1.3
+ its only production callers are the canonicalizing writers of §2.1, which
+ convert an existing `VAL_INT`, and none exists. `phase2_check.sh --through
+ R1.2` checks this: it derives the callers of `slot_from_int` and
+ `make_int` from the source and fails on any caller outside that list
+ (the int-source check). A producer can also write the tag bits without
+ calling the writer, so the check fails on `TAG_SMI` or the SMI bit pattern
+ anywhere outside its definitions in `value_slot.h`, and its dynamic half
+ runs the corpus on a `make smi-trap` build, where every slot classifier
+ aborts on an SMI and `slot_from_int` aborts on any call (the R1.1 fix
+ round, after the critic's plants P3-P5);
+- the lexer produces only float literals until R1.3, and the tape cannot
+ carry an int until v7 (R1.3).
+
+**Every consumer of an int is converted AT R1.3**, either by code forced and
+written earlier (R1.1, R1.2) that goes live, or by code R1.3 adds:
+
+| consumer | converted by | how it treats an int at R1.3 |
+|---|---|---|
+| VM arithmetic, comparison, negation (N2, N3, N4b, (A)) | R1.1 forced the arms (`slot_as_double` deleted); R1.3 adds the int semantics | exact int64, overflow raises |
+| `/` and `%` | R1.3 (transitional) | converted to doubles, today's semantics, until R1.4 |
+| bitwise operators | R1.5 | exact int64 operands (#1740), int results and Lua 5.4 shifts |
+| equality, ordering, hashing, the constant pool | R1.2 (forced arms) | by value across kinds (N2); the pool keys on kind |
+| printing (`value_to_string`, f-strings) | R1.2a (the exhaustive switch) | exact digits, as today for small ints |
+| `type of`, `num_type` | R1.2a (`val_type_name`), R1.3 (`num_type`) | `"num"`; `"int"` |
+| containers | R1.2 (the four canonicalizing writers) | an SMI, or a box outside the SMI range (§2.1) |
+| the observer | R1.1 (SMI sites, D1) and R1.2 (the boxed arm at eigenscript.c:785, the entropy switch) | the numeric channel, as a double until round 3 (§7) |
+| the tape | R1.3: v7 writer and parser | ints as digits, floats marked as floats; a v6 tape is refused |
+| the JIT | nothing to convert: its guards bail on an SMI or a `VAL_INT` box (§4.1), and its scanner refuses `OP_INT_*` | interpreted |
+| the leaf evaluator | R1.1 (its arithmetic and index arms bail) | the generic path computes it |
+| builtins | R1.2's number tests admit ints; most readers retain the bridge | a double until R1.6, except the exact `sort`/`sort_by` comparators and `json_build` text reader (§6); `json_path`'s exact text reader is also complete, awaiting decoder int production in R1.6 |
+| JSON, store and db encoders | R1.2a (their `VAL_INT` arms) | exact digits |
+| the embed API (`eigs_value_as_num`) | R1.2 (its forced `VAL_NUM` test) | converts to a double (Q3) |
+| lint and the LSP | R1.3 (the AST payload rename) | the literal's kind |
+
+### Round 1: the core model (Alt 1R)
+
+1. **R1.1, the slot layer: the SMI exists, every slot reader is forced (no
+ producer).**
+ - Add `TAG_SMI`, `slot_is_smi`, `slot_from_int`, `slot_smi_val`,
+ `slot_f64_value` and `slot_int_value` (§2.6).
+ - The token changes: `slot_is_num` -> `slot_is_f64`, `SLOT_NUM_RAW` ->
+ `SLOT_F64_RAW`, `slot_from_num` -> `slot_from_f64`; `slot_is_numeric`
+ deleted; **`slot_as_double` deleted**. Its 15 calls are rewritten with
+ an explicit int decision: the arithmetic and compare arms take their
+ float fast path through `slot_f64_value` and send anything else to the
+ slow path; the leaf evaluator's `INDEX_GET` and `ADD`/`SUB`/`MUL` arms
+ bail on anything that is not a float. 223 sites first forced (§2.3a).
+ - The D2/D3 dispositions (§2.5): `eigs_slot_ref`, `obs_slot_observable`,
+ `slot_as_ptr` and `slot_as_heap` refuse an SMI (abort; `slot_as_ptr`
+ in ASan and debug builds); `slot_type_name` gains its SMI arm.
+ - `tools/phase2_check.sh` lands here with its calibration (§9.2).
+ - Gates (a mechanical round, zero MAP rows): the compile errors, the C
+ slot test (§2.5) and plant 1 (§9.3); the int-source check; language
+ behaviour unchanged (differential over the suite's `.eigs` corpus).
+2. **R1.2, the Value layer (behaviour-preserving; no builtin decision).**
+ - R1.2a: `VAL_INT` appended; `VAL_NUM` -> `VAL_FLOAT`, `num_` -> `f64_`,
+ `VAL_NUM_RAW`/`VAL_NUM_OFFSET` renamed, with deprecated aliases as the
+ progress meter; `val_type_name(VAL_INT) = "num"`; **the `default:` arm
+ of `eigs_json_encode_value` (builtins.c:998) deleted**, so
+ `-Werror=switch` reaches the last switch that could absorb `VAL_INT`
+ silently.
+ - R1.2b-n: one commit per file group. Each forced site gets its final
+ int arm (dead until R1.3), and a builtin's number test admits an int
+ and normally reads it through the bridge (§2.7). The exact sorting and
+ JSON text readers are completed here (§6), to avoid a later lossy
+ conversion. The bridge's names are NOT touched; no behaviour changes
+ before ints exist. 464 sites first forced.
+ - The three reader arms scheduled with the Value kind are complete in
+ R1.2i: `slot_type` types an SMI, `slot_view` exposes an exact int
+ facade, and `slot_int_value` extracts a boxed int. None produces an
+ int; R1.3 does not implement these arms a second time.
+ - R1.2z: delete the deprecated aliases. A straggler fails to compile.
+ - Gates (a mechanical round, zero MAP rows): the compile errors; the
+ differential over the suite's `.eigs` corpus (identical to the
+ baseline); the int-source check.
+3. **R1.3, the flip: the first int values.** See the table above.
+ - N1, with its token changes in this same commit: the lexer produces an
+ int literal; the AST payload `num` and the token's `num_val` become an
+ `EigsNum`; **`OP_INT_ZERO`, `OP_INT_ONE` and `OP_INT_CONST` are
+ appended** (the four exhaustive OpCode switches; three new interpreter
+ CASEs; the JIT and leaf scanners refuse them by their `default:`);
+ `make_int` is introduced. `OP_NUM_ZERO`/`OP_NUM_ONE`/`OP_CONST` keep
+ their float meaning in all three executors.
+ - The exact `sort`/`sort_by` comparators and `json_build` reader written
+ in R1.2 go live; `N2.sort-exact` is checked here, not R1.6 (§6).
+ - N3 with (A), N2's exact cross-kind comparison, N4b `-x`, N7 `num_type`;
+ EQ/NE's only bit fast path is "both SMI" (§2.1).
+ - The JIT scanner refuses `BAND`/`BOR`/`BXOR`/`SHL`/`SHR`/`BNOT`/`MOD`.
+ - **Tape v7 in this commit**: a v6 writer would print `3` for both kinds
+ and replay would turn ints into floats. The float token marks its kind
+ now (`3.0`); N6's repr replaces it in R1.7, still inside v7.
+ - The remaining `eigs_int_unwired` placeholders are deleted, including:
+ `slot_to_value` boxes an SMI with
+ `make_int`, `write_slot` writes v7 digits, and `slot_from_int`
+ boxes an int outside the SMI range (each R1.1 placeholder, 2.5).
+ - The observer routes an SMI and a boxed int into the numeric channel
+ (§7).
+ - The `trace` tier (the leaf differential) and the tier witnesses gate
+ from here.
+ - Risks: JIT int loops leave the JIT (§3: +70% Ir on a counter loop),
+ and the leaf stops computing int helpers; declare the CodSpeed baseline
+ move first. `tools/phase2_cost.sh` reads per-workload and per-tier TSV
+ six-column declarations in `tests/phase2/cost_expect.txt` (`workload`, `tier`,
+ `max_local`, `max_ci`, `recover_round`, `reason`). R1.8 applies #1939:
+ each maximum comes from its own gate’s measured delta. The local gate
+ enforces and STALE-checks only `max_local`; undeclared rows retain
+ +0.3%. Declare only measured R1.3 costs with §3 as the reason and
+ `recover_round=R5`, the JIT/leaf int fast-path round. A row above its
+ declared maximum fails, and so does a row more than 0.5 points below it
+ (STALE, the R1.3 CI fix): a round that recovers cost must bring the
+ declaration down, or a new regression hides under it. Each method’s
+ maximum is its own measured delta plus 0.2 points (the largest measured
+ shift: a different workload path moves `alloc_heavy` by 0.16 points), rounded up
+ to 0.1. A row whose cost another round moves names that component with
+ its measurement: R1.6's int `range` alone removes 58% (jit) and 66%
+ (off) of `scalar_loop`'s excess, and raises 14 other rows by up to 44
+ points (the range-int control, measured at R1.3).
+ `bench/check_regression.sh --vs` (CI's bench job, the default tier)
+ reads the same `jit` rows through
+ `phase2_cost.sh --declared jit --method ci`: a declared workload is
+ bounded and STALE-checked only against `max_ci`, and prints its
+ recovery round; an undeclared one keeps the 5% bound. The CI maximum
+ comes from the CI bench gate’s own log for the pushed head, never
+ from a local run or a value chosen to satisfy both methods (#1939).
+ The observer oracle’s known-wrong capability
+ verdicts are declared the same way (#1830): `tests/phase2/oracle_expect.txt`
+ (`arm`, `row@checkpoint`, `predicate`, `recover_round`, `reason`) lists the
+ 32 `int64_count` verdicts R1.3 first measures, `recover_round=R3`, the round
+ whose predicate definitions make `int64_count` its acceptance row.
+ `make observer-oracle` and CI's lane pass it to `score.py --declared`; an
+ undeclared wrong verdict fails, and so does a stale declaration (its verdict
+ is right now, or was not examined). **Pre-merge condition (#1776):**
+ `tools/phase2_cost.sh --final BASE CAND` must pass, which requires both
+ `tests/phase2/cost_expect.txt` and `tests/phase2/oracle_expect.txt` to be
+ empty; temporary costs and known-wrong verdicts must be recovered before
+ integration merges.
+ Consumer hashes that relied on double rounding now raise
+ ((A)); they move to the N9 wrapping builtins. The stdlib's own
+ real-valued formulas must not: `tools/phase2_lib_diff.sh` (§9.5, #1848)
+ runs every exported lib function with int arguments on main and the
+ branch, from R1.3 in every round.
+4. **R1.4, N4.** `//` (token, parser, `OP_IDIV` appended,
+ `op_name`/`op_stack_effect`/obs marker/ABI test), `//=` beside `/=` (Q6),
+ `%` floor modulo for ints and floats, int division by zero raises,
+ `INT64_MIN % -1` is 0.
+5. **R1.5, N8.** Bitwise operators on ints with Lua's shift rules.
+6. **R1.6, the bridge deletion: every builtin's §6 semantics.** The bridge's
+ 21 tokens are deleted in per-file-group commits. Each commit moves its
+ sites off the bridge and gives those builtins their final behaviour in
+ that same commit (§2.7). The binding R1.6 plan splits the work as follows;
+ a builtin's file is where its inventory C function is defined.
+
+ | sub-round | work | table rows | MAP rows or mechanical gates |
+ |---|---|---:|---|
+ | R1.6a | No behaviour change: §2.6 readers except `eigs_geom_shape`; bounded and magnitude included; opt-in meter; pre-registered table and tool; round order and records | none migrate | mechanical: C reader test and table calibration |
+ | R1.6b | Every LOSS row in any file, plus `builtins.c`'s other int-preserving rows | 46 | `A.sum-overflow-raises`, `A.tensor-scalar-overflow-raises`, `N5.range-fraction-raises`, `N5.store-key-fraction-raises`, `N10.partial-refused`, `N10.radix`, `N10.int-or-float`, `N14.json-path-int-exact`, `N14.decode-kinds` |
+ | R1.6c | Remaining `builtins.c` (including `set_exit_code`); `builtins_host.c` (including `read_text`); `vm.c`'s builtin | 77 | `N5.substr-fraction-raises` |
+ | R1.6d | Non-LOSS `builtins_tensor.c`, `builtins_buf.c`, `model_io.c`, `model_infer.c`, `model_train.c`; re-decide `buffer_read_num` sites as f64, byte or index reads | 53 | `N5.gather-index-fraction-raises`, `N5.pcm16-byte-raises` |
+ | R1.6e | `ext_gfx.c`, the Q7 readers and whole-shape `eigs_geom_shape` clipping (Liang-Barsky for lines) | 38 | `gate.N5.q7-readers`; table run |
+ | R1.6f | Non-LOSS `ext_store.c`, `ext_db.c`, `ext_http.c`, `ext_net.c` | 19 | mechanical: table run |
+ | R1.6g | N9's 77 new width-family builtins in `builtins_int.c`, Rust std oracle | +77 | `N9.wrapping`, `N9.checked-raises`, `N9.saturating`, `N9.unsigned-ops`, `N9.fround` |
+ | R1.6z | Remaining bridge sites in runtime, containers, embed, trace, compiler and task; delete all 21 tokens and all helpers left without callers | none | mechanical: deletion compile and final full table |
+
+ The pre-N9 rows summed to 233; R1.6g added 77 for a total of 310.
+ S10-D2 later retires `proc_read_buf` from round c: current c=76, total=309.
+ Its two frozen cases and dependent plants are explicitly retired in
+ `tests/phase2/builtins/RULINGS.md` and `builtins_changes.md`; the unchanged
+ `read_bytes_buf` case preserves the host-byte buffer f64 witness. Ruling R1.6a-D1 added `read_text`, already
+ present in the R1.6a start (`1fd4422c`) through main's #1841/#1668, to
+ round c: the original plan's c=75 and total=231 became c=76 and total=232
+ at that import. Sync #5's R1.6-S2 adds main's `set_exit_code` to round c,
+ so the pre-N9 counts became c=77 and total=233; `rm` had no table row and
+ its deletion under R1.6-S1 does not change the counts.
+ `tools/phase2_builtins.sh` derives and checks the sub-round by this rule. Every sub-round uses the table gate
+ (§9.4); later builders add their completed round to
+ `tests/phase2/builtins_landed.txt` in that commit. R1.6a leaves it empty;
+ it migrates no builtin and its declaration is not required for later
+ sub-rounds to advance the derived prefix. Its reader/table gates always run.
+ The declaration and complete MAP row files together determine CI's
+ `--through`, so a file that arrived early cannot advance it.
+
+ **R1.6a meter.** `EIGS_R16_DEPRECATED(msg)` marks the 21 bridge tokens
+ only with `make EIGS_PHASE2_METER=1`; ordinary builds have no deprecation
+ attribute. The meter is a progress count, not a behaviour change.
+ Native Make meter builds use the server-db and zlib profiles to include
+ every capability. Counts below include emitted diagnostics per profile
+ and the union of distinct file:line:column warning locations (a header
+ location repeated across translation units counts once in that union).
+
+ | File | server-db emitted | zlib emitted | Distinct locations |
+ |---|---:|---:|---:|
+ | `src/builtins.c` | 218 | 218 | 201 |
+ | `src/builtins_buf.c` | 49 | 53 | 51 |
+ | `src/builtins_host.c` | 38 | 38 | 37 |
+ | `src/builtins_tensor.c` | 123 | 123 | 119 |
+ | `src/compiler.c` | 1 | 1 | 1 |
+ | `src/container.h` | 420 | 348 | 12 |
+ | `src/eigenscript.c` | 21 | 21 | 21 |
+ | `src/eigenscript.h` | 105 | 87 | 3 |
+ | `src/eigs_embed.c` | 2 | 2 | 2 |
+ | `src/ext_db.c` | 2 | 0 | 2 |
+ | `src/ext_gfx.c` | 194 | 194 | 194 |
+ | `src/ext_http.c` | 11 | 0 | 11 |
+ | `src/ext_net.c` | 14 | 0 | 14 |
+ | `src/ext_store.c` | 34 | 34 | 33 |
+ | `src/model_infer.c` | 15 | 0 | 15 |
+ | `src/model_train.c` | 4 | 0 | 4 |
+ | `src/task.c` | 5 | 5 | 5 |
+ | `src/trace.c` | 1 | 1 | 1 |
+ | `src/vm.c` | 80 | 80 | 67 |
+ | **Total** | **1337** | **1205** | **793** |
+
+7. **R1.7, the float text (N6) and N14's writers.**
+ - Python-repr floats; ints as exact digits.
+ - The constant dedup compares bits, so `-0.0` survives.
+ - JSON writes floats with `.0`; #1762's writers (`store_json_encode`,
+ `db_build_query`) go through the same N6 writer, unless #1762 landed on
+ main first.
+ - `src/freestanding/mini_fmt.c` and `mini_strtod.c` follow, gated by
+ `make freestanding-libc-diff`.
+ - This is the largest visible change: every golden that prints an
+ integral float.
+8. **R1.8, the embed API (Q3), lint/LSP type knowledge and docs.**
+ `eigs_value_as_num` converts an int (like `lua_tonumber`);
+ `eigs_value_new_int`, `eigs_value_as_int` (with a success flag, like
+ `lua_tointegerx`) and `eigs_value_is_int` are added; `EIGS_TYPE_NUM`
+ covers both kinds. Gate: `gate.N7.embed-int` (the embed smoke test), the
+ executed doc fences (suite [89]/[90]) and the LSP and lint suite sections.
+ Docs: SPEC, COMPARISON, README, llms.txt; the CLAUDE.md same-PR docs rule
+ applies to the integration PR as a whole.
+
+### Round 2: IEEE (N11-N14 remainder), on the experiment's evidence
+
+1. **R2.1, canonical NaN at the single double writer** (`slot_from_f64`)
+ **and the JIT NaN bails** (§4.3), while `num_guard` still keeps NaN out:
+ memory safety first. A mechanical round (no program can make a NaN yet):
+ the C slot test writes every tag-spelling NaN payload, and the #1746
+ plant (a raw store) segfaults in `slot_incref`.
+2. **R2.2, retire `num_guard`, `num_guard_named`, `EIGS_NUM_MAX` and the
+ 1e308 clamp** (74 sites, the leaf's included; 47 first forced here).
+ `EIGS_STRICT` becomes an opt-in trap (state.c:62). `sqrt of -1` is NaN,
+ `log of 0` is -inf, `x/0` is ±inf, `0/0` and float `x%0` are NaN. Q8:
+ `sort`/`sort_by` order floats by IEEE `totalOrder` (NaN after +inf), and
+ `min`/`max` propagate NaN. `values_equal_impl`'s identity shortcut
+ excludes floats and extends the container reflexivity predicate to NaN
+ floats, following §2.6a (R1.6z-D1/D2).
+3. **R2.3, N13 refusal inside the writers:** the JSON encoder, the store
+ encoder (delete the pre-walk `store_nonfinite_scalar`), the byte
+ encoders and the int conversion. This removes the class #1746 chased for
+ three rounds, rather than its instances.
+4. **R2.4, N12.** `f64_bits`/`f64_from_bits`, buffer bits get/set, and a
+ canonical NaN at the embed boundary.
+5. **R2.5, tape v7 `inf`/`-inf`/`nan` tokens; N6 non-finite printing;
+ N14 `1e400` raises.**
+6. **R2.6, the #1746 residue:**
+ - the internal `(int)` casts the Q7 readers do not cover (the audio
+ generators' `dur * rate`, the model builtins);
+ - `net_send` continuing after a raise (ext_net.c:364);
+ - `buf_to_pcm16le` with a NaN sample;
+ - the `numerical_grad` waiver.
+
+### Round 5 obligations (recorded by earlier rounds)
+
+Round 5 gives the JIT and the leaf evaluator their int fast paths (§4.4).
+It must also recover the declared interpreter costs in
+`tests/phase2/cost_expect.txt`: an `off` row runs with `EIGS_JIT_OFF=1`, so
+native execution alone cannot recover it. R1.5's fresh R1.4 controls attribute
+the DMG-shaped benchmark's N8 increment to exact-int state reaching generic
+dictionary/boxed Value paths. The other moved rows have no bitwise operations;
+their measured increment is shared VM/helper code generation, including an
+out-of-line `slot_from_int`. Interpreter int/slot paths are therefore part of
+R5's recovery obligation; each row retains its measured bound and component.
+
+Until then a check that needs EMITTED code for an int program cannot fire,
+so its fixture runs a float program instead. Each such fixture gets its int
+twin back when R5 compiles ints:
+- `tests/tsan_spawn_emitted_jit.eigs` (TSan shape fixture, R1.3 CI fix).
+ It races a spawn against emitted JIT code, and its row requires
+ `[jit] compiled>=1` and `hot`'s native RET after 80 warm calls. The R1.2
+ version multiplied ints and stopped compiling at R1.3. It now writes float
+ literals and float arguments. At R5 the int version returns beside it as
+ its own `SHAPE_FIXTURES` row with the same witness, and `SHAPE_DECLARED`
+ grows by one.
+- Five more JIT-purposed fixtures (#1858, the R1.3 hardening round). Each
+ compiled less, or nothing, once int literals were refused. Each now writes
+ floats in the code its JIT purpose needs. Each int twin returns at R5 beside
+ its float version, run by the same row:
+ - `tests/test_spawn_jit.eigs` (suite [100] and the TSan slice). The
+ main-thread `check`, the shared `helper` and `worker` count in floats.
+ - `tests/test_underarity_1661_repro.eigs` and
+ `tests/test_underarity_1661_trycatch.eigs` (suite [0fd], in all three tiers,
+ the forced-JIT tier included). `g` compares, counts and returns floats.
+ - `tests/tsan_mt_clear_respawn.eigs` (the TSan slice). `hot`, its argument,
+ and the module's two loops (OSR after each join) count in floats.
+ - `tests/test_spawn_jit_warm.eigs` (suite [129] and the TSan slice). `helper`,
+ `warm`, `worker` and `gmul` are floats, so `helper`'s thunk runs to its
+ return again.
+
+ The compile counts (`EIGS_JIT_STATS`, summed over threads) were 1, 1, 1, 4
+ and 1 on main 94657581. The tip before the change compiled 0, 0, 0, 2 and 1;
+ after it, 1, 1, 1, 4 and 1. These rows assert no JIT witness of their own
+ (#1858's second Done-when), so another refusal would again pass them
+ silently.
+
+### Risks across both rounds
+
+- The AOT is a round of its own (ouroboros, pinned to the integration SHA).
+ Its numeric model is C `double` throughout: 294 `double` in aot_rt.h and
+ 228 in compile.eigs, with `make_num` 40+27 and `num_guard` 61+27. Under
+ Alt 1R its generated C fails to compile against the renamed runtime,
+ which is loud.
+ - The AOT's existing int census (a `long` storage regime for counters)
+ must follow literal kinds, not inference.
+ - `fuzzdiff` must never apply `ULP_BOUND` to an int result (the
+ aot-differential skill: "Integer data compare exactly even inside a
+ reduction arm").
+- Consumers. N6 printing moves goldens in all 16; N4 changes `%` on
+ negatives; (A) raises where doubles lost precision silently; Q1 raises on
+ a non-integral count, handle or byte, and on a byte outside 0..255 that
+ used to wrap; Q7 raises on a duration or window size the C call cannot
+ honour. The DMG emulator feeds `ppu_render_frame` its memory buffer
+ through `buffer_read_byte`; the consumer acceptance run (Done-when) shows
+ whether any program stored a non-byte there.
+- Observer. Until round 3, ints are observed as their double value (the
+ distance channel works on doubles), so `where`/`why`/`how` keep today's
+ meaning.
+- The leaf evaluator. It is off while recording, so every recorded run and
+ every live run compute leaf helpers on different evaluators. The `trace`
+ tier is the only thing that keeps them equal; it runs on every row.
+
+---
+
+## 9. Test plan
+
+### 9.1 `tests/phase2/MAP.md`
+
+The skeleton is committed with this design. Every item (N1-N14, N4b, (A),
+(B), T1, O1-O4) has planned rows, each recording:
+- an id;
+- the program;
+- the expected branch output;
+- the round that lands it;
+- why the row **fails on the v0.46.0 tag**;
+- its **shape** and its **tier witness** (below).
+
+Behaviour the branch must *preserve* (`1 == 1.0` is true on both binaries)
+cannot fail on the tag, so it is pinned two ways:
+- inside a mapped row that also checks the kind (the row fails on the tag
+ because the kind is invisible there);
+- as a separate **control** row, counted on its own line and never in
+ `tag_fail` (§121: each population pinned separately).
+
+**Shapes.** The round-0 rows were all module-level straight-line code. The
+critic ran them: both JIT tiers printed `[jit] scanned=0 compiled=0`, so
+`phase2_check.sh`'s three tiers would have run the interpreter three times,
+and no row reached a JIT producer emitter or the leaf evaluator. Every row
+now has one of four shapes:
+- `S`: straight-line module code. It runs interpreted in every tier and
+ claims nothing about tiers (witness `none`). These rows pin semantics.
+- `L`: a one-line helper, `define h(x) as: return `, called K >= 1
+ times: the leaf evaluator's shape (§4.5).
+- `F`: a multi-statement function called at least 60 times, above the
+ default entry threshold of 50 (jit.c:2478): the function JIT's shape.
+ `EIGS_JIT_HOT` records a chunk's LAST run, so the witnessed call must be
+ the function's last call (round 2's two truthiness controls ended on a
+ float call and could not hold; both are reordered).
+- `O`: a `loop while true:` loop, in a function or at module level, run in
+ the `osr` tier (`EIGS_JIT_OSR_THRESHOLD=1`), **whose only exit is a
+ `return` placed right after the claimed ops** (`if : return `; at
+ module level, `print of ` first). The OSR thunk's record holds only its
+ last entry, and with a `loop while :` header that entry is always the
+ final condition check, which exits through the header whether or not the
+ body ever ran natively. With the return inside the body, the only way the
+ last entry ends at the return sentinel is to run the whole body natively
+ first.
+
+The `L`, `F` and `O` rows (44 in the MAP) cover every JIT-emitter and leaf
+arm that holds a numeric site (the inventory's `## Numeric arms`, 20 today;
+§9.2 checks the coverage): the literals (`NUM_ZERO`, `NUM_ONE`, `CONST` and
+the new `INT_*`), the arithmetic, `DIV`/`MOD`, `NEG`/`NOT`/`BNOT`, the
+comparisons, the bitwise ops, the conditional jumps, the field, local, name
+and index stores, and the leaf's `INDEX_GET`, over ints and floats, with
+overflow rows for the leaf, the function JIT and OSR.
+
+**Tier witnesses.** A row that claims a tier must prove the tier ran its
+operation; otherwise it is **VACUOUS**, a FAIL with its own counter. The
+witness line is `witness: ... []` (a `leaf` or `leaf-gen`
+witness may name several helpers, each of which must hold), followed by
+`ops: ...`, the opcodes whose JIT or leaf arm the row exercises:
+
+| kind | holds when (read from the branch run) |
+|---|---|
+| `none` | no claim (shape `S`) |
+| `leaf ` | in every tier, the normal run's `EIGS_JIT_HOT` table has no entries for ``, and the `trace` run (leaf off) has K >= 1: the leaf computed every call |
+| `leaf-gen ` | the normal run and the `trace` run both show the same K >= 1 entries: every call took the generic path (the scanner refused the chunk, or the leaf bailed) |
+| `fn ` | in the default and `osr` tiers, ``'s row has `jit yes` and `adv RET`: its last call ran natively to its return |
+| `fn-stop ` | `jit yes`, and the opcode at offset `adv` of ``'s bytecode (`EIGS_DUMP_BC=1`) is ``: the thunk handed back at ``, by a compile stop or a guard bail |
+| `osr ` | in the `osr` tier, ``'s row has `osr yes` and `oadv == len`: the OSR thunk's last entry reached the return sentinel (jit_advance -1, which `EIGS_JIT_HOT` prints as the chunk length, jit.c:244), so it ran the whole body natively on the way. Round 2's form (`oent + oadv` past `JUMP_BACK`) is withdrawn: the critic's plant ran native `INDEX_SET` 0 times and still passed it |
+| `osr-stop ` | `osr yes`, `oadv != len`, and the opcode at `oent + oadv` is ``: every entry handed back at `` |
+
+Why `EIGS_JIT_HOT` and not only `EIGS_JIT_STATS`/`EIGS_JIT_STOPS`:
+`STATS` counts are process-wide (`scanned`, `compiled`), and `STOPS` records
+where each compile's prefix ended. Neither says whether a run executed the
+operation natively. `HOT` records, per chunk, the last run's native advance
+(`adv` for the function thunk, `oadv` from `oent` for the OSR thunk, jit.c:
+237-245), which is exactly that. `phase2_check.sh` reads all three: `STATS`
+must show `compiled >= 1` for an `fn`/`osr` claim, and for a `-stop` claim
+whose stop is a compile stop, `STOPS` must name ``. The leaf has no
+counter; its witness is the entry count, because both generic call paths
+count an entry (vm.c:2617, vm.c:4353) and the leaf path never does.
+
+**Calibrated on the main binary: every kind holds on its honest row and is
+VACUOUS on a plant where the claimed op did not run as claimed** (design
+round 3; a scratch model of the predicates read `EIGS_JIT_HOT` and
+`EIGS_DUMP_BC`; the `osr` pair also counted native `jit_helper_index_set`
+calls under gdb, with the critic's `gdbcount.gdb`):
+
+| kind | honest row (main) | evidence | plant | evidence |
+|---|---|---|---|---|
+| `leaf sq` | `define sq(x) as: return x * x`, one call with 1.5 | `sq normal=0 trace=1`: HOLDS | the body split into two statements (not leaf-eligible) | `sq normal=1 trace=1`: VACUOUS |
+| `leaf-gen nth` | `define nth(xs, i) as: return xs[i]`, called on a string (the leaf's `INDEX_GET` bails) | `nth normal=1 trace=1`: HOLDS | called on a list (the leaf computes it) | `nth normal=0 trace=1`: VACUOUS |
+| `fn keep` | `t is q / return t`, 60 calls with 2.5 | `keep jit=yes adv=RET`: HOLDS | `zz is [q]` first (the scanner stops at `LIST`) | `keep jit=no stop=LIST`: VACUOUS |
+| `fn-stop cat ADD` | `t is a + b / return t`, 60 calls with two strings (the `ADD` guard bails) | `cat jit=yes adv=11`, offset 11 is `ADD`: HOLDS | 60 calls with two floats | `adv=RET`: VACUOUS |
+| `osr fill` | the critic's `osr_ret`: `loop while true: (xs[i] is big / i is i + 1.0 / if i >= n: return xs)` | `osr=yes oadv=123 len=123 oent=37`; 3 native `INDEX_SET` helper calls: HOLDS | the critic's `osr_ret_PLANT2`: `s is s + "a"` first in the body | `oadv=20 len=157`; 0 native calls: VACUOUS |
+| `osr fill`, round 2's shape | `loop while i < n:` (the critic's `osr_index_set`) | 3 native calls, but no sentinel: VACUOUS under the new rule | `zz is [i]` first (`osr_index_set_PLANT`) | 0 native calls; `oent + oadv` = 109, past `JUMP_BACK` at 106: the round-2 rule PASSED it; the new rule is VACUOUS |
+| `osr-stop g ADD` | `loop while true: (s is s + "a" / i is i + 1.0 / if i >= n: return s)` | `oent=33 oadv=20`, offset 53 is `ADD`: HOLDS | `s` a float (`s is s + 1.5`) | `oadv=122=len` (the sentinel): VACUOUS |
+
+The witness uses only existing diagnostics: nothing in production changes so
+a test can observe it.
+
+**What the witnesses expect in round 1.** The JIT and the leaf bail on an
+int that meets an arithmetic, comparison, bitwise or index op, and refuse an
+int literal, until round 5 (§4), so such a row's honest witness is
+`fn-stop`, `osr-stop` or `leaf-gen`: it proves the guard fired (or the
+scanner refused) and the interpreter produced the row's output. An int that
+only moves, into a local, a field, a list element or a module binding,
+passes through the JIT's C helpers and the leaf's borrows, so those rows
+(`N1.fn-int-local`, `N1.fn-dot-fields`, `N1.osr-global-int-store`,
+`N5.osr-index-set`, `N1.leaf-dot-get`) have native witnesses already in
+round 1. A float row's witness is native too: it proves the float meaning of
+`NUM_ZERO`/`NUM_ONE`/`CONST` survived in the JIT and the leaf. Round 5 flips
+the int witnesses to native, one row at a time, and each flip is a
+deliberate edit of the row.
+
+### 9.2 `tools/phase2_check.sh [--through ] ` (R1.1 builds it)
+
+**The landed-round filter.** Each MAP row names the commit that makes it
+pass (`[R1.3]`; a row needing two commits names both, `[R2.1/R2.4]`, and
+lands with the later). `--through Rk` selects the rows that have landed by
+Rk, in the order R1.1 < ... < R1.5 < R1.6a < R1.6b < R1.6c < R1.6d <
+R1.6e < R1.6f < R1.6g < R1.6z < R1.7 < R1.8 < R2.1 < ... < R2.6 < R3; controls
+run in every mode; the default is the whole plan. A round is checked against
+exactly its landed rows, so R1.3's check cannot fail on an R1.7 row, and the
+final check runs all of them. Each round has a non-zero set of its own rows,
+or a mechanical guarantee stated here, which `--through` runs instead of
+failing on zero rows:
+
+| round | its own MAP rows | its gates, or the mechanical guarantee |
+|---|---:|---|
+| R1.1 | 0 | mechanical: the compile errors (223 sites), the C slot test (§2.5; `examined` equals the derived classifier count), plant 1, the int-source check, and the differential over the suite's `.eigs` corpus |
+| R1.2 | 0 | mechanical: the compile errors (464 sites), the int-source check, and the differential over the suite's `.eigs` corpus (behaviour-preserving) |
+| R1.3 | 51 | `gate.N5.iterator-metadata` |
+| R1.4 | 14 | |
+| R1.5 | 7 | |
+| R1.6a | 0 | mechanical: C reader test (`tools/phase2_readers.sh`) and the per-builtin table (§9.4) |
+| R1.6b | 9 | per-builtin table |
+| R1.6c | 1 | per-builtin table |
+| R1.6d | 2 | per-builtin table |
+| R1.6e | 1 | `N5.audio-channel-domain` (R1.6e-D1); mechanical: per-builtin table, `gate.N5.q7-readers` through the no-SDL amalgamation, and the pre-registered `gate.N13.audio-buffer-boundary` / `gate.N5.gfx-byte-boundary` actual-client fixtures (all compiled and executed here and at every later `--through`) |
+| R1.6f | 3 | R1.6f-D1 shared store handle refusals; R1.6d-D1 net_send byte refusals; mechanical: per-builtin table and the `gate.N5.net-byte-boundary` actual-client socketpair fixture (R1.6f-D2; compiled and executed here and at every later `--through`) |
+| R1.6g | 5 | per-builtin table, including the new N9 rows |
+| R1.6z | 0 | mechanical: bridge-token deletion compile and final full table |
+| R1.7 | 13 (11 change rows, 2 invariant controls) | `gate.N14.db-param-exact`, `gate.N6.const-dedup-bits` (R1.7-D3/D4) |
+| R1.8 | 0 | `gate.N7.embed-int` (the embed smoke test), the executed doc fences (suite [89]/[90]), the LSP and lint suite sections |
+| R2.1 | 0 | mechanical: the C slot test's NaN-payload rows and plant 2 (no program can make a NaN before R2.2) |
+| R2.2 | 9 | |
+| R2.3 | 3 | |
+| R2.4 | 6 | |
+| R2.5 | 6 | |
+| R2.6 | 1 | |
+| R3 | 5 | `gate.O1.oracle-floor`, `gate.O2.predicate-floors` |
+
+(The counts are the MAP's at this commit; `phase2_check.sh` derives them, and
+fails if a round named here has neither rows nor its mechanical gates.)
+
+- **Rows** live in `tests/phase2/rows/.eigs`. Each has a header
+ `# ROW: `, `# ITEMS: N1 N2`, `# EXPECT-RC: `, `# SHAPE: S|L|F|O`,
+ `# WITNESS: ... []`, `# OPS: ...`, plus `.out`
+ (the exact branch stdout). Controls live in `tests/phase2/controls/`.
+ A row that needs a capability build adds `# PROFILE: server|db|zlib`. The
+ checker then runs it on `$TAG_BIN_`/`$BRANCH_BIN_
`, the per-builtin
+ table's matched builds (ruling A3), after each prints `profile-ok` from its
+ `_profiles/
.eigs` witness. An unset or wrong build is a population
+ failure; a row never rewrites its own binary path (R1.6f-D4).
+- **Population:**
+ - the row set is the directory listing (it cannot drift from a list);
+ - every row id appears in MAP.md and every MAP id has a file (both
+ directions), with the same shape and witness;
+ - every item in the fixed list (N1-N14, N4b, A, B, T1, O1-O4, transcribed
+ from #1758 Revision 1) has at least one row;
+ - the `ops:` of the `L`, `F` and `O` rows together cover every
+ JIT-emitter arm and leaf-evaluator arm that holds a numeric site, read
+ from the inventory file's `## Numeric arms` (15 emitter arms and 5 leaf
+ arms today, each with its opcode chain; an arm is covered when some
+ row's `ops:` names an opcode of its chain that the row runs natively or
+ stops exactly at; the MAP's shaped rows cover all 20);
+ - zero rows is a FAIL, except a mechanical round under `--through`
+ (the table above), which runs its named gates instead.
+- **Tiers:**
+ - `off` = `EIGS_JIT_OFF=1`;
+ - `default`;
+ - `osr` = `EIGS_JIT_OSR_THRESHOLD=1`;
+ - `trace` = `EIGS_TRACE=/t.tape` with the default JIT. The leaf
+ evaluator is off under trace (§4.5), so this tier is the **leaf
+ differential**: every `.eigs` row must print exactly what it prints in
+ the other tiers, so an `L` row compares the leaf with the generic path
+ on its int, float or overflow program. (`sh` rows that set `EIGS_TRACE`
+ themselves skip this tier.)
+ - Every run is bounded at 30 s, with `= 128 is `SIGNAL`. Both
+ are a FAIL on the branch; on the tag they count as `tag_fail` and are
+ also shown in their own counters.
+- **Output, one line per tier:** `tier= examined=N rows tag_fail=N
+ branch_pass=N vacuous=0 tag_timeout=K tag_signal=K`, then `controls=M
+ both_pass=M`, `witnessed=W` (the rows whose witness held),
+ `controls_witnessed=C controls_vacuous=0` (shaped controls must prove their
+ tier on the branch too), and an environment banner (both binaries' `--version` and sha256).
+- **Exit 0** iff in every tier examined == tag_fail == branch_pass > 0
+ (or, for a mechanical round under `--through`, its named gates pass),
+ vacuous == 0, the controls all pass on both, and the population checks
+ pass.
+- **Calibration** (one-time, in the commit that adds it):
+ - tag passed as both binaries: `branch_pass=0`, exit 1;
+ - one `.out` corrupted: exit 1, naming the row;
+ - an empty rows directory: exit 1;
+ - a MAP id with no file, and a file with no MAP id: exit 1 each;
+ - a `leaf` row run against a scratch build whose leaf scanner returns
+ early (`chunk_scan_leaf_accessor` never sets the flag): `vacuous=1`,
+ exit 1;
+ - an `fn` row whose function is called 10 times instead of 60: `vacuous=1`
+ (the JIT never compiled it);
+ - an `L` row run against a scratch build whose leaf `MUL` arm computes
+ `a * b + 1`: the `trace` tier differs from the others, exit 1, naming
+ the row;
+ - **one negative plant per witness kind, each a row where the claimed op
+ did not run as claimed** (the rows of §9.1's calibration table, which
+ this design ran on main): `leaf` (the helper split into two statements),
+ `leaf-gen` (the helper computed by the leaf), `fn` (an unsupported op
+ before the claimed one), `fn-stop` (the claimed op runs natively), `osr`
+ (the critic's `osr_ret_PLANT2`: a string concat ahead of the claimed op,
+ 0 native `INDEX_SET` calls), `osr-stop` (the claimed op runs natively to
+ the sentinel). Each must give `vacuous=1`, exit 1, naming the row;
+ - `--through R1.2` on a tree with no R1.1/R1.2 rows: exit 0 by the
+ mechanical gates, `mapped=0`; and with the int-source check planted (a
+ production `slot_from_int(count)` call): exit 1.
+
+**As built in R1.1** (`tools/phase2_check.sh`, a wrapper over
+`tools/phase2_check.py`, whose docstring is the reference):
+- **Controls land with R1.3**, the first round with rows of its own. Under a
+ mechanical `--through` they are not required: their witnesses are about
+ ints (`ctl.N1.fn-int-truthiness` expects the int call to bail), which
+ cannot hold before an int exists.
+- **The leaf witness is read in the default and `osr` tiers.** With
+ `EIGS_JIT_OFF=1` the hot table counts no call at all (probed on the R1.1
+ binary: a function called 60 times has no row), so the `off` tier cannot
+ witness either way.
+- **The token gate** checks, in every mode, that each token a landed round
+ deleted (2.7) occurs in no code line of `src/` or `tests/` C files.
+ (Until the second R1.1 fix round it ran only in the mechanical rounds;
+ it runs in every mode now, as this line said.)
+- **The inventory gate** runs in every mode too:
+ `tools/phase2_inventory.py --check` must PASS, so a slot classifier with
+ no reviewed disposition row fails the check, and so does a committed
+ `docs/design/phase2-inventory.txt` that is not exactly the generated
+ file (#1804: nothing ran the inventory's verdict, and the committed file
+ had gone stale).
+- **The corpus differential** is `tools/phase2_corpus_diff.sh
+ `: both binaries run every `.eigs` under the branch tree's
+ `tests/` from the branch tree's `src/` (the suite's cwd), so the file and
+ whatever it loads by a relative path are the same bytes for both; only
+ the binary and its stdlib differ. A file only the branch has (a new
+ fixture) is counted as `new`; one only the baseline has is a DELETED
+ failure. R1.6a declares `tests/phase2/builtins/` in
+ `tests/phase2/corpus_exclude.txt`: those programs are owned by
+ `tools/phase2_builtins.sh`, which provides their fixtures, observation
+ hooks, matched capability profiles and stdin. The corpus applies the same
+ declared directory exclusions to both inventories and reports their paths,
+ counts, owners and reasons. Unsafe, overlapping, stale or ownerless
+ declarations, and a zero remaining population, fail. This file is the sole
+ ownership list: JIT/OBS, replay, the Phase 2 corpus differential, the full
+ Valgrind corpus and the road fixture walker all use
+ `tools/phase2_corpus_exclude.py`. Each filters its broad population before
+ narrowing to its existing subset, reports the excluded count and rejects a
+ declaration that excludes zero files. These probes remain covered by §9.4's
+ table gate and its once-only calibration plants.
+
+ **As corrected in the R1.1 fix round** (the critic's plants B
+ and D passed the first version):
+ - each binary runs each file TWICE. A file is deterministic when the
+ baseline's two runs agree, and then any branch run that differs is a
+ DIFF: a branch that is wrong on every other run is wrong;
+ - randomness runs LIVE with a pinned seed. The runtime has no seed flag
+ or variable, so the tool builds an `LD_PRELOAD` shim. The drand48
+ self-seed is `srand48(tv_sec ^ tv_nsec ^ getpid())` from a
+ `CLOCK_MONOTONIC` reading taken just before; the shim records each
+ thread's last such reading and pins a seed equal to that formula,
+ whatever its value, while a script's `seed_random of N` passes
+ unchanged. (The first fix round's shim pinned seeds >= 2^20 instead,
+ and the self-seed falls below that: 5 of 3,000 draws went unpinned; the
+ second round's pinned 3,000 of 3,000.) libc `rand()`, which
+ `audio_noise` draws from and `main()` seeds with `srand(time(NULL))`,
+ is pinned at every `srand` call. `random_hex` reads `/dev/urandom` by
+ design and stays unpinned; the shim marks every run that opens it, and
+ those files are listed (`UNPINNED`) with their verdicts. The tool
+ checks the shim first (two pinned draws agree). A random builtin changed
+ on the branch therefore shows wherever the corpus observes it;
+ `test_system.eigs` now draws `random_int` 300 times against its bounds;
+ - a tape is the last resort, for a file whose baseline disagrees with
+ itself even pinned (timing, threads): the baseline records it and
+ replays it twice, the branch replays it once and must match
+ (`same_under_replay`). Such a file's nondeterministic builtins did not
+ run live, so their behaviour is invisible there; the report lists every
+ file adjudicated that way (`REPLAYED`). Files whose replays disagree with
+ each other stay NONDET, also listed;
+ - no blanket memory cap: only files that drive the gfx, ui or audio
+ surface run under `ulimit -v 2097152` (listed as `CAPPED`).
+ `handles_full.eigs` varied only because of the old blanket cap.
+
+ **As corrected in the second R1.1 fix round** (the second critic's plants
+ C and B2 passed the whole oracle while the suite went red on C):
+ - each file runs under its run conditions from the one loader the suite,
+ `jit_diff.sh` and `replay_diff.sh` use (`tests/suite_program_env.sh`,
+ policy `tests/suite_program_env.txt`). Before, every file started in
+ the default strict mode, so 14 fail-soft numeric-boundary files died at
+ an early strict raise on both binaries, about 394 asserts never ran, and
+ the two early deaths were counted `same`. The four files the suite ran
+ with an inline `EIGS_STRICT=0` (and one with an inline
+ `SDL_VIDEODRIVER=dummy`) now have rows there, so there is no second
+ source;
+ - a file with conditions must complete (rc 0) on both baseline runs, the
+ rule `jit_diff.sh` already applied; otherwise it is INCOMPLETE and
+ never compared;
+ - every file whose baseline exits nonzero must be declared, with that
+ code and a reason, in `tests/phase2/corpus_nonzero.txt` (32 rows: the
+ runaway guard's fixture, deliberate errors, fixtures of other hosts,
+ capabilities the release build lacks). An undeclared exit is
+ UNDECLARED-NONZERO and a row whose file exits otherwise is STALE: both
+ fail the run. So removing a file's run conditions turns the
+ differential red, and so does a new test that dies early. Every
+ declared file is listed with its code (`SAME-NONZERO`);
+ - a timeout or a crash all four runs agree on is counted as
+ `timeout_both` or `crash_both`, no longer also as `same`;
+ - every run uses SDL's dummy audio device (`SDL_AUDIODRIVER=dummy`): a
+ real capture device delivers samples or not by timing, so
+ `test_audio.eigs` printed a SKIP on some runs, and with its conditions
+ applied the two-run baseline check let it through as deterministic and
+ a branch run that differed read as a DIFF.
+- **The smi-trap gate** (`--smi-trap BIN`, R1.1 and R1.2) runs the same
+ differential with a `make smi-trap` build as the branch; until R1.3 it
+ must show no difference, because no program may make an int.
+- **The calibration** is `phase2_check.sh --calibrate [--build-plants]
+ `, on the fixtures in `tests/phase2/calib/`: each ends in a line
+ that fails on the tag (#1740: `5.5 & 1` raises `value` on the branch and
+ truncates on v0.46.0), so an honest fixture has tag_fail == branch_pass.
+ `--through R1.2` cannot pass on the R1.1 tree (its token gate would find
+ `VAL_NUM`, correctly), so the R1.1 run of that item is `--through R1.1`;
+ the int-source plant runs against a planted copy of `src/`. Cases the
+ design did not list were added for the plan-level checks (an item with no
+ row, an arm no row exercises, a round with neither rows nor gates) and
+ for the token gate (a reintroduced `slot_is_num`). The fix round added the
+ critic's plants: the differential's A (a deterministic file prints one
+ more line), B (a branch wrong on alternate runs, both phases) and D (a
+ build whose `random_int` answers lo+1..hi+1), each of which must make the
+ differential name the file DIFF; the int-source plants P1-P5, each of
+ which the static half must name at R1.1 (at R1.2, P4 sits inside an
+ allowed writer and the static half passes it); and P4 built into a
+ smi-trap binary, which the dynamic half must stop. The second fix round
+ added the second critic's plants C (a build whose buffer equality reads
+ raw elements) and B2 (an int whose tag bits are spelled from
+ `SLOT_QNAN_MASK`, which the static half does not see, on the smi-trap
+ build), each of which must make the differential name a conditioned file
+ DIFF; a removed policy row, which must be UNDECLARED-NONZERO; a ledger row
+ for a completing file (STALE); a conditioned file that does not complete
+ (INCOMPLETE); and the inventory gate's plants (`vm.c:critic_new_reader`
+ with no disposition row, and a stale committed inventory). A restricted
+ run (the calibration's file list) prints `RESTRICTED` and exits 3, never
+ PASS.
+ The calibration is a `tools/selftests.txt` row (`phase2_check.py
+ --selftest`, which builds v0.46.0 and the four planted binaries itself):
+ it runs when the checker or its fixtures change, and nightly, never on
+ every suite or CI run (docs/PRINCIPLES.md 5: a check proves it fires once,
+ when it is written or changed).
+
+### 9.3 `tools/phase2_plants.sh` (round 1 builds it; detached, about 7 release builds)
+
+Each plant:
+1. `git archive HEAD` into a scratch dir (no `.git`);
+2. applies one `sed`, asserting the original text occurs exactly once before
+ and the planted text after (a plant that did not apply is `BROKEN`);
+3. builds the release with plain `make`, once at a time, `timeout 1500`;
+4. runs **its named check** and requires the check's own FAIL line naming
+ the row (keyed on the gate's message, not on a nonzero exit, §179).
+
+Verdicts: `KILLED`, `SURVIVED`, or `BROKEN` (patch, build or timeout).
+Output: `plants=7 killed=7 survived=0 broken=0`; exit 0 only for exactly
+that.
+
+| # | plant | where | named check that must turn RED |
+|---|---|---|---|
+| 1 | an int that fits inline stored boxed | `eigs_container_slot` (or `slot_from_int`) skips the small-int rule and stores a `VAL_INT` box | `tests/phase2/slot_layer.c`: every canonicalizing writer of §2.1, driven with every boundary value, must give tag 0xFFFA inside ±2^47 and a box outside. No program row can see this plant (§2.1: everything compares by value), so the round-0 claim that `N1.canon-eq` would turn red is withdrawn; that row now pins only semantics |
+| 2 | a NaN stored raw in a slot | `slot_from_f64` without the canonical branch | row `N12.nan-payload-tags` (`f64_from_bits` of every tag-spelling payload into a variable, list, dict and buffer; `type of` each is "num", under ASan in the plant build) |
+| 3 | a writer accepting a non-finite value | the refusal inside `eigs_json_encode_value` deleted; separately inside the store encoder | rows `N13.json-refuses-inf` and `N13.store-refuses-nan` (raise `value`, and nothing written: the store row re-reads the record count) |
+| 4 | `+` wrapping on overflow | the SMI+boxed add path uses wrapping `uint64` add | row `A.add-overflow-raises` (`9223372036854775807 + 1` raises `value` naming `+`) |
+| 5 | `-INT64_MIN` wrapping | `CASE(NEG)`'s int arm without the INT64_MIN check | row `N4b.neg-min-raises` |
+| 6 | `num_type of 3.0` returning "int" | `num_type` answers "int" for an integral float | row `N7.num-type-float` |
+| 7 | a v6 tape accepted | the version check accepts 6 | row `T1.v6-refused` (the committed v6 fixture must exit nonzero naming version 6) |
+
+Each plant is transverse (§100): deleting its named check's assertion must
+let the plant through. That is shown once per plant during calibration by
+running the plant against a copy of the check with its assertion removed.
+
+The tier-witness and leaf-differential plants (§9.2's calibration) belong to
+`phase2_check.sh`'s own calibration, not to this list: they test the
+instrument, not the semantics.
+
+As built in R1.1, the tool holds the plants whose named checks exist: plant
+1 (`slot_from_int` tags an int that fits as a heap pointer; its named line
+is `slot-layer-test: FAIL slot_from_int(0): bits ...`). Plants 2-7 join the
+table in the commits that land their rows, and its output then reads
+`plants=7`. Its transverse run deletes exactly the named assertion
+(`if (s.u != smi(v).u)` becomes `if (0)`) and requires the plant to pass.
+
+The R1.1 fix round added three instrument plants on the slot test, each
+with its own transverse run: `arm-refusal` (`NUM_CMP`'s int refusal
+deleted; named line `slot-layer-test: FAIL arm lt with an int: got ...`),
+`stderr-noise` (the population scan writes a line to stderr; named line
+`slot-layer: FAIL: the population scan wrote to stderr`) and
+`stdout-noise` (the scan prints a line outside its grammar; named line
+`slot-layer: FAIL: the population scan printed lines outside its
+grammar`). The tool also fails a plant whose check writes to stderr. Like
+the calibration, the plants are a `tools/selftests.txt` row (`phase2_plants.sh
+--selftest`), keyed on the checks they name (the slot test, its runner and
+the population scan), so they run when those change, never per suite or CI
+run (docs/PRINCIPLES.md 5).
+
+### 9.4 The per-builtin behaviour table (R1.6's gate; replaces the int-fuzz class check)
+
+The table exercises every numeric builtin with a valid call. Integer
+results can come from non-numeric inputs (`len`, `ord`, `index_of`,
+`buf_len`, `scan_ints`, `read_bytes`); other calls need a valid handle or
+shape (`shared_size`). Feeding every builtin a bare int cannot classify
+those results.
+
+**R1.6a amendments**, pre-registered before migration and recorded with the
+orchestrator's rulings in `tests/phase2/builtins/{BAR,RULINGS}.md`:
+
+- **(a) Migration changes compare `.branch` with `.pre`, the R1.5 baseline.**
+ `changes_vs_pre` is the vacuity guard; `changes_vs_tag` remains an observed
+ comparison. The original release bar recorded 202 differences from
+ v0.46.0 among 206 rows. After ruling R1.6a-D1 adds `read_text`, the updated
+ pre-registered bar records 203 among 207. These are historical tag
+ comparisons: the tag has no `num_type`, so comparing only with `.tag`
+ would make the migration check vacuous. The remaining 25 rows use matched
+ `server`, `zlib` or `server-db` builds, with PostgreSQL for db probes
+ (ruling A3).
+ The original `.pre` records came from `4ac31e67`; R1.6a checks them against
+ its start, `1fd4422c`, and records the permitted #1668 file-failure or
+ file-replay changes. The new `read_text` row's in-scope `.pre` is recorded
+ on `1fd4422c`, where #1841/#1668 already landed, so its `changes_vs_pre`
+ is `no`. Its `.tag` remains the v0.46.0 observation. The current explicit
+ baseline instruction supersedes BAR.md's handoff parenthetical suggesting
+ that `.tag` also be recorded on `1fd4422c`.
+- **(b) Probes and expectations are files.** `builtins/.eigs` is a
+ program; `.branch`, `.pre` and `.tag` are exact stdout followed by a
+ final `rc=` line. Stderr is retained for diagnostics and is never
+ compared. `cases.tsv` names secondary cases, hook and stdin fixtures;
+ `rowmeta.tsv` records each builtin's profile and any earlier closure.
+ Ruling R1.6a-D2 makes probes print predicates over OS-selected ports,
+ handles, clocks and paths instead of those variable values themselves.
+ The deterministic injected-clock witness still asserts its exact payload.
+- **(c) Landed sub-rounds are declared.**
+ `tests/phase2/builtins_landed.txt` holds completed `R1.6a`..`R1.6z`
+ tokens, one per line (comments and blank lines allowed). Each later
+ builder adds its round in the completing commit. Empty in R1.6a means
+ every current row must still equal `.pre`. A row's round follows §8's
+ rule applied to the file defining its inventory C function.
+
+`tests/phase2/builtins/builtins.tsv` is checked by
+`tools/phase2_builtins.sh `, which calls the bar's
+single table checker and runner. Its checks are:
+
+1. **Membership, both directions.** Rows equal the inventory's `## Builtins`
+ list and are nonempty; `class`, `loss` and `q7` equal §6, parsed with the
+ inventory's own table parser; the defining C file determines `round`.
+2. **Class consistency of `.branch`.** An int-preserving result shows
+ `"int"`; a float-producing result shows `"float"`; an int-requiring
+ call raises `value` on a fraction and succeeds with an integral float.
+ Every Q7 class named in the row is exercised. Where stdout cannot
+ identify that property, a machine-readable probe header names the
+ exercised path and the checker validates it. Boundary hooks assert
+ delivered PCM, mixer gain/volume, an injected clock or injected events.
+3. **Run.** A landed row equals `.branch`; every other row equals `.pre`.
+ The tag equals `.tag`. Each run gets a fresh `EIGS_PROBE_TMP`, headless
+ graphics and audio as in the suite, the recorded stdin, hooks and
+ capabilities. Non-release profiles use `TAG_BIN_SERVER/ZLIB/DB` and
+ `BRANCH_BIN_SERVER/ZLIB/DB`; db rows require `DATABASE_URL`. A missing
+ capability or a timeout fails, rather than supplying an expectation.
+4. **Vacuity.** `changes_vs_pre` is `yes` exactly when `.branch` differs
+ from `.pre`. Every LOSS, int-requiring and Q7 row needs `yes`, except an
+ individually recorded `closed_in` ruling (A2). That earlier closure is
+ checked by requiring `.pre == .branch` on all its cases. No blanket
+ waiver is allowed; `.branch` files are never edited by a builder.
+
+A passing run prints exactly one
+`phase2-builtins: PASS rows= landed= migrated= unchanged=`
+line; a failure names the row and reason. `make_table.py --final` rejects
+pending profile records. The same checker enforces the complete `.branch`
+pathname/content set against the approved git object pinned in `make_table.py`,
+read with `git ls-tree` and `git show`. A missing pin fails; the phase2 workflow
+fetches it explicitly. Every changed, added or deleted path needs an exact
+amendment in `tests/phase2/builtins_changes.md`, with its new SHA-256 (or
+`DELETED`), orchestrator ruling and reason. Accepted amendments print their
+ruling id; unlisted differences and stale amendments fail by path.
+
+**Calibration**, once when the gate is written or changed: a `len` result
+planted as `make_float(count)` turns its row red; declaring R1.6b landed
+on the R1.6a build turns every b row with changes red; deleting a row and
+adding a stale row fail membership in both directions; editing `class`
+fails; making a LOSS row's `.branch` equal `.pre` is VACUOUS. The bar's
+own plants additionally pin its fixture, profile, hook and stdin checks.
+After the matched profile records are complete, `determinism.sh` records
+all cases three times (including a pinned, lowered-priority run) and
+compares every `.pre` and `.tag`; any unstable record fails (R1.6a-D2).
+`read_text` uses the `1fd4422c` pre binary for each recording, while every
+tag recording continues to use v0.46.0.
+
+The int-fuzz sweep, if kept, is only a crash sweep: each builtin fed ints
+in numeric slots must raise or return, never crash.
+
+### 9.5 `tools/phase2_lib_diff.sh ` (#1848, from R1.3)
+
+A real-valued stdlib formula can compute in int64 through an int literal of
+its own and an int argument: `rest_energy of 103` (an int speed of light)
+and `poisson_pmf of [21, 5.0]` (an int `k!` denominator) raised where main
+answered. A grep cannot find the second kind, so this is a differential.
+- **Population:** every tracked `lib/*.eigs` module's import namespace (its
+ `fn` keys), checked against the modules' column-0 `define` lines in both
+ directions. A function is called once, through its own module, or through
+ a module that re-exports it when its own cannot be imported alone (`ui`'s
+ `ui_w_*` fragments). `lib/pkg.eigs` is the one declared non-library module
+ (importing it runs the `--pkg` CLI); any other unexamined module fails.
+- **Same input:** both binaries run a copy of the branch's `lib/` under a
+ scratch project root, so only the runtime differs.
+- **Calls, the stated range:** every int n in [-3, 110]; arity 1 calls f(n),
+ arity k >= 2 the diagonal and, per position, n there with 1 elsewhere.
+- **Verdict:** a call is listed when main completes it and the branch raises
+ or never reaches it. A listed function must be integer-domain and declared
+ in `tests/phase2/lib_int_domain.txt` with its reason (decision (A)); a
+ declaration no call needs is stale. Both fail.
+- At R1.3: 889 functions, 281,331 calls; `probability.factorial` is the one
+ declaration. `rest_energy`, `poisson_pmf`/`poisson_cdf` and
+ `fmt_num`/`fmt_percent` were fixed (a float constant, a float `k!`
+ denominator, a float scale factor). The fixed `poisson_pmf` is
+ byte-identical to main on 888 points and within 1.3e-13 of SciPy's
+ `poisson.pmf` (the oracle). Calibration: the int `SPEED_OF_LIGHT` planted
+ back lists `rest_energy` UNDECLARED; a declaration for `permutations` is
+ STALE.
+- **R1.4, N4 successful value changes.** The original OK/RAISE differential
+ cannot see a changed successful remainder. The same tool now derives a
+ conservative modulo-user/named-caller closure from the tracked module
+ source (including private helpers), and compares JSON result values for
+ its exported functions, ignoring only numeric-kind differences. Every
+ changed function needs an `N4:` reason in
+ `tests/phase2/lib_floor_mod.txt`; an unused declaration is STALE. The
+ integer-domain raise declarations remain independent and unchanged.
+ Negative boundary probes (-7 and -719469) reach weekday and civil-era
+ boundaries missed by [-3, 110]. Typed probes cover the meta-interpreter's
+ source/AST entry points, accepted negative numeric checksum buffers, and
+ supervision's negative heartbeat observer feed as well as its return.
+ The closure is conservative by name, not a full resolver; the release
+ census reviews dynamic dispatch and caller domains separately. Successful
+ values outside that closure remain outside this extra N4 comparison.
+- **Range boundary:** ints at most 110 keep a polynomial of degree 9 in the
+ caller's own ints inside int64, so a listed call points at the library.
+ A literal that overflows only for larger arguments is not seen.
+
+---
+
+## 10. The Lua differential tool (`tools/phase2_lua_diff.py`, round 1)
+
+**Oracle.**
+`/home/jon/src/InauguralSystems/EigenScriptEcosystem/EigenLua/.ref/bin/lua`
+(PUC-Rio 5.4.9, pristine).
+- Checked at start with `EigenLua/tools/build_ref.sh --check-not-compat
+ `. The binary argument is required; the bare form fails with
+ `$2: unbound variable`. It exited 0 on this box.
+- A failed check stops the run as an instrument failure (exit 2), never as
+ a verdict.
+
+**Cases.** Expressions over operands drawn from the edge set {0, ±1, ±2^31,
+±(2^47 - 1), ±2^47 (the SMI boundary), ±2^53, ±(2^53 + 1), ±2^62,
+INT64_MIN, INT64_MAX, 0.5, -0.0, 1e308, 2^63 as a float}. Operators:
+- arithmetic `+ - * / // %`;
+- unary `-`;
+- bitwise `& | ~ << >>`;
+- comparisons `== < <=`;
+- `math.abs`, `math.floor`, `math.ceil`, `math.max`, `math.min` against
+ `abs`/`floor`/`ceil`/`max`/`min`;
+- `math.type` against `num_type`.
+
+Translation: Lua binary `~` is EigenScript `^`, Lua `^` is `pow of`, and
+Lua unary `~` is `~`.
+
+String operands are excluded: Lua coerces `"10" + 1` and EigenScript does
+not. That divergence is documented and not exercised.
+
+**Comparison by value, never by text.**
+- Each side prints its result in a canonical machine form. Lua uses
+ `math.type(x)` plus `string.format("%d" | "%a", x)`. EigenScript uses
+ `num_type` plus `str` for ints, and `f64_bits` once round 2 lands it;
+ until then, float text parsed to a double by Python, whose float parsing
+ is correctly rounded.
+- Kind and value must match.
+- A raise on both sides compares as `RAISE` (for example Lua `n//0` "attempt
+ to perform 'n//0'" against EigenScript `value`).
+
+**Deliberate divergences**, each a rule with its own counter, never a skip:
+
+| rule | Lua 5.4.9 (probed) | EigenScript | how the tool checks it |
+|---|---|---|---|
+| (A) | `math.maxinteger + 1` gives `-9223372036854775808` (wraps) | raises `value` naming the operator | the tool computes the exact result in Python (arbitrary precision); when Lua's int result differs from it, EigenScript must raise |
+| N6 | prints `%.14g` (`0.3`, `1e+15`, `-nan`) | Python `repr` (`0.30000000000000004`, `1000000000000000.0`, `nan`) | values compared by bits; EigenScript's text is checked against Python `repr` separately (Python is the N6 oracle, not Lua) |
+| (B) | `type(3)` is "number", `math.type(3)` is "integer", `math.type("3")` is nil | `type of 3` is "num", `num_type of 3` is "int", `num_type of "3"` is null | name mapping number to num, integer to int, nil to null |
+| N4b | `-mininteger`, `math.abs(mininteger)` and `mininteger//-1` all give mininteger | each raises `value` | expected RAISE; `mininteger % -1` is 0 on both (no divergence) |
+
+**Output:** `examined=N agree=N rule_A=N rule_N6=N rule_B=N rule_N4b=N
+disagree=0`. It exits 1 on any unexplained disagreement or on examined=0.
+
+**Calibration plants:**
+- flip rule (A) to expect wrapping: red on the overflow cases;
+- compare by text instead of value: red on the N6 cases;
+- a fake `lua` that prints `float` for an int: red.
+
+---
+
+## 11. Decided questions (Q1-Q8)
+
+Q1-Q6 were open in round 0. They were decided on 2026-10-14 by the
+standard answer, each verified by probe, in
+[#1758 (comment)](https://github.com/InauguralSystems/EigenScript/issues/1758#issuecomment-6171744498).
+The owner may overrule any of them; until then this design follows them.
+
+- **Q1, N5's scope: DECIDED.** The parameter's meaning decides, not the C
+ type.
+ - Integer-valued parameters (indices, counts, sizes, byte values,
+ handles, ports, seeds) follow N5: `1.5` raises `value`. Lua:
+ `string.sub("abc", 1.5)` raises "number has no integer representation".
+ - Continuous parameters (gfx coordinates and sizes, colour channels,
+ durations, audio gain) accept floats; where the C API needs an int, the
+ conversion rounds to nearest, replacing truncation toward zero (SDL3's
+ float coordinates, LÖVE's numeric drawing API).
+ - Applied: §6's rows (16 moved from other to int-requiring; byte
+ consumers no longer wrap), `eigs_byte_arg`, `eigs_int_arg_range`, and
+ the Q7 readers (§2.6), R1.6 (§8).
+- **Q2, an int stored into float storage: DECIDED.** Convert with IEEE
+ round-to-nearest, documented as the container's contract (Python
+ `float(2**53+1)`, numpy, JS `Float64Array`). Applied: `buf_set`,
+ `buf_from_list`, `copy_into`, `set_at` on a buffer, `stream_write`; the
+ inventory's `buffer_read_num` sites keep their token (§2.3a).
+- **Q3, the embed API: DECIDED, Lua's C API.** `eigs_value_as_num(int)`
+ converts (lossy above 2^53, like `lua_tonumber`); add
+ `eigs_value_new_int`, `eigs_value_as_int` (succeeds only for an exact
+ int64, with a success flag, like `lua_tointegerx`) and `eigs_value_is_int`
+ (like `lua_isinteger`); `EIGS_TYPE_NUM` covers both kinds (like
+ `LUA_TNUMBER`). Applied: R1.8; the 48 `eigs_value_as_num` sites keep
+ their token (§2.3a).
+- **Q4, `round`: DECIDED.** Keep half away from zero (C and Rust `round`);
+ the result is an int when it fits int64 and a float otherwise, like Lua's
+ `math.floor`/`math.ceil` (`math.floor(1e300)` stays `1e+300`). Applied:
+ §6 (`round` int-preserving, LOSS), R1.6; the Q7 readers use the same
+ tie rule.
+- **Q5, `why is x` for an int binding: DECIDED.** An int when Δv fits in
+ int64, otherwise a float. A readout never raises; (A) applies to
+ operators, not to observer readouts. Applied: round 3 (O4).
+- **Q6, `//=`: DECIDED.** Add it beside `/=`. Applied: R1.4.
+
+- **Q7, a finite continuous parameter outside the C range: DECIDED**
+ ([#1758 comment 6174302316](https://github.com/InauguralSystems/EigenScript/issues/1758#issuecomment-6174302316)).
+ It follows the quantity it represents:
+ - geometry (coordinates and sizes): clip in float space against the
+ drawable area, then round to nearest. Saturating each argument
+ separately is wrong: `gfx_rect of [-3e9, 0, 6e9, 10]` would cover half
+ the screen instead of all of it;
+ - bounded quantities (colour channels, gain, volume): clamp to the domain;
+ - unbounded magnitudes the C call cannot honour (durations, sleeps): raise
+ `value`, naming the builtin (Python's `time.sleep(3e12)` raises
+ `OverflowError`).
+ - Applied: the builtin table's validated `q7` column (§6); the readers
+ `eigs_geom_shape`, `eigs_bounded_arg`, `eigs_magnitude_arg` (§2.6),
+ landing in R1.6. Two classifications this design made inside the
+ decided categories: a window size (`gfx_open`) and a measurement's
+ scale (`gfx_text_width`/`height`) have no drawable area to clip against,
+ so they are magnitudes.
+- **Q8, NaN in an ordering: DECIDED** (same comment). IEEE 754-2019
+ `totalOrder`: the canonical (positive, quiet) NaN sorts after +inf,
+ deterministically, in `sort` and `sort_by` (numpy's `sort` and Rust's
+ `f64::total_cmp` give the same order); `min` and `max` propagate NaN
+ (IEEE 754-2019 `minimum`/`maximum`). Lua and Python produce a garbage
+ order here (probed for #1758: Lua's `table.sort` left a list holding NaNs
+ unsorted without an error). Applied: R2.2, when NaN first becomes
+ reachable; rows `N11.sort-total-order` and `N11.min-max-nan`.
+
+
+## 12. R1.2 verification notes
+
+The fix round merged `origin/main` at `540f0bcf` before measuring. It keeps
+`VAL_DICT = 7` after main's removal of `VAL_JSON_RAW`, with `VAL_INT` still
+appended. Main's taped `audio_noise` is included: the merged replay gate
+reported `258 programs record+replay; 21 at the documented boundary;
+0 nondeterministic; 1 ledgered`, confirming the retired `test_audio` row.
+
+**Float cost.** The former three-probe report missed for-in and boxed-index
+costs. `tools/phase2_cost.sh BASE CAND` now derives its in-repo workloads
+from `codspeed.yml`, deduplicates tier aliases and measures every workload
+with the JIT on and off. The CodSpeed list now includes all tracked
+`bench/*.eigs` and `tests/bench_*.eigs` files. A Git-derived coverage check
+rejects missing or stale entries; unsupported commands also fail before
+measurement. The three planted config faults were rejected (3/3), and the
+CodSpeed target check reported 19 declared targets with its 7/7 self-test. The declared regression threshold is +0.3% Ir;
+any result above +0.1% needs attribution. A failed program, sanitizer binary,
+missing instruction count or different result fails the tool; only printed
+elapsed milliseconds are normalized. Logs retain each run's stdout, stderr
+and cachegrind data. This is the narrow, externally anchored check required
+by principle 5, Checks are anchored outside the system, narrow, and few.
+
+Same-environment release builds (gcc 13.3.0, default Makefile -O2 flags),
+Valgrind 3.26.0.codspeed7, cache simulation disabled. Both columns compare
+against the fresh `540f0bcf` build. The unfixed `a49ebe58` binary failed
+with `workloads=15 rows=30 over_0.3pct=17 FAIL`; the corrected binary passed
+with `workloads=15 rows=30 over_0.3pct=0 PASS`. Every corrected delta is
+negative, so no positive delta above +0.1% needs attribution.
+
+| workload | unfixed JIT | corrected JIT | unfixed interpreter | corrected interpreter |
+|---|---:|---:|---:|---:|
+| `bench/alloc_heavy.eigs` | -0.7750% | -0.8015% | -1.3411% | -1.3247% |
+| `bench/conservative_observed_loop.eigs` | +2.4924% | -1.9758% | +1.2051% | -1.7020% |
+| `bench/conservative_unobserved_loop.eigs` | +3.1284% | -2.4944% | +1.3736% | -2.1230% |
+| `bench/dict_ops.eigs` | +0.7736% | -0.5208% | +0.7736% | -0.2752% |
+| `bench/observed_loop.eigs` | +4.6862% | -3.5071% | +1.7607% | -2.7203% |
+| `bench/scalar_loop.eigs` | +4.1131% | -3.1679% | +1.5824% | -2.5250% |
+| `bench/string_build.eigs` | +1.0707% | -0.0827% | +0.8484% | -0.0920% |
+| `bench/unobserved_armed_loop.eigs` | -0.5739% | -0.5734% | -2.6672% | -2.5973% |
+| `bench/unobserved_loop.eigs` | +4.6856% | -3.5065% | +1.7599% | -2.7201% |
+| `tests/bench_buf_mix.eigs` | -0.4679% | -0.4750% | -1.3133% | -1.6209% |
+| `tests/bench_buf_resample.eigs` | +0.3307% | -0.1916% | -0.6375% | -1.0595% |
+| `tests/bench_dmg_shape.eigs` | -0.1762% | -0.1807% | -1.5526% | -1.9744% |
+| `tests/bench_idxset.eigs` | +0.2827% | -0.4619% | +0.6823% | -0.5958% |
+| `tests/bench_pcm_codec.eigs` | -0.4691% | -0.4698% | -0.8728% | -1.3577% |
+| `tests/bench_perf.eigs` | +0.8874% | -0.9616% | -0.1238% | -1.4599% |
+
+Function counts and disassembly localized the remaining cost after merely
+moving the four refusals: numeric views and their cleanup checks still
+classified ints on float paths, and iterator metadata was read through
+unnecessary facades. The corrected iterator uses the checked float reader
+once per metadata slot and borrows its reference-valued iterable directly;
+int metadata still reaches the named R1.3 refusal after a failed float read.
+`slot_view` and valid-view cleanup now exit before int checks. Index
+validation and reference acquisition remain inline. No int producer was
+introduced, and the sort-key pair layout was retained.
+
+**Integer readers and refusals.** The supplied critic harness was rerun over
+all 341 supplied scenario blocks, in the interpreter, default JIT and forced
+OSR tiers, with float, SMI and boxed-int inputs. Across 6,138 transcript and
+exit-status files, results and statuses were unchanged from the critic's
+R1.2 binary. The only 198 byte differences were heap addresses in the named
+boxed-slot refusal diagnostic; comparisons retained the diagnostic, slot
+tag, SIGABRT status and every other output byte. There were no unexpected
+crashes. The separate C checks reported `checks=409 fails=0`, including
+`sizeof(SortByPair)=16`, union payload offset 0, kind offset 8 and index
+offset 12.
+
+A focused iterator probe used the state `[[42], 0.0, 1.0]`: the two float
+controls yielded 42; replacing either metadata slot with an SMI or boxed
+int reached its named R1.3 refusal in both executors. Result:
+`cases=10 float_controls=2 int_refusals=8 failures=0`.
+A separate malformed-iterable differential agreed with the original R1.2
+binary in four JIT-helper cases: two int-index refusals and two snapshot
+bails, with no failures. This preserves the helper's early-return order.
+
+**Earlier gate calibration.** The original R1.2 calibration inserted a
+`VAL_NUM` token in `builtin_usleep` (token hits=1 and compile exit 2),
+removed the JSON encoder's `VAL_INT` arm (compile exit 2 under
+`-Werror=switch`), and removed the boxed `slot_int_value` reader (five
+assertion failures). These are historical one-time fault checks, not
+additional reruns in this fix round. Restored slot coverage remains
+33 sites, 165 calls and 396 matrix cells with no failures.
+
+**Builds.** Default release builds at -O0, -O1 and -O2 each exited 0 with
+zero warnings. The final -O2 rebuild was byte-identical to the measured
+candidate (`04c10293ab1b94b92ac5ae56063655038c3b47b8a245ab3d763ca0978682614b`).
+
+**Completed suites.** Full release: `4289/4289 passed, 0 failed, 9 skipped`.
+Full ASan with `detect_leaks=1` and no virtual-memory limit:
+`4281/4281 passed, 0 failed, 8 skipped`; LeakSanitizer nonzero-exit
+tally 0 and sanitizer error reports 0. Both child-script ledgers reported
+0 nonzero exits. `make jit-smoke`: `JIT smoke: all cases passed.`
+
+**GUI conditions.** An initial native-X11 JIT differential run omitted the
+graphics memory cap and reported two extra OBS rows, `ui_canvas_events`
+and `ui_dock`. Both examples are among the nine nonterminating GUI
+comparisons already tracked by [#1823](https://github.com/InauguralSystems/EigenScript/issues/1823).
+A focused check on fresh main and the candidate used X11 and dummy SDL,
+REF and OBS, and a 1,500,000 KiB virtual-memory cap: all 16 runs agreed
+with empty output and exit 124. These are timeout agreements, not completed
+GUI comparisons. The existing ledger was retained.
+
+A process-wide 1,500,000 KiB JIT cap changed `handles_full` on both main
+and the candidate: uncapped REF/OBS each reported `spawned=255 raises=45`,
+while the blanket cap caused variable earlier spawn failures. The final
+JIT/replay invocations therefore apply the existing corpus graphics-source
+selector to each program, exec the same release binary, and leave nongraphics
+thread fixtures uncapped. Strict graphics checks use their existing child
+limit. The corpus graphics ceiling is now 1,500,000 KiB too; a restricted
+real-VM probe failed its ceiling assertion at the previous 2 GiB setting
+and passed at the machine setting (1/1 capped program), with the expected
+restricted exit 3 in both calibration runs. The temporary probes were removed.
+
+**JIT differential.** With graphics-only caps and dummy SDL, the unchanged
+ledger passed: `258 programs x {jit, osr} vs the interpreter; suite-env
+completed=15; OBS examined=647 denied=12; 32 arms adjudicated; 2 ledgered`.
+The nine interactive timeout agreements in #1823 remain a coverage limitation.
+
+**Replay differential.** The unchanged fidelity ledger passed under the same
+graphics-only resource policy: `258 programs record+replay; 21 at the
+documented boundary; 0 nondeterministic; 1 ledgered`.
+
+**Strict differential.** The real-X11 gate passed. Typed fixed-shape
+controls: `declared=139 examined=139 passed=130 failed=0 unavailable=9
+backend_absent=0 pending=0 unrun=0`. All 37 pixel rows were identical with
+strict mode off; the 23 wrong-input pixel rows all raised under strict
+mode, with silent=0 and misattributed=0.
+
+**Candidate corpus.** Against fresh main, the full corpus comparison reported
+`examined=458 same=452 same_nonzero=42 same_under_replay=4 nondet=1 diff=0
+new=14 deleted=0 crash_both=0 timeout_both=1 conditioned=15 incomplete=0
+undeclared=0 stale=0 ledger_refused=0 unpinned=1 capped=13` and PASS.
+The 11 newly merged deliberate-error fixtures account for the increase
+from the earlier 31 declared nonzero results to 42.
+
+**Final Phase 2 gate.** `phase2_check.sh --through R1.2` used the fresh
+main baseline and a separately built SMI-trap binary. The trap corpus
+reported the same full 458-file summary as the candidate corpus, including
+`diff=0 incomplete=0 undeclared=0 stale=0` and 13 graphics-only caps.
+Both corpus halves passed; `mechanical smi-trap: PASS` and
+`phase2-check: PASS`. The retired-token scan reported `tokens=9 files=89
+hits=0`; inventory `examined=2012 classified=2012 unclassified=0`; int
+source `make_int_defs=0 outside_writers=0`; slot matrix
+`examined=33 calls=165 matrix_cells=396 arm_rows=32 failures=0`.
+The changelog fragment check exited 0 after all fourteen malformed fragment
+bullets were repaired. No push or CI run is claimed by these local results.
+
+### R1.7 rulings and executable text oracle
+
+R1.7-D2 permits only the mechanical N6 float-text re-render of seven frozen
+probes. `tools/phase2_float_amend.py` checks numeric positions and double bits,
+source-bound live kinds, exact integer spellings and every other byte. Six
+probes change; json_path already has the N6 spelling. Accepted SHA amendments
+and earlier authorities are recorded in `tests/phase2/builtins_changes.md`.
+
+R1.7-D3 distinguishes CHANGE rows from `[R1.7, invariant]` controls. Actual
+b6f9db38 already printed the D1 boundary literal with 15..17 round-trip digits;
+printing it directly still passes. D1 and exact int text are invariants.
+T1 adds 0.1 as a discriminating shortest-text witness. Every change is base-red
+and every invariant base-green. Invariants run all declared branch tiers;
+the older v0.46 anchor predates their already-landed contract.
+
+R1.7-D4 keeps the signed-zero program and adds the actual constant-pool C
+probe through `tools/phase2_readers.sh`: distinct indices and memcmp-exact
+stored signed-zero doubles. A complete dedup-by-value plant restores both
+zero-normalizing hash keys and numeric equality, and must fail that probe.
+
+The actual shared writer is also checked by `tests/phase2/num_text.c`, Python
+repr comparisons over binary64 patterns, and the same production function
+linked to mini_fmt/mini_strtod. The runtime retains its inherited default
+round-to-nearest floating-point environment assumption.
+
+Cost comparisons opt in to `tools/phase2_cost.sh --n6-text`: finite value bits
+and exact integer digits stay equal, changed float tokens must be Python repr,
+and nonnumeric bytes/positions stay exact. The default mode stays byte strict.
+This cost-only comparison cannot establish a legacy unadorned token's source
+kind; the semantic gates and D2 live kind witnesses do. Corpus diffs remain
+exact and are attributed individually from observed evidence.
diff --git a/docs/llms.txt b/docs/llms.txt
index 9a4739ff4..790b22f98 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -1,5 +1,21 @@
# EigenScript — single-file reference for language models
+
+Width arithmetic uses explicit wrapping, checked and saturating builtins.
+N5 converts integral floats, then checks the named type range without masking.
+Width results are ints; checked overflow raises `value`. `u64` uses int64 bit
+patterns. `fround` returns a float after double-to-float32-to-double conversion
+(ints convert to double first). See [the width builtins](BUILTINS.md#width-arithmetic-n9).
+
+```eigenscript
+print of [i64_wrapping_add of [9223372036854775807, 1], u8_wrapping_add of [255, 1]]
+print of (fround of 0.1)
+```
+```output
+[-9223372036854775808, 0]
+0.10000000149011612
+```
+
> EigenScript is a small dynamically-typed language with one distinctive
> feature: an **observer** tracks every value's entropy/trend on each
> assignment, so convergence, stability, and oscillation are first-class. This
@@ -8,6 +24,9 @@
> are where that fails. When unsure, generate, then run the validation ladder at
> the end.
+Builtin `sort` sorts a list in place; `sort_by` returns a new list using numeric keys.
+Both are stable on every platform: equal elements or keys keep their input order.
+
## Calls use `of`, never `f(x)`
Hosted release includes lazy graphics. The server profile adds HTTP, raw TCP,
@@ -108,7 +127,8 @@ not any outer `n`.
binding; reading it after the loop is `undefined variable` (a pre-existing
parameter/local/module binding is restored instead). Plain `is` in its body
binds in the enclosing scope, including an imported module. Comprehension
- variables DO leak.
+ binders are local to the comprehension's filter/result, and catch binders
+ are local to their handlers; neither creates or overwrites an outer name.
## Reserved and soft keywords (cannot be plain variable names)
@@ -183,9 +203,17 @@ sqrt(2) = 1.414213562373095
`W024`) — give each entity its own named binding or a closure per entity.
- `converged` on a numeric binding needs a full window whose relative steps are
all below the fixed settle deadband, with finite raw-step guards; a constant
- therefore converges regardless of its magnitude. Non-numeric bindings use the
- entropy classifier. See docs/PREDICATES.md for the routed definitions and the
- fixed tolerance's authoritative source.
+ therefore converges regardless of its magnitude. Non-numeric `converged`
+ and ordinarily `stable` compare bounded, detached assignment snapshots;
+ finite alias mutations remain visible on reassignment, and refused or
+ oversized snapshots leave both false. For an accepted snapshot containing
+ a NaN buffer, raw IEEE equality keeps `converged` false while `stable` uses
+ its entropy-window rule after a full bounded comparison window (S10-D1).
+ Refusal clears that eligibility even if entropy has a full window already.
+ The real matmul scenario's equality /
+ stability / convergence is `false` / `true` / `false`, without equality
+ changing data or math flags. The other predicates and `report` use entropy.
+ See docs/PREDICATES.md for the routed definitions and fixed tolerance.
- Temporal reads: `prev of x` (value before the last assignment),
`what is x at 12` / `state_at of 12` (past-line state). Bare `x at 12` is a
parse error. A statement spanning several lines is filed under its FIRST
@@ -212,24 +240,75 @@ sqrt(2) = 1.414213562373095
`false`, `0`, `null`, `""`, `[]`, `{}` are falsy.
- `+` concatenates **strings only**. List concatenation is `append of [xs, v]`
(in place) or a comprehension — `xs + ys` is a runtime error.
-- Numbers are f64 and finite by construction: overflow saturates at ±1e308.
- Integer bit ops are `bit_and`/`&` etc. on int64; exactness ends at 2^53.
+- Numbers are signed int64 or finite f64. Decimal integer literals within
+ int64 are exact; a point/exponent or out-of-range decimal literal gives
+ float. Hex literals wrap modulo 2^64. `type of` says "num" for both;
+ `num_type of` says "int"/"float", or null for a non-number. Int +, -, *
+ raise value on overflow; unary minus and abs raise at INT64_MIN. Mixed
+ arithmetic converts to float, while cross-kind comparison is exact.
+ True division `/` produces a float. Counts and indices produce ints;
+ other builtin result kinds are documented individually. Float overflow
+ saturates at ±1e308.
+ Integer bit ops and bit_* builtins accept exact int64 operands and return ints.
+ Right shifts zero-fill, negative counts reverse direction, and counts outside
+ (-64,64) give zero (Lua 5.4).
A bit-op operand that is not an exact integer (`1.5 | 0`, `1e30 & 1`)
raises a `value` error; it is never truncated.
Strict mode is the DEFAULT: out-of-domain math (`sqrt of -1`), a NaN
result, a wrong-typed builtin argument (`abs of "x"`) and a malformed
`json_path` document raise catchable errors. `EIGS_STRICT=0` is the per-run
- opt-out that brings the finite stand-ins back (`NaN` collapses to `0`,
- `sqrt of -1` is `0`, `abs of "x"` is `0`).
- Raw non-finite tensor results receive the same guard whenever an element is
- read from a buffer, independent of the consuming operator. Structural buffer
- equality, scalar reductions, mixed buffer/list materialization and numeric
- byte/sample/device conversions normalize their buffer inputs too and stop at
- the first raised read. Numeric bytes truncate and wrap modulo 256 afterward;
+ opt-out that brings the finite stand-ins back (`NaN` collapses to float `0.0`,
+ `sqrt of -1` is `0.0`, `abs of "x"` is `0.0`).
+ Indexing and iteration refuse stored NaN and infinity with `value` in
+ every strict mode, matching `get_at` (N13). Strict NaN keeps its first
+ diagnostic and sets `math_flags.invalid`; every execution tier agrees on
+ the error kind, message and source line.
+ The other scalar reductions, mixed buffer/list materialization
+ and numeric byte/sample/device conversions normalize their inputs and stop at
+ the first raised read. `write_bytes` requires exact bytes in 0..255
+ (N5/Q1); other byte converters retain truncation and wrapping at this stage;
raw copies, serialization and buffer-only work areas preserve representation.
`type_mismatch` error by default; `EIGS_STRICT=0` retains numeric-zero
substitution. `str_from_bytes` stops at numeric NUL without inspecting later
elements; byte codecs consume the complete list.
+
+Bitwise operators and `bit_*` builtins return ints, including when an
+operand is an integral float. They use Lua 5.4's 64-bit shift rules: `>>`
+zero-fills; a negative count shifts in the opposite direction; a count of
+64 or more, or -64 or less, gives zero. Each operand must be an exact int64;
+a fractional or out-of-range float raises `value`.
+
+```eigenscript
+print of [1 << 64, 1 << -1, -1 >> 1, 8 >> -1, 1 << 63]
+print of [bit_shr of [-1, 1], bit_shl of [8, -1], num_type of (3.0 & 1)]
+```
+```output
+[0, 0, 9223372036854775807, 16, -9223372036854775808]
+[9223372036854775807, 4, "int"]
+```
+
+`//` floors the quotient; `%` has the divisor's sign. `//=` applies the same
+operation to its assignment target, like `/=`. Zero divisors raise `value`
+for int, float and mixed operands. Two ints produce an int for
+`//` and `%`; a float operand produces a float. `/` always produces a float.
+Float and mixed `//` use Python’s `divmod` quotient correction: `1.0 // 0.1`
+is `9.0`, and `1e-300 // -1e300` is `-1.0`. `//=` uses the same rule.
+
+```eigenscript
+x is -7
+x //= 2
+print of [x, 7 // -2, -7 % 3, 7 % -3, -7.5 % 2]
+print of [num_type of (7 // 2), num_type of (7.0 // 2)]
+y is 1.0
+y //= 0.1
+print of [1.0 // 0.1, 1 // 0.1, 1e-300 // -1e300, y, num_type of y]
+```
+```output
+[-4, -4, 2, -2, 0.5]
+["int", "float"]
+[9.0, 9.0, -1.0, 9.0, "float"]
+```
+
- Hex integer literals: `0xFF`, `0X10` (digits only). Hex-float forms
(`0x1p4`) are parse errors. No `mod` keyword — the operator is `%`.
- Precedence: comparisons bind tighter than word-form `not`; `not` binds
@@ -237,6 +316,85 @@ sqrt(2) = 1.414213562373095
`not (1 < 2)`. Arithmetic unary `-` and bitwise unary `~` bind more tightly.
- f-strings interpolate: `f"step {i}: {report of x}"`. Escapes: `\n \t \r \\ \" \{ \}`. Each `{…}` converts with the builtin `str`, even if you rebind `str`.
+
+Empty `min`/`max` raises `value` in both call forms (R1.6b-D2); empty
+`sum` remains int zero (C2). `sign_extend` reads exact int64 arguments and
+raises `value` if its existing subtraction underflows (R1.6b-D1).
+
+Counts and indices are ints. `floor`, `ceil` and half-away-from-zero `round`
+return an int when the result fits int64. `min`/`max` return the winning
+argument with its kind; the first argument wins a tie. `num` requires a
+complete numeric-literal string (N10). JSON integer text fitting int64
+decodes exactly (N14).
+
+The [C embedding API](EMBEDDING.md#value-handles) follows Lua’s numeric
+accessors: `eigs_value_new_int` constructs an exact int64, `eigs_value_is_int`
+tests the stored kind (`lua_isinteger`), and `eigs_value_as_int` reports
+whether a number converts exactly to int64 (`lua_tointegerx`).
+`eigs_value_as_num` converts either kind to double (`lua_tonumber`);
+`EIGS_TYPE_NUM` covers both.
+
+LSP hover on a numeric literal reports `int` or `float` from the literal’s
+kind: `3.0` remains a float even though its value is integral.
+
+```eigenscript
+print of [type of 3, num_type of 3, num_type of 3.0]
+print of [9007199254740993, 3.0, -0.0]
+print of [num_type of (len of "abc"), num_type of (floor of -2.5)]
+print of [num of "9007199254740993", num_type of (num of "3.0")]
+print of [num_type of (min of [3, 3.0]), num_type of (min of [3.0, 3])]
+print of (json_path of ["{\"n\":9223372036854775807}", "n"])
+try:
+ min of []
+catch e:
+ print of [e.kind, (index_of of [e.message, "min"]) >= 0]
+try:
+ print of (num of "12abc")
+catch e:
+ print of e.kind
+```
+```output
+["num", "int", "float"]
+[9007199254740993, 3.0, -0.0]
+["int", "int"]
+[9007199254740993, "float"]
+["int", "float"]
+9223372036854775807
+["value", true]
+value
+```
+
+`add`, `subtract` and `multiply` preserve exact ints for scalar operands and
+for each pair of list elements, with `value` on int64 overflow. `negative`
+preserves int scalar and list elements and raises at INT64_MIN. Buffer
+arithmetic remains float. A buffer kernel stops at its first raised result,
+releases its partial output and does not read later elements (#1417).
+An all-int `sum` returns an exact int, while a
+float anywhere in a nested input selects float addition for the entire
+reduction. A buffer selects float addition too. An empty list's sum is int
+zero, in both call forms. `sum` refuses non-finite buffer elements with a
+`value` error in every strict mode (N13). In strict mode, it stops at the first NaN with a "not a number" diagnostic and sets `math_flags.invalid`; later elements are not read (#1417).
+
+```eigenscript
+exact is sum of [9007199254740993, 1]
+print of [exact, num_type of exact]
+mixed is sum of [9223372036854775807, 1, 0.5]
+print of [num_type of mixed, mixed == 9223372036854775808.0]
+print of [num_type of (sum of []), num_type of (sum of ([]))]
+print of (add of [[9007199254740993, 2], [1, 3]])
+try:
+ negative of (-9223372036854775807 - 1)
+catch e:
+ print of e.kind
+```
+```output
+[9007199254740994, "int"]
+["float", true]
+["int", "int"]
+[9007199254740994, 5]
+value
+```
+
## Numeric vectors: `buffer`, not `list`
- `buffer of n` (and `zeros of n`, which returns a **buffer**) is the flat
@@ -295,7 +453,7 @@ print of (buf_get of [(recv of c), 0]) # the BUFFER copied
```
```output
[1, 2]
-1
+1.0
```
- The measured kind-by-kind table lives in docs/CONCURRENCY.md and is itself an
@@ -342,8 +500,56 @@ match x:
`vm_run_bytecode` raises a catchable `value` error naming a rejected chunk descriptor; a valid program may still return `null`. `sandbox_run` reports descriptor rejection in its structured `{ok: false, error: ...}` result. Its optional fourth limit, `max_work`, bounds cumulative bytecode instructions across function calls and callback re-entry (default 10,000,000), independently of loop and allocation limits. This meters VM work, not elapsed time: a blocking native callback must return before the sandbox can stop.
+The positions and counts of `char_at`, `substr`, `get_at`, `set_at`,
+`copy_into`, `fill` and the `list_*` mutation/slice builtins use exact conversion
+(N5). So do handle fields, bytecode ABI/counts/bytes, sandbox limits and numeric
+exit codes. Integral floats are accepted; fractions and values outside the
+receiving C type raise `value` before indexing or effects.
+`recv_timeout` rounds milliseconds half away from zero and raises beyond the
+C long range, with no one-year cap. `usleep` rounds microseconds and raises
+beyond the C int range. `record_history` returns its previous flag as an int;
+nonzero numeric flags still enable history. `task_detach` and sandbox error lines return ints.
+`read_bytes` produces int bytes; `write_bytes` requires exact bytes in 0..255
+and returns an int count. File caps and corpus/stream counts follow N5; an
+integral stream count outside its cap still raises `limit`. Subprocess exit
+statuses are ints, and timeout seconds round to representable poll milliseconds.
+
+```eigenscript
+print of (substr of ["hello", 1.0, 2.0])
+try:
+ print of (substr of ["hello", 1.5, 2])
+catch e:
+ print of e.kind
+print of (num_type of (record_history of 0.5))
+print of (usleep of 1.5)
+try:
+ usleep of 1e300
+catch e:
+ print of e.kind
+path is mktemp of null
+written is write_bytes of [path, [65.0, 66.0]]
+bytes is read_bytes of path
+print of [num_type of written, num_type of bytes[0], str_from_bytes of bytes]
+try:
+ write_bytes of [path, [65.5]]
+catch e:
+ print of e.kind
+remove_file of path
+```
+```output
+el
+value
+int
+null
+value
+["int", "int", "AB"]
+value
+```
+
Fixed- or optional-shape builtin argument lists reject surplus outer elements in strict mode with a catchable `type_mismatch` error naming the builtin and maximum width. The check precedes the call's mutation, I/O and tape effects. `EIGS_STRICT=0` keeps each builtin's legacy result, including existing null/error stand-ins. Scalar overloads, lists used as data and genuinely variadic arguments keep their documented meaning.
+Runtime-published counts and indices are exact ints (S9-K1): `__loop_iterations__`, live and historical `when` assignment counts, aggregate `what` lengths, trajectory `window`, error `line`, and scheduler trace `seq` and task ids. Scheduler `tick` remains a continuous virtual time.
+
Sandbox execution does not update shared temporal history: assignment values, names, counts and observer snapshots stay outside that history even when recording is armed. Ordinary tape assignment records still emit. Host and trusted descriptor history recording resumes normally outside the sandbox; sandbox temporal reads remain refused.
**Division by zero RAISES** — it is a catchable `value`-kind error, not a
@@ -363,6 +569,52 @@ value
division by zero
```
+## Stored records
+
+Store handles carry integer `_store_id` and `_store_gen` fields. Every store operation requires exact, in-range integer handle fields; integral floats convert, while fractional, non-finite or out-of-range numeric fields raise `value` before store access (R1.6f-D1). `store_count` returns an integer.
+
+`store_get`, `store_delete`, and `store_update` accept string keys or exact
+int64 numeric keys. Integral floats convert; fractional or out-of-range floats
+raise `value` before lookup or mutation. Numeric keys retain all int64 digits.
+Stored decimal integer text within int64 decodes as an int; a decimal point or
+exponent selects a float. Buffer payloads remain floats. Store records preserve
+int64 digits and use shortest round-trip float text, including `.0` for
+integral floats and the sign of `-0.0`, so `3.0` reads back as a float.
+
+See the [stored-record example](SPEC.md#stored-records).
+
+PostgreSQL integer columns (`smallint`, `integer`, `bigint`, `oid`) return
+exact int64 values through `db_query_value`; `db_query_json` retains their
+exact decimal digits (N14, Q-DB253). A `bigint` beyond 2^53 is an int,
+including both int64 boundaries. `numeric` remains text, and finite real
+columns returned directly remain floats. Float parameters use N6 shortest round-trip text, preserving `.0` and
+signed zero. Store records use the same float writer and exact int64 digits
+(N14, #1762). The executable DB examples and boundary assertions
+in `tests/test_db.eigs` run on the server-db profile with PostgreSQL.
+
+TCP handles, ports and sent-byte counts are integers. Numeric socket handles, ports, receive counts and outgoing bytes require exact integers (integral floats convert); bytes must be in 0..255. `net_send` validates the entire payload before sending any byte. Poll timeouts are continuous milliseconds rounded to the C int range; an unrepresentable duration raises `value`.
+
+`http_early_bind` and `http_serve` require an exact integer port in 0..65535; integral floats convert and port zero requests an ephemeral port. Fractional or out-of-range ports raise `value` before binding. `shared_delete` and `shared_size` return integer counts.
+
+N6 prints each float with Python's shortest round-trip spelling: integral
+floats retain `.0`, negative zero is `-0.0`, and scientific notation starts
+below `1e-4` or at `1e16`. Ints keep their exact decimal digits. `print`,
+`write`, `str`, f-strings and JSON share the number writer. The executed
+examples are `tests/phase2/rows/N6.float-repr.eigs` and
+`tests/phase2/rows/N6.str-fstring.eigs`.
+
+The HTTP shared counter `shared_incr` follows numeric `+`: two int operands
+return an exact int and int64 overflow raises `value` without changing the
+stored counter (A). A missing counter starts at int zero. A float operand
+produces a float. The stored operand is its JSON-decoded value; N6 preserves an
+integer-valued float as a float by retaining its `.0`. Executable shared-counter examples in
+`tests/test_http.eigs` cover exact values, both overflow directions and the
+next operation after a raise.
+
+`http_post` also preserves every digit of JSON integer header values within
+int64 before sending them as text (N14, B6). The executed loopback example
+is `tests/phase2/builtins/builtins/http_post.eigs`.
+
## Before hand-rolling a helper
Cooperative task IDs are opaque numeric handles. Do not perform arithmetic on
@@ -454,6 +706,97 @@ See docs/TRACE.md and docs/EMBEDDING.md for binding and format compatibility.
Replay refuses unreconstructable host effects: `mktemp` raises a catchable
filesystem-boundary error before creating a file; ordinary `mktemp` still
creates the file and returns its path for later `remove_file`.
+
+Tensor sizes and indices follow N5: an integral float is accepted and a
+fraction raises `value`. `gather` keeps the selected list cell's int/float
+kind; buffer elements and numeric tensor calculations are floats. `shape`
+returns integer dimension counts. `mean` and `norm` refuse non-finite buffer
+cells with `value` in both strict modes.
+
+```eigenscript
+g is gather of [[[1, 2], [3, 4]], [1.0, 0]]
+print of [g, num_type of g[0], num_type of (shape of g)[0]]
+try:
+ gather of [[10, 20], 0.5]
+catch e:
+ print of e.kind
+```
+```output
+[[2, 3], "int", "int"]
+value
+```
+
+Buffer sizes, indices, offsets and counts follow N5. Byte inputs to
+`str_from_bytes`, `f64_from_bytes`, `inflate` and `buf_from_pcm16le` must be
+exact integers in 0..255; fractions and out-of-range bytes raise `value`.
+`buf_len`, `f64_to_bytes` and `inflate` return integer counts or bytes;
+buffer storage and scalar buffer reads are floats. `buf_get`, `buf_peak`,
+`buf_dot` and the byte/PCM codecs refuse non-finite input cells with `value`.
+
+Model token ids and generation limits follow N5 and the C int range;
+fractional ids or token counts raise `value`. `eigen_generate` returns int
+token ids, while `eigen_eval_loss` returns a float. Temperature, top_p and
+training learning rates remain float quantities. Model JSON weights stay
+float data; the loader status strings are unchanged.
+
+Audio playback (`audio_play`, `audio_play_loop`, `audio_stream_push`) refuses
+stored non-finite buffer samples at the scalar boundary in both strict modes
+(N13). Strict NaN preserves the first diagnostic and INVALID flag. Finite
+samples clamp to [-1,1], then round PCM16 half away from zero (Q7/Q4).
+Channel indices and loop counts follow N5; continuous mixer gain stays
+fractional after clamping to [0,4].
+
+Q7 magnitude validation also applies before SDL is loaded:
+
+```eigenscript
+try:
+ gfx_delay of 1e300
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Stored buffer data boundary
+
+Buffer kernels can retain non-finite data. Scalar indexing and iteration
+raise `value` when that data is read, in every strict mode (N13).
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+try:
+ print of p[0]
+catch e:
+ print of e.kind
+```
+```output
+value
+```
+
+### Buffer equality
+
+Buffers compare their stored doubles with IEEE equality: NaN differs from
+everything, even itself; infinities compare by sign; signed zeros are equal.
+Equality never raises or changes math flags, including under strict mode.
+
+```eigenscript
+p is buf_from_list of ([1e308])
+g is buf_from_list of ([1e308])
+sgd_update of [p, g, 1e308]
+print of (p == p)
+print of (p == g)
+sgd_update of [p, p, 1.0]
+print of (p == p)
+```
+```output
+true
+false
+false
+```
+
Recorded builtins validate deterministic argument refusals before their tape
boundary. A builtin call that raises during recording must raise with the same
kind and message during strict replay, never return a recorded stand-in.
diff --git a/lib/format.eigs b/lib/format.eigs
index 5f388bb02..33d56e512 100644
--- a/lib/format.eigs
+++ b/lib/format.eigs
@@ -18,13 +18,18 @@ define fmt_num(value, decimals) as:
return "-" + (fmt_num of [0 - value, decimals])
if decimals == 0:
return str of (round of value)
- factor is 1
+ # A float scale: value * 10^decimals is real-valued, and an int factor
+ # overflows int64 at 19 decimals (#1848).
+ factor is 1.0
for i in range of decimals:
factor is factor * 10
rounded is round of (value * factor)
int_part is floor of (rounded / factor)
frac_part is rounded - (int_part * factor)
frac_str is str of frac_part
+ # N6: fixed decimals need digits, without integral float text suffix.
+ if ends_with of [frac_str, ".0"]:
+ frac_str is substr of [frac_str, 0, (len of frac_str) - 2]
loop while (len of frac_str) < decimals:
frac_str is "0" + frac_str
return (str of int_part) + "." + frac_str
diff --git a/lib/physics.eigs b/lib/physics.eigs
index ac91dcca7..021b55b82 100644
--- a/lib/physics.eigs
+++ b/lib/physics.eigs
@@ -16,7 +16,7 @@
# Fundamental Constants
# ============================================================
-SPEED_OF_LIGHT is 299792458 # m/s (exact)
+SPEED_OF_LIGHT is 299792458.0 # m/s (exact; a float, so m·c² with an int m stays real, #1848)
PLANCK is 6.62607015e-34 # J·s (exact)
HBAR is 1.054571817e-34 # J·s (reduced Planck, h/2π)
BOLTZMANN is 1.380649e-23 # J/K (exact)
diff --git a/lib/probability.eigs b/lib/probability.eigs
index 498b33733..1baf4f13e 100644
--- a/lib/probability.eigs
+++ b/lib/probability.eigs
@@ -71,22 +71,38 @@ define binomial_cdf(n, k, p) as:
s is s + binomial_pmf of [n, i, p]
return s
+# _factorial_real: n! as a FLOAT, for real-valued formulas (#1848). Same domain
+# sentinel as factorial (0 for a negative n); its product is a float, so an int
+# n past 20 does not raise as `factorial of 21` does (decision (A), #1758).
+define _factorial_real(n) as:
+ if n < 0:
+ return 0.0
+ result is 1.0
+ i is 2
+ loop while i <= n:
+ result is result * i
+ i is i + 1
+ return result
+
# poisson_pmf: P(X=k) = lambda^k * e^(-lambda) / k!
+# A probability is real-valued: k! is a float denominator (#1848).
define poisson_pmf(k, lam) as:
# Preserve the direct calculation (and its exact rounding) whenever all
# three intermediates (lambda^k, e^-lambda, k!) are normal doubles.
# Otherwise compute log(k!) alongside the exponent, so an intermediate
# that overflows or underflows cannot destroy the result.
if lam <= 0:
- return (pow of [lam, k]) * (exp of (0 - lam)) / (factorial of k)
+ return (pow of [lam, k]) * (exp of (0 - lam)) / (_factorial_real of k)
power_log is k * (log of lam)
if k < 171 and lam <= 708.3964185322641 and power_log >= -708.3964185322641 and power_log <= 709.782712893384:
- return (pow of [lam, k]) * (exp of (0 - lam)) / (factorial of k)
+ return (pow of [lam, k]) * (exp of (0 - lam)) / (_factorial_real of k)
if k > 170:
# Stirling's series avoids accumulating hundreds of rounded logs.
- k2 is k * k
+ # Float factors keep an int k's products in floats, as on main: in
+ # ints, 1260 * k * k2 * k2 would raise on overflow from k = 1490 (#1758).
+ k2 is 1.0 * k * k
log_factorial is (k + 0.5) * (log of k) - k + 0.9189385332046727
- log_factorial is log_factorial + 1 / (12 * k) - 1 / (360 * k * k2) + 1 / (1260 * k * k2 * k2)
+ log_factorial is log_factorial + 1 / (12.0 * k) - 1 / (360.0 * k * k2) + 1 / (1260.0 * k * k2 * k2)
else:
log_factorial is 0
i is 2
diff --git a/lib/validate.eigs b/lib/validate.eigs
index aa4451257..1148cdab0 100644
--- a/lib/validate.eigs
+++ b/lib/validate.eigs
@@ -130,7 +130,6 @@ define is_alpha(value) as:
lc is str_lower of value
for i in range of (len of lc):
c is char_at of [lc, i]
- code is num of c
# a-z check via lowercase comparison
if (not (contains of ["abcdefghijklmnopqrstuvwxyz", c])):
return false
diff --git a/spec/lean/EigenCore/OracleTests.lean b/spec/lean/EigenCore/OracleTests.lean
index 7b2483394..3533b8f90 100644
--- a/spec/lean/EigenCore/OracleTests.lean
+++ b/spec/lean/EigenCore/OracleTests.lean
@@ -18,7 +18,7 @@ example : recordTwo.stdout == "2\n" := by native_decide
example : replayOne == recordOne := by native_decide
private def replayAcrossSession : RunResult := run 100 counterexample (.replayStrict
- ["B 0 1 1 0 root -", "V 8 0.47.0", "B 0 1 1 0 root -", "N 0 random_int=1"])
+ ["B 0 1 1 0 root -", "V 9 0.47.0", "B 0 1 1 0 root -", "N 0 random_int=1"])
example : replayAcrossSession.error == some (.runtime
"random_int: replay stream 0 has no matching recorded N value") &&
diff --git a/spec/lean/EigenCore/Tape.lean b/spec/lean/EigenCore/Tape.lean
index bff245e7c..5561e361e 100644
--- a/spec/lean/EigenCore/Tape.lean
+++ b/spec/lean/EigenCore/Tape.lean
@@ -23,7 +23,7 @@ private def serializeRecord (record : String) : List String :=
def tapeSerialize (records : List String) : String :=
let physical := records.flatMap serializeRecord
- "V 8 0.47.0\nB 0 1 1 0 root -\n" ++ String.intercalate "\n" physical ++
+ "V 9 0.47.0\nB 0 1 1 0 root -\n" ++ String.intercalate "\n" physical ++
(if physical.isEmpty then "" else "\n")
private def gatherContinuations (count : Nat) : Nat → String → List String →
@@ -61,7 +61,7 @@ private partial def assembleRecords : List String → Except String (List String
def tapeParse (text : String) : Except String (List String) :=
match text.splitOn "\n" with
- | header::rest => if header == "V 8 0.47.0" then
+ | header::rest => if header == "V 9 0.47.0" then
assembleRecords (rest.filter (· != ""))
else throw "invalid tape header"
| _ => throw "empty tape"
diff --git a/src/builtins.c b/src/builtins.c
index 61bef9476..325aca8d4 100644
--- a/src/builtins.c
+++ b/src/builtins.c
@@ -39,6 +39,14 @@ static Value *builtin_dict_view(Value *dict, const char *key, EigsView *view) {
static void wake_channels_for_exit(EigsState *st);
+/* N5 followed by the range of the C API receiving the argument. Borrowed
+ * input; callers stop on the error latch before indexing or doing work. */
+static int builtin_c_int(const Value *v, const char *who) {
+ int64_t n = 0;
+ eigs_int_arg_range(v, INT_MIN, INT_MAX, &n, who);
+ return (int)n;
+}
+
/* #744: the extension ENTRY POINTS, not the extensions' private headers.
* This TU calls five registrars and uses no extension type; pulling
* ext_db_internal.h for one declaration dragged into the core
@@ -161,8 +169,9 @@ Value* builtin_flush(Value *arg) {
/* usleep of microseconds — pause execution */
Value* builtin_usleep(Value *arg) {
BOOL_REFUSE(arg, "usleep");
- if (!arg || arg->type != VAL_NUM) return make_null();
- int us = (int)eigs_num_arg(arg, __func__);
+ if (!arg || !val_is_number(arg)) return make_null();
+ int64_t us;
+ if (!eigs_magnitude_arg(arg, INT_MIN, INT_MAX, &us, "usleep")) return make_null();
if (us > 0) {
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
@@ -330,26 +339,12 @@ Value* builtin_text_builder_to_string(Value *arg) {
return make_str_owned(xstrdup(arg->data.text_builder.data ? arg->data.text_builder.data : ""));
}
-/* ==== Bitwise operations ====
- * Semantics: operate on 32-bit two's-complement ints. Shift amounts are
- * masked to [0,31] so large/negative shifts are defined behavior, not UB.
- * Non-numeric args raise a runtime error (consistent with the strict error
- * model used by the arithmetic operators and the '& | ^ << >> ~' operators). */
-
-/* The bit_* builtins are the SAME operation as the infix operators
- * (& | ^ << >>): int64 two's-complement over the numeric value (vm.c's
- * INT_BINOP). They used to be a second, divergent implementation —
- * (uint32_t)(int32_t) conversion, UB for inputs >= 2^31 and
- * sign-extended results — so `0xEDB88320 & x` worked while
- * `bit_and of [0xEDB88320, x]` returned garbage (found by the CRC-32
- * stdlib module, the first consumer needing the full unsigned-32
- * range). Shift counts are masked to 0..63 like the hardware would. */
-/* #1740: an operand that is not an exact int64 raises `value` (Lua 5.4's
- * rule, the same as the infix operators). Returns 0 after raising. */
-static int bit_int(double x, int64_t *out, const char *name) {
- if (num_to_i64_exact(x, out)) return 1;
- rt_error(EK_VALUE, 0, EIGS_NO_INT_REP_FMT, name, x);
- return 0;
+/* #1758 N8: the bit_* builtins share Lua's exact int64 bit patterns and
+ * shift-count rules with the operators. No int operand crosses double. */
+/* #1740: an operand without an exact int64 representation raises value. */
+static int bit_int(Value *v, int64_t *out, const char *name) {
+ *out = eigs_int_arg(v, name);
+ return !g_has_error;
}
/* 1 = both operands converted; 0 = wrong shape (caller raises `type`);
@@ -358,9 +353,9 @@ static int bit_pair(Value *arg, int64_t *a_out, int64_t *b_out, const char *name
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) return 0;
Value *va = BUILTIN_LIST_VIEW(arg, 0);
Value *vb = BUILTIN_LIST_VIEW(arg, 1);
- if (!va || va->type != VAL_NUM || !vb || vb->type != VAL_NUM) return 0;
- if (!bit_int(eigs_num_arg(va, __func__), a_out, name) ||
- !bit_int(eigs_num_arg(vb, __func__), b_out, name)) return -1;
+ if (!va || !val_is_number(va) || !vb || !val_is_number(vb)) return 0;
+ if (!bit_int(va, a_out, name) ||
+ !bit_int(vb, b_out, name)) return -1;
return 1;
}
@@ -369,7 +364,7 @@ Value* builtin_bit_and(Value *arg) {
int64_t a, b;
int ok = bit_pair(arg, &a, &b, "bit_and");
if (ok <= 0) { if (ok == 0) rt_error(EK_TYPE, 0, "bit_and expects [number, number]"); return make_null(); }
- return make_num((double)(a & b));
+ return make_int(a & b);
}
Value* builtin_bit_or(Value *arg) {
@@ -377,7 +372,7 @@ Value* builtin_bit_or(Value *arg) {
int64_t a, b;
int ok = bit_pair(arg, &a, &b, "bit_or");
if (ok <= 0) { if (ok == 0) rt_error(EK_TYPE, 0, "bit_or expects [number, number]"); return make_null(); }
- return make_num((double)(a | b));
+ return make_int(a | b);
}
Value* builtin_bit_xor(Value *arg) {
@@ -385,14 +380,14 @@ Value* builtin_bit_xor(Value *arg) {
int64_t a, b;
int ok = bit_pair(arg, &a, &b, "bit_xor");
if (ok <= 0) { if (ok == 0) rt_error(EK_TYPE, 0, "bit_xor expects [number, number]"); return make_null(); }
- return make_num((double)(a ^ b));
+ return make_int(a ^ b);
}
Value* builtin_bit_not(Value *arg) {
- if (!arg || arg->type != VAL_NUM) { rt_error(EK_TYPE, 0, "bit_not expects a number"); return make_null(); }
+ if (!arg || !val_is_number(arg)) { rt_error(EK_TYPE, 0, "bit_not expects a number"); return make_null(); }
int64_t a;
- if (!bit_int(eigs_num_arg(arg, __func__), &a, "bit_not")) return make_null();
- return make_num((double)(~a));
+ if (!bit_int(arg, &a, "bit_not")) return make_null();
+ return make_int(~a);
}
Value* builtin_bit_shift_left(Value *arg) {
@@ -400,7 +395,7 @@ Value* builtin_bit_shift_left(Value *arg) {
int64_t a, b;
int ok = bit_pair(arg, &a, &b, "bit_shl");
if (ok <= 0) { if (ok == 0) rt_error(EK_TYPE, 0, "bit_shl expects [number, number]"); return make_null(); }
- return make_num((double)(int64_t)((uint64_t)a << (b & 63))); /* unsigned: no UB on negative a */
+ return make_int(eigs_bit_shift(a, b, 0));
}
Value* builtin_bit_shift_right(Value *arg) {
@@ -408,10 +403,7 @@ Value* builtin_bit_shift_right(Value *arg) {
int64_t a, b;
int ok = bit_pair(arg, &a, &b, "bit_shr");
if (ok <= 0) { if (ok == 0) rt_error(EK_TYPE, 0, "bit_shr expects [number, number]"); return make_null(); }
- /* Arithmetic shift, like the infix >> on int64 (negative operands
- * keep their sign); mask a non-negative operand first for a
- * logical u32 shift. */
- return make_num((double)(a >> (b & 63)));
+ return make_int(eigs_bit_shift(a, b, 1));
}
/* monotonic_ns of null — nanoseconds from CLOCK_MONOTONIC */
@@ -419,7 +411,13 @@ Value* builtin_monotonic_ns(Value *arg) {
(void)arg;
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
- TRACE_NONDET_RET("monotonic_ns", make_num((double)ts.tv_sec * 1e9 + (double)ts.tv_nsec));
+ int64_t seconds = (int64_t)ts.tv_sec, nanos;
+ if (__builtin_mul_overflow(seconds, INT64_C(1000000000), &nanos) ||
+ __builtin_add_overflow(nanos, (int64_t)ts.tv_nsec, &nanos)) {
+ rt_error(EK_VALUE, 0, "monotonic_ns: int64 overflow");
+ return make_null();
+ }
+ TRACE_NONDET_RET("monotonic_ns", make_int(nanos));
}
/* monotonic_ms of null — milliseconds from CLOCK_MONOTONIC */
@@ -427,7 +425,7 @@ Value* builtin_monotonic_ms(Value *arg) {
(void)arg;
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
- TRACE_NONDET_RET("monotonic_ms", make_num((double)ts.tv_sec * 1e3 + (double)ts.tv_nsec / 1e6));
+ TRACE_NONDET_RET("monotonic_ms", make_float((double)ts.tv_sec * 1e3 + (double)ts.tv_nsec / 1e6));
}
/* clock_unix of null — seconds since the Unix epoch from CLOCK_REALTIME
@@ -438,22 +436,22 @@ Value* builtin_clock_unix(Value *arg) {
(void)arg;
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
- TRACE_NONDET_RET("clock_unix", make_num((double)ts.tv_sec + (double)ts.tv_nsec / 1e9));
+ TRACE_NONDET_RET("clock_unix", make_float((double)ts.tv_sec + (double)ts.tv_nsec / 1e9));
}
Value* builtin_len(Value *arg) {
if (arg->type == VAL_LIST)
- return make_num(arg->data.list.count);
+ return make_int(arg->data.list.count);
if (arg->type == VAL_STR)
- return make_num(val_str_len(arg));
+ return make_int(val_str_len(arg));
if (arg->type == VAL_DICT) {
eigs_module_ns_sync(arg); /* #1057 whole-dict reader */
- return make_num(arg->data.dict.count);
+ return make_int(arg->data.dict.count);
}
if (arg->type == VAL_BUFFER)
- return make_num(arg->data.buffer.count);
+ return make_int(arg->data.buffer.count);
if (arg->type == VAL_TEXT_BUILDER)
- return make_num((double)arg->data.text_builder.len);
+ return make_int(arg->data.text_builder.len);
/* #508: a type with no length (number, fn, null, …) used to fold to 0,
* silently masking a wrong value. Raise. Callers that legitimately mean
* "empty if absent" already guard with `x != null and len of x` — and
@@ -463,7 +461,7 @@ Value* builtin_len(Value *arg) {
/* fs:CHANNEL the rt_error above has already raised UNCONDITIONALLY — this
* is only the C return value the compiler requires. `len` is the reform's
* finished state, not a candidate for it. */
- return make_num(0);
+ return make_int(0);
}
Value* builtin_str(Value *arg) {
@@ -480,62 +478,94 @@ Value* builtin_str(Value *arg) {
return v;
}
-/* Every `return make_num(0)` in this function is fs:ANSWER, and the reason is
- * one reason: BUILTINS.md documents `num` as "Convert to number (parse string
- * or COERCE)". A coercion's whole contract is that it accepts anything and
- * answers, so 0 here is the documented result rather than a stand-in for a
- * rejected argument — the opposite of `cos of "hello"`.
- *
- * This is the sharpest ANSWER case in the file and worth stating plainly: it
- * IS reachable as a laundering path (`num of [1,2,3]` is 0, and a typo that
- * hands `num` a list scores as a real zero). Making it loud is a change to
- * `num`'s documented contract, not a completion of this reform, so it is out
- * of scope here and belongs in its own issue if wanted. */
+/* N1/N10/N14: classify signed decimal integer text without a double.
+ * 1 = exact int64, 0 = not integer text, -1 = integer outside int64.
+ * Validate the entire token before range-checking (scanners skip 12abc). */
+static int builtin_decimal_int(const char *s, size_t len, int64_t *out) {
+ size_t first = 0;
+ int neg = len && s[0] == '-';
+ if (len && (s[0] == '-' || s[0] == '+')) first++;
+ if (first == len) return 0;
+ for (size_t i = first; i < len; i++)
+ if (s[i] < '0' || s[i] > '9') return 0;
+ uint64_t limit = (uint64_t)INT64_MAX + (unsigned)neg, n = 0;
+ for (size_t i = first; i < len; i++) {
+ unsigned d = (unsigned)(s[i] - '0');
+ if (n > (limit - d) / 10) return -1;
+ n = n * 10 + d;
+ }
+ *out = neg && n ? -1 - (int64_t)(n - 1) : (int64_t)n;
+ return 1;
+}
+
+static Value *builtin_number_value(EigsNum n) {
+ return n.is_int ? make_int(n.value.i64) : make_float(n.value.f64);
+}
+
+static int builtin_ascii_space(unsigned char c) {
+ return c == ' ' || (c >= '\t' && c <= '\r');
+}
+
+/* N10 (#1758 comment 6304880619): complete lexer-grammar text, with a
+ * surrounding ASCII space/sign allowance, as Lua tonumber. Hex floats are
+ * the declared divergence. No partial, empty, inf/nan or separator input. */
Value* builtin_num(Value *arg) {
- if (!arg) return make_num(0); /* fs:ANSWER coercion contract, see above */
- if (arg->type == VAL_NUM) return arg;
- /* #1637: a bool is not a number, and the coercion contract's 0 for a
- * non-string would make `num of true` answer 0 -- a silent wrong value.
- * Whether `num of b` should convert is open (#1637 leaves it undecided),
- * so it raises in every mode rather than pick an answer. */
+ if (!arg) return make_float(0);
+ if (val_is_number(arg)) return arg;
if (arg->type == VAL_BOOL) {
rt_error(EK_TYPE, 0, "num: cannot convert a bool to a number (use `if b:` to choose one)");
- return make_num(0);
- }
- if (arg->type == VAL_STR) {
- /* Hex strings are converted HERE, never by strtod — same contract
- * as the lexer (#378/#381): glibc's strtod reads hex floats
- * ("0x.8" -> 0.5, "0x1p4" -> 16) while the freestanding
- * mini_strtod reads 0, a silent profile divergence through the
- * string path. Hex is integer-only; conversion stops at the
- * first non-hex-digit (matching strtod's partial-parse shape,
- * "12abc" -> 12); a prefix with no hex digit converts to 0
- * like any other non-numeric string. */
- const char *p = arg->data.str;
- while (*p == ' ' || *p == '\t') p++;
- int neg = 0;
- if (*p == '+' || *p == '-') { neg = (*p == '-'); p++; }
- if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) {
- /* fs:ANSWER "0x" with no hex digit converts to 0 like any other
- * non-numeric string — the partial-parse contract this comment
- * block documents. */
- if (!isxdigit((unsigned char)p[2])) return make_num(0);
- double v = 0;
- p += 2;
- while (isxdigit((unsigned char)*p)) {
- int d = *p <= '9' ? *p - '0' : (*p | 32) - 'a' + 10;
- v = v * 16 + d;
- p++;
- }
- return make_num(neg ? -v : v);
+ return make_null();
+ }
+ if (arg->type != VAL_STR) return make_float(0); /* existing non-text coercion */
+ const char *start = arg->data.str, *end = start + val_str_len(arg);
+ while (start < end && builtin_ascii_space((unsigned char)*start)) start++;
+ while (end > start && builtin_ascii_space((unsigned char)end[-1])) end--;
+ const char *p = start;
+ int neg = p < end && *p == '-';
+ if (p < end && (*p == '+' || *p == '-')) p++;
+ if (end - p >= 2 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) {
+ uint64_t bits = 0;
+ p += 2;
+ const char *digits = p;
+ while (p < end && isxdigit((unsigned char)*p)) {
+ unsigned d = (unsigned)(*p <= '9' ? *p - '0' : (*p | 32) - 'a' + 10);
+ bits = bits * UINT64_C(16) + d;
+ p++;
}
- /* #971: strtod reads "nan"/"inf" — the in-language route to a NaN.
- * EIGS_STRICT=0 collapses to 0 (+ EIGS_MATH_INVALID); strict (the
- * default) raises, named. */
- return make_num(num_guard_named(strtod(arg->data.str, NULL), "num"));
+ if (p == digits || p != end) goto invalid;
+ if (neg) bits = UINT64_C(0) - bits;
+ int64_t n = bits <= INT64_MAX ? (int64_t)bits : -1 - (int64_t)(UINT64_MAX - bits);
+ return make_int(n);
+ }
+ int digits = 0;
+ while (p < end && *p >= '0' && *p <= '9') { p++; digits = 1; }
+ if (p < end && *p == '.') {
+ p++;
+ while (p < end && *p >= '0' && *p <= '9') { p++; digits = 1; }
}
- if (arg->type == VAL_NULL) return make_num(0); /* fs:ANSWER coercion contract */
- return make_num(0); /* fs:ANSWER coercion contract */
+ if (!digits) goto invalid;
+ if (p < end && (*p == 'e' || *p == 'E')) {
+ p++;
+ if (p < end && (*p == '+' || *p == '-')) p++;
+ const char *exponent = p;
+ while (p < end && *p >= '0' && *p <= '9') p++;
+ if (p == exponent) goto invalid;
+ }
+ if (p != end) goto invalid;
+ int64_t n;
+ if (builtin_decimal_int(start, (size_t)(end - start), &n) == 1) return make_int(n);
+ /* Isolate the validated span; no locale whitespace or suffix can leak in. */
+ size_t len = (size_t)(end - start);
+ char *text = xmalloc(len + 1);
+ memcpy(text, start, len);
+ text[len] = '\0';
+ double d = strtod(text, NULL);
+ free(text);
+ d = num_guard_named(d, "num");
+ return g_has_error ? make_null() : make_float(d);
+invalid:
+ rt_error(EK_VALUE, 0, "num: expected complete numeric-literal text");
+ return make_null();
}
Value* builtin_append(Value *arg) {
@@ -554,7 +584,7 @@ Value* builtin_append(Value *arg) {
EigsSlot item = list_slot(arg, 1);
if (!target || target->type != VAL_LIST) {
rt_error(EK_TYPE, 0, "append: target must be a list (got %s)",
- val_type_name(target ? target->type : VAL_NUM));
+ val_type_name(target ? target->type : VAL_FLOAT));
return make_null();
}
list_append_slot(target, item);
@@ -578,9 +608,9 @@ Value* builtin_report(Value *arg) {
Value* builtin_get_observer_thresholds(Value *arg) {
(void)arg;
Value *result = make_list(3);
- list_append_owned(result, make_num(g_obs_dh_zero));
- list_append_owned(result, make_num(g_obs_dh_small));
- list_append_owned(result, make_num(g_obs_h_low));
+ list_append_owned(result, make_float(g_obs_dh_zero));
+ list_append_owned(result, make_float(g_obs_dh_small));
+ list_append_owned(result, make_float(g_obs_h_low));
return result;
}
@@ -607,13 +637,14 @@ Value* builtin_get_observer_thresholds(Value *arg) {
Value* builtin_exit(Value *arg) {
STRICT_LIST_MAX(arg, 1, "exit");
int code = 0;
- if (arg && arg->type == VAL_NUM) {
- code = (int)eigs_num_arg(arg, __func__);
+ if (arg && val_is_number(arg)) {
+ code = builtin_c_int(arg, "exit");
} else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 1 &&
BUILTIN_LIST_VIEW(arg, 0) &&
- BUILTIN_LIST_VIEW(arg, 0)->type == VAL_NUM) {
- code = (int)eigs_list_num(arg, 0, __func__);
+ val_is_number(BUILTIN_LIST_VIEW(arg, 0))) {
+ code = builtin_c_int(BUILTIN_LIST_VIEW(arg, 0), "exit");
}
+ if (g_has_error) return make_null();
g_exit_code = code;
g_exit_requested = 1; /* this thread's unwind: uncatchable */
eigs_state_request_exit(eigs_current_state(), code);
@@ -625,15 +656,12 @@ Value* builtin_exit(Value *arg) {
/* set_exit_code of N — set the status used if the state completes gracefully.
* Like Node.js process.exitCode, this does not stop execution. Last store wins. */
Value* builtin_set_exit_code(Value *arg) {
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_VALUE, 0, "set_exit_code requires an integer in 0..255");
return make_null();
}
- double num = eigs_num_arg(arg, __func__);
- if (num < 0 || num > 255 || num != (double)(int)num) {
- rt_error(EK_VALUE, 0, "set_exit_code requires an integer in 0..255 (got %g)", num);
- return make_null();
- }
+ int64_t num;
+ if (!eigs_int_arg_range(arg, 0, 255, &num, "set_exit_code")) return make_null();
eigs_state_set_graceful_exit_code(eigs_current_state(), (int)num);
return make_null();
}
@@ -768,9 +796,9 @@ Value* builtin_observe(Value *arg) {
(void)arg;
Value *list = make_list(4);
list_append_owned(list, make_str("equilibrium"));
- list_append_owned(list, make_num(0.0));
- list_append_owned(list, make_num(0.0));
- list_append_owned(list, make_num(0.0));
+ list_append_owned(list, make_float(0.0));
+ list_append_owned(list, make_float(0.0));
+ list_append_owned(list, make_float(0.0));
return list;
}
@@ -852,7 +880,8 @@ Value* builtin_math_flags(Value *arg) {
Value* builtin_type(Value *arg) {
if (!arg) return make_str("none");
switch (arg->type) {
- case VAL_NUM: return make_str("num");
+ case VAL_FLOAT: return make_str("num");
+ case VAL_INT: return make_str("num"); /* #1758 (B): one number type at the surface */
case VAL_STR: return make_str("str");
case VAL_LIST: return make_str("list");
case VAL_FN: return make_str("fn");
@@ -867,6 +896,14 @@ Value* builtin_type(Value *arg) {
return make_str("none");
}
+/* #1758 N7: inspect numeric representation without splitting surface type
+ * "num". Every other value, including bool, answers null. */
+Value* builtin_num_type(Value *arg) {
+ if (arg && arg->type == VAL_INT) return make_str("int");
+ if (arg && arg->type == VAL_FLOAT) return make_str("float");
+ return make_null();
+}
+
/* Bound JSON nesting depth: each array/object descent is a C recursion, so
* untrusted input like "[[[[...]]]]" would otherwise exhaust the C stack and
* crash (SIGSEGV). 200 is far beyond any legitimate document. Shared by the
@@ -899,14 +936,20 @@ static int eigs_json_encode_value(Value *v, strbuf *out, int depth) {
if ((v->type == VAL_LIST || v->type == VAL_DICT) && depth >= JSON_ENCODE_MAX_DEPTH)
return -1;
switch (v->type) {
- case VAL_NUM: {
+ case VAL_FLOAT: {
/* #875: one shared rule with `str of` — exact integers to 2^53
* bare, otherwise the shortest round-tripping form. The local
* %d fast path stopped at 2^31 and handed everything above it
* to %.15g, so integer IDs between 2^31 and 2^53 came back as a
* different number. */
char nb[32];
- eigs_num_text(nb, sizeof(nb), eigs_num_arg(v, __func__));
+ eigs_num_text(nb, sizeof(nb), eigs_float_arg(v, __func__));
+ strbuf_append(out, nb);
+ break;
+ }
+ case VAL_INT: { /* #1758 R1.2a: an int encodes as its exact digits */
+ char nb[24];
+ eigs_int_text(nb, sizeof(nb), VAL_I64_RAW(v));
strbuf_append(out, nb);
break;
}
@@ -978,7 +1021,14 @@ static int eigs_json_encode_value(Value *v, strbuf *out, int depth) {
strbuf_append_char(out, '}');
break;
}
- default:
+ /* #1758 R1.2a: the `default:` that answered "null" is gone, so a new
+ * ValType is a build error here (-Werror=switch) instead of a silent
+ * null. These are the types it covered, unchanged: null, fn and
+ * builtin (also caught above), and a buffer. */
+ case VAL_NULL:
+ case VAL_FN:
+ case VAL_BUILTIN:
+ case VAL_BUFFER:
strbuf_append(out, "null");
break;
}
@@ -1264,14 +1314,14 @@ static Value* eigs_json_parse_number(const char *s, int *pos) {
if (s[p] == '-') p++;
if (!isdigit((unsigned char)s[p])) {
/* fs:CHANNEL failure is signalled out-of-band via g_json_parse_err; the 0 is a placeholder the caller never reads */
- g_json_parse_err = 1; *pos = p; return make_num(0);
+ g_json_parse_err = 1; *pos = p; return make_float(0);
}
while (isdigit((unsigned char)s[p])) p++;
if (s[p] == '.') {
p++;
if (!isdigit((unsigned char)s[p])) { /* "1." — frac needs digits */
/* fs:CHANNEL failure is signalled out-of-band via g_json_parse_err; the 0 is a placeholder the caller never reads */
- g_json_parse_err = 1; *pos = p; return make_num(0);
+ g_json_parse_err = 1; *pos = p; return make_float(0);
}
while (isdigit((unsigned char)s[p])) p++;
}
@@ -1280,10 +1330,15 @@ static Value* eigs_json_parse_number(const char *s, int *pos) {
if (s[p] == '+' || s[p] == '-') p++;
if (!isdigit((unsigned char)s[p])) { /* "1e", "1e+" — exp needs digits */
/* fs:CHANNEL failure is signalled out-of-band via g_json_parse_err; the 0 is a placeholder the caller never reads */
- g_json_parse_err = 1; *pos = p; return make_num(0);
+ g_json_parse_err = 1; *pos = p; return make_float(0);
}
while (isdigit((unsigned char)s[p])) p++;
}
+ int64_t integer;
+ if (builtin_decimal_int(s + start, (size_t)(p - start), &integer) == 1) {
+ *pos = p;
+ return make_int(integer);
+ }
char *endp;
double d = strtod(s + start, &endp);
if (endp != s + p) {
@@ -1299,7 +1354,7 @@ static Value* eigs_json_parse_number(const char *s, int *pos) {
free(iso);
}
*pos = p;
- return make_num(d);
+ return make_float(d);
}
/* JSON_MAX_DEPTH is defined above the encoder, which shares it (#730).
@@ -1470,9 +1525,10 @@ Value* builtin_json_build(Value *arg) {
free(key);
strbuf_append_n(&out, ": ", 2);
Value *val = BUILTIN_LIST_VIEW(arg, i + 1);
- if (val->type == VAL_NUM) {
+ if (val_is_number(val)) {
char nb[32]; /* #875: the shared rule */
- eigs_num_text(nb, sizeof(nb), eigs_num_arg(val, __func__));
+ if (val->type == VAL_INT) eigs_int_text(nb, sizeof(nb), VAL_I64_RAW(val));
+ else eigs_num_text(nb, sizeof(nb), eigs_float_arg(val, "json_build"));
strbuf_append(&out, nb);
} else if (val->type == VAL_NULL) {
strbuf_append(&out, "null");
@@ -1634,59 +1690,20 @@ Value* builtin_scan_ints(Value *arg) {
line_leading = 0;
const char *start = p;
- int neg = 0;
- if (*p == '-' || *p == '+') {
- neg = (*p == '-');
- p++;
- }
-
- int digits = 0;
- double value = 0.0;
- while (*p && isdigit((unsigned char)*p)) {
- value = value * 10.0 + (double)(*p - '0');
- p++;
- digits++;
- }
-
- int valid = digits > 0;
- while (*p && !isspace((unsigned char)*p)) {
- valid = 0;
- p++;
- }
-
- if (valid) {
- if (neg) value = -value;
- list_append_owned(out, make_num(value));
- } else if (p == start) {
- p++;
+ while (*p && !isspace((unsigned char)*p)) p++;
+ int64_t value = 0;
+ int valid = builtin_decimal_int(start, (size_t)(p - start), &value);
+ if (valid < 0) {
+ rt_error(EK_VALUE, 0, "scan_ints: integer token '%.*s' is outside int64", (int)(p - start), start);
+ val_decref(out);
+ return make_null();
}
+ if (valid) list_append_owned(out, make_int(value));
}
return out;
}
-static int scan_integer_token_value(const char *start, size_t len, double *out_value) {
- if (!start || len == 0) return 0;
-
- size_t i = 0;
- int neg = 0;
- if (start[i] == '-' || start[i] == '+') {
- neg = (start[i] == '-');
- i++;
- if (i >= len) return 0;
- }
-
- double value = 0.0;
- for (; i < len; i++) {
- unsigned char ch = (unsigned char)start[i];
- if (!isdigit(ch)) return 0;
- value = value * 10.0 + (double)(ch - '0');
- }
-
- if (neg) value = -value;
- if (out_value) *out_value = value;
- return 1;
-}
/* scan_int_tokens of text
* scan_int_tokens of [text, comment_marker]
@@ -1765,17 +1782,23 @@ Value* builtin_scan_int_tokens(Value *arg) {
memcpy(token, start, len);
token[len] = '\0';
- double int_value = 0.0;
- int is_int = scan_integer_token_value(start, len, &int_value);
+ int64_t int_value = 0;
+ int is_int = builtin_decimal_int(start, len, &int_value);
+ if (is_int < 0) {
+ rt_error(EK_VALUE, 0, "scan_int_tokens: integer token '%s' is outside int64", token);
+ free(token);
+ val_decref(out);
+ return make_null();
+ }
Value *row = make_list(7);
list_append_owned(row, make_str(token));
- list_append_owned(row, make_num((double)token_line));
- list_append_owned(row, make_num((double)token_col));
- list_append_owned(row, make_num((double)(start - base)));
- list_append_owned(row, make_num((double)(p - base)));
- list_append_owned(row, make_num((double)is_int));
- list_append_owned(row, make_num(int_value));
+ list_append_owned(row, make_int(token_line));
+ list_append_owned(row, make_int(token_col));
+ list_append_owned(row, make_int((start - base)));
+ list_append_owned(row, make_int((p - base)));
+ list_append_owned(row, make_int(is_int));
+ list_append_owned(row, make_int(int_value));
list_append_owned(out, row); /* adopt the freshly-built row */
free(token);
}
@@ -1878,9 +1901,10 @@ Value* builtin_char_at(Value *arg) {
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "char_at", "[string, index]", make_str(""));
Value *str_val = BUILTIN_LIST_VIEW(arg, 0);
Value *idx_val = BUILTIN_LIST_VIEW(arg, 1);
- ARG_GUARD(!str_val || str_val->type != VAL_STR || !idx_val || idx_val->type != VAL_NUM,
+ ARG_GUARD(!str_val || str_val->type != VAL_STR || !idx_val || !val_is_number(idx_val),
"char_at", "[string, number]", make_str(""));
- int idx = (int)eigs_num_arg(idx_val, __func__);
+ int idx = builtin_c_int(idx_val, "char_at");
+ if (g_has_error) return make_null();
int len = val_str_len(str_val);
if (idx < 0) idx += len;
/* fs:ANSWER an index outside the string has no character; "" is the
@@ -1914,11 +1938,12 @@ Value* builtin_substr(Value *arg) {
Value *start_val = BUILTIN_LIST_VIEW(arg, 1);
Value *len_val = BUILTIN_LIST_VIEW(arg, 2);
ARG_GUARD(!str_val || str_val->type != VAL_STR, "substr", "a string as its first argument", make_str(""));
- ARG_GUARD(!start_val || start_val->type != VAL_NUM, "substr", "a number as its start", make_str(""));
- ARG_GUARD(!len_val || len_val->type != VAL_NUM, "substr", "a number as its length", make_str(""));
+ ARG_GUARD(!start_val || !val_is_number(start_val), "substr", "a number as its start", make_str(""));
+ ARG_GUARD(!len_val || !val_is_number(len_val), "substr", "a number as its length", make_str(""));
int slen = val_str_len(str_val);
- int start = (int)eigs_num_arg(start_val, __func__);
- int rlen = (int)eigs_num_arg(len_val, __func__);
+ int start = builtin_c_int(start_val, "substr");
+ int rlen = builtin_c_int(len_val, "substr");
+ if (g_has_error) return make_null();
/* #504: a negative start counts from the end, matching char_at and the
* [] operator (was a flat clamp-to-0 — inconsistent). A start still
* negative after the adjustment (before the string) clamps to 0. */
@@ -1948,20 +1973,20 @@ Value* builtin_substr(Value *arg) {
Value* builtin_index_of(Value *arg) {
STRICT_LIST_MAX(arg, 2, "index_of");
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2,
- "index_of", "[haystack, needle]", make_num(-1));
+ "index_of", "[haystack, needle]", make_int(-1));
Value *h = BUILTIN_LIST_VIEW(arg, 0), *n = BUILTIN_LIST_VIEW(arg, 1);
/* #1008: the -1 for non-string operands is a deliberate choice (#316 —
* folding to "" gave a false positive at index 0), so ARG_GUARD is
* exactly the right shape: the answer is preserved with the flag off and
* the mistake becomes loud with it on. */
ARG_GUARD(!h || h->type != VAL_STR || !n || n->type != VAL_STR,
- "index_of", "two strings", make_num(-1));
+ "index_of", "two strings", make_int(-1));
const char *p = strstr(h->data.str, n->data.str);
/* fs:ANSWER the needle is genuinely not in the haystack — -1 is
* index_of's documented miss, and the one -1 here that is a RESULT
* rather than a rejected argument. */
- if (!p) return make_num(-1);
- return make_num((double)(p - h->data.str));
+ if (!p) return make_int(-1);
+ return make_int((p - h->data.str));
}
/* ================================================================
@@ -1969,105 +1994,151 @@ Value* builtin_index_of(Value *arg) {
* ================================================================ */
Value* builtin_sin(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "sin", "a number", make_num(0));
- return make_num(sin(eigs_num_arg(arg, __func__)));
+ ARG_GUARD(!arg || !val_is_number(arg), "sin", "a number", make_float(0));
+ return make_float(sin(eigs_float_arg(arg, __func__)));
}
Value* builtin_cos(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "cos", "a number", make_num(0));
- return make_num(cos(eigs_num_arg(arg, __func__)));
+ ARG_GUARD(!arg || !val_is_number(arg), "cos", "a number", make_float(0));
+ return make_float(cos(eigs_float_arg(arg, __func__)));
}
Value* builtin_tan(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "tan", "a number", make_num(0));
- return make_num(tan(eigs_num_arg(arg, __func__)));
+ ARG_GUARD(!arg || !val_is_number(arg), "tan", "a number", make_float(0));
+ return make_float(tan(eigs_float_arg(arg, __func__)));
}
Value* builtin_asin(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "asin", "a number", make_num(0));
- double x = eigs_num_arg(arg, __func__);
+ ARG_GUARD(!arg || !val_is_number(arg), "asin", "a number", make_float(0));
+ double x = eigs_float_arg(arg, __func__);
/* #865/#971: an out-of-domain argument is clamped (invalid bit records it),
* unless strict math is on, in which case it raises. */
if (x < -1.0 || x > 1.0) {
/* fs:STRICT already loud under strict (#977); the soft 0 is the flag-off path */
- if (g_strict) { rt_error(EK_VALUE, 0, "asin: argument out of domain [-1, 1]"); return make_num(0); }
+ if (g_strict) { rt_error(EK_VALUE, 0, "asin: argument out of domain [-1, 1]"); return make_float(0); }
g_math_flags |= EIGS_MATH_INVALID;
x = (x < -1.0) ? -1.0 : 1.0;
}
- return make_num(asin(x));
+ return make_float(asin(x));
}
Value* builtin_acos(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "acos", "a number", make_num(0));
- double x = eigs_num_arg(arg, __func__);
+ ARG_GUARD(!arg || !val_is_number(arg), "acos", "a number", make_float(0));
+ double x = eigs_float_arg(arg, __func__);
/* #865/#971: an out-of-domain argument is clamped (invalid bit records it),
* unless strict math is on, in which case it raises. */
if (x < -1.0 || x > 1.0) {
/* fs:STRICT already loud under strict (#977); the soft 0 is the flag-off path */
- if (g_strict) { rt_error(EK_VALUE, 0, "acos: argument out of domain [-1, 1]"); return make_num(0); }
+ if (g_strict) { rt_error(EK_VALUE, 0, "acos: argument out of domain [-1, 1]"); return make_float(0); }
g_math_flags |= EIGS_MATH_INVALID;
x = (x < -1.0) ? -1.0 : 1.0;
}
- return make_num(acos(x));
+ return make_float(acos(x));
}
Value* builtin_atan(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "atan", "a number", make_num(0));
- return make_num(atan(eigs_num_arg(arg, __func__)));
+ ARG_GUARD(!arg || !val_is_number(arg), "atan", "a number", make_float(0));
+ return make_float(atan(eigs_float_arg(arg, __func__)));
}
Value* builtin_atan2(Value *arg) {
STRICT_LIST_MAX(arg, 2, "atan2");
- ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "atan2", "[y, x]", make_num(0));
+ ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "atan2", "[y, x]", make_float(0));
Value *y = BUILTIN_LIST_VIEW(arg, 0);
Value *x = BUILTIN_LIST_VIEW(arg, 1);
- ARG_GUARD(!y || y->type != VAL_NUM || !x || x->type != VAL_NUM, "atan2", "two numbers", make_num(0));
- return make_num(atan2(eigs_num_arg(y, __func__), eigs_num_arg(x, __func__)));
+ ARG_GUARD(!y || !val_is_number(y) || !x || !val_is_number(x), "atan2", "two numbers", make_float(0));
+ return make_float(atan2(eigs_float_arg(y, __func__), eigs_float_arg(x, __func__)));
}
Value* builtin_floor(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "floor", "a number", make_num(0));
- return make_num(floor(eigs_num_arg(arg, __func__)));
+ ARG_GUARD(!arg || !val_is_number(arg), "floor", "a number", make_float(0));
+ EigsNum n = eigs_number_arg(arg, "floor");
+ if (g_has_error) return make_null();
+ if (n.is_int) return make_int(n.value.i64);
+ double result = floor(n.value.f64);
+ int64_t integer;
+ return num_to_i64_exact(result, &integer) ? make_int(integer) : make_float(result);
}
Value* builtin_ceil(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "ceil", "a number", make_num(0));
- return make_num(ceil(eigs_num_arg(arg, __func__)));
+ ARG_GUARD(!arg || !val_is_number(arg), "ceil", "a number", make_float(0));
+ EigsNum n = eigs_number_arg(arg, "ceil");
+ if (g_has_error) return make_null();
+ if (n.is_int) return make_int(n.value.i64);
+ double result = ceil(n.value.f64);
+ int64_t integer;
+ return num_to_i64_exact(result, &integer) ? make_int(integer) : make_float(result);
}
Value* builtin_round(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "round", "a number", make_num(0));
- return make_num(round(eigs_num_arg(arg, __func__)));
+ ARG_GUARD(!arg || !val_is_number(arg), "round", "a number", make_float(0));
+ EigsNum n = eigs_number_arg(arg, "round");
+ if (g_has_error) return make_null();
+ if (n.is_int) return make_int(n.value.i64);
+ double result = round(n.value.f64);
+ int64_t integer;
+ return num_to_i64_exact(result, &integer) ? make_int(integer) : make_float(result);
}
Value* builtin_abs(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "abs", "a number", make_num(0));
- return make_num(fabs(eigs_num_arg(arg, __func__)));
+ ARG_GUARD(!arg || !val_is_number(arg), "abs", "a number", make_float(0));
+ EigsNum n = eigs_number_arg(arg, "abs");
+ if (g_has_error) return make_null();
+ if (n.is_int) {
+ if (n.value.i64 == INT64_MIN) {
+ rt_error(EK_VALUE, 0, "abs: int64 overflow");
+ return make_null();
+ }
+ return make_int(n.value.i64 < 0 ? -n.value.i64 : n.value.i64);
+ }
+ return make_float(fabs(n.value.f64));
}
/* #317: min/max are N-ary reductions over a flat list of numbers — the old
* code silently read only items[0..1] (`max of [1,2,3]` was 2) and returned 0
* for a 1-element list. A single bare number returns itself (mirrors `sum`);
- * an empty list or any non-number element keeps the old 0 fallback rather
- * than inventing a partial answer. */
+ * empty input raises (R1.6b-D2); a non-number element retains its existing
+ * strict type error / non-strict zero fallback. */
static Value* minmax_reduce(Value *arg, int want_max) {
- if (arg && arg->type == VAL_NUM) return make_num(eigs_num_arg(arg, __func__));
- /* Split from the empty-list case below: #317 chose the 0 fallback for BOTH
- * a wrong type and an empty list in one condition, and only the first half
- * is a type mistake. Splitting keeps the non-strict answer identical while
- * letting strict speak about the half it should. */
- ARG_GUARD(!arg || arg->type != VAL_LIST, want_max ? "max" : "min", "a number or a list of numbers", make_num(0));
- /* fs:EMPTY no elements to reduce over; #317 chose 0 rather than inventing a
- * partial answer, and an empty list is not a type mistake. */
- if (arg->data.list.count < 1) return make_num(0);
- double best = 0.0;
+ const char *who = want_max ? "max" : "min";
+ if (arg && val_is_number(arg)) {
+ EigsNum n = eigs_number_arg(arg, who);
+ return g_has_error ? make_null() : builtin_number_value(n);
+ }
+ /* Keep the public guard names literal for the source/probe census. */
+ if (want_max) {
+ ARG_GUARD(!arg || arg->type != VAL_LIST, "max", "a number or a list of numbers", make_float(0));
+ } else {
+ ARG_GUARD(!arg || arg->type != VAL_LIST, "min", "a number or a list of numbers", make_float(0));
+ }
+ /* R1.6b-D2: no winner exists for an empty reduction, in either call
+ * form and either strict mode. C2's integer identity belongs to sum. */
+ if (arg->data.list.count < 1) {
+ rt_error(EK_VALUE, 0, "%s: expected at least one number", who);
+ return make_null();
+ }
+ EigsNum best = {0};
for (int i = 0; i < arg->data.list.count; i++) {
Value *v = BUILTIN_LIST_VIEW(arg, i);
- ARG_GUARD(!v || v->type != VAL_NUM, want_max ? "max" : "min", "every element to be a number", make_num(0));
- if (i == 0 || (want_max ? eigs_num_arg(v, __func__) > best : eigs_num_arg(v, __func__) < best))
- best = eigs_num_arg(v, __func__);
+ /* Keep the public guard names literal for the source/probe census. */
+ if (want_max) {
+ ARG_GUARD(!v || !val_is_number(v), "max", "every element to be a number", make_float(0));
+ } else {
+ ARG_GUARD(!v || !val_is_number(v), "min", "every element to be a number", make_float(0));
+ }
+ EigsNum n = eigs_number_arg(v, who);
+ if (g_has_error) return make_null();
+ int cmp = 0;
+ if (i) {
+ if (n.is_int && best.is_int)
+ cmp = (n.value.i64 > best.value.i64) - (n.value.i64 < best.value.i64);
+ else if (n.is_int) cmp = eigs_cmp_int_float(n.value.i64, best.value.f64);
+ else if (best.is_int) cmp = -eigs_cmp_int_float(best.value.i64, n.value.f64);
+ else cmp = (n.value.f64 > best.value.f64) - (n.value.f64 < best.value.f64);
+ }
+ if (!i || (want_max ? cmp > 0 : cmp < 0)) best = n;
}
- return make_num(best);
+ return builtin_number_value(best);
}
Value* builtin_min(Value *arg) { return minmax_reduce(arg, 0); }
@@ -2076,7 +2147,7 @@ Value* builtin_max(Value *arg) { return minmax_reduce(arg, 1); }
Value* builtin_pi(Value *arg) {
(void)arg;
- return make_num(3.14159265358979323846);
+ return make_float(3.14159265358979323846);
}
/* ================================================================
@@ -2132,7 +2203,7 @@ Value* builtin_random(Value *arg) {
/* Seed is part of the live source so a replay take / fail-loud raise
* does not race g_random_seeded (#1142 TSan on two workers under
* EIGS_REPLAY). */
- TRACE_NONDET_RET("random", make_num(eigs_random_double()));
+ TRACE_NONDET_RET("random", make_float(eigs_random_double()));
}
/* random_int of [lo, hi] → integer in [lo, hi] inclusive */
@@ -2141,47 +2212,41 @@ Value* builtin_random_int(Value *arg) {
/* #971 Phase D: a malformed range answered 0 — a number in nobody's
* range. Taped shape: the soft half still records/replays as before. */
ARG_GUARD_TAPED(!arg || arg->type != VAL_LIST || arg->data.list.count < 2,
- "random_int", "[lo, hi]", make_num(0));
+ "random_int", "[lo, hi]", make_float(0));
Value *lo = BUILTIN_LIST_VIEW(arg, 0);
Value *hi = BUILTIN_LIST_VIEW(arg, 1);
- ARG_GUARD_TAPED(!lo || lo->type != VAL_NUM || !hi || hi->type != VAL_NUM,
- "random_int", "numeric bounds", make_num(0));
- /* Range-check as doubles before any integer cast — a double outside the
- * int64_t range (or non-finite) makes the cast itself UB (#698 fixed the
- * same cast-before-range-check class in value_to_string). */
- double lo_d = eigs_num_arg(lo, __func__);
- double hi_d = eigs_num_arg(hi, __func__);
- if (!isfinite(lo_d) || !isfinite(hi_d) ||
- lo_d < -9223372036854775808.0 || hi_d < -9223372036854775808.0 ||
- lo_d >= 9223372036854775808.0 || hi_d >= 9223372036854775808.0) {
- rt_error(EK_VALUE, 0,
- "random_int: bounds must be finite and within int64 range");
- return make_null();
- }
- int64_t lo_i = (int64_t)lo_d;
- int64_t hi_i = (int64_t)hi_d;
- if (hi_i < lo_i) TRACE_NONDET_RET("random_int", make_num(lo_i));
- /* Span in uint64_t: a wide-but-legal int64 range (e.g. INT64_MIN..0) would
- * overflow a signed int64 subtraction before the rejection below. Cannot
- * wrap uint64: the double guard above keeps hi_d < 2^63, and the largest
- * double below 2^63 is 2^63 - 1024, so hi_i - lo_i + 1 <= 2^64 - 1023. */
+ ARG_GUARD_TAPED(!lo || !val_is_number(lo) || !hi || !val_is_number(hi),
+ "random_int", "numeric bounds", make_float(0));
+ int64_t lo_i = eigs_int_arg(lo, "random_int");
+ int64_t hi_i = eigs_int_arg(hi, "random_int");
+ if (g_has_error) return make_null();
+ if (hi_i < lo_i) TRACE_NONDET_RET("random_int", make_int(lo_i));
+ /* Unsigned subtraction handles all int64 bounds; span zero is 2^64. */
uint64_t span = (uint64_t)hi_i - (uint64_t)lo_i + 1;
- if (span > (uint64_t)INT32_MAX + 1) {
+ if (span == 0 || span > (uint64_t)INT32_MAX + 1) {
rt_error(EK_VALUE, 0,
"random_int: range span %llu exceeds 2^31 (lrand48 supplies 31 bits)",
(unsigned long long)span);
return make_null();
}
- TRACE_NONDET_RET("random_int", make_num(lo_i + (eigs_random_long() % (int64_t)span)));
+ TRACE_NONDET_RET("random_int", make_int(lo_i + (eigs_random_long() % (int64_t)span)));
}
-/* seed_random of n → seeds the RNG, returns true */
-Value* builtin_seed_random(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "seed_random", "a number", make_bool(0));
+/* The one way to seed the shared stream: `seed_random` and the CLI's
+ * EIGS_SEED_RANDOM (#2120, src/main.c) both come through here. */
+void eigs_seed_random(long seed) {
pthread_mutex_lock(&g_random_lock);
- srand48((long)eigs_num_arg(arg, __func__));
+ srand48(seed);
__atomic_store_n(&g_random_seeded, 1, __ATOMIC_RELEASE);
pthread_mutex_unlock(&g_random_lock);
+}
+
+/* seed_random of n → seeds the RNG, returns true */
+Value* builtin_seed_random(Value *arg) {
+ ARG_GUARD(!arg || !val_is_number(arg), "seed_random", "a number", make_bool(0));
+ int64_t seed = eigs_int_arg(arg, "seed_random");
+ if (g_has_error) return make_null();
+ eigs_seed_random((long)seed);
return make_bool(1);
}
@@ -2407,9 +2472,10 @@ Value* builtin_json_path(Value *arg) {
val_decref(root);
return r;
}
- if (current->type == VAL_NUM) {
+ if (val_is_number(current)) {
char buf[64];
- eigs_num_text(buf, sizeof(buf), eigs_num_arg(current, __func__)); /* #875 */
+ if (current->type == VAL_INT) eigs_int_text(buf, sizeof(buf), VAL_I64_RAW(current));
+ else eigs_num_text(buf, sizeof(buf), eigs_float_arg(current, "json_path")); /* #875 */
val_decref(root);
return make_str(buf);
}
@@ -2518,8 +2584,9 @@ void tokenlist_user_spelling(TokenList *tl) {
Value* builtin_token_name(Value *arg) {
/* #971 Phase D: "?" is the documented answer for an UNKNOWN id (below);
* for a non-number it was laundering a type mistake into that answer. */
- ARG_GUARD(!arg || arg->type != VAL_NUM, "token_name", "a token id", make_str("?"));
- int id = (int)eigs_num_arg(arg, __func__);
+ ARG_GUARD(!arg || !val_is_number(arg), "token_name", "a token id", make_str("?"));
+ int id = builtin_c_int(arg, "token_name");
+ if (g_has_error) return make_null();
static const char *names[] = {
"NUM", "STR", "IDENT",
"IS", "OF", "DEFINE", "AS",
@@ -2555,8 +2622,10 @@ Value* builtin_token_name(Value *arg) {
* the dict snapshot it returns is independent of subsequent program state. */
Value* builtin_state_at(Value *arg) {
BOOL_REFUSE(arg, "state_at");
- if (!arg || arg->type != VAL_NUM) return make_null();
- Value *d = trace_state_at((int)eigs_num_arg(arg, __func__));
+ if (!arg || !val_is_number(arg)) return make_null();
+ int line = builtin_c_int(arg, "state_at");
+ if (g_has_error) return make_null();
+ Value *d = trace_state_at(line);
return d ? d : make_null();
}
@@ -2602,16 +2671,17 @@ Value* builtin_secure_equals(Value *arg) {
* else raises too — the old silent empty-string return for >127 hid every
* high-byte construction bug (#435). */
Value* builtin_chr(Value *arg) {
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "chr requires a number");
return make_null();
}
- double num = eigs_num_arg(arg, __func__);
- if (num != (double)(long long)num || num < 1 || num > 255) {
+ int64_t num = eigs_int_arg(arg, "chr");
+ if (g_has_error) return make_null();
+ if (num < 1 || num > 255) {
if (num == 0)
rt_error(EK_VALUE, 0, "chr of 0: strings are NUL-terminated and cannot hold a NUL byte (use a buffer for binary with NULs)");
else
- rt_error(EK_VALUE, 0, "chr requires an integer byte in 1..255 (got %g); for a codepoint use utf8_encode (lib/utf8.eigs)", num);
+ rt_error(EK_VALUE, 0, "chr requires an integer byte in 1..255 (got %lld); for a codepoint use utf8_encode (lib/utf8.eigs)", (long long)num);
return make_null();
}
char buf[2] = { (char)(int)num, '\0' };
@@ -2621,29 +2691,26 @@ Value* builtin_chr(Value *arg) {
/* ==== BUILTIN: hex ==== */
/* hex of n → uppercase hex string, minimal digits ("0" for 0).
* hex of [n, nibbles] → zero-padded to at least `nibbles` digits.
- * n must be a non-negative integer (exact up to 2^53); anything else
+ * n must be a non-negative int64 integer (N5); anything else
* raises — hex of a fraction or a negative has no single right answer
* and silently picking one is the #316/#368 tolerance class.
* Demanded by the emulator repos (DMG GAP-DMG-010): addresses/opcodes/
* registers all want hex diagnostics and every consumer hand-rolled it. */
Value* builtin_hex(Value *arg) {
STRICT_LIST_MAX(arg, 2, "hex");
- double num;
- long long width = 0;
- if (arg && arg->type == VAL_NUM) {
- num = eigs_num_arg(arg, __func__);
- } else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2 &&
- BUILTIN_LIST_VIEW(arg, 0) && BUILTIN_LIST_VIEW(arg, 0)->type == VAL_NUM &&
- BUILTIN_LIST_VIEW(arg, 1) && BUILTIN_LIST_VIEW(arg, 1)->type == VAL_NUM) {
- num = eigs_list_num(arg, 0, __func__);
- width = (long long)eigs_list_num(arg, 1, __func__);
- if (g_has_error) return make_null();
+ int64_t num, width = 0;
+ if (arg && val_is_number(arg)) {
+ num = eigs_int_arg(arg, "hex");
+ } else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
+ num = eigs_list_int(arg, 0, "hex");
+ width = eigs_list_int(arg, 1, "hex");
} else {
rt_error(EK_TYPE, 0, "hex requires a number or [number, nibbles]");
return make_null();
}
- if (num < 0 || num != (double)(long long)num || num > 9007199254740992.0) {
- rt_error(EK_VALUE, 0, "hex requires a non-negative integer (got %g)", num);
+ if (g_has_error) return make_null();
+ if (num < 0) {
+ rt_error(EK_VALUE, 0, "hex requires a non-negative integer");
return make_null();
}
if (width < 0) width = 0;
@@ -2663,11 +2730,11 @@ Value* builtin_ord(Value *arg) {
* the same rule that #1008 applied to sum/mean/norm. Non-strict is
* byte-identical: both halves still answer -1. */
ARG_GUARD(!arg || arg->type != VAL_STR || !arg->data.str,
- "ord", "a string", make_num(-1));
+ "ord", "a string", make_float(-1));
/* fs:ANSWER an empty string has no first byte, so -1 is ord's documented
* result for it, not a rejected argument. */
- if (arg->data.str[0] == '\0') return make_num(-1);
- return make_num((double)(unsigned char)arg->data.str[0]);
+ if (arg->data.str[0] == '\0') return make_int(-1);
+ return make_int((unsigned char)arg->data.str[0]);
}
/* ==== BUILTIN: try_parse ==== */
@@ -2784,7 +2851,7 @@ static const char *vm_desc_abi_error(Value *desc, char *buf, size_t buflen) {
if (!desc || desc->type != VAL_LIST || desc->data.list.count < 1)
return "chunk descriptor must be a list";
Value *rev = BUILTIN_LIST_VIEW(desc, 0);
- if (!rev || rev->type != VAL_NUM) {
+ if (!rev || !val_is_number(rev)) {
snprintf(buf, buflen,
"chunk descriptor carries no bytecode ABI revision (element 0 "
"must be the number %d, found %s) — this producer predates the "
@@ -2792,12 +2859,14 @@ static const char *vm_desc_abi_error(Value *desc, char *buf, size_t buflen) {
EIGS_BYTECODE_ABI, rev ? val_type_name(rev->type) : "nothing");
return buf;
}
- if ((int)eigs_num_arg(rev, __func__) != EIGS_BYTECODE_ABI) {
+ int revision = builtin_c_int(rev, "vm_run_bytecode");
+ if (g_has_error) return "bytecode ABI revision must be an integer";
+ if (revision != EIGS_BYTECODE_ABI) {
snprintf(buf, buflen,
"chunk was produced for bytecode ABI revision %d, but this "
"runtime speaks revision %d — regenerate it with a matching "
"producer",
- (int)eigs_num_arg(rev, __func__), EIGS_BYTECODE_ABI);
+ revision, EIGS_BYTECODE_ABI);
return buf;
}
return NULL;
@@ -2893,7 +2962,8 @@ static void desc_leave(DescVerify *ctx) {
static Value *desc_isolate_const(DescVerify *ctx, Value *v) {
if (!v || !desc_spend(ctx, 1)) return NULL;
switch (v->type) {
- case VAL_NUM:
+ case VAL_FLOAT:
+ case VAL_INT: /* #1758 R1.2a: an immutable scalar, like a float */
case VAL_STR:
case VAL_NULL:
case VAL_BOOL:
@@ -3010,12 +3080,14 @@ static EigsChunk *vm_build_chunk_desc_body(Value *desc, int off, int sandbox_mod
Value *b = BUILTIN_LIST_VIEW(code, i);
/* #1637: a non-number code element (a bool) is refused, never
* emitted as byte 0 (OP_CONST's opcode). */
- if (!b || b->type != VAL_NUM) {
+ if (!b || !val_is_number(b)) {
chunk_free(chunk);
vm_desc_error(why, whyn, "descriptor code must be a list of byte numbers");
return NULL;
}
- chunk_emit(chunk, (uint8_t)((int)eigs_num_arg(b, __func__) & 0xFF), 1);
+ uint8_t byte = eigs_byte_arg(b, "vm_run_bytecode");
+ if (g_has_error) { chunk_free(chunk); return NULL; }
+ chunk_emit(chunk, byte, 1);
}
/* Positional: the code stream indexes this pool by position, so neither
* the dedup collapse nor a skipped NULL may shift an entry (#721). A hole
@@ -3071,8 +3143,9 @@ static EigsChunk *vm_build_chunk_desc_body(Value *desc, int off, int sandbox_mod
vm_desc_error(why, whyn, "descriptor param_count must be a number");
return NULL;
}
- int param_count = (n >= 4 && d[3] && d[3]->type == VAL_NUM)
- ? (int)eigs_num_arg(d[3], __func__) : 0;
+ int param_count = (n >= 4 && d[3] && val_is_number(d[3]))
+ ? builtin_c_int(d[3], "vm_run_bytecode") : 0;
+ if (g_has_error) { chunk_free(chunk); return NULL; }
chunk->param_count = param_count;
chunk->first_default = param_count; /* no defaults */
@@ -3165,12 +3238,14 @@ static EigsChunk *vm_build_chunk_desc(Value *desc, int off, int sandbox_mode,
Value* builtin_vm_run_bytecode(Value *arg) {
char abibuf[256];
const char *abi_err = vm_desc_abi_error(arg, abibuf, sizeof abibuf);
+ if (g_has_error) return make_null();
if (abi_err) { rt_error(EK_VALUE, 0, "%s", abi_err); return make_null(); }
char why[192];
EigsChunk *chunk = vm_build_chunk_desc(arg, 1, 0, why, sizeof why);
if (!chunk) {
- rt_error(EK_VALUE, 0, "invalid chunk descriptor: %s",
- why[0] ? why : "verification failed");
+ if (!g_has_error)
+ rt_error(EK_VALUE, 0, "invalid chunk descriptor: %s",
+ why[0] ? why : "verification failed");
return make_null();
}
/* #915: ARM the observer for what this descriptor itself does, exactly as
@@ -3246,6 +3321,84 @@ static const char *SANDBOX_ALLOW[] = {
"zeros_like", "fill", "leaky_relu", "relu", "softmax", "log_softmax",
"sgd_update", "sgd_update_cols", "sgd_update_rows", "numerical_grad",
"numerical_grad_cols", "numerical_grad_rows",
+ /* N9 pure width arithmetic */
+ "i8_wrapping_add",
+ "i8_wrapping_sub",
+ "i8_wrapping_mul",
+ "i8_checked_add",
+ "i8_checked_sub",
+ "i8_checked_mul",
+ "i8_saturating_add",
+ "i8_saturating_sub",
+ "i8_saturating_mul",
+ "i16_wrapping_add",
+ "i16_wrapping_sub",
+ "i16_wrapping_mul",
+ "i16_checked_add",
+ "i16_checked_sub",
+ "i16_checked_mul",
+ "i16_saturating_add",
+ "i16_saturating_sub",
+ "i16_saturating_mul",
+ "i32_wrapping_add",
+ "i32_wrapping_sub",
+ "i32_wrapping_mul",
+ "i32_checked_add",
+ "i32_checked_sub",
+ "i32_checked_mul",
+ "i32_saturating_add",
+ "i32_saturating_sub",
+ "i32_saturating_mul",
+ "i64_wrapping_add",
+ "i64_wrapping_sub",
+ "i64_wrapping_mul",
+ "i64_checked_add",
+ "i64_checked_sub",
+ "i64_checked_mul",
+ "i64_saturating_add",
+ "i64_saturating_sub",
+ "i64_saturating_mul",
+ "u8_wrapping_add",
+ "u8_wrapping_sub",
+ "u8_wrapping_mul",
+ "u8_checked_add",
+ "u8_checked_sub",
+ "u8_checked_mul",
+ "u8_saturating_add",
+ "u8_saturating_sub",
+ "u8_saturating_mul",
+ "u16_wrapping_add",
+ "u16_wrapping_sub",
+ "u16_wrapping_mul",
+ "u16_checked_add",
+ "u16_checked_sub",
+ "u16_checked_mul",
+ "u16_saturating_add",
+ "u16_saturating_sub",
+ "u16_saturating_mul",
+ "u32_wrapping_add",
+ "u32_wrapping_sub",
+ "u32_wrapping_mul",
+ "u32_checked_add",
+ "u32_checked_sub",
+ "u32_checked_mul",
+ "u32_saturating_add",
+ "u32_saturating_sub",
+ "u32_saturating_mul",
+ "u64_wrapping_add",
+ "u64_wrapping_sub",
+ "u64_wrapping_mul",
+ "u64_checked_add",
+ "u64_checked_sub",
+ "u64_checked_mul",
+ "u64_saturating_add",
+ "u64_saturating_sub",
+ "u64_saturating_mul",
+ "u64_compare",
+ "u64_div",
+ "u64_rem",
+ "u64_to_string",
+ "fround",
/* bit ops */
"bit_and", "bit_not", "bit_or", "bit_shl", "bit_shr", "bit_xor",
/* list / sequence (operate on the value handed in) */
@@ -3274,7 +3427,7 @@ static const char *SANDBOX_ALLOW[] = {
/* path string manipulation (no fs access) */
"path_base", "path_dir", "path_ext", "path_join",
/* type / value utilities */
- "type", "coalesce", "num_copy", "secure_equals",
+ "type", "num_type", "coalesce", "num_copy", "secure_equals",
/* observer reads touch only the sandbox's own values, never globals.
* record_history is intentionally not here.
* state_at is also excluded: temporal history belongs to the host thread,
@@ -3390,28 +3543,35 @@ Value* builtin_sandbox_run(Value *arg) {
const char *source = source_form ? desc->data.str : NULL;
int max_iter = 1000000;
if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2 &&
- BUILTIN_LIST_VIEW(arg, 1) && BUILTIN_LIST_VIEW(arg, 1)->type == VAL_NUM)
- max_iter = (int)eigs_list_num(arg, 1, __func__);
+ BUILTIN_LIST_VIEW(arg, 1) && val_is_number(BUILTIN_LIST_VIEW(arg, 1)))
+ max_iter = builtin_c_int(BUILTIN_LIST_VIEW(arg, 1), "sandbox_run");
+ if (g_has_error) return make_null();
/* #292: optional max_bytes (3rd element) — the allocation budget for this
* run. Default 256 MiB: ample for the short generated snippets grade()
* runs, small enough that even a few of them can't thrash a 4 GB box. */
size_t max_bytes = (size_t)256 * 1024 * 1024;
if (arg && arg->type == VAL_LIST && arg->data.list.count >= 3 &&
- BUILTIN_LIST_VIEW(arg, 2) && BUILTIN_LIST_VIEW(arg, 2)->type == VAL_NUM &&
- eigs_list_num(arg, 2, __func__) > 0)
- max_bytes = (size_t)eigs_list_num(arg, 2, __func__);
+ BUILTIN_LIST_VIEW(arg, 2) && val_is_number(BUILTIN_LIST_VIEW(arg, 2))) {
+ int64_t requested = eigs_list_int(arg, 2, "sandbox_run");
+ if (g_has_error) return make_null();
+ if (requested > 0) {
+ if ((uint64_t)requested > SIZE_MAX) {
+ rt_error(EK_VALUE, 0, "sandbox_run: byte limit exceeds size_t");
+ return make_null();
+ }
+ max_bytes = (size_t)requested;
+ }
+ }
/* VM instructions, cumulative across callback re-entry and function
* frames. This is intentionally distinct from the compatibility loop
* limit. Ten million keeps normal generated programs comfortable while
* placing a finite ceiling on straight-line/call-heavy computation. */
uint64_t max_work = UINT64_C(10000000);
if (arg && arg->type == VAL_LIST && arg->data.list.count >= 4 &&
- BUILTIN_LIST_VIEW(arg, 3) && BUILTIN_LIST_VIEW(arg, 3)->type == VAL_NUM) {
- double requested = eigs_list_num(arg, 3, __func__);
- /* A double rounds UINT64_MAX up to 2^64, so use a strict bound;
- * accepting equality and casting it would itself be undefined. */
- if (isfinite(requested) && requested >= 1.0 &&
- requested < 18446744073709551616.0)
+ BUILTIN_LIST_VIEW(arg, 3) && val_is_number(BUILTIN_LIST_VIEW(arg, 3))) {
+ int64_t requested = eigs_list_int(arg, 3, "sandbox_run");
+ if (g_has_error) return make_null();
+ if (requested >= 1)
max_work = (uint64_t)requested;
}
@@ -3427,10 +3587,27 @@ Value* builtin_sandbox_run(Value *arg) {
* gets its own message — a grading ladder must be able to tell "your
* producer is stale" from "your bytecode is malformed" without re-running. */
char abibuf[256];
+ /* A descriptor is guest data: numeric validation failures join the
+ * existing structured descriptor rejection. Optional HOST limits above
+ * still raise to the caller (N5, Q-SANDBOX). Suppress an uncaught print
+ * until this boundary has consumed a numeric reader's error latch. */
+ int validation_try_depth = g_try_depth;
+ int validation_latched = g_sandbox_error_latched;
+ int validation_pending = g_error_print_pending;
+ g_try_depth++;
const char *abi_err = source_form ? NULL :
vm_desc_abi_error(desc, abibuf, sizeof abibuf);
EigsChunk *chunk = source_form ? NULL :
(abi_err ? NULL : vm_build_chunk_desc(desc, 1, 1, NULL, 0));
+ g_try_depth = validation_try_depth;
+ if (g_has_error && g_error_kind == EK_VALUE) {
+ snprintf(abibuf, sizeof abibuf, "%.255s", g_error_raw);
+ abi_err = abibuf;
+ g_has_error = 0;
+ g_sandbox_error_latched = validation_latched;
+ g_error_print_pending = validation_pending;
+ eigs_clear_error_value();
+ }
if (chunk && chunk_reads_shared_temporal(chunk)) {
chunk_free(chunk);
chunk = NULL;
@@ -3451,7 +3628,7 @@ Value* builtin_sandbox_run(Value *arg) {
dict_set_owned(ev, "kind", make_str(err_kind_name(EK_VALUE)));
dict_set_owned(ev, "message",
make_str(abi_err ? abi_err : "invalid chunk descriptor"));
- dict_set_owned(ev, "line", make_num(0));
+ dict_set_owned(ev, "line", make_int(0));
dict_set_owned(ev, "file", make_str(""));
dict_set_owned(out, "error", ev);
return sandbox_finish_run(out);
@@ -3714,7 +3891,7 @@ Value* builtin_sandbox_run(Value *arg) {
"loop budget exhausted (max_iterations=%d): partial run",
loop_max);
dict_set_owned(ev, "message", make_str(capmsg));
- dict_set_owned(ev, "line", make_num(0));
+ dict_set_owned(ev, "line", make_int(0));
dict_set_owned(ev, "file", make_str(""));
dict_set_owned(out, "error", ev);
}
@@ -3727,7 +3904,7 @@ Value* builtin_sandbox_run(Value *arg) {
dict_set_owned(ev, "kind",
make_str(err_kind_name((ErrKind)g_sandbox_refusal_kind)));
dict_set_owned(ev, "message", make_str(g_sandbox_refusal_msg));
- dict_set_owned(ev, "line", make_num((double)g_sandbox_refusal_line));
+ dict_set_owned(ev, "line", make_int(g_sandbox_refusal_line));
dict_set_owned(ev, "file", make_str(g_sandbox_refusal_file));
dict_set_owned(out, "error", ev);
if (g_has_error) {
@@ -3742,7 +3919,7 @@ Value* builtin_sandbox_run(Value *arg) {
Value *ev = make_dict(4);
dict_set_owned(ev, "kind", make_str(err_kind_name((ErrKind)g_error_kind)));
dict_set_owned(ev, "message", make_str(g_error_raw));
- dict_set_owned(ev, "line", make_num((double)g_error_line));
+ dict_set_owned(ev, "line", make_int(g_error_line));
dict_set_owned(ev, "file", make_str(g_error_file));
dict_set_owned(out, "error", ev);
g_has_error = 0;
@@ -3777,7 +3954,7 @@ Value* builtin_sandbox_run(Value *arg) {
"budget); refusing to cross the boundary"
: "sandbox result contains a callable; "
"functions cannot cross the sandbox boundary"));
- dict_set_owned(ev, "line", make_num(0));
+ dict_set_owned(ev, "line", make_int(0));
dict_set_owned(ev, "file", make_str(""));
dict_set_owned(out, "error", ev);
}
@@ -3806,18 +3983,20 @@ Value* builtin_sandbox_run(Value *arg) {
* compiles a temporal query; a self-hosted compiler (whose output runs via
* vm_run_bytecode) calls this to do the same. Returns the previous setting. */
Value* builtin_record_history(Value *arg) {
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "record_history requires a number flag (nonzero=on, 0=off), got %s",
arg ? val_type_name(arg->type) : "null");
return make_null();
}
int prev = g_trace_hist;
- int on = (eigs_num_arg(arg, __func__) != 0.0) ? 1 : 0;
+ double flag = eigs_float_arg(arg, "record_history");
+ if (g_has_error) return make_null();
+ int on = flag != 0.0;
/* #827: no name to narrow on — a self-hosted compiler calling this is
* standing in for the whole-program arming, so it gets the wildcard. */
if (on) { trace_arm_history_all(); trace_flag_store(g_trace_obs_hist_storage, 1); }
else trace_history_disable();
- return make_num((double)prev);
+ return make_int(prev);
}
/* ==== BUILTIN: tensor_save ==== */
@@ -3845,12 +4024,13 @@ Value* builtin_copy_into(Value *arg) {
Value *dest = list_get_ref(arg, 0);
Value *offv = BUILTIN_LIST_VIEW(arg, 1);
Value *src = BUILTIN_LIST_VIEW(arg, 2);
- if (!offv || offv->type != VAL_NUM) {
+ if (!offv || !val_is_number(offv)) {
rt_error(EK_TYPE, 0, "copy_into: offset must be a number, got %s (the order is [dest, offset, src])",
offv ? val_type_name(offv->type) : "null");
return make_null();
}
- int offset = (int)eigs_num_arg(offv, __func__);
+ int offset = builtin_c_int(offv, "copy_into");
+ if (g_has_error) return make_null();
if (offset < 0) { rt_error(EK_INDEX, 0, "copy_into: offset %d is negative", offset); return make_null(); }
if (dest && dest->type == VAL_BUFFER) {
/* buffer destination: numbers from a buffer or a list of numbers */
@@ -3862,12 +4042,14 @@ Value* builtin_copy_into(Value *arg) {
if (src && src->type == VAL_LIST) {
for (int i = 0; i < src->data.list.count && offset + i < dest->data.buffer.count; i++) {
Value *it = BUILTIN_LIST_VIEW(src, i);
- if (!it || it->type != VAL_NUM) {
+ if (!it || !val_is_number(it)) {
rt_error(EK_TYPE, 0, "cannot store %s in a buffer (copy_into element %d; buffers hold numbers)",
it ? val_type_name(it->type) : "null", i);
return make_null();
}
- dest->data.buffer.data[offset + i] = eigs_num_arg(it, __func__);
+ double value = eigs_float_arg(it, "copy_into");
+ if (g_has_error) return make_null();
+ dest->data.buffer.data[offset + i] = value;
}
return dest; /* borrowed, like the list path below */
}
@@ -3900,11 +4082,12 @@ Value* builtin_list_slice(Value *arg) {
Value *end_v = BUILTIN_LIST_VIEW(arg, 2);
BOOL_REFUSE(start_v, "list_slice");
BOOL_REFUSE(end_v, "list_slice");
- if (!start_v || start_v->type != VAL_NUM || !end_v || end_v->type != VAL_NUM)
+ if (!start_v || !val_is_number(start_v) || !end_v || !val_is_number(end_v))
return make_null();
int n = list->data.list.count;
- int start = (int)eigs_num_arg(start_v, __func__);
- int end = (int)eigs_num_arg(end_v, __func__);
+ int start = builtin_c_int(start_v, "list_slice");
+ int end = builtin_c_int(end_v, "list_slice");
+ if (g_has_error) return make_null();
if (start < 0) start += n;
if (end < 0) end += n;
if (start < 0) start = 0;
@@ -3926,8 +4109,9 @@ Value* builtin_list_slice(Value *arg) {
*/
Value* builtin_num_copy(Value *arg) {
BOOL_REFUSE(arg, "num_copy");
- if (!arg || arg->type != VAL_NUM) return make_null();
- return make_num(eigs_num_arg(arg, __func__));
+ if (!arg || !val_is_number(arg)) return make_null();
+ EigsNum n = eigs_number_arg(arg, "num_copy");
+ return g_has_error ? make_null() : builtin_number_value(n);
}
/* ==== BUILTIN: concat ==== */
@@ -3956,12 +4140,10 @@ Value* builtin_concat(Value *arg) {
* range of [start, end, step] → [start, start+step, ...] while < end (or > end if step < 0) */
/* A bound outside int range was a UB cast (libFuzzer run 5). No such
* range could be materialized anyway, so it raises. */
-static int range_bound(double d, int *out) {
- if (!(d >= (double)INT_MIN && d <= (double)INT_MAX)) {
- rt_error(EK_VALUE, 0, "range: bound %g is outside the int range", d);
- return 0;
- }
- *out = (int)d;
+static int range_bound(const Value *v, int *out) {
+ int64_t n;
+ if (!eigs_int_arg_range(v, INT_MIN, INT_MAX, &n, "range")) return 0;
+ *out = (int)n;
return 1;
}
@@ -3971,16 +4153,16 @@ Value* builtin_range(Value *arg) {
if (!arg) return make_list(0);
- if (arg->type == VAL_NUM) {
+ if (val_is_number(arg)) {
/* range of n */
- if (!range_bound(eigs_num_arg(arg, __func__), &end)) return make_list(0);
+ if (!range_bound(arg, &end)) return make_list(0);
} else if (arg->type == VAL_LIST) {
int argc = arg->data.list.count;
/* #497: every provided bound must be numeric. A non-number used to
* be silently treated as 0 (or fold the whole call to []) — a
* silent-wrong loop count. */
for (int i = 0; i < argc && i < 3; i++) {
- if (BUILTIN_LIST_VIEW(arg, i)->type != VAL_NUM) {
+ if (!val_is_number(BUILTIN_LIST_VIEW(arg, i))) {
rt_error(EK_TYPE, 0,
"range: argument %d must be a number, got %s", i,
val_type_name(BUILTIN_LIST_VIEW(arg, i)->type));
@@ -3988,14 +4170,14 @@ Value* builtin_range(Value *arg) {
}
}
if (argc >= 1) {
- if (!range_bound(eigs_list_num(arg, 0, __func__), &start)) return make_list(0);
+ if (!range_bound(BUILTIN_LIST_VIEW(arg, 0), &start)) return make_list(0);
}
if (argc >= 2) {
- if (!range_bound(eigs_list_num(arg, 1, __func__), &end)) return make_list(0);
+ if (!range_bound(BUILTIN_LIST_VIEW(arg, 1), &end)) return make_list(0);
} else
{ end = start; start = 0; } /* single-element list: treat as range of n */
if (argc >= 3) {
- if (!range_bound(eigs_list_num(arg, 2, __func__), &step)) return make_list(0);
+ if (!range_bound(BUILTIN_LIST_VIEW(arg, 2), &step)) return make_list(0);
/* The Euler-like update that feeds step can never produce
* exactly zero — it trades the zero singularity for infinity.
* This bound catches the infinity side: a step that would
@@ -4037,9 +4219,12 @@ Value* builtin_range(Value *arg) {
if (!sandbox_charge((size_t)count * (sizeof(Value) + sizeof(Value *)))) return make_list(0);
Value *result = make_list(count);
- /* Index by k: `i += step` overflowed int near INT_MAX. */
- for (int k = 0; k < count; k++) {
- Value *v = make_num((double)((int64_t)start + (int64_t)k * step));
+ /* Keep the induction value in int64: int bounds/step and the 1M cap
+ * also keep the final increment in range. Count down without a second
+ * increasing index in this allocation loop. */
+ int64_t value = start;
+ for (int remaining = count; remaining > 0; --remaining, value += step) {
+ Value *v = make_int(value);
list_append(result, v);
val_decref(v);
}
@@ -4055,7 +4240,7 @@ Value* builtin_fill(Value *arg) {
return make_list(0);
}
BOOL_REFUSE(BUILTIN_LIST_VIEW(arg, 0), "fill"); /* the count; the value may be anything */
- int count = (int)eigs_list_num(arg, 0, __func__);
+ int count = builtin_c_int(BUILTIN_LIST_VIEW(arg, 0), "fill");
if (g_has_error) return make_null(); /* #1637: a non-number slot raised */
EigsSlot val = list_slot(arg, 1);
if (count < 0) count = 0;
@@ -4077,15 +4262,16 @@ Value* builtin_fill(Value *arg) {
* operator, which raises index_range. These helpers raise the same kinds. */
static int at_index(Value *idx_val, int count, const char *what,
int *out) {
- if (!idx_val || idx_val->type != VAL_NUM) {
+ if (!idx_val || !val_is_number(idx_val)) {
rt_error(EK_VALUE, 0, "%s index must be an integer", what);
return 0;
}
- int idx = (int)eigs_num_arg(idx_val, __func__);
+ int idx = builtin_c_int(idx_val, what);
+ if (g_has_error) return 0;
if (idx < 0) idx += count; /* #312: negative counts from end */
if (idx < 0 || idx >= count) {
rt_error(EK_INDEX, 0, "index %d out of range (list length %d)",
- (int)eigs_num_arg(idx_val, __func__), count);
+ builtin_c_int(idx_val, what), count);
return 0;
}
*out = idx;
@@ -4095,17 +4281,18 @@ static int at_index(Value *idx_val, int count, const char *what,
/* #1093: the buffer twin of at_index — same negative-from-the-end rule, but
* the diagnostic `b[i]` and `buf_get` already use for a buffer, so `set_at of
* [buf, 9, v]` and `buf[9]` do not report the same fault two different ways. */
-static int buf_at_index(Value *idx_val, int count, int *out) {
- if (!idx_val || idx_val->type != VAL_NUM) {
+static int buf_at_index(Value *idx_val, int count, int *out, const char *who) {
+ if (!idx_val || !val_is_number(idx_val)) {
rt_error(EK_VALUE, 0, "buffer index must be a number, got %s",
val_type_name(idx_val ? idx_val->type : VAL_NULL));
return 0;
}
- int idx = (int)eigs_num_arg(idx_val, __func__);
+ int idx = builtin_c_int(idx_val, who);
+ if (g_has_error) return 0;
if (idx < 0) idx += count;
if (idx < 0 || idx >= count) {
rt_error(EK_INDEX, 0, "buffer index %d out of range (length %d)",
- (int)eigs_num_arg(idx_val, __func__), count);
+ builtin_c_int(idx_val, who), count);
return 0;
}
*out = idx;
@@ -4131,26 +4318,28 @@ Value* builtin_set_at(Value *arg) {
if (argc == 3) {
int idx;
if (!buf_at_index(BUILTIN_LIST_VIEW(arg, 1), buf->data.buffer.count,
- &idx)) return make_null();
+ &idx, "set_at")) return make_null();
off = idx;
} else if (argc == 4 && buf->data.buffer.rows > 0) {
int row, col;
if (!buf_at_index(BUILTIN_LIST_VIEW(arg, 1), buf->data.buffer.rows,
- &row)) return make_null();
+ &row, "set_at")) return make_null();
if (!buf_at_index(BUILTIN_LIST_VIEW(arg, 2), buf->data.buffer.cols,
- &col)) return make_null();
+ &col, "set_at")) return make_null();
off = (int64_t)row * buf->data.buffer.cols + col;
} else {
rt_error(EK_TYPE, 0, "set_at: [buffer, row, col, value] needs a "
"shaped buffer (see reshape)");
return make_null();
}
- if (!val || val->type != VAL_NUM) {
+ if (!val || !val_is_number(val)) {
rt_error(EK_TYPE, 0, "cannot store %s in a buffer (buffers hold numbers)",
val_type_name(val ? val->type : VAL_NULL));
return make_null();
}
- buf->data.buffer.data[off] = eigs_num_arg(val, __func__);
+ double value = eigs_float_arg(val, "set_at");
+ if (g_has_error) return make_null();
+ buf->data.buffer.data[off] = value;
/* Direct child of the arg vector — the borrow protocol (#720,
* vm_borrow_compensate) compensates at the call site, exactly as for
* the list path's `return list`. */
@@ -4210,16 +4399,16 @@ Value* builtin_get_at(Value *arg) {
if (argc == 2) {
int idx;
if (!buf_at_index(BUILTIN_LIST_VIEW(arg, 1), buf->data.buffer.count,
- &idx)) return make_null();
- return make_num(buffer_read_num(buf, idx));
+ &idx, "get_at")) return make_null();
+ return make_float(buffer_read_f64(buf, idx));
}
if (argc == 3 && buf->data.buffer.rows > 0) {
int row, col;
if (!buf_at_index(BUILTIN_LIST_VIEW(arg, 1), buf->data.buffer.rows,
- &row)) return make_null();
+ &row, "get_at")) return make_null();
if (!buf_at_index(BUILTIN_LIST_VIEW(arg, 2), buf->data.buffer.cols,
- &col)) return make_null();
- return make_num(buffer_read_num(buf, (int64_t)row * buf->data.buffer.cols + col));
+ &col, "get_at")) return make_null();
+ return make_float(buffer_read_f64(buf, (int64_t)row * buf->data.buffer.cols + col));
}
rt_error(EK_TYPE, 0, "get_at: [buffer, row, col] needs a shaped buffer "
"(see reshape)");
@@ -4604,14 +4793,14 @@ Value* builtin_spawn(Value *arg) {
return make_null();
}
Value *d = make_dict(8);
- dict_set_owned(d, "_handle_id", make_num((double)hid));
+ dict_set_owned(d, "_handle_id", make_int(hid));
/* #1146 (2): the slot GENERATION travels with the handle. Ids recycle
* round-robin over 255 slots, so without this a handle held across 255
* spawn/join cycles named a slot a different thread now owned — and
* joining it returned THAT thread's value while the real one returned
* null, at exit status 0. */
- dict_set_owned(d, "_handle_gen", make_num((double)hgen));
- dict_set_owned(d, "done", make_num(0));
+ dict_set_owned(d, "_handle_gen", make_int(hgen));
+ dict_set_owned(d, "done", make_int(0));
return d;
}
@@ -4636,12 +4825,15 @@ Value* builtin_thread_join(Value *arg) {
}
Value *hv = BUILTIN_DICT_VIEW(arg, "_handle_id");
Value *gv = BUILTIN_DICT_VIEW(arg, "_handle_gen");
- if (!hv || hv->type != VAL_NUM) {
+ if (!hv || !val_is_number(hv)) {
rt_error(EK_TYPE, 0, "thread_join requires a thread handle");
return make_null();
}
- int hid = (int)eigs_num_arg(hv, __func__);
- uint32_t hgen = (gv && gv->type == VAL_NUM) ? (uint32_t)eigs_num_arg(gv, __func__) : 0;
+ int hid = builtin_c_int(hv, "thread_join");
+ int64_t gen = 0;
+ if (gv && val_is_number(gv)) eigs_int_arg_range(gv, 0, UINT32_MAX, &gen, "thread_join");
+ if (g_has_error) return make_null();
+ uint32_t hgen = (uint32_t)gen;
int why = HANDLE_CLAIM_GONE;
ThreadHandle *h = (ThreadHandle*)handle_claim(hid, hgen, HANDLE_THREAD, &why);
if (!h) {
@@ -4739,23 +4931,25 @@ static void wake_channels_for_exit(EigsState *st) {
* `_channel_gen` is forged or stripped is reachable right now and must be
* refused rather than served. `out_id` is set even on failure so the refusal
* can name the slot. */
-static Channel* get_channel_why(Value *v, int *why, int *out_id) {
+static Channel* get_channel_why(Value *v, int *why, int *out_id, const char *who) {
*why = HANDLE_CLAIM_BADVALUE;
*out_id = 0;
if (!v || v->type != VAL_DICT) return NULL;
Value *cv = BUILTIN_DICT_VIEW(v, "_channel_id");
- if (!cv || cv->type != VAL_NUM) return NULL;
- *out_id = (int)eigs_num_arg(cv, __func__);
+ if (!cv || !val_is_number(cv)) return NULL;
+ *out_id = builtin_c_int(cv, who);
Value *gv = BUILTIN_DICT_VIEW(v, "_channel_gen");
- uint32_t gen = (gv && gv->type == VAL_NUM) ? (uint32_t)eigs_num_arg(gv, __func__) : 0;
- return (Channel*)handle_lookup(*out_id, gen, HANDLE_CHANNEL, why);
+ int64_t gen = 0;
+ if (gv && val_is_number(gv)) eigs_int_arg_range(gv, 0, UINT32_MAX, &gen, who);
+ if (g_has_error) return NULL;
+ return (Channel*)handle_lookup(*out_id, (uint32_t)gen, HANDLE_CHANNEL, why);
}
/* Non-raising form. ONE caller by design: `channel_closed`, whose documented
* ANSWER for an unknown/reclaimed channel is 1 (#971 Phase D), not an error. */
static Channel* get_channel(Value *v) {
int why = 0, id = 0;
- return get_channel_why(v, &why, &id);
+ return get_channel_why(v, &why, &id, "channel_closed");
}
/* ROUND 2 (#1146 G2/G3): resolve or RAISE, in the shared wording. Every
@@ -4764,8 +4958,8 @@ static Channel* get_channel(Value *v) {
* tell a stale handle from one that was never valid. */
static Channel* channel_arg(Value *v, const char *who) {
int why = 0, id = 0;
- Channel *ch = get_channel_why(v, &why, &id);
- if (!ch) handle_raise_unresolved(who, "channel", id, why, "closed");
+ Channel *ch = get_channel_why(v, &why, &id, who);
+ if (!ch && !g_has_error) handle_raise_unresolved(who, "channel", id, why, "closed");
return ch;
}
@@ -4812,8 +5006,8 @@ Value* builtin_channel(Value *arg) {
return make_null();
}
Value *d = make_dict(8);
- dict_set_owned(d, "_channel_id", make_num((double)hid));
- dict_set_owned(d, "_channel_gen", make_num((double)cgen));
+ dict_set_owned(d, "_channel_id", make_int(hid));
+ dict_set_owned(d, "_channel_gen", make_int(cgen));
return d;
}
@@ -4901,10 +5095,8 @@ Value* builtin_try_recv(Value *arg) {
return val ? val : make_null();
}
-/* recv_timeout of [channel, ms] — bounded wait. Returns the value if one
- * arrives (or is already buffered) before the deadline, else null. ms is
- * interpreted as milliseconds; fractional values are honored (ns precision
- * on Linux). Negative ms degenerates to a try_recv. */
+/* recv_timeout of [channel, ms] — milliseconds rounded half away from zero
+ * (Q7 magnitude), within the wait call's long range. Negative ms polls. */
Value* builtin_recv_timeout(Value *arg) {
STRICT_LIST_MAX(arg, 2, "recv_timeout");
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) {
@@ -4913,29 +5105,21 @@ Value* builtin_recv_timeout(Value *arg) {
}
Channel *ch = channel_arg(BUILTIN_LIST_VIEW(arg, 0), "recv_timeout");
if (!ch) return make_null();
- if (replay_blocks("recv_timeout", "subprocess/concurrency")) return make_null(); /* #1072: after validation */
Value *ms_v = BUILTIN_LIST_VIEW(arg, 1);
- if (ms_v->type != VAL_NUM) {
+ if (!val_is_number(ms_v)) {
rt_error(EK_TYPE, 0, "recv_timeout: ms must be a number");
return make_null();
}
- double ms = eigs_num_arg(ms_v, __func__);
- /* Sanitize ms before the (long) cast (#151). NaN, +inf, or any value
- * above ~LONG_MAX is undefined behavior when cast to long, and in
- * practice produced a garbage deadline that fired immediately or
- * waited essentially forever. Cap at one year of ms (3.15e10) — well
- * below LONG_MAX on 32-bit time_t hosts and far beyond any plausible
- * channel timeout. NaN degenerates to try_recv (ms=0). */
- if (isnan(ms)) ms = 0.0;
- else if (ms < 0.0) ms = 0.0;
- else if (ms > 3.15e10) ms = 3.15e10;
+ int64_t ms;
+ if (!eigs_magnitude_arg(ms_v, LONG_MIN, LONG_MAX, &ms, "recv_timeout")) return make_null();
+ if (ms < 0) ms = 0;
+ if (replay_blocks("recv_timeout", "subprocess/concurrency")) return make_null(); /* all refusals precede replay */
struct timespec deadline;
clock_gettime(EIGS_CHANNEL_CLOCK, &deadline);
long whole_ms = (long)ms;
- long extra_ns = (long)((ms - (double)whole_ms) * 1e6);
deadline.tv_sec += whole_ms / 1000;
- deadline.tv_nsec += (whole_ms % 1000) * 1000000L + extra_ns;
+ deadline.tv_nsec += (whole_ms % 1000) * 1000000L;
if (deadline.tv_nsec >= 1000000000L) {
deadline.tv_sec += deadline.tv_nsec / 1000000000L;
deadline.tv_nsec %= 1000000000L;
@@ -5145,7 +5329,7 @@ Value* builtin_task_spawn(Value *arg) {
t->id = id;
t->hgen = tgen;
task_sched_on_spawn(id); /* enqueue + arm the scheduler */
- return make_num(task_handle_pack(id, tgen));
+ return make_int(task_handle_pack(id, tgen));
}
/* Resolve an EigenScript-visible packed task id to the scheduler's raw slot.
@@ -5155,8 +5339,11 @@ Value* builtin_task_spawn(Value *arg) {
static Task *task_handle_resolve(Value *arg, const char *who, int *out_id) {
int id = 0;
uint32_t gen = 0;
- if (!arg || arg->type != VAL_NUM ||
- !task_handle_unpack(eigs_num_arg(arg, __func__), &id, &gen)) return NULL;
+ if (!arg || !val_is_number(arg)) return NULL;
+ int64_t packed = eigs_int_arg(arg, who);
+ if (g_has_error || packed <= 0 || packed > INT64_C(9007199254740992)) return NULL;
+ /* The packed handle API is a double, but every allowed id is exact. */
+ if (!task_handle_unpack((double)packed, &id, &gen)) return NULL;
int why = HANDLE_CLAIM_GONE;
Task *t = (Task *)handle_lookup(id, gen, HANDLE_TASK, &why);
if (!t && (why == HANDLE_CLAIM_STALE || why == HANDLE_CLAIM_TYPE))
@@ -5207,7 +5394,9 @@ Value* builtin_task_yield(Value *arg) {
* returns immediately. Joining an unknown id, self, or with no scheduler
* returns null. Same nesting restriction as task_yield. */
Value* builtin_task_join(Value *arg) {
- if (!arg || arg->type != VAL_NUM || !g_task_sched) return make_null();
+ if (!arg || !val_is_number(arg)) return make_null();
+ (void)eigs_int_arg(arg, "task_join");
+ if (g_has_error || !g_task_sched) return make_null();
int target = 0;
Task *t = task_handle_resolve(arg, "task_join", &target);
if (!t) return make_null();
@@ -5252,15 +5441,16 @@ Value* builtin_task_send(Value *arg) {
* trigger, and the differential's probe (which spawns a task first) could
* not see it. `task_kill` and `stream_write` already order it this way.
* Non-strict is unchanged: both halves answer 0. */
- ARG_GUARD(!idv || idv->type != VAL_NUM, "task_send", "a task id (number)", make_bool(0));
+ ARG_GUARD(!idv || !val_is_number(idv), "task_send", "a task id (number)", make_bool(0));
/* fs:ANSWER no scheduler means there is no task to deliver to, which is
* the dead-letter drop this function's contract documents ("0 if
* dropped") — split out of the arity guard above, which is a mistake. */
- if (!g_task_sched) return make_bool(0);
+ int64_t packed = eigs_int_arg(idv, "task_send");
+ if (g_has_error || !g_task_sched) return make_bool(0);
int target = 0;
/* Main is the reserved public id 0 and has no handle-table generation,
* but it does have a scheduler mailbox (reply-to-supervisor pattern). */
- if (eigs_num_arg(idv, __func__) != 0 &&
+ if (packed != 0 &&
!task_handle_resolve(idv, "task_send", &target)) return make_bool(0);
Value *copy = val_clone_for_send(BUILTIN_LIST_VIEW(arg, 1)); /* share-nothing */
int sent = task_deliver(target, copy);
@@ -5283,10 +5473,11 @@ Value* builtin_task_recv(Value *arg) {
* and suspended slice, wake any joiner with an `interrupt` error, mark it
* dead. Returns 1 if killed, 0 for a bad/self/finished target. */
Value* builtin_task_kill(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "task_kill", "a task id (number)", make_bool(0));
+ ARG_GUARD(!arg || !val_is_number(arg), "task_kill", "a task id (number)", make_bool(0));
/* fs:ANSWER no scheduler means no such target; 0 is the documented
* "bad/self/finished target" answer, split from the type guard above. */
- if (!g_task_sched) return make_bool(0);
+ (void)eigs_int_arg(arg, "task_kill");
+ if (g_has_error || !g_task_sched) return make_bool(0);
int target = 0;
if (!task_handle_resolve(arg, "task_kill", &target)) return make_bool(0);
return make_bool(task_do_kill(target));
@@ -5295,7 +5486,7 @@ Value* builtin_task_kill(Value *arg) {
/* task_alive of id → true while the task is READY/RUNNING/SUSPENDED, else false
* (DONE, DEAD, or an unknown id). */
Value* builtin_task_alive(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "task_alive", "a task id (number)", make_bool(0));
+ ARG_GUARD(!arg || !val_is_number(arg), "task_alive", "a task id (number)", make_bool(0));
Task *t = task_handle_resolve(arg, "task_alive", NULL);
/* fs:ANSWER an unknown id is NOT alive; 0 is this function's documented
* answer. Four lines above, an identical `return make_bool(0)` is a type
@@ -5317,13 +5508,14 @@ Value* builtin_task_alive(Value *arg) {
Value* builtin_task_sleep(Value *arg) {
/* #1637: the type check comes before the no-scheduler short-circuit, so
* `task_sleep of true` raises with or without a scheduler. */
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "task_sleep requires a number of ticks");
return make_null();
}
- if (!g_task_sched) return make_null(); /* no scheduler: nothing to wait for */
+ double ticks = eigs_float_arg(arg, "task_sleep");
+ if (g_has_error || !g_task_sched) return make_null(); /* no scheduler: nothing to wait for */
if (no_yield_forbidden("task_sleep")) return make_null(); /* #488 */
- task_request_sleep(eigs_num_arg(arg, __func__));
+ task_request_sleep(ticks);
return make_null(); /* placeholder: execution resumes when the clock wakes it */
}
@@ -5355,7 +5547,7 @@ Value* builtin_must_not_yield(Value *arg) {
* state, so it records no tape nondet. */
Value* builtin_task_self(Value *arg) {
(void)arg;
- return make_num(task_current_id());
+ return make_int(task_current_id());
}
/* task_detach of id -> 1 (0 for main/unknown). Marks the task fire-and-forget
@@ -5369,15 +5561,15 @@ Value* builtin_task_self(Value *arg) {
* uncatchable; task.c sched_fatal_stop). Joining a reaped id returns null,
* like any unknown id. Deterministic; no tape participation. */
Value* builtin_task_detach(Value *arg) {
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "task_detach requires a task id (a number)");
return make_null();
}
int target = 0;
- if (!task_handle_resolve(arg, "task_detach", &target)) return make_num(0);
+ if (!task_handle_resolve(arg, "task_detach", &target)) return make_int(0);
int detached = task_do_detach(target);
if (detached < 0) return make_null(); /* #1736: the stop is already raised */
- return make_num((double)detached);
+ return make_int(detached);
}
/* task_sched_seed of n — install a scheduling seed. By default tasks run FIFO
@@ -5388,11 +5580,13 @@ Value* builtin_task_detach(Value *arg) {
* simulation tester uses to search interleavings. Typically called once at
* program start. Returns null. */
Value* builtin_task_sched_seed(Value *arg) {
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "task_sched_seed requires a number (the seed)");
return make_null();
}
- task_sched_set_seed(eigs_num_arg(arg, __func__));
+ int64_t seed = eigs_int_arg(arg, "task_sched_seed");
+ if (g_has_error) return make_null();
+ task_sched_set_seed(seed);
return make_null();
}
@@ -5541,11 +5735,11 @@ Value* builtin_nearest_in_range(Value *arg) {
Value *entities = BUILTIN_LIST_VIEW(arg, 0);
if (!entities || entities->type != VAL_LIST) return make_null();
BOOL_REFUSE(arg, "nearest_in_range"); /* x, y, range, world: numbers */
- double px = eigs_list_num(arg, 1, __func__);
- double py = eigs_list_num(arg, 2, __func__);
- double range = eigs_list_num(arg, 3, __func__);
- double ww = eigs_list_num(arg, 4, __func__);
- double wh = eigs_list_num(arg, 5, __func__);
+ double px = eigs_list_float(arg, 1, __func__);
+ double py = eigs_list_float(arg, 2, __func__);
+ double range = eigs_list_float(arg, 3, __func__);
+ double ww = eigs_list_float(arg, 4, __func__);
+ double wh = eigs_list_float(arg, 5, __func__);
if (g_has_error) return make_null(); /* #1637: a non-number slot raised */
const char *px_key = "px", *py_key = "py", *active_key = "active";
if (arg->data.list.count >= 7 && BUILTIN_LIST_VIEW(arg, 6)->type == VAL_STR)
@@ -5602,7 +5796,7 @@ Value* builtin_nearest_in_range(Value *arg) {
}
/* #1637: "active" is a bool (false skips); the older 1/0 number
* still reads as before. */
- if (av && ((av->type == VAL_NUM && eigs_num_arg(av, __func__) != 1.0)
+ if (av && ((val_is_number(av) && eigs_float_arg(av, __func__) != 1.0)
|| (av->type == VAL_BOOL && !av->data.boolean))) continue;
Value *ex;
@@ -5626,13 +5820,14 @@ Value* builtin_nearest_in_range(Value *arg) {
/* #1637 round 4: a bool coordinate raises (any other non-number
* keeps the documented skip). */
if ((ex && ex->type == VAL_BOOL) || (ey && ey->type == VAL_BOOL)) {
- eigs_num_arg_slow((ex && ex->type == VAL_BOOL) ? ex : ey, "nearest_in_range");
+ eigs_float_arg((ex && ex->type == VAL_BOOL) ? ex : ey, "nearest_in_range");
return make_null();
}
- if (!ex || !ey || ex->type != VAL_NUM || ey->type != VAL_NUM) continue;
+ if (!ex || !ey || !val_is_number(ex) || !val_is_number(ey)) continue;
- double dx = eigs_num_arg(ex, __func__) - px;
- double dy = eigs_num_arg(ey, __func__) - py;
+ double dx = eigs_float_arg(ex, __func__) - px;
+ double dy = eigs_float_arg(ey, __func__) - py;
+ if (g_has_error) return make_null();
double hw = ww * 0.5, hh = wh * 0.5;
if (dx > hw) dx -= ww; else if (dx < -hw) dx += ww;
if (dy > hh) dy -= wh; else if (dy < -hh) dy += wh;
@@ -5646,10 +5841,10 @@ Value* builtin_nearest_in_range(Value *arg) {
}
if (best_idx < 0) return make_null();
Value *result = make_dict(8);
- dict_set_owned(result, "index", make_num(best_idx));
- dict_set_owned(result, "dist", make_num(sqrt(best_sq)));
- dict_set_owned(result, "dx", make_num(best_dx));
- dict_set_owned(result, "dy", make_num(best_dy));
+ dict_set_owned(result, "index", make_int(best_idx));
+ dict_set_owned(result, "dist", make_float(sqrt(best_sq)));
+ dict_set_owned(result, "dx", make_float(best_dx));
+ dict_set_owned(result, "dy", make_float(best_dy));
return result;
}
@@ -5674,9 +5869,9 @@ Value* builtin_nearest_in_range_all(Value *arg) {
Value *entities = BUILTIN_LIST_VIEW(arg, 0);
if (!entities || entities->type != VAL_LIST) return make_null();
BOOL_REFUSE(arg, "nearest_in_range_all"); /* range, world: numbers */
- double range = eigs_list_num(arg, 1, __func__);
- double ww = eigs_list_num(arg, 2, __func__);
- double wh = eigs_list_num(arg, 3, __func__);
+ double range = eigs_list_float(arg, 1, __func__);
+ double ww = eigs_list_float(arg, 2, __func__);
+ double wh = eigs_list_float(arg, 3, __func__);
if (g_has_error) return make_null(); /* #1637: a non-number slot raised */
const char *px_key = "px", *py_key = "py", *active_key = "active";
if (arg->data.list.count >= 5 && BUILTIN_LIST_VIEW(arg, 4)->type == VAL_STR)
@@ -5731,7 +5926,7 @@ Value* builtin_nearest_in_range_all(Value *arg) {
}
/* active default: 1 (matches single-call behavior where missing/non-num
* is treated as active). Explicit 0/non-1 value flips to inactive. */
- active_arr[i] = (av && ((av->type == VAL_NUM && eigs_num_arg(av, __func__) != 1.0)
+ active_arr[i] = (av && ((val_is_number(av) && eigs_float_arg(av, __func__) != 1.0)
|| (av->type == VAL_BOOL && !av->data.boolean))) ? 0 : 1; /* #1637 */
Value *ex;
@@ -5751,15 +5946,16 @@ Value* builtin_nearest_in_range_all(Value *arg) {
ey = (idx >= 0) ? slot_view(dict_value_slot(e, idx), &ey_view) : NULL;
}
if ((ex && ex->type == VAL_BOOL) || (ey && ey->type == VAL_BOOL)) { /* #1637 round 4 */
- eigs_num_arg_slow((ex && ex->type == VAL_BOOL) ? ex : ey, "nearest_in_range_all");
+ eigs_float_arg((ex && ex->type == VAL_BOOL) ? ex : ey, "nearest_in_range_all");
break;
}
- if (!ex || !ey || ex->type != VAL_NUM || ey->type != VAL_NUM) {
+ if (!ex || !ey || !val_is_number(ex) || !val_is_number(ey)) {
valid_arr[i] = 0;
continue;
}
- px_arr[i] = eigs_num_arg(ex, __func__);
- py_arr[i] = eigs_num_arg(ey, __func__);
+ px_arr[i] = eigs_float_arg(ex, __func__);
+ py_arr[i] = eigs_float_arg(ey, __func__);
+ if (g_has_error) break;
valid_arr[i] = 1;
}
if (g_has_error) { /* #1637 round 4: a bool coordinate raised above */
@@ -5800,10 +5996,10 @@ Value* builtin_nearest_in_range_all(Value *arg) {
list_append_owned(result, make_null());
} else {
Value *r = make_dict(8);
- dict_set_owned(r, "index", make_num(best_idx));
- dict_set_owned(r, "dist", make_num(sqrt(best_sq)));
- dict_set_owned(r, "dx", make_num(best_dx));
- dict_set_owned(r, "dy", make_num(best_dy));
+ dict_set_owned(r, "index", make_int(best_idx));
+ dict_set_owned(r, "dist", make_float(sqrt(best_sq)));
+ dict_set_owned(r, "dx", make_float(best_dx));
+ dict_set_owned(r, "dy", make_float(best_dy));
list_append_owned(result, r); /* adopt the freshly-built dict */
}
}
@@ -5826,24 +6022,22 @@ Value* builtin_nearest_in_range_all(Value *arg) {
* E.g. sign_extend of [0xFF, 8] → -1 */
Value* builtin_sign_extend(Value *arg) {
STRICT_LIST_MAX(arg, 2, "sign_extend");
- ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "sign_extend", "[val, bits]", make_num(0));
- /* The ELEMENT type check below is new, and it is the one place in this
- * change where the non-strict result is not byte-identical to before.
- * It was not fail-softness — it was a union type-pun: both reads below
- * took `eigs_num_arg(&(), __func__)` off items that were never checked, so
- * `sign_extend of ["x", 8]` reinterpreted a `char *` as a `double` and
- * sign-extended whatever that produced. There is no previous behaviour
- * to preserve, because the previous behaviour was undefined; 0 is the
- * same stand-in every sibling here uses. Filed as its own issue. */
+ ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "sign_extend", "[val, bits]", make_float(0));
Value *v0 = BUILTIN_LIST_VIEW(arg, 0), *v1 = BUILTIN_LIST_VIEW(arg, 1);
- ARG_GUARD(!v0 || v0->type != VAL_NUM || !v1 || v1->type != VAL_NUM, "sign_extend", "two numbers", make_num(0));
- double val = eigs_num_arg(v0, __func__);
- int bits = (int)eigs_num_arg(v1, __func__);
- if (bits <= 0 || bits > 32) return make_num(val);
- int64_t mask = 1LL << (bits - 1);
- if ((int64_t)val & mask)
- return make_num((double)((int64_t)val - (1LL << bits)));
- return make_num(val);
+ ARG_GUARD(!v0 || !val_is_number(v0) || !v1 || !val_is_number(v1), "sign_extend", "two numbers", make_float(0));
+ int64_t val = eigs_list_int(arg, 0, "sign_extend");
+ int64_t bits = eigs_list_int(arg, 1, "sign_extend");
+ if (g_has_error) return make_null();
+ if (bits <= 0 || bits > 32) return make_int(val);
+ if ((uint64_t)val & (UINT64_C(1) << (bits - 1))) {
+ int64_t result;
+ if (__builtin_sub_overflow(val, INT64_C(1) << bits, &result)) {
+ rt_error(EK_VALUE, 0, "sign_extend: int64 overflow");
+ return make_null();
+ }
+ return make_int(result);
+ }
+ return make_int(val);
}
/* list_truncate of [list, new_len] — shrink list in-place to new_len items.
@@ -5860,11 +6054,12 @@ Value* builtin_list_truncate(Value *arg) {
rt_error(EK_TYPE, 0, "list_truncate: first argument must be a list");
return make_null();
}
- if (!len_val || len_val->type != VAL_NUM) {
+ if (!len_val || !val_is_number(len_val)) {
rt_error(EK_TYPE, 0, "list_truncate: new_len must be a number");
return make_null();
}
- int new_len = (int)eigs_num_arg(len_val, __func__);
+ int new_len = builtin_c_int(len_val, "list_truncate");
+ if (g_has_error) return make_null();
/* #503: a negative new_len used to silently empty the list (an
* undocumented soft clamp to 0). Raise instead. */
if (new_len < 0) {
@@ -5890,8 +6085,9 @@ Value* builtin_list_remove_at(Value *arg) {
if (!list || list->type != VAL_LIST) return make_null();
/* #1637: a non-number index raises under EIGS_STRICT (a bool in every
* mode) instead of silently doing nothing. */
- ARG_GUARD(!idx_val || idx_val->type != VAL_NUM, "list_remove_at", "a number index", list);
- int idx = (int)eigs_num_arg(idx_val, __func__);
+ ARG_GUARD(!idx_val || !val_is_number(idx_val), "list_remove_at", "a number index", list);
+ int idx = builtin_c_int(idx_val, "list_remove_at");
+ if (g_has_error) return make_null();
if (idx < 0 || idx >= list->data.list.count) return list;
slot_decref(list_slot(list, idx));
int tail = list->data.list.count - idx - 1;
@@ -5912,8 +6108,9 @@ Value* builtin_list_insert_at(Value *arg) {
if (!list || list->type != VAL_LIST) return make_null();
/* #1637: a non-number index raises under EIGS_STRICT (a bool in every
* mode) instead of silently doing nothing. */
- ARG_GUARD(!idx_val || idx_val->type != VAL_NUM, "list_insert_at", "a number index", list);
- int idx = (int)eigs_num_arg(idx_val, __func__);
+ ARG_GUARD(!idx_val || !val_is_number(idx_val), "list_insert_at", "a number index", list);
+ int idx = builtin_c_int(idx_val, "list_insert_at");
+ if (g_has_error) return make_null();
int count = list->data.list.count;
if (idx < 0 || idx > count) return list;
if (idx == count) {
@@ -5944,21 +6141,21 @@ Value* builtin_list_insert_at(Value *arg) {
Value* builtin_list_index_of(Value *arg) {
STRICT_LIST_MAX(arg, 2, "list_index_of");
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2,
- "list_index_of", "[list, value]", make_num(-1));
+ "list_index_of", "[list, value]", make_int(-1));
Value *list = list_get_ref(arg, 0);
Value *needle = BUILTIN_LIST_VIEW(arg, 1);
ARG_GUARD(!list || list->type != VAL_LIST,
- "list_index_of", "a list as its first argument", make_num(-1));
+ "list_index_of", "a list as its first argument", make_int(-1));
for (int i = 0; i < list->data.list.count; i++) {
/* #1637: the `==` comparison, raising on bool vs num like `==`. */
int equal = values_equal_op(BUILTIN_LIST_VIEW(list, i), needle, "list_index_of");
/* #1417: structural buffer comparison can raise on a strict NaN. */
if (g_has_error) return make_null();
- if (equal) return make_num((double)i);
+ if (equal) return make_int(i);
}
/* fs:ANSWER the whole list was scanned and nothing equalled the needle —
* -1 is list_index_of's documented miss. */
- return make_num(-1);
+ return make_int(-1);
}
/* list_contains of [list, value] — true if any element structurally equals
@@ -5985,14 +6182,22 @@ Value* builtin_list_contains(Value *arg) {
/* sort_by of [list, key_fn] — sort list by numeric keys from key_fn.
* Evaluates key_fn once per element, then qsorts by key (ascending).
* Stable tiebreak by original index. Returns a NEW sorted list. */
-typedef struct { double key; int index; } SortByPair;
+typedef struct { union { double key; int64_t int_key; }; int is_int; int index; } SortByPair;
static int sort_by_pair_cmp(const void *a, const void *b) {
- double da = ((const SortByPair *)a)->key;
- double db = ((const SortByPair *)b)->key;
- if (da < db) return -1;
- if (da > db) return 1;
- return ((const SortByPair *)a)->index - ((const SortByPair *)b)->index;
+ const SortByPair *pa = a, *pb = b;
+ int cmp;
+ if (pa->is_int && pb->is_int)
+ cmp = (pa->int_key > pb->int_key) - (pa->int_key < pb->int_key);
+ else if (pa->is_int)
+ cmp = eigs_cmp_int_float(pa->int_key, pb->key);
+ else if (pb->is_int) {
+ cmp = eigs_cmp_int_float(pb->int_key, pa->key);
+ if (cmp != 2) cmp = -cmp;
+ } else
+ cmp = (pa->key > pb->key) - (pa->key < pb->key);
+ if (cmp != 0 && cmp != 2) return cmp;
+ return pa->index - pb->index;
}
Value* builtin_sort_by(Value *arg) {
@@ -6021,7 +6226,7 @@ Value* builtin_sort_by(Value *arg) {
}
/* #501: a non-numeric key used to coerce to 0.0 — a silent wrong
* order. Raise instead (mirrors `sort` #368). */
- if (!kv || kv->type != VAL_NUM) {
+ if (!kv || !val_is_number(kv)) {
const char *kt = kv ? val_type_name(kv->type) : "null";
if (kv) val_decref(kv);
free(pairs);
@@ -6030,7 +6235,9 @@ Value* builtin_sort_by(Value *arg) {
"gave %s)", i, kt);
return make_null();
}
- pairs[i].key = eigs_num_arg(kv, __func__);
+ pairs[i].is_int = kv->type == VAL_INT;
+ if (pairs[i].is_int) pairs[i].int_key = VAL_I64_RAW(kv);
+ else pairs[i].key = eigs_float_arg(kv, __func__);
pairs[i].index = i;
val_decref(kv);
}
@@ -6049,8 +6256,20 @@ Value* builtin_sort_by(Value *arg) {
* with libc-dependent element order on top (qsort gives no stability
* guarantee for all-equal elements). Record sorting is sort_by's job. */
static int sort_cmp_num(const void *a, const void *b) {
- double da = eigs_slot_num(*(const EigsSlot *)a, __func__);
- double db = eigs_slot_num(*(const EigsSlot *)b, __func__);
+ EIGS_VIEW(av); EIGS_VIEW(bv);
+ Value *va = slot_view(*(const EigsSlot *)a, &av);
+ Value *vb = slot_view(*(const EigsSlot *)b, &bv);
+ if (va->type == VAL_INT && vb->type == VAL_INT)
+ return (VAL_I64_RAW(va) > VAL_I64_RAW(vb)) - (VAL_I64_RAW(va) < VAL_I64_RAW(vb));
+ if (va->type == VAL_INT) {
+ int cmp = eigs_cmp_int_float(VAL_I64_RAW(va), eigs_float_arg(vb, __func__));
+ return cmp == 2 ? 0 : cmp;
+ }
+ if (vb->type == VAL_INT) {
+ int cmp = eigs_cmp_int_float(VAL_I64_RAW(vb), eigs_float_arg(va, __func__));
+ return cmp == 2 ? 0 : -cmp;
+ }
+ double da = eigs_float_arg(va, __func__), db = eigs_float_arg(vb, __func__);
return (da > db) - (da < db);
}
@@ -6068,14 +6287,15 @@ Value* builtin_sort(Value *arg) {
: VAL_NULL;
for (int i = 0; i < arg->data.list.count; i++) {
Value *v = BUILTIN_LIST_VIEW(arg, i);
- if (!v || v->type != t || (t != VAL_NUM && t != VAL_STR)) {
+ int numeric = (t == VAL_FLOAT || t == VAL_INT);
+ if (!v || (numeric ? !val_is_number(v) : t != VAL_STR || v->type != VAL_STR)) {
rt_error(EK_TYPE, 0, "sort requires all numbers or all strings "
"(element %d is %s); use sort_by for records",
i, v ? val_type_name(v->type) : "null");
return make_null();
}
}
- list_sort(arg, t == VAL_NUM ? sort_cmp_num : sort_cmp_str);
+ list_sort(arg, (t == VAL_FLOAT || t == VAL_INT) ? sort_cmp_num : sort_cmp_str);
return arg;
}
@@ -6096,24 +6316,25 @@ Value* builtin_dispatch(Value *arg) {
/* Key validation mirrors OP_DISPATCH (#353): a non-number key used to
* reinterpret whatever union member the value carried as a double. */
- if (!key_v || key_v->type != VAL_NUM) {
+ if (!key_v || !val_is_number(key_v)) {
rt_error(EK_TYPE, 0, "dispatch: key must be a number");
return make_null();
}
- double key_d = eigs_num_arg(key_v, __func__);
- int key;
- /* Same order as native OP_DISPATCH, through the same predicate (vm.h):
- * a fractional key is a value error; an integral one outside int range
- * is index_range. */
- if (!vm_index_is_int(key_d, &key)) {
+ EigsNum key_n = eigs_number_arg(key_v, "dispatch");
+ if (g_has_error) return make_null();
+ int64_t key_i;
+ if (key_n.is_int) key_i = key_n.value.i64;
+ else if (!num_to_i64_exact(key_n.value.f64, &key_i)) {
rt_error(EK_VALUE, 0, "dispatch key must be an integer, got %g",
- key_d);
+ key_n.value.f64);
return make_null();
}
- if (key_d < (double)INT_MIN || key_d > (double)INT_MAX) {
- rt_error(EK_INDEX, 0, "dispatch key %g out of range", key_d);
+ /* #1849: preserve the index-range diagnostic after N5's exact read. */
+ if (key_i < INT_MIN || key_i > INT_MAX) {
+ rt_error(EK_INDEX, 0, "dispatch key %lld out of range", (long long)key_i);
return make_null();
}
+ int key = (int)key_i;
if (!table || table->type != VAL_LIST) {
rt_error(EK_TYPE, 0, "dispatch: table must be a list");
@@ -6131,13 +6352,18 @@ Value* builtin_dispatch(Value *arg) {
/* free_val CONSUMES a reference, and fn_arg is a child of our own
* arg vector, which still points at it — lend it a ref of our own
* making rather than the arg vector's (#720). */
- /* #1665: a numeric fn_arg is a stack view in THIS frame. A builtin
- * that returns its argument (`num`) would hand our caller a pointer
- * into a dead frame, so a number goes in as a fresh box we own. */
- if (fn_arg && fn_arg->type == VAL_NUM) {
- Value *boxed = make_num(eigs_num_arg(fn_arg, __func__));
+ /* #1665: an immediate numeric fn_arg is a stack view in THIS frame.
+ * Lift it before calling a builtin that returns its argument (`num`).
+ * HEAP numbers stay shared and follow the direct-child borrow rule. */
+ if (fn_arg && val_is_number(fn_arg)) {
+ Value *boxed = list_get_owned(arg, 2);
Value *res = eigs_call_builtin(fn->data.builtin, boxed);
- if (res == boxed) return boxed; /* our ref transfers */
+ if (res == boxed) {
+ /* A HEAP number can alias our direct child. Return that
+ * borrow for the outer scan; only fresh boxes transfer. */
+ if (boxed == fn_arg) val_decref(boxed);
+ return res;
+ }
val_decref(boxed);
return res ? res : make_null();
}
@@ -6211,21 +6437,21 @@ Value* builtin_dispatch(Value *arg) {
* the result still respects EigenScript's no-NaN/Inf invariant. */
static Value* builtin_dot(Value *arg) {
STRICT_LIST_MAX(arg, 2, "dot");
- ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "dot", "[a, b]", make_num(0));
+ ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "dot", "[a, b]", make_float(0));
Value *a = BUILTIN_LIST_VIEW(arg, 0);
Value *b = BUILTIN_LIST_VIEW(arg, 1);
- ARG_GUARD(!a || !b || a->type != VAL_BUFFER || b->type != VAL_BUFFER, "dot", "two buffers", make_num(0));
+ ARG_GUARD(!a || !b || a->type != VAL_BUFFER || b->type != VAL_BUFFER, "dot", "two buffers", make_float(0));
int n = a->data.buffer.count;
if (b->data.buffer.count < n) n = b->data.buffer.count;
double s = 0.0;
for (int i = 0; i < n; i++) {
- double av = buffer_read_num(a, i);
+ double av = buffer_read_f64(a, i);
if (g_has_error) return make_null();
- double bv = buffer_read_num(b, i);
+ double bv = buffer_read_f64(b, i);
if (g_has_error) return make_null();
s = num_guard(s + num_guard(av * bv));
}
- return make_num(s);
+ return make_float(s);
}
#if EIGS_BORROW_GUARD
@@ -6246,7 +6472,7 @@ static Value* builtin_dot(Value *arg) {
* BA_POS(i) is element i of the argument list (`f of [x, true]` is position
* 1; a one-argument builtin given a list sees the list's elements here).
* The gate scans the first EIGS_BOOL_GATE_SCAN positions; beyond that the
- * builtin's own typed reads (eigs_num_arg family, ARG_GUARD) decide.
+ * builtin's own typed readers and ARG_GUARD decide.
* tests/test_bool_fuzz.sh is the oracle: it puts true and false in every
* slot of every builtin and checks this table against its own reviewed
* any-value list (tests/bool_fuzz_anyvalue.txt). */
@@ -6259,6 +6485,7 @@ static const struct { const char *name; unsigned mask; const char *why; } k_bool
{"write", BA_ANY, "renders any value"},
{"str", BA_ANY, "converts any value to text (\"true\"/\"false\")"},
{"type", BA_ANY, "names any value's type"},
+ {"num_type", BA_ANY, "names a numeric kind or returns null for any other value"},
{"json_encode", BA_ANY, "JSON has true/false"},
{"observe", BA_ANY, "the observer measures bools (entropy 0/1)"},
{"classify", BA_ANY, "the observer classifies any value"},
@@ -6375,6 +6602,85 @@ int eigs_bool_gate_slow(BuiltinFn fn, const Value *arg) {
void register_builtins(Env *env) {
/* ---- Core language builtins (always available) ---- */
+ /* #1758 N9: width operations and unsigned bit-pattern utilities. */
+ env_set_local_owned(env, "i8_wrapping_add", make_builtin(builtin_i8_wrapping_add));
+ env_set_local_owned(env, "i8_wrapping_sub", make_builtin(builtin_i8_wrapping_sub));
+ env_set_local_owned(env, "i8_wrapping_mul", make_builtin(builtin_i8_wrapping_mul));
+ env_set_local_owned(env, "i8_checked_add", make_builtin(builtin_i8_checked_add));
+ env_set_local_owned(env, "i8_checked_sub", make_builtin(builtin_i8_checked_sub));
+ env_set_local_owned(env, "i8_checked_mul", make_builtin(builtin_i8_checked_mul));
+ env_set_local_owned(env, "i8_saturating_add", make_builtin(builtin_i8_saturating_add));
+ env_set_local_owned(env, "i8_saturating_sub", make_builtin(builtin_i8_saturating_sub));
+ env_set_local_owned(env, "i8_saturating_mul", make_builtin(builtin_i8_saturating_mul));
+ env_set_local_owned(env, "i16_wrapping_add", make_builtin(builtin_i16_wrapping_add));
+ env_set_local_owned(env, "i16_wrapping_sub", make_builtin(builtin_i16_wrapping_sub));
+ env_set_local_owned(env, "i16_wrapping_mul", make_builtin(builtin_i16_wrapping_mul));
+ env_set_local_owned(env, "i16_checked_add", make_builtin(builtin_i16_checked_add));
+ env_set_local_owned(env, "i16_checked_sub", make_builtin(builtin_i16_checked_sub));
+ env_set_local_owned(env, "i16_checked_mul", make_builtin(builtin_i16_checked_mul));
+ env_set_local_owned(env, "i16_saturating_add", make_builtin(builtin_i16_saturating_add));
+ env_set_local_owned(env, "i16_saturating_sub", make_builtin(builtin_i16_saturating_sub));
+ env_set_local_owned(env, "i16_saturating_mul", make_builtin(builtin_i16_saturating_mul));
+ env_set_local_owned(env, "i32_wrapping_add", make_builtin(builtin_i32_wrapping_add));
+ env_set_local_owned(env, "i32_wrapping_sub", make_builtin(builtin_i32_wrapping_sub));
+ env_set_local_owned(env, "i32_wrapping_mul", make_builtin(builtin_i32_wrapping_mul));
+ env_set_local_owned(env, "i32_checked_add", make_builtin(builtin_i32_checked_add));
+ env_set_local_owned(env, "i32_checked_sub", make_builtin(builtin_i32_checked_sub));
+ env_set_local_owned(env, "i32_checked_mul", make_builtin(builtin_i32_checked_mul));
+ env_set_local_owned(env, "i32_saturating_add", make_builtin(builtin_i32_saturating_add));
+ env_set_local_owned(env, "i32_saturating_sub", make_builtin(builtin_i32_saturating_sub));
+ env_set_local_owned(env, "i32_saturating_mul", make_builtin(builtin_i32_saturating_mul));
+ env_set_local_owned(env, "i64_wrapping_add", make_builtin(builtin_i64_wrapping_add));
+ env_set_local_owned(env, "i64_wrapping_sub", make_builtin(builtin_i64_wrapping_sub));
+ env_set_local_owned(env, "i64_wrapping_mul", make_builtin(builtin_i64_wrapping_mul));
+ env_set_local_owned(env, "i64_checked_add", make_builtin(builtin_i64_checked_add));
+ env_set_local_owned(env, "i64_checked_sub", make_builtin(builtin_i64_checked_sub));
+ env_set_local_owned(env, "i64_checked_mul", make_builtin(builtin_i64_checked_mul));
+ env_set_local_owned(env, "i64_saturating_add", make_builtin(builtin_i64_saturating_add));
+ env_set_local_owned(env, "i64_saturating_sub", make_builtin(builtin_i64_saturating_sub));
+ env_set_local_owned(env, "i64_saturating_mul", make_builtin(builtin_i64_saturating_mul));
+ env_set_local_owned(env, "u8_wrapping_add", make_builtin(builtin_u8_wrapping_add));
+ env_set_local_owned(env, "u8_wrapping_sub", make_builtin(builtin_u8_wrapping_sub));
+ env_set_local_owned(env, "u8_wrapping_mul", make_builtin(builtin_u8_wrapping_mul));
+ env_set_local_owned(env, "u8_checked_add", make_builtin(builtin_u8_checked_add));
+ env_set_local_owned(env, "u8_checked_sub", make_builtin(builtin_u8_checked_sub));
+ env_set_local_owned(env, "u8_checked_mul", make_builtin(builtin_u8_checked_mul));
+ env_set_local_owned(env, "u8_saturating_add", make_builtin(builtin_u8_saturating_add));
+ env_set_local_owned(env, "u8_saturating_sub", make_builtin(builtin_u8_saturating_sub));
+ env_set_local_owned(env, "u8_saturating_mul", make_builtin(builtin_u8_saturating_mul));
+ env_set_local_owned(env, "u16_wrapping_add", make_builtin(builtin_u16_wrapping_add));
+ env_set_local_owned(env, "u16_wrapping_sub", make_builtin(builtin_u16_wrapping_sub));
+ env_set_local_owned(env, "u16_wrapping_mul", make_builtin(builtin_u16_wrapping_mul));
+ env_set_local_owned(env, "u16_checked_add", make_builtin(builtin_u16_checked_add));
+ env_set_local_owned(env, "u16_checked_sub", make_builtin(builtin_u16_checked_sub));
+ env_set_local_owned(env, "u16_checked_mul", make_builtin(builtin_u16_checked_mul));
+ env_set_local_owned(env, "u16_saturating_add", make_builtin(builtin_u16_saturating_add));
+ env_set_local_owned(env, "u16_saturating_sub", make_builtin(builtin_u16_saturating_sub));
+ env_set_local_owned(env, "u16_saturating_mul", make_builtin(builtin_u16_saturating_mul));
+ env_set_local_owned(env, "u32_wrapping_add", make_builtin(builtin_u32_wrapping_add));
+ env_set_local_owned(env, "u32_wrapping_sub", make_builtin(builtin_u32_wrapping_sub));
+ env_set_local_owned(env, "u32_wrapping_mul", make_builtin(builtin_u32_wrapping_mul));
+ env_set_local_owned(env, "u32_checked_add", make_builtin(builtin_u32_checked_add));
+ env_set_local_owned(env, "u32_checked_sub", make_builtin(builtin_u32_checked_sub));
+ env_set_local_owned(env, "u32_checked_mul", make_builtin(builtin_u32_checked_mul));
+ env_set_local_owned(env, "u32_saturating_add", make_builtin(builtin_u32_saturating_add));
+ env_set_local_owned(env, "u32_saturating_sub", make_builtin(builtin_u32_saturating_sub));
+ env_set_local_owned(env, "u32_saturating_mul", make_builtin(builtin_u32_saturating_mul));
+ env_set_local_owned(env, "u64_wrapping_add", make_builtin(builtin_u64_wrapping_add));
+ env_set_local_owned(env, "u64_wrapping_sub", make_builtin(builtin_u64_wrapping_sub));
+ env_set_local_owned(env, "u64_wrapping_mul", make_builtin(builtin_u64_wrapping_mul));
+ env_set_local_owned(env, "u64_checked_add", make_builtin(builtin_u64_checked_add));
+ env_set_local_owned(env, "u64_checked_sub", make_builtin(builtin_u64_checked_sub));
+ env_set_local_owned(env, "u64_checked_mul", make_builtin(builtin_u64_checked_mul));
+ env_set_local_owned(env, "u64_saturating_add", make_builtin(builtin_u64_saturating_add));
+ env_set_local_owned(env, "u64_saturating_sub", make_builtin(builtin_u64_saturating_sub));
+ env_set_local_owned(env, "u64_saturating_mul", make_builtin(builtin_u64_saturating_mul));
+ env_set_local_owned(env, "u64_compare", make_builtin(builtin_u64_compare));
+ env_set_local_owned(env, "u64_div", make_builtin(builtin_u64_div));
+ env_set_local_owned(env, "u64_rem", make_builtin(builtin_u64_rem));
+ env_set_local_owned(env, "u64_to_string", make_builtin(builtin_u64_to_string));
+ env_set_local_owned(env, "fround", make_builtin(builtin_fround));
+
env_set_local_owned(env, "print", make_builtin(builtin_print));
env_set_local_owned(env, "write", make_builtin(builtin_write));
env_set_local_owned(env, "flush", make_builtin(builtin_flush));
@@ -6413,6 +6719,7 @@ void register_builtins(Env *env) {
env_set_local_owned(env, "observe", make_builtin(builtin_observe));
env_set_local_owned(env, "classify", make_builtin(builtin_classify));
env_set_local_owned(env, "type", make_builtin(builtin_type));
+ env_set_local_owned(env, "num_type", make_builtin(builtin_num_type));
env_set_local_owned(env, "math_flags", make_builtin(builtin_math_flags)); /* #865 */
env_set_local_owned(env, "json_encode", make_builtin(builtin_json_encode));
env_set_local_owned(env, "json_decode", make_builtin(builtin_json_decode));
diff --git a/src/builtins_buf.c b/src/builtins_buf.c
index c7b1a33d1..c4eb2ef9c 100644
--- a/src/builtins_buf.c
+++ b/src/builtins_buf.c
@@ -32,10 +32,12 @@
Value* builtin_buffer(Value *arg) {
/* buffer of [rows, cols] -> shaped 2-D buffer (flat double[rows*cols]) */
if (arg && arg->type == VAL_LIST && arg->data.list.count == 2 &&
- list_elem_type(arg, 0) == VAL_NUM &&
- list_elem_type(arg, 1) == VAL_NUM) {
- int r = (int)eigs_list_num(arg, 0, __func__);
- int c = (int)eigs_list_num(arg, 1, __func__);
+ (list_elem_type(arg, 0) == VAL_FLOAT || list_elem_type(arg, 0) == VAL_INT) &&
+ (list_elem_type(arg, 1) == VAL_FLOAT || list_elem_type(arg, 1) == VAL_INT)) {
+ int64_t r64, c64;
+ if (!eigs_list_int_range(arg, 0, INT_MIN, INT_MAX, &r64, "buffer") ||
+ !eigs_list_int_range(arg, 1, INT_MIN, INT_MAX, &c64, "buffer")) return make_null();
+ int r = (int)r64, c = (int)c64;
if (r < 0) r = 0;
if (c < 0) c = 0;
long total = (long)r * (long)c;
@@ -53,8 +55,12 @@ Value* builtin_buffer(Value *arg) {
int count = 0;
/* #971 Phase D: a non-number size (or a malformed [rows, cols]) made an
* EMPTY buffer — a plausible object with nothing in it. */
- STRICT_REQUIRE(!arg || arg->type != VAL_NUM, "buffer", "a size or [rows, cols]");
- if (arg && arg->type == VAL_NUM) count = (int)eigs_num_arg(arg, __func__);
+ STRICT_REQUIRE(!arg || !val_is_number(arg), "buffer", "a size or [rows, cols]");
+ if (arg && val_is_number(arg)) {
+ int64_t n;
+ if (!eigs_int_arg_range(arg, INT_MIN, INT_MAX, &n, "buffer")) return make_null();
+ count = (int)n;
+ }
if (count < 0) count = 0;
if (count > EIGS_TENSOR_MAX_ELEMENTS) count = EIGS_TENSOR_MAX_ELEMENTS;
if (!sandbox_charge((size_t)count * sizeof(double))) return make_null(); /* #292 */
@@ -75,10 +81,12 @@ Value* builtin_reshape(Value *arg) {
Value *b = list_get_view(arg, 0, &b_view);
if (b->type != VAL_BUFFER) return make_null();
BOOL_REFUSE(arg, "reshape"); /* rows/cols; the buffer is not a bool */
- if (list_elem_type(arg, 1) != VAL_NUM ||
- list_elem_type(arg, 2) != VAL_NUM) return make_null();
- int r = (int)eigs_list_num(arg, 1, __func__);
- int c = (int)eigs_list_num(arg, 2, __func__);
+ if ((list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT) ||
+ (list_elem_type(arg, 2) != VAL_FLOAT && list_elem_type(arg, 2) != VAL_INT)) return make_null();
+ int64_t r64, c64;
+ if (!eigs_list_int_range(arg, 1, INT_MIN, INT_MAX, &r64, "reshape") ||
+ !eigs_list_int_range(arg, 2, INT_MIN, INT_MAX, &c64, "reshape")) return make_null();
+ int r = (int)r64, c = (int)c64;
if (r < 0 || c < 0 || (long)r * (long)c != (long)b->data.buffer.count) return make_null();
/* Same buffer chokepoint as buf_from_list — reshape copies the payload. */
if (!sandbox_charge((b->data.buffer.count > 0 ? (size_t)b->data.buffer.count : 1) * sizeof(double)))
@@ -102,24 +110,24 @@ Value* builtin_buf_get(Value *arg) {
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) {
rt_error(EK_TYPE, 0, "buf_get requires [buffer, index]");
/* fs:CHANNEL the rt_error above already raised */
- return make_num(0);
+ return make_float(0);
}
EIGS_VIEW(buf_view);
Value *buf = list_get_view(arg, 0, &buf_view);
if (!buf || buf->type != VAL_BUFFER) {
rt_error(EK_TYPE, 0, "buf_get: first argument must be a buffer");
/* fs:CHANNEL the rt_error above already raised */
- return make_num(0);
+ return make_float(0);
}
- int idx = (int)eigs_list_num(arg, 1, __func__);
- if (g_has_error) return make_num(0); /* fs:CHANNEL #1637: a non-number index raised */
+ int64_t idx = eigs_list_int(arg, 1, "buf_get");
+ if (g_has_error) return make_float(0); /* fs:CHANNEL #1637: a non-number index raised */
if (idx < 0 || idx >= buf->data.buffer.count) {
- rt_error(EK_INDEX, 0, "buffer index %d out of range (length %d)",
- idx, buf->data.buffer.count);
+ rt_error(EK_INDEX, 0, "buffer index %lld out of range (length %d)",
+ (long long)idx, buf->data.buffer.count);
/* fs:CHANNEL the EK_INDEX rt_error above already raised (#502) */
- return make_num(0);
+ return make_float(0);
}
- return make_num(buffer_read_num(buf, idx));
+ return make_float(buffer_read_f64(buf, idx));
}
/* buf_set of [buf, index, value] — O(1) indexed write */
@@ -138,20 +146,20 @@ Value* builtin_buf_set(Value *arg) {
/* #1061: both operands were read through the num union member unchecked
* -- a string index or value read garbage bits (the #1007 type-pun class).
* Loud, like the `b[i] is v` opcode path. */
- if (list_elem_type(arg, 1) != VAL_NUM) {
+ if ((list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT)) {
rt_error(EK_TYPE, 0, "buf_set: index must be a number, got %s", val_type_name(list_elem_type(arg, 1)));
return make_null();
}
- if (list_elem_type(arg, 2) != VAL_NUM) {
+ if ((list_elem_type(arg, 2) != VAL_FLOAT && list_elem_type(arg, 2) != VAL_INT)) {
rt_error(EK_TYPE, 0, "cannot store %s in a buffer (buffers hold numbers)", val_type_name(list_elem_type(arg, 2)));
return make_null();
}
- int idx = (int)eigs_list_num(arg, 1, __func__);
- double val = eigs_list_num(arg, 2, __func__);
+ int64_t idx = eigs_list_int(arg, 1, "buf_set");
+ double val = eigs_list_float(arg, 2, "buf_set");
if (g_has_error) return make_null(); /* #1637: a non-number index raised; nothing written */
if (idx < 0 || idx >= buf->data.buffer.count) {
- rt_error(EK_INDEX, 0, "buffer index %d out of range (length %d)",
- idx, buf->data.buffer.count);
+ rt_error(EK_INDEX, 0, "buffer index %lld out of range (length %d)",
+ (long long)idx, buf->data.buffer.count);
return make_null();
}
buf->data.buffer.data[idx] = val;
@@ -160,8 +168,8 @@ Value* builtin_buf_set(Value *arg) {
/* buf_len of buf — return buffer length */
Value* builtin_buf_len(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_BUFFER, "buf_len", "a buffer", make_num(0));
- return make_num(arg->data.buffer.count);
+ ARG_GUARD(!arg || arg->type != VAL_BUFFER, "buf_len", "a buffer", make_int(0));
+ return make_int(arg->data.buffer.count);
}
/* buf_from_list of list — convert list of numbers to buffer */
@@ -181,8 +189,9 @@ Value* builtin_buf_from_list(Value *arg) {
v->data.buffer.data = xcalloc(n > 0 ? n : 1, sizeof(double));
v->refcount = 1;
for (int i = 0; i < n; i++) {
- if (list_elem_type(arg, i) == VAL_NUM) {
- v->data.buffer.data[i] = eigs_slot_num(list_slot(arg, i), __func__);
+ if ((list_elem_type(arg, i) == VAL_FLOAT || list_elem_type(arg, i) == VAL_INT)) {
+ v->data.buffer.data[i] = eigs_slot_float(list_slot(arg, i), "buf_from_list");
+ if (g_has_error) { val_decref(v); return make_null(); }
} else {
/* #1061: a non-number element silently stayed 0.0. */
const char *tn = val_type_name(list_elem_type(arg, i));
@@ -203,15 +212,19 @@ static int strict_numeric_byte_list(Value *arg, const char *who, int nul_ends) {
EIGS_VIEW(item_view);
Value *item = list_get_view(arg, i, &item_view);
/* #1637: a bool byte is refused in every strict mode. */
- if (!item || (item->type != VAL_NUM && (g_strict || item->type == VAL_BOOL))) {
+ if (!item || (!val_is_number(item) && (g_strict || item->type == VAL_BOOL))) {
rt_error(EK_TYPE, 0, "%s: expected numeric byte values", who);
return 0;
}
/* EIGS_STRICT=0 legacy: a non-number element (a bool never reaches
* here) ends the string as a NUL would -- the documented truncation
* the compatibility mode keeps. It is never read as a number. */
- if (nul_ends && (item->type != VAL_NUM
- || finite_num_to_byte(eigs_num_arg(item, __func__)) == 0)) break;
+ if (nul_ends && !val_is_number(item)) break;
+ if (val_is_number(item)) {
+ uint8_t byte = eigs_byte_arg(item, who);
+ if (g_has_error) return 0;
+ if (nul_ends && byte == 0) break;
+ }
}
return 1;
}
@@ -235,11 +248,13 @@ Value* builtin_str_from_bytes(Value *arg) {
char *s = xcalloc((size_t)(n > 0 ? n : 0) + 1, 1);
int len = 0;
for (int i = 0; i < n; i++) {
- double dv = arg->type == VAL_LIST
- ? (list_elem_type(arg, i) == VAL_NUM ? eigs_slot_num(list_slot(arg, i), __func__) : 0.0)
- : buffer_read_num(arg, i);
+ uint8_t b = 0;
+ if (arg->type == VAL_BUFFER) {
+ if (!buffer_byte_at(arg, i, &b, "str_from_bytes")) { free(s); return make_null(); }
+ } else if (list_elem_type(arg, i) == VAL_FLOAT || list_elem_type(arg, i) == VAL_INT) {
+ b = eigs_slot_byte(list_slot(arg, i), "str_from_bytes");
+ }
if (g_has_error) { free(s); return make_null(); }
- int b = finite_num_to_byte(dv);
if (b == 0) break; /* C-string terminates at NUL */
s[len++] = (char)b;
}
@@ -257,14 +272,15 @@ Value* builtin_str_from_bytes(Value *arg) {
* explicit shifts, yielding the standard IEEE-754 layout on any platform. */
Value* builtin_f64_to_bytes(Value *arg) {
/* #971 Phase D: a non-number encoded as 0.0's eight bytes. */
- STRICT_REQUIRE(!arg || arg->type != VAL_NUM, "f64_to_bytes", "a number");
- double d = (arg && arg->type == VAL_NUM) ? eigs_num_arg(arg, __func__) : 0.0;
+ STRICT_REQUIRE(!arg || !val_is_number(arg), "f64_to_bytes", "a number");
+ double d = (arg && val_is_number(arg)) ? eigs_float_arg(arg, "f64_to_bytes") : 0.0;
+ if (g_has_error) return make_null();
uint64_t bits;
memcpy(&bits, &d, sizeof(bits));
Value *list = make_list(8);
for (int i = 0; i < 8; i++) {
int shift = 8 * (7 - i);
- list_append_owned(list, make_num((double)((bits >> shift) & 0xFFu)));
+ list_append_owned(list, make_int((int64_t)((bits >> shift) & 0xFFu)));
}
return list;
}
@@ -272,16 +288,17 @@ Value* builtin_f64_to_bytes(Value *arg) {
/* f64_from_bytes of → the decoded double.
* Inverse of f64_to_bytes; reads exactly the first 8 bytes. */
Value* builtin_f64_from_bytes(Value *arg) {
- double bytes_in[8] = {0,0,0,0,0,0,0,0};
+ uint8_t bytes_in[8] = {0,0,0,0,0,0,0,0};
if (arg && arg->type == VAL_LIST) {
int n = arg->data.list.count;
if (n < 8) {
rt_error(EK_VALUE, 0, "f64_from_bytes: needs 8 bytes, got %d", n);
return make_null();
}
- for (int i = 0; i < 8; i++)
- if (list_elem_type(arg, i) == VAL_NUM)
- bytes_in[i] = eigs_slot_num(list_slot(arg, i), __func__);
+ for (int i = 0; i < 8; i++) {
+ bytes_in[i] = eigs_slot_elem_byte(list_slot(arg, i), "f64_from_bytes");
+ if (g_has_error) return make_null();
+ }
} else if (arg && arg->type == VAL_BUFFER) {
int n = arg->data.buffer.count;
if (n < 8) {
@@ -289,39 +306,37 @@ Value* builtin_f64_from_bytes(Value *arg) {
return make_null();
}
for (int i = 0; i < 8; i++) {
- bytes_in[i] = buffer_read_num(arg, i);
+ if (!buffer_byte_at(arg, i, &bytes_in[i], "f64_from_bytes")) return make_null();
if (g_has_error) return make_null();
}
} else {
- ARG_GUARD(1, "f64_from_bytes", "a list or buffer of 8 byte values", make_num(0));
+ ARG_GUARD(1, "f64_from_bytes", "a list or buffer of 8 byte values", make_float(0));
}
uint64_t bits = 0;
for (int i = 0; i < 8; i++)
- bits = (bits << 8) | (uint64_t)finite_num_to_byte(bytes_in[i]);
+ bits = (bits << 8) | (uint64_t)bytes_in[i];
double d;
memcpy(&d, &bits, sizeof(d));
/* #971: eight arbitrary bytes can spell a NaN; collapse (default) or
* raise (strict) under this builtin's own name. */
- return make_num(num_guard_named(d, "f64_from_bytes"));
+ return make_float(num_guard_named(d, "f64_from_bytes"));
}
/* ---- DEFLATE codec implementation (#684) ----
* Thin wrappers over the system zlib (-lz), gated behind
* EIGENSCRIPT_EXT_ZLIB — the same EIGENSCRIPT_EXT_* mechanism the http
* variant uses. Default OFF so the minimal build stays zero-dependency;
- * compiled without zlib the four names stay registered but raise a
+ * compiled without zlib the name stays registered but raises a
* catchable runtime error, so a script can feature-detect with
* try/catch instead of dying on "undefined variable".
*
* Byte representation mirrors read_bytes/write_bytes exactly: input is
- * a list of ints 0-255 (values taken mod 256) or a VAL_BUFFER; output is
+ * a list of exact bytes 0-255 or a VAL_BUFFER with exact byte cells; output is
* always a fresh list of ints 0-255. Strict mode rejects nonnumeric list
* elements; compatibility mode retains their numeric-zero conversion.
*
* The raw codec uses windowBits -15 — the ZIP member format, so
- * .xlsx/.ods entries are readable. The wrapped implementation uses windowBits
- * 15+32, which auto-detects zlib AND gzip headers — that is what makes
- * plain .gz files readable.
+ * .xlsx/.ods entries are readable.
*/
#if EIGENSCRIPT_EXT_ZLIB
@@ -332,7 +347,7 @@ Value* builtin_f64_from_bytes(Value *arg) {
* sandbox_run default allocation budget. */
#define EIGS_INFLATE_MAX_OUT ((unsigned long)256 * 1024 * 1024)
-/* Shared argument extraction for the four codecs: accept the byte
+/* Argument extraction for inflate: accept the byte
* representations write_bytes accepts and copy them into a malloc'd
* byte array. Returns 1 on success; on a wrong-shape argument raises
* `type` and returns 0. */
@@ -354,11 +369,14 @@ static int zlib_bytes_arg(Value *arg, const char *who,
if (!strict_numeric_byte_list(arg, who, 0)) return 0;
unsigned char *b = xmalloc((size_t)(n > 0 ? n : 1));
for (int i = 0; i < n; i++) {
- double dv = arg->type == VAL_LIST
- ? (list_elem_type(arg, i) == VAL_NUM ? eigs_slot_num(list_slot(arg, i), __func__) : 0.0)
- : buffer_read_num(arg, i);
+ uint8_t byte = 0;
+ if (arg->type == VAL_BUFFER) {
+ if (!buffer_byte_at(arg, i, &byte, who)) { free(b); return 0; }
+ } else if (list_elem_type(arg, i) == VAL_FLOAT || list_elem_type(arg, i) == VAL_INT) {
+ byte = eigs_slot_byte(list_slot(arg, i), who);
+ }
if (g_has_error) { free(b); return 0; }
- b[i] = finite_num_to_byte(dv);
+ b[i] = byte;
}
*out = b;
*out_n = (size_t)n;
@@ -379,7 +397,7 @@ static Value *zlib_bytes_result(const unsigned char *buf, unsigned long n) {
return make_null();
Value *result = make_list((int)n);
for (unsigned long i = 0; i < n; i++)
- list_append_owned(result, make_num((double)buf[i]));
+ list_append_owned(result, make_int(buf[i]));
return result;
}
@@ -470,8 +488,7 @@ static Value *zlib_inflate_impl(const char *who, int window_bits, Value *arg) {
return result;
}
-/* Shared compression core (dual of the decompression implementation). The output buffer is
- * deflateBound-sized up front, so a single Z_FINISH pass always fits. */
+/* Raw DEFLATE entry point. */
Value* builtin_inflate(Value *arg) { return zlib_inflate_impl("inflate", -15, arg); }
#else /* !EIGENSCRIPT_EXT_ZLIB */
@@ -498,11 +515,12 @@ Value* builtin_inflate(Value *arg) { (void)arg; return zlib_unavailable("in
* #490-#512 direction (buf_get/buf_set/set_at) — no silent truncation:
* a clamped audio mix is a silently wrong render. */
static int buf_count_arg(const char *who, EigsSlot cnt, long long *out) {
- if (slot_type(cnt) != VAL_NUM) {
+ if ((slot_type(cnt) != VAL_FLOAT && slot_type(cnt) != VAL_INT)) {
rt_error(EK_VALUE, 0, "%s: count must be a number", who);
return 0;
}
- long long c = (long long)eigs_slot_num(cnt, __func__);
+ long long c = eigs_slot_int(cnt, who);
+ if (g_has_error) return 0;
if (c < 0) {
rt_error(EK_VALUE, 0, "%s: count must be non-negative (got %lld)",
who, c);
@@ -512,14 +530,14 @@ static int buf_count_arg(const char *who, EigsSlot cnt, long long *out) {
return 1;
}
-static int buf_num_arg(const char *who, const char *what, EigsSlot s,
+static int buf_float_arg(const char *who, const char *what, EigsSlot s,
double *out) {
- if (slot_type(s) != VAL_NUM) {
+ if ((slot_type(s) != VAL_FLOAT && slot_type(s) != VAL_INT)) {
rt_error(EK_VALUE, 0, "%s: %s must be a number", who, what);
return 0;
}
- *out = eigs_slot_num(s, __func__);
- return 1;
+ *out = eigs_slot_float(s, who);
+ return !g_has_error;
}
/* Validate one (buffer, offset, count) window. On success writes the
@@ -531,16 +549,17 @@ static int buf_window_arg(const char *who, Value *buf, EigsSlot off,
rt_error(EK_TYPE, 0, "%s: expected a buffer", who);
return 0;
}
- if (slot_type(off) != VAL_NUM) {
+ if ((slot_type(off) != VAL_FLOAT && slot_type(off) != VAL_INT)) {
rt_error(EK_VALUE, 0, "%s: offset must be a number", who);
return 0;
}
- long long offset = (long long)eigs_slot_num(off, __func__);
+ long long offset = eigs_slot_int(off, who);
+ if (g_has_error) return 0;
long long n = buf->data.buffer.count;
if (offset < 0 || offset > n - count) {
rt_error(EK_INDEX, 0,
- "%s: window [%lld, %lld) out of range (length %lld)",
- who, offset, offset + count, n);
+ "%s: window offset %lld count %lld out of range (length %lld)",
+ who, offset, count, n);
return 0;
}
*out_off = offset;
@@ -595,7 +614,7 @@ Value* builtin_buf_mix(Value *arg) {
if (!buf_count_arg("buf_mix", list_slot(arg, 4), &count) ||
!buf_window_arg("buf_mix", dst, list_slot(arg, 2), count, &dst_off) ||
!buf_window_arg("buf_mix", src, list_slot(arg, 3), count, &src_off) ||
- !buf_num_arg("buf_mix", "gain", list_slot(arg, 5), &gain))
+ !buf_float_arg("buf_mix", "gain", list_slot(arg, 5), &gain))
return make_null();
double *dd = &dst->data.buffer.data[dst_off];
double *sd = &src->data.buffer.data[src_off];
@@ -619,7 +638,7 @@ Value* builtin_buf_scale_range(Value *arg) {
double gain;
if (!buf_count_arg("buf_scale_range", list_slot(arg, 2), &count) ||
!buf_window_arg("buf_scale_range", buf, list_slot(arg, 1), count, &off) ||
- !buf_num_arg("buf_scale_range", "gain", list_slot(arg, 3), &gain))
+ !buf_float_arg("buf_scale_range", "gain", list_slot(arg, 3), &gain))
return make_null();
double *d = &buf->data.buffer.data[off];
for (long long i = 0; i < count; i++)
@@ -642,7 +661,7 @@ Value* builtin_buf_fill(Value *arg) {
double val;
if (!buf_count_arg("buf_fill", list_slot(arg, 2), &count) ||
!buf_window_arg("buf_fill", buf, list_slot(arg, 1), count, &off) ||
- !buf_num_arg("buf_fill", "value", list_slot(arg, 3), &val))
+ !buf_float_arg("buf_fill", "value", list_slot(arg, 3), &val))
return make_null();
double *d = &buf->data.buffer.data[off];
for (long long i = 0; i < count; i++)
@@ -657,7 +676,7 @@ Value* builtin_buf_peak(Value *arg) {
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) {
rt_error(EK_TYPE, 0, "buf_peak requires [buffer, off, count]");
/* fs:CHANNEL the rt_error above already raised */
- return make_num(0);
+ return make_float(0);
}
EIGS_VIEW(buf_view);
Value *buf = list_get_view(arg, 0, &buf_view);
@@ -665,15 +684,15 @@ Value* builtin_buf_peak(Value *arg) {
if (!buf_count_arg("buf_peak", list_slot(arg, 2), &count) ||
!buf_window_arg("buf_peak", buf, list_slot(arg, 1), count, &off))
/* fs:CHANNEL buf_count_arg/buf_window_arg raise before returning 0 */
- return make_num(0);
+ return make_float(0);
double m = 0.0;
for (long long i = 0; i < count; i++) {
- double x = buffer_read_num(buf, off + i);
+ double x = buffer_read_f64(buf, off + i);
if (g_has_error) return make_null();
double a = x < 0 ? -x : x;
if (a > m) m = a;
}
- return make_num(m);
+ return make_float(m);
}
/* buf_dot of [a, b, a_off, b_off, count] — windowed dot product:
@@ -687,7 +706,7 @@ Value* builtin_buf_dot(Value *arg) {
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 5) {
rt_error(EK_TYPE, 0, "buf_dot requires [a, b, a_off, b_off, count]");
/* fs:CHANNEL the rt_error above already raised */
- return make_num(0);
+ return make_float(0);
}
EIGS_VIEW(a_view);
Value *a = list_get_view(arg, 0, &a_view);
@@ -698,16 +717,16 @@ Value* builtin_buf_dot(Value *arg) {
!buf_window_arg("buf_dot", a, list_slot(arg, 2), count, &a_off) ||
!buf_window_arg("buf_dot", b, list_slot(arg, 3), count, &b_off))
/* fs:CHANNEL buf_count_arg/buf_window_arg raise before returning 0 */
- return make_num(0);
+ return make_float(0);
double s = 0.0;
for (long long i = 0; i < count; i++) {
- double av = buffer_read_num(a, a_off + i);
+ double av = buffer_read_f64(a, a_off + i);
if (g_has_error) return make_null();
- double bv = buffer_read_num(b, b_off + i);
+ double bv = buffer_read_f64(b, b_off + i);
if (g_has_error) return make_null();
s = num_guard(s + num_guard(av * bv));
}
- return make_num(s);
+ return make_float(s);
}
/* ---- Bulk PCM16LE codec kernels (#602) ----
@@ -759,10 +778,11 @@ Value* builtin_buf_from_pcm16le(Value *arg) {
if (!out) return make_null();
double *od = out->data.buffer.data;
for (long long i = 0; i < count; i++) {
- double lo = buffer_read_num(src, off + 2*i);
- if (g_has_error) { val_decref(out); return make_null(); }
- double hi = buffer_read_num(src, off + 2*i + 1);
- if (g_has_error) { val_decref(out); return make_null(); }
+ uint8_t lo, hi;
+ if (!buffer_byte_at(src, off + 2*i, &lo, "buf_from_pcm16le") ||
+ !buffer_byte_at(src, off + 2*i + 1, &hi, "buf_from_pcm16le")) {
+ val_decref(out); return make_null();
+ }
double v = num_guard(lo + num_guard(256.0 * hi));
if (g_has_error) { val_decref(out); return make_null(); }
if (v >= 32768.0) v = num_guard(v - 65536.0);
@@ -799,7 +819,7 @@ Value* builtin_buf_to_pcm16le(Value *arg) {
if (!out) return make_null();
double *od = out->data.buffer.data;
for (long long i = 0; i < count; i++) {
- double x = buffer_read_num(src, off + i);
+ double x = buffer_read_f64(src, off + i);
if (g_has_error) { val_decref(out); return make_null(); }
if (x < -1.0) x = -1.0;
if (x > 1.0) x = 1.0;
@@ -832,12 +852,13 @@ Value* builtin_buf_deinterleave(Value *arg) {
Value *ch_v = list_get_view(arg, 1, &ch_v_view);
EIGS_VIEW(nch_v_view);
Value *nch_v = list_get_view(arg, 2, &nch_v_view);
- if (!ch_v || ch_v->type != VAL_NUM || !nch_v || nch_v->type != VAL_NUM) {
+ if (!ch_v || !val_is_number(ch_v) || !nch_v || !val_is_number(nch_v)) {
rt_error(EK_VALUE, 0, "buf_deinterleave: channel and nch must be numbers");
return make_null();
}
- long long nch = (long long)eigs_num_arg(nch_v, __func__);
- long long channel = (long long)eigs_num_arg(ch_v, __func__);
+ long long nch = eigs_int_arg(nch_v, "buf_deinterleave");
+ long long channel = eigs_int_arg(ch_v, "buf_deinterleave");
+ if (g_has_error) return make_null();
if (nch < 1) {
rt_error(EK_VALUE, 0, "buf_deinterleave: nch must be >= 1 (got %lld)", nch);
return make_null();
@@ -848,7 +869,7 @@ Value* builtin_buf_deinterleave(Value *arg) {
return make_null();
}
long long n = src->data.buffer.count;
- long long avail = channel < n ? (n - channel + nch - 1) / nch : 0;
+ long long avail = channel < n ? 1 + (n - 1 - channel) / nch : 0;
long long count = avail;
if (arg->data.list.count >= 4 && list_elem_type(arg, 3) != VAL_NULL) {
if (!buf_count_arg("buf_deinterleave", list_slot(arg, 3), &count))
diff --git a/src/builtins_host.c b/src/builtins_host.c
index 60ba4a6e9..230aa41a4 100644
--- a/src/builtins_host.c
+++ b/src/builtins_host.c
@@ -22,6 +22,7 @@
#include "builtins_internal.h"
#include "fsutil.h"
#include "trace.h"
+#include
#if EIGENSCRIPT_FREESTANDING
@@ -34,6 +35,13 @@ void register_host_builtins(Env *env) { (void)env; }
#include
+/* N5 followed by the range of the receiving hosted C API. */
+static int builtin_host_c_int(const Value *v, const char *who) {
+ int64_t n = 0;
+ eigs_int_arg_range(v, INT_MIN, INT_MAX, &n, who);
+ return (int)n;
+}
+
/* ==== BUILTIN: match — regex match, return list of groups ==== */
/* match of [string, pattern] -> [full_match, group1, ...] or [] */
Value* builtin_match(Value *arg) {
@@ -215,14 +223,14 @@ Value* builtin_stream_open(Value *arg) {
Value *path_val = list_get_view(arg, 0, &path_val_view);
EIGS_VIEW(count_val_view);
Value *count_val = list_get_view(arg, 1, &count_val_view);
- ARG_GUARD(!path_val || path_val->type != VAL_STR || !count_val || count_val->type != VAL_NUM,
+ ARG_GUARD(!path_val || path_val->type != VAL_STR || !count_val || !val_is_number(count_val),
"stream_open", "[a string path, a number count]", make_bool(0));
- if (eigs_num_arg(count_val, __func__) < 1 ||
- eigs_num_arg(count_val, __func__) > EIGS_TENSOR_MAX_ELEMENTS ||
- eigs_num_arg(count_val, __func__) != floor(eigs_num_arg(count_val, __func__))) {
+ int64_t requested_count = eigs_int_arg(count_val, "stream_open");
+ if (g_has_error) return make_bool(0);
+ if (requested_count < 1 || requested_count > EIGS_TENSOR_MAX_ELEMENTS) {
rt_error(EK_LIMIT, 0,
- "stream_open: '%s' count %.17g is outside the 1..%d element cap",
- path_val->data.str, eigs_num_arg(count_val, __func__),
+ "stream_open: '%s' count %lld is outside the 1..%d element cap",
+ path_val->data.str, (long long)requested_count,
EIGS_TENSOR_MAX_ELEMENTS);
return make_bool(0);
}
@@ -238,7 +246,7 @@ Value* builtin_stream_open(Value *arg) {
* comment over this section documents stream_open as answering 1 — so 0
* is the "could not open" answer, not a type mistake. */
if (!g_stream_file) return make_bool(0);
- uint32_t count = (uint32_t)eigs_num_arg(count_val, __func__);
+ uint32_t count = (uint32_t)requested_count;
uint32_t header[4] = { 1, 1, count, 0 }; /* ndim=1, rows=1, cols=count, flags=0 */
if (fwrite(header, sizeof(uint32_t), 4, g_stream_file) != 4) {
fclose(g_stream_file);
@@ -262,12 +270,13 @@ Value* builtin_stream_write(Value *arg) {
* state, and strict must not report it as a type error. Both halves still
* answer make_bool(0) with strict off, in either order, so the non-strict
* result is unchanged. */
- ARG_GUARD(!arg || arg->type != VAL_NUM, "stream_write", "a number", make_bool(0));
+ ARG_GUARD(!arg || !val_is_number(arg), "stream_write", "a number", make_bool(0));
/* fs:ANSWER no stream is open — the state, not the argument (which the
* guard above already accepted); 0 is the failure bit paired with the
* make_bool(1) success value below. */
if (!g_stream_file) return make_bool(0);
- double val = eigs_num_arg(arg, __func__);
+ double val = eigs_float_arg(arg, "stream_write");
+ if (g_has_error) return make_bool(0);
if (fwrite(&val, sizeof(double), 1, g_stream_file) != 1) {
fclose(g_stream_file);
g_stream_file = NULL;
@@ -416,11 +425,27 @@ typedef struct {
int count;
} IdentEntry;
+/* Both refusal sites belong only to build_corpus. Numeric preflight may
+ * precede replay, while malformed nonnumeric shapes retain replay's refusal. */
+static int build_corpus_replay_blocks(void) {
+ if (!__builtin_expect(g_replay_enabled, 0)) return 0;
+ rt_error(EK_IO, 0, "build_corpus: not replayable under EIGS_REPLAY "
+ "(filesystem boundary; see docs/TRACE.md)");
+ return 1;
+}
+
Value* builtin_build_corpus(Value *arg) {
STRICT_LIST_MAX(arg, 7, "build_corpus");
- if (__builtin_expect(g_replay_enabled, 0)) {
- rt_error(EK_IO, 0, "build_corpus: not replayable under EIGS_REPLAY "
- "(filesystem boundary; see docs/TRACE.md)");
+ if (__builtin_expect(g_replay_enabled, 0) &&
+ (!arg || arg->type != VAL_LIST || arg->data.list.count < 4 ||
+ list_elem_type(arg, 0) != VAL_LIST ||
+ (list_elem_type(arg, 1) != VAL_INT && list_elem_type(arg, 1) != VAL_FLOAT) ||
+ list_elem_type(arg, 2) != VAL_STR || list_elem_type(arg, 3) != VAL_STR ||
+ (arg->data.list.count >= 6 && list_elem_type(arg, 5) != VAL_NULL &&
+ list_elem_type(arg, 5) != VAL_INT && list_elem_type(arg, 5) != VAL_FLOAT) ||
+ (arg->data.list.count >= 7 && list_elem_type(arg, 6) != VAL_NULL &&
+ list_elem_type(arg, 6) != VAL_INT && list_elem_type(arg, 6) != VAL_FLOAT))) {
+ (void)build_corpus_replay_blocks();
return make_null();
}
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 4)
@@ -438,11 +463,12 @@ Value* builtin_build_corpus(Value *arg) {
if (!file_list || file_list->type != VAL_LIST) return make_null();
BOOL_REFUSE(topn_val, "build_corpus");
- if (!topn_val || topn_val->type != VAL_NUM) return make_null();
+ if (!topn_val || !val_is_number(topn_val)) return make_null();
if (!stream_path_val || stream_path_val->type != VAL_STR) return make_null();
if (!vocab_path_val || vocab_path_val->type != VAL_STR) return make_null();
- int top_n = (int)eigs_num_arg(topn_val, __func__);
+ int top_n = builtin_host_c_int(topn_val, "build_corpus");
+ if (g_has_error) return make_null();
int n_files = file_list->data.list.count;
/* ---- SLOT MODE (optional 6th arg: slot_count > 0) --------------------
@@ -473,8 +499,8 @@ Value* builtin_build_corpus(Value *arg) {
if (arg->data.list.count >= 6) {
EIGS_VIEW(sv_view);
Value *sv = list_get_view(arg, 5, &sv_view);
- double d = 0;
- if (eigs_opt_num(sv, &d, "build_corpus")) slot_count = (int)d; /* #1637: a bool raises */
+ int64_t d = 0;
+ if (eigs_opt_int_range(sv, INT_MIN, INT_MAX, &d, "build_corpus")) slot_count = (int)d;
if (g_has_error) return make_null();
}
if (slot_count < 0) slot_count = 0;
@@ -502,11 +528,12 @@ Value* builtin_build_corpus(Value *arg) {
if (arg->data.list.count >= 7) {
EIGS_VIEW(iv_view);
Value *iv = list_get_view(arg, 6, &iv_view);
- double d = 0;
- if (eigs_opt_num(iv, &d, "build_corpus")) int_count = (int)d; /* #1637: a bool raises */
+ int64_t d = 0;
+ if (eigs_opt_int_range(iv, INT_MIN, INT_MAX, &d, "build_corpus")) int_count = (int)d;
if (g_has_error) return make_null();
}
if (int_count < 0) int_count = 0;
+ if (build_corpus_replay_blocks()) return make_null();
/* Source of truth: the size of the TokType enum. Hardcoding 54 (which is
* what this was before) silently aliases TOK_LBRACKET (=54) onto the most
* common extended ident slot whenever the enum grows. */
@@ -728,13 +755,13 @@ Value* builtin_build_corpus(Value *arg) {
tid = FIRST_IDENT + actual_top + hit;
}
} else if (int_count > 0 && tid == TOK_NUM) {
- /* Give small non-negative integer literals exact tokens; the
- * region sits directly after the slots. floor==value rejects
- * 3.5 etc.; the lexer emits '-' as its own token so num_val is
- * never negative for a literal, but guard anyway. */
- double nv = tl.tokens[i].num_val;
- if (nv >= 0.0 && nv < (double)int_count && nv == (double)(long)nv)
- tid = FIRST_IDENT + actual_top + slot_count + (int)nv;
+ /* Only int literals receive the compact integer token.
+ * Preserve literal kinds: `3.0` remains TOK_NUM. Hex literals
+ * can wrap negative, so check the range before the cast. */
+ EigsNum number = tl.tokens[i].number;
+ if (number.is_int && number.value.i64 >= 0 &&
+ number.value.i64 < int_count)
+ tid = FIRST_IDENT + actual_top + slot_count + (int)number.value.i64;
}
double d = (double)tid;
fwrite(&d, sizeof(double), 1, stream_file);
@@ -833,9 +860,9 @@ Value* builtin_build_corpus(Value *arg) {
/* Return [stream_length, distinct_identifiers, files_found] */
Value *result = make_list(3);
- list_append_owned(result, make_num(stream_pos));
- list_append_owned(result, make_num(n_idents));
- list_append_owned(result, make_num(files_found));
+ list_append_owned(result, make_int(stream_pos));
+ list_append_owned(result, make_int(n_idents));
+ list_append_owned(result, make_int(files_found));
return result;
}
@@ -1196,18 +1223,16 @@ static const char *reader_cap_parse(const char *fn, Value *arg, Value **path,
if (arg->data.list.count >= 2) {
EIGS_VIEW(mv_view);
Value *mv = list_get_view(arg, 1, &mv_view);
- if (!mv || mv->type != VAL_NUM) {
+ if (!mv || !val_is_number(mv)) {
return "max_bytes to be a number";
}
- double n = eigs_num_arg(mv, __func__);
- int64_t exact;
- if (isfinite(n) && num_to_i64_exact(n, &exact) && exact < 1)
- return "max_bytes to be a positive number";
- if (!isfinite(n) || !num_to_i64_exact(n, &exact) ||
- exact > READ_BYTES_BUF_HARD_CAP) {
+ int64_t exact = eigs_int_arg(mv, fn);
+ if (g_has_error) return NULL;
+ if (exact < 1) return "max_bytes to be a positive number";
+ if (exact > READ_BYTES_BUF_HARD_CAP) {
rt_error(EK_VALUE, 0,
- "%s: max_bytes must be an exact integer in 1..%lld (got %g)",
- fn, READ_BYTES_BUF_HARD_CAP, n);
+ "%s: max_bytes must be an exact integer in 1..%lld (got %lld)",
+ fn, READ_BYTES_BUF_HARD_CAP, (long long)exact);
return NULL;
}
*cap = (long long)exact;
@@ -1223,7 +1248,7 @@ static const char *reader_cap_parse(const char *fn, Value *arg, Value **path,
static void reader_cap_raise(const char *fn, const char *path, long long len,
long long cap) {
if (__builtin_expect(g_trace_enabled, 0)) {
- Value *sz = make_num((double)len);
+ Value *sz = make_int((int64_t)len);
trace_nondet_value(fn, sz);
val_decref(sz);
}
@@ -1273,15 +1298,15 @@ static int read_stream_bounded(FILE *f, size_t cap, unsigned char **out,
}
static int replay_reader_size(const char *fn, Value *v, long long *out) {
- double n = eigs_num_arg(v, __func__);
- int64_t exact;
- if (!isfinite(n) || n < 0.0 || !num_to_i64_exact(n, &exact)) {
+ /* v8 records sizes as exact ints. A float-kind token, even an integral
+ * one, is corrupt metadata; never round it through the numeric bridge. */
+ if (!v || v->type != VAL_INT || VAL_I64_RAW(v) < 0) {
rt_error(EK_IO, 0,
"%s: replay record size is not an exact integer in range; "
"refusing to replay", fn);
return 0;
}
- *out = (long long)exact;
+ *out = (long long)VAL_I64_RAW(v);
return 1;
}
@@ -1296,7 +1321,7 @@ Value* builtin_read_bytes(Value *arg) {
ARG_GUARD_PRETAKE(*guard, "read_bytes", guard, make_null());
Value *replayed = NULL;
if (file_io_replay("read_bytes", &replayed)) {
- if (replayed->type == VAL_NUM) {
+ if (replayed->type == VAL_INT || replayed->type == VAL_FLOAT) {
long long len;
if (!replay_reader_size("read_bytes", replayed, &len)) {
val_decref(replayed);
@@ -1334,7 +1359,7 @@ Value* builtin_read_bytes(Value *arg) {
}
Value *result = make_list((int)nread);
for (size_t i = 0; i < nread; i++)
- list_append_owned(result, make_num((double)buf[i]));
+ list_append_owned(result, make_int(buf[i]));
free(buf);
TRACE_NONDET_RECORD("read_bytes", result);
}
@@ -1349,7 +1374,7 @@ Value* builtin_read_bytes(Value *arg) {
fclose(f);
Value *result = make_list((int)nread);
for (size_t i = 0; i < nread; i++)
- list_append_owned(result, make_num((double)buf[i]));
+ list_append_owned(result, make_int(buf[i]));
free(buf);
TRACE_NONDET_RECORD("read_bytes", result);
}
@@ -1382,12 +1407,12 @@ Value* builtin_read_bytes_buf(Value *arg) {
/* Tape boundary. Success records the VAL_BUFFER; unopenable file
* records an impossible-success error list. A regular-file over-cap
- * raise records its known size as a VAL_NUM; an unseekable stream records
+ * raise records its known size as a VAL_INT; an unseekable stream records
* its non-exact error message. Both replay without live filesystem I/O. */
if (__builtin_expect(g_replay_enabled, 0)) {
Value *tv = NULL;
if (file_io_replay("read_bytes_buf", &tv)) {
- if (tv && tv->type == VAL_NUM) {
+ if (tv && (tv->type == VAL_INT || tv->type == VAL_FLOAT)) {
long long len;
if (!replay_reader_size("read_bytes_buf", tv, &len)) {
val_decref(tv);
@@ -1462,7 +1487,7 @@ Value* builtin_read_text(Value *arg) {
ARG_GUARD_PRETAKE(*guard, "read_text", guard, make_str(""));
Value *replayed = NULL;
if (file_io_replay("read_text", &replayed)) {
- if (replayed->type == VAL_NUM) {
+ if (replayed->type == VAL_INT || replayed->type == VAL_FLOAT) {
long long len;
if (!replay_reader_size("read_text", replayed, &len)) {
val_decref(replayed);
@@ -1592,7 +1617,7 @@ Value* builtin_write_text(Value *arg) {
static Value* exec_capture_result(int code, const char *text) {
Value *result = make_list(2);
- list_append_num(result, code);
+ list_append_owned(result, make_int(code));
list_append_owned(result, make_str(text));
return result;
}
@@ -1600,7 +1625,7 @@ static Value* exec_capture_result(int code, const char *text) {
Value* builtin_exec_capture(Value *arg) {
/* fs:CHANNEL replay refuses the exec; the refusal belongs to the replay
* layer and [-1, ""] is exec_capture's documented "did not run". */
- /* #1008: these launder through a HELPER, so no `return make_num(-1)`
+ /* #1008: these launder through a HELPER, so no `return make_float(-1)`
* appears and the classifier's matcher — which enumerates literal soft
* returns — cannot see them at all. ARG_GUARD's soft argument is
* evaluated only on the non-strict path, so the helper call is fine
@@ -1612,19 +1637,28 @@ Value* builtin_exec_capture(Value *arg) {
* layer and [-1, ""] is exec_capture's documented "did not run". #1072:
* after the argument guard, so a wrong-typed call errs the same way in
* both modes. */
- if (replay_blocks("exec_capture", "subprocess/concurrency")) return exec_capture_result(-1, "");
/* Detect timeout form: [["cmd", ...], timeout_num] */
- double timeout_sec = -1;
+ int64_t timeout_ms = -1;
Value *cmd_list = arg;
if (arg->data.list.count == 2
&& list_elem_type(arg, 0) == VAL_LIST
- && list_elem_type(arg, 1) == VAL_NUM) {
+ && (list_elem_type(arg, 1) == VAL_FLOAT || list_elem_type(arg, 1) == VAL_INT)) {
cmd_list = list_get_ref(arg, 0);
- timeout_sec = eigs_list_num(arg, 1, __func__);
- ARG_GUARD(cmd_list->data.list.count < 1,
- "exec_capture", "a non-empty command list", exec_capture_result(-1, ""));
+ double seconds = eigs_list_float(arg, 1, "exec_capture");
+ if (g_has_error) return make_null();
+ /* The receiving API is poll(2), whose timeout is C int milliseconds.
+ * Own the scaled temporary until the borrowing magnitude reader has
+ * finished, including on its raise path. */
+ Value *milliseconds = make_float(seconds * 1000.0);
+ int ok = eigs_magnitude_arg(milliseconds, INT_MIN, INT_MAX, &timeout_ms,
+ "exec_capture");
+ val_decref(milliseconds);
+ if (!ok || g_has_error) return make_null();
}
+ if (replay_blocks("exec_capture", "subprocess/concurrency")) return exec_capture_result(-1, "");
+ ARG_GUARD(cmd_list->data.list.count < 1,
+ "exec_capture", "a non-empty command list", exec_capture_result(-1, ""));
int total = cmd_list->data.list.count;
@@ -1686,11 +1720,11 @@ Value* builtin_exec_capture(Value *arg) {
/* Compute deadline */
struct timespec deadline;
- int has_timeout = (timeout_sec >= 0);
+ int has_timeout = (timeout_ms >= 0);
if (has_timeout) {
clock_gettime(CLOCK_MONOTONIC, &deadline);
- deadline.tv_sec += (time_t)timeout_sec;
- deadline.tv_nsec += (long)((timeout_sec - (time_t)timeout_sec) * 1e9);
+ deadline.tv_sec += (time_t)(timeout_ms / 1000);
+ deadline.tv_nsec += (long)(timeout_ms % 1000) * 1000000L;
if (deadline.tv_nsec >= 1000000000L) {
deadline.tv_sec++;
deadline.tv_nsec -= 1000000000L;
@@ -1765,8 +1799,9 @@ Value* builtin_exec_capture(Value *arg) {
Value* builtin_random_hex(Value *arg) {
/* #971 Phase D: a non-number length answered "" (as does a length of 0,
* which stays the answer). Taped: the soft half records as before. */
- ARG_GUARD_TAPED(!arg || arg->type != VAL_NUM, "random_hex", "a number of hex digits", make_str(""));
- int n = (arg && arg->type == VAL_NUM) ? (int)eigs_num_arg(arg, __func__) : 0;
+ ARG_GUARD_TAPED(!arg || !val_is_number(arg), "random_hex", "a number of hex digits", make_str(""));
+ int n = builtin_host_c_int(arg, "random_hex");
+ if (g_has_error) return make_null();
if (n <= 0 || n > 256) TRACE_NONDET_RET("random_hex", make_str(""));
int bytes_needed = (n + 1) / 2;
unsigned char raw[128];
@@ -1783,7 +1818,7 @@ Value* builtin_random_hex(Value *arg) {
}
/* write_bytes of [path, data, append?] — write raw bytes to a file.
- * `data` is a list of byte ints or a buffer (values taken mod 256). `append`
+ * `data` is a list or buffer of exact bytes in 0..255 (N5/Q1). `append`
* (optional, default 0): 0 truncates the file, nonzero appends. Returns the
* number of bytes written; I/O failure raises EK_IO. Unlike write_text this is
* binary-clean — NUL bytes are written verbatim, not treated as terminators —
@@ -1792,13 +1827,13 @@ Value* builtin_random_hex(Value *arg) {
Value* builtin_write_bytes(Value *arg) {
STRICT_LIST_MAX(arg, 3, "write_bytes");
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2,
- "write_bytes", "[path, data] (optionally [path, data, append])", make_num(0));
+ "write_bytes", "[path, data] (optionally [path, data, append])", make_int(0));
EIGS_VIEW(path_val_view);
Value *path_val = list_get_view(arg, 0, &path_val_view);
EIGS_VIEW(data_view);
Value *data = list_get_view(arg, 1, &data_view);
ARG_GUARD(!path_val || path_val->type != VAL_STR,
- "write_bytes", "a string path as its first argument", make_num(0));
+ "write_bytes", "a string path as its first argument", make_int(0));
/* #1637: append is a flag -- a bool, or the older nonzero number. Any
* other value raises rather than silently meaning "truncate". */
int append = 0;
@@ -1806,8 +1841,8 @@ Value* builtin_write_bytes(Value *arg) {
EIGS_VIEW(fl_view);
Value *fl = list_get_view(arg, 2, &fl_view);
if (fl->type == VAL_BOOL) append = fl->data.boolean;
- else if (fl->type != VAL_NULL) append = (eigs_num_arg(fl, "write_bytes") != 0.0);
- if (g_has_error) return make_num(0);
+ else if (fl->type != VAL_NULL) append = (eigs_float_arg(fl, "write_bytes") != 0.0);
+ if (g_has_error) return make_int(0);
}
int n = 0;
@@ -1820,7 +1855,7 @@ Value* builtin_write_bytes(Value *arg) {
* byte ints nor a buffer, the only two forms the header documents. */
ARG_GUARD(1, "write_bytes",
"a list of byte values or a buffer as its second argument",
- make_num(0));
+ make_int(0));
}
unsigned char *out = xmalloc((size_t)(n > 0 ? n : 1));
@@ -1830,17 +1865,17 @@ Value* builtin_write_bytes(Value *arg) {
* two zero bytes and reported success. Same class as join/str_replace
* — no stand-in return, so invisible to the classifier. The free()
* matters: STRICT_REQUIRE returns, and `out` is already allocated. */
- if (data->type == VAL_LIST && list_elem_type(data, i) != VAL_NUM
+ if (data->type == VAL_LIST && (list_elem_type(data, i) != VAL_FLOAT && list_elem_type(data, i) != VAL_INT)
&& (g_strict || list_elem_type(data, i) == VAL_BOOL)) { /* #1637: a bool in every mode */
free(out);
rt_error(EK_TYPE, 0, "write_bytes: expected every element to be a number");
return make_null();
}
- double dv = data->type == VAL_LIST
- ? eigs_slot_elem_num(list_slot(data, i), __func__)
- : buffer_read_num(data, i);
+ if (data->type == VAL_LIST)
+ out[i] = eigs_slot_elem_byte(list_slot(data, i), "write_bytes");
+ else
+ buffer_byte_at(data, i, &out[i], "write_bytes");
if (g_has_error) { free(out); return make_null(); }
- out[i] = finite_num_to_byte(dv);
}
Value *replayed = NULL;
if (file_io_replay("write_bytes", &replayed)) { free(out); return replayed; }
@@ -1861,7 +1896,7 @@ Value* builtin_write_bytes(Value *arg) {
if (!close_ok || written != (size_t)n)
return file_io_error("write_bytes", path_val->data.str,
write_err ? write_err : (close_err ? close_err : EIO));
- TRACE_NONDET_RECORD("write_bytes", make_num((double)written));
+ TRACE_NONDET_RECORD("write_bytes", make_int((int64_t)written));
}
void register_host_builtins(Env *env) {
diff --git a/src/builtins_int.c b/src/builtins_int.c
new file mode 100644
index 000000000..6c301064f
--- /dev/null
+++ b/src/builtins_int.c
@@ -0,0 +1,577 @@
+/* #1758 N9: all width operations share this descriptor table and engine.
+ * N5/range validation precedes arithmetic; unsigned intermediates avoid UB. */
+#include "eigenscript.h"
+#include "container.h"
+#include "builtins_internal.h"
+#include
+#include
+#include
+
+typedef enum { N9_WRAP, N9_CHECK, N9_SAT } N9Mode;
+typedef enum { N9_ADD, N9_SUB, N9_MUL } N9Op;
+typedef struct {
+ const char *name;
+ unsigned width;
+ int is_signed;
+ N9Mode mode;
+ N9Op op;
+ int64_t lo, hi;
+} N9Spec;
+static const N9Spec n9_specs[] = {
+ {"i8_wrapping_add", 8, 1, N9_WRAP, N9_ADD, (-INT64_C(128)), INT64_C(127)},
+ {"i8_wrapping_sub", 8, 1, N9_WRAP, N9_SUB, (-INT64_C(128)), INT64_C(127)},
+ {"i8_wrapping_mul", 8, 1, N9_WRAP, N9_MUL, (-INT64_C(128)), INT64_C(127)},
+ {"i8_checked_add", 8, 1, N9_CHECK, N9_ADD, (-INT64_C(128)), INT64_C(127)},
+ {"i8_checked_sub", 8, 1, N9_CHECK, N9_SUB, (-INT64_C(128)), INT64_C(127)},
+ {"i8_checked_mul", 8, 1, N9_CHECK, N9_MUL, (-INT64_C(128)), INT64_C(127)},
+ {"i8_saturating_add", 8, 1, N9_SAT, N9_ADD, (-INT64_C(128)), INT64_C(127)},
+ {"i8_saturating_sub", 8, 1, N9_SAT, N9_SUB, (-INT64_C(128)), INT64_C(127)},
+ {"i8_saturating_mul", 8, 1, N9_SAT, N9_MUL, (-INT64_C(128)), INT64_C(127)},
+ {"i16_wrapping_add", 16, 1, N9_WRAP, N9_ADD, (-INT64_C(32768)), INT64_C(32767)},
+ {"i16_wrapping_sub", 16, 1, N9_WRAP, N9_SUB, (-INT64_C(32768)), INT64_C(32767)},
+ {"i16_wrapping_mul", 16, 1, N9_WRAP, N9_MUL, (-INT64_C(32768)), INT64_C(32767)},
+ {"i16_checked_add", 16, 1, N9_CHECK, N9_ADD, (-INT64_C(32768)), INT64_C(32767)},
+ {"i16_checked_sub", 16, 1, N9_CHECK, N9_SUB, (-INT64_C(32768)), INT64_C(32767)},
+ {"i16_checked_mul", 16, 1, N9_CHECK, N9_MUL, (-INT64_C(32768)), INT64_C(32767)},
+ {"i16_saturating_add", 16, 1, N9_SAT, N9_ADD, (-INT64_C(32768)), INT64_C(32767)},
+ {"i16_saturating_sub", 16, 1, N9_SAT, N9_SUB, (-INT64_C(32768)), INT64_C(32767)},
+ {"i16_saturating_mul", 16, 1, N9_SAT, N9_MUL, (-INT64_C(32768)), INT64_C(32767)},
+ {"i32_wrapping_add", 32, 1, N9_WRAP, N9_ADD, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i32_wrapping_sub", 32, 1, N9_WRAP, N9_SUB, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i32_wrapping_mul", 32, 1, N9_WRAP, N9_MUL, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i32_checked_add", 32, 1, N9_CHECK, N9_ADD, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i32_checked_sub", 32, 1, N9_CHECK, N9_SUB, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i32_checked_mul", 32, 1, N9_CHECK, N9_MUL, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i32_saturating_add", 32, 1, N9_SAT, N9_ADD, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i32_saturating_sub", 32, 1, N9_SAT, N9_SUB, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i32_saturating_mul", 32, 1, N9_SAT, N9_MUL, (-INT64_C(2147483648)), INT64_C(2147483647)},
+ {"i64_wrapping_add", 64, 1, N9_WRAP, N9_ADD, INT64_MIN, INT64_MAX},
+ {"i64_wrapping_sub", 64, 1, N9_WRAP, N9_SUB, INT64_MIN, INT64_MAX},
+ {"i64_wrapping_mul", 64, 1, N9_WRAP, N9_MUL, INT64_MIN, INT64_MAX},
+ {"i64_checked_add", 64, 1, N9_CHECK, N9_ADD, INT64_MIN, INT64_MAX},
+ {"i64_checked_sub", 64, 1, N9_CHECK, N9_SUB, INT64_MIN, INT64_MAX},
+ {"i64_checked_mul", 64, 1, N9_CHECK, N9_MUL, INT64_MIN, INT64_MAX},
+ {"i64_saturating_add", 64, 1, N9_SAT, N9_ADD, INT64_MIN, INT64_MAX},
+ {"i64_saturating_sub", 64, 1, N9_SAT, N9_SUB, INT64_MIN, INT64_MAX},
+ {"i64_saturating_mul", 64, 1, N9_SAT, N9_MUL, INT64_MIN, INT64_MAX},
+ {"u8_wrapping_add", 8, 0, N9_WRAP, N9_ADD, INT64_C(0), INT64_C(255)},
+ {"u8_wrapping_sub", 8, 0, N9_WRAP, N9_SUB, INT64_C(0), INT64_C(255)},
+ {"u8_wrapping_mul", 8, 0, N9_WRAP, N9_MUL, INT64_C(0), INT64_C(255)},
+ {"u8_checked_add", 8, 0, N9_CHECK, N9_ADD, INT64_C(0), INT64_C(255)},
+ {"u8_checked_sub", 8, 0, N9_CHECK, N9_SUB, INT64_C(0), INT64_C(255)},
+ {"u8_checked_mul", 8, 0, N9_CHECK, N9_MUL, INT64_C(0), INT64_C(255)},
+ {"u8_saturating_add", 8, 0, N9_SAT, N9_ADD, INT64_C(0), INT64_C(255)},
+ {"u8_saturating_sub", 8, 0, N9_SAT, N9_SUB, INT64_C(0), INT64_C(255)},
+ {"u8_saturating_mul", 8, 0, N9_SAT, N9_MUL, INT64_C(0), INT64_C(255)},
+ {"u16_wrapping_add", 16, 0, N9_WRAP, N9_ADD, INT64_C(0), INT64_C(65535)},
+ {"u16_wrapping_sub", 16, 0, N9_WRAP, N9_SUB, INT64_C(0), INT64_C(65535)},
+ {"u16_wrapping_mul", 16, 0, N9_WRAP, N9_MUL, INT64_C(0), INT64_C(65535)},
+ {"u16_checked_add", 16, 0, N9_CHECK, N9_ADD, INT64_C(0), INT64_C(65535)},
+ {"u16_checked_sub", 16, 0, N9_CHECK, N9_SUB, INT64_C(0), INT64_C(65535)},
+ {"u16_checked_mul", 16, 0, N9_CHECK, N9_MUL, INT64_C(0), INT64_C(65535)},
+ {"u16_saturating_add", 16, 0, N9_SAT, N9_ADD, INT64_C(0), INT64_C(65535)},
+ {"u16_saturating_sub", 16, 0, N9_SAT, N9_SUB, INT64_C(0), INT64_C(65535)},
+ {"u16_saturating_mul", 16, 0, N9_SAT, N9_MUL, INT64_C(0), INT64_C(65535)},
+ {"u32_wrapping_add", 32, 0, N9_WRAP, N9_ADD, INT64_C(0), INT64_C(4294967295)},
+ {"u32_wrapping_sub", 32, 0, N9_WRAP, N9_SUB, INT64_C(0), INT64_C(4294967295)},
+ {"u32_wrapping_mul", 32, 0, N9_WRAP, N9_MUL, INT64_C(0), INT64_C(4294967295)},
+ {"u32_checked_add", 32, 0, N9_CHECK, N9_ADD, INT64_C(0), INT64_C(4294967295)},
+ {"u32_checked_sub", 32, 0, N9_CHECK, N9_SUB, INT64_C(0), INT64_C(4294967295)},
+ {"u32_checked_mul", 32, 0, N9_CHECK, N9_MUL, INT64_C(0), INT64_C(4294967295)},
+ {"u32_saturating_add", 32, 0, N9_SAT, N9_ADD, INT64_C(0), INT64_C(4294967295)},
+ {"u32_saturating_sub", 32, 0, N9_SAT, N9_SUB, INT64_C(0), INT64_C(4294967295)},
+ {"u32_saturating_mul", 32, 0, N9_SAT, N9_MUL, INT64_C(0), INT64_C(4294967295)},
+ {"u64_wrapping_add", 64, 0, N9_WRAP, N9_ADD, INT64_MIN, INT64_MAX},
+ {"u64_wrapping_sub", 64, 0, N9_WRAP, N9_SUB, INT64_MIN, INT64_MAX},
+ {"u64_wrapping_mul", 64, 0, N9_WRAP, N9_MUL, INT64_MIN, INT64_MAX},
+ {"u64_checked_add", 64, 0, N9_CHECK, N9_ADD, INT64_MIN, INT64_MAX},
+ {"u64_checked_sub", 64, 0, N9_CHECK, N9_SUB, INT64_MIN, INT64_MAX},
+ {"u64_checked_mul", 64, 0, N9_CHECK, N9_MUL, INT64_MIN, INT64_MAX},
+ {"u64_saturating_add", 64, 0, N9_SAT, N9_ADD, INT64_MIN, INT64_MAX},
+ {"u64_saturating_sub", 64, 0, N9_SAT, N9_SUB, INT64_MIN, INT64_MAX},
+ {"u64_saturating_mul", 64, 0, N9_SAT, N9_MUL, INT64_MIN, INT64_MAX},
+};
+
+static int n9_pair(Value *arg, int64_t lo, int64_t hi,
+ int64_t *a, int64_t *b, const char *who) {
+ if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) {
+ rt_error(EK_TYPE, 0, "%s: expected [a, b]", who);
+ return 0;
+ }
+ if (arg->data.list.count > 2) {
+ rt_error(EK_TYPE, 0, "%s: expected a fixed-shape list with at most 2 elements", who);
+ return 0;
+ }
+ EigsView av, bv;
+ return eigs_int_arg_range(list_get_view(arg, 0, &av), lo, hi, a, who)
+ && eigs_int_arg_range(list_get_view(arg, 1, &bv), lo, hi, b, who);
+}
+
+/* Decode bits without an out-of-range unsigned-to-signed cast. */
+static int64_t n9_i64_bits(uint64_t bits) {
+ int64_t value;
+ memcpy(&value, &bits, sizeof(value));
+ return value;
+}
+
+static Value *n9_width(Value *arg, const N9Spec *s) {
+ int64_t a, b;
+ if (!n9_pair(arg, s->lo, s->hi, &a, &b, s->name)) return make_null();
+ uint64_t ua = (uint64_t)a, ub = (uint64_t)b;
+ uint64_t mask = s->width == 64 ? UINT64_MAX : (UINT64_C(1) << s->width) - 1;
+ uint64_t bits = 0;
+ switch (s->op) {
+ case N9_ADD: bits = ua + ub; break;
+ case N9_SUB: bits = ua - ub; break;
+ case N9_MUL: bits = ua * ub; break;
+ }
+ if (s->mode != N9_WRAP) {
+ int overflow = 0, high = 0;
+ if (s->is_signed) {
+ int64_t result = 0;
+ switch (s->op) {
+ case N9_ADD:
+ overflow = __builtin_add_overflow(a, b, &result);
+ high = b > 0;
+ break;
+ case N9_SUB:
+ overflow = __builtin_sub_overflow(a, b, &result);
+ high = b < 0;
+ break;
+ case N9_MUL:
+ overflow = __builtin_mul_overflow(a, b, &result);
+ high = (a < 0) == (b < 0);
+ break;
+ }
+ if (!overflow) {
+ overflow = result < s->lo || result > s->hi;
+ high = result > s->hi;
+ }
+ } else {
+ switch (s->op) {
+ case N9_ADD: overflow = bits < ua || bits > mask; high = 1; break;
+ case N9_SUB: overflow = ua < ub; high = 0; break;
+ case N9_MUL: overflow = ub != 0 && ua > mask / ub; high = 1; break;
+ }
+ }
+ if (overflow) {
+ if (s->mode == N9_CHECK) {
+ rt_error(EK_VALUE, 0, "%s: integer overflow", s->name);
+ return make_null();
+ }
+ if (s->is_signed) return make_int(high ? s->hi : s->lo);
+ return make_int(high ? n9_i64_bits(mask) : 0);
+ }
+ }
+ bits &= mask;
+ if (s->is_signed && s->width < 64 && (bits & (UINT64_C(1) << (s->width - 1))))
+ return make_int(-1 - (int64_t)(mask - bits));
+ return make_int(n9_i64_bits(bits));
+}
+
+/* i8_wrapping_add of [a, b] — i8 wrapping add; N5 operands, int result */
+Value* builtin_i8_wrapping_add(Value *arg) {
+ return n9_width(arg, &n9_specs[0]);
+}
+
+/* i8_wrapping_sub of [a, b] — i8 wrapping sub; N5 operands, int result */
+Value* builtin_i8_wrapping_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[1]);
+}
+
+/* i8_wrapping_mul of [a, b] — i8 wrapping mul; N5 operands, int result */
+Value* builtin_i8_wrapping_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[2]);
+}
+
+/* i8_checked_add of [a, b] — i8 checked add; N5 operands, int result */
+Value* builtin_i8_checked_add(Value *arg) {
+ return n9_width(arg, &n9_specs[3]);
+}
+
+/* i8_checked_sub of [a, b] — i8 checked sub; N5 operands, int result */
+Value* builtin_i8_checked_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[4]);
+}
+
+/* i8_checked_mul of [a, b] — i8 checked mul; N5 operands, int result */
+Value* builtin_i8_checked_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[5]);
+}
+
+/* i8_saturating_add of [a, b] — i8 saturating add; N5 operands, int result */
+Value* builtin_i8_saturating_add(Value *arg) {
+ return n9_width(arg, &n9_specs[6]);
+}
+
+/* i8_saturating_sub of [a, b] — i8 saturating sub; N5 operands, int result */
+Value* builtin_i8_saturating_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[7]);
+}
+
+/* i8_saturating_mul of [a, b] — i8 saturating mul; N5 operands, int result */
+Value* builtin_i8_saturating_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[8]);
+}
+
+/* i16_wrapping_add of [a, b] — i16 wrapping add; N5 operands, int result */
+Value* builtin_i16_wrapping_add(Value *arg) {
+ return n9_width(arg, &n9_specs[9]);
+}
+
+/* i16_wrapping_sub of [a, b] — i16 wrapping sub; N5 operands, int result */
+Value* builtin_i16_wrapping_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[10]);
+}
+
+/* i16_wrapping_mul of [a, b] — i16 wrapping mul; N5 operands, int result */
+Value* builtin_i16_wrapping_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[11]);
+}
+
+/* i16_checked_add of [a, b] — i16 checked add; N5 operands, int result */
+Value* builtin_i16_checked_add(Value *arg) {
+ return n9_width(arg, &n9_specs[12]);
+}
+
+/* i16_checked_sub of [a, b] — i16 checked sub; N5 operands, int result */
+Value* builtin_i16_checked_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[13]);
+}
+
+/* i16_checked_mul of [a, b] — i16 checked mul; N5 operands, int result */
+Value* builtin_i16_checked_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[14]);
+}
+
+/* i16_saturating_add of [a, b] — i16 saturating add; N5 operands, int result */
+Value* builtin_i16_saturating_add(Value *arg) {
+ return n9_width(arg, &n9_specs[15]);
+}
+
+/* i16_saturating_sub of [a, b] — i16 saturating sub; N5 operands, int result */
+Value* builtin_i16_saturating_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[16]);
+}
+
+/* i16_saturating_mul of [a, b] — i16 saturating mul; N5 operands, int result */
+Value* builtin_i16_saturating_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[17]);
+}
+
+/* i32_wrapping_add of [a, b] — i32 wrapping add; N5 operands, int result */
+Value* builtin_i32_wrapping_add(Value *arg) {
+ return n9_width(arg, &n9_specs[18]);
+}
+
+/* i32_wrapping_sub of [a, b] — i32 wrapping sub; N5 operands, int result */
+Value* builtin_i32_wrapping_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[19]);
+}
+
+/* i32_wrapping_mul of [a, b] — i32 wrapping mul; N5 operands, int result */
+Value* builtin_i32_wrapping_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[20]);
+}
+
+/* i32_checked_add of [a, b] — i32 checked add; N5 operands, int result */
+Value* builtin_i32_checked_add(Value *arg) {
+ return n9_width(arg, &n9_specs[21]);
+}
+
+/* i32_checked_sub of [a, b] — i32 checked sub; N5 operands, int result */
+Value* builtin_i32_checked_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[22]);
+}
+
+/* i32_checked_mul of [a, b] — i32 checked mul; N5 operands, int result */
+Value* builtin_i32_checked_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[23]);
+}
+
+/* i32_saturating_add of [a, b] — i32 saturating add; N5 operands, int result */
+Value* builtin_i32_saturating_add(Value *arg) {
+ return n9_width(arg, &n9_specs[24]);
+}
+
+/* i32_saturating_sub of [a, b] — i32 saturating sub; N5 operands, int result */
+Value* builtin_i32_saturating_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[25]);
+}
+
+/* i32_saturating_mul of [a, b] — i32 saturating mul; N5 operands, int result */
+Value* builtin_i32_saturating_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[26]);
+}
+
+/* i64_wrapping_add of [a, b] — i64 wrapping add; N5 operands, int result */
+Value* builtin_i64_wrapping_add(Value *arg) {
+ return n9_width(arg, &n9_specs[27]);
+}
+
+/* i64_wrapping_sub of [a, b] — i64 wrapping sub; N5 operands, int result */
+Value* builtin_i64_wrapping_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[28]);
+}
+
+/* i64_wrapping_mul of [a, b] — i64 wrapping mul; N5 operands, int result */
+Value* builtin_i64_wrapping_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[29]);
+}
+
+/* i64_checked_add of [a, b] — i64 checked add; N5 operands, int result */
+Value* builtin_i64_checked_add(Value *arg) {
+ return n9_width(arg, &n9_specs[30]);
+}
+
+/* i64_checked_sub of [a, b] — i64 checked sub; N5 operands, int result */
+Value* builtin_i64_checked_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[31]);
+}
+
+/* i64_checked_mul of [a, b] — i64 checked mul; N5 operands, int result */
+Value* builtin_i64_checked_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[32]);
+}
+
+/* i64_saturating_add of [a, b] — i64 saturating add; N5 operands, int result */
+Value* builtin_i64_saturating_add(Value *arg) {
+ return n9_width(arg, &n9_specs[33]);
+}
+
+/* i64_saturating_sub of [a, b] — i64 saturating sub; N5 operands, int result */
+Value* builtin_i64_saturating_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[34]);
+}
+
+/* i64_saturating_mul of [a, b] — i64 saturating mul; N5 operands, int result */
+Value* builtin_i64_saturating_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[35]);
+}
+
+/* u8_wrapping_add of [a, b] — u8 wrapping add; N5 operands, int result */
+Value* builtin_u8_wrapping_add(Value *arg) {
+ return n9_width(arg, &n9_specs[36]);
+}
+
+/* u8_wrapping_sub of [a, b] — u8 wrapping sub; N5 operands, int result */
+Value* builtin_u8_wrapping_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[37]);
+}
+
+/* u8_wrapping_mul of [a, b] — u8 wrapping mul; N5 operands, int result */
+Value* builtin_u8_wrapping_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[38]);
+}
+
+/* u8_checked_add of [a, b] — u8 checked add; N5 operands, int result */
+Value* builtin_u8_checked_add(Value *arg) {
+ return n9_width(arg, &n9_specs[39]);
+}
+
+/* u8_checked_sub of [a, b] — u8 checked sub; N5 operands, int result */
+Value* builtin_u8_checked_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[40]);
+}
+
+/* u8_checked_mul of [a, b] — u8 checked mul; N5 operands, int result */
+Value* builtin_u8_checked_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[41]);
+}
+
+/* u8_saturating_add of [a, b] — u8 saturating add; N5 operands, int result */
+Value* builtin_u8_saturating_add(Value *arg) {
+ return n9_width(arg, &n9_specs[42]);
+}
+
+/* u8_saturating_sub of [a, b] — u8 saturating sub; N5 operands, int result */
+Value* builtin_u8_saturating_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[43]);
+}
+
+/* u8_saturating_mul of [a, b] — u8 saturating mul; N5 operands, int result */
+Value* builtin_u8_saturating_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[44]);
+}
+
+/* u16_wrapping_add of [a, b] — u16 wrapping add; N5 operands, int result */
+Value* builtin_u16_wrapping_add(Value *arg) {
+ return n9_width(arg, &n9_specs[45]);
+}
+
+/* u16_wrapping_sub of [a, b] — u16 wrapping sub; N5 operands, int result */
+Value* builtin_u16_wrapping_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[46]);
+}
+
+/* u16_wrapping_mul of [a, b] — u16 wrapping mul; N5 operands, int result */
+Value* builtin_u16_wrapping_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[47]);
+}
+
+/* u16_checked_add of [a, b] — u16 checked add; N5 operands, int result */
+Value* builtin_u16_checked_add(Value *arg) {
+ return n9_width(arg, &n9_specs[48]);
+}
+
+/* u16_checked_sub of [a, b] — u16 checked sub; N5 operands, int result */
+Value* builtin_u16_checked_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[49]);
+}
+
+/* u16_checked_mul of [a, b] — u16 checked mul; N5 operands, int result */
+Value* builtin_u16_checked_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[50]);
+}
+
+/* u16_saturating_add of [a, b] — u16 saturating add; N5 operands, int result */
+Value* builtin_u16_saturating_add(Value *arg) {
+ return n9_width(arg, &n9_specs[51]);
+}
+
+/* u16_saturating_sub of [a, b] — u16 saturating sub; N5 operands, int result */
+Value* builtin_u16_saturating_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[52]);
+}
+
+/* u16_saturating_mul of [a, b] — u16 saturating mul; N5 operands, int result */
+Value* builtin_u16_saturating_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[53]);
+}
+
+/* u32_wrapping_add of [a, b] — u32 wrapping add; N5 operands, int result */
+Value* builtin_u32_wrapping_add(Value *arg) {
+ return n9_width(arg, &n9_specs[54]);
+}
+
+/* u32_wrapping_sub of [a, b] — u32 wrapping sub; N5 operands, int result */
+Value* builtin_u32_wrapping_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[55]);
+}
+
+/* u32_wrapping_mul of [a, b] — u32 wrapping mul; N5 operands, int result */
+Value* builtin_u32_wrapping_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[56]);
+}
+
+/* u32_checked_add of [a, b] — u32 checked add; N5 operands, int result */
+Value* builtin_u32_checked_add(Value *arg) {
+ return n9_width(arg, &n9_specs[57]);
+}
+
+/* u32_checked_sub of [a, b] — u32 checked sub; N5 operands, int result */
+Value* builtin_u32_checked_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[58]);
+}
+
+/* u32_checked_mul of [a, b] — u32 checked mul; N5 operands, int result */
+Value* builtin_u32_checked_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[59]);
+}
+
+/* u32_saturating_add of [a, b] — u32 saturating add; N5 operands, int result */
+Value* builtin_u32_saturating_add(Value *arg) {
+ return n9_width(arg, &n9_specs[60]);
+}
+
+/* u32_saturating_sub of [a, b] — u32 saturating sub; N5 operands, int result */
+Value* builtin_u32_saturating_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[61]);
+}
+
+/* u32_saturating_mul of [a, b] — u32 saturating mul; N5 operands, int result */
+Value* builtin_u32_saturating_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[62]);
+}
+
+/* u64_wrapping_add of [a, b] — u64 wrapping add; N5 operands, int result */
+Value* builtin_u64_wrapping_add(Value *arg) {
+ return n9_width(arg, &n9_specs[63]);
+}
+
+/* u64_wrapping_sub of [a, b] — u64 wrapping sub; N5 operands, int result */
+Value* builtin_u64_wrapping_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[64]);
+}
+
+/* u64_wrapping_mul of [a, b] — u64 wrapping mul; N5 operands, int result */
+Value* builtin_u64_wrapping_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[65]);
+}
+
+/* u64_checked_add of [a, b] — u64 checked add; N5 operands, int result */
+Value* builtin_u64_checked_add(Value *arg) {
+ return n9_width(arg, &n9_specs[66]);
+}
+
+/* u64_checked_sub of [a, b] — u64 checked sub; N5 operands, int result */
+Value* builtin_u64_checked_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[67]);
+}
+
+/* u64_checked_mul of [a, b] — u64 checked mul; N5 operands, int result */
+Value* builtin_u64_checked_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[68]);
+}
+
+/* u64_saturating_add of [a, b] — u64 saturating add; N5 operands, int result */
+Value* builtin_u64_saturating_add(Value *arg) {
+ return n9_width(arg, &n9_specs[69]);
+}
+
+/* u64_saturating_sub of [a, b] — u64 saturating sub; N5 operands, int result */
+Value* builtin_u64_saturating_sub(Value *arg) {
+ return n9_width(arg, &n9_specs[70]);
+}
+
+/* u64_saturating_mul of [a, b] — u64 saturating mul; N5 operands, int result */
+Value* builtin_u64_saturating_mul(Value *arg) {
+ return n9_width(arg, &n9_specs[71]);
+}
+
+/* u64_compare of [a, b] — unsigned ordering of int64 bit patterns */
+Value* builtin_u64_compare(Value *arg) {
+ int64_t a, b;
+ if (!n9_pair(arg, INT64_MIN, INT64_MAX, &a, &b, "u64_compare")) return make_null();
+ return make_int(((uint64_t)a > (uint64_t)b) - ((uint64_t)a < (uint64_t)b));
+}
+static Value *n9_unsigned_div(Value *arg, int remainder, const char *who) {
+ int64_t a, b;
+ if (!n9_pair(arg, INT64_MIN, INT64_MAX, &a, &b, who)) return make_null();
+ if (b == 0) {
+ rt_error(EK_VALUE, 0, "division by zero");
+ return make_null();
+ }
+ uint64_t value = remainder ? (uint64_t)a % (uint64_t)b : (uint64_t)a / (uint64_t)b;
+ return make_int(n9_i64_bits(value));
+}
+/* u64_div of [a, b] — unsigned division; zero raises value */
+Value* builtin_u64_div(Value *arg) {
+ return n9_unsigned_div(arg, 0, "u64_div");
+}
+/* u64_rem of [a, b] — unsigned remainder; zero raises value */
+Value* builtin_u64_rem(Value *arg) {
+ return n9_unsigned_div(arg, 1, "u64_rem");
+}
+/* u64_to_string of x — unsigned decimal text from an int64 bit pattern */
+Value* builtin_u64_to_string(Value *arg) {
+ EigsView view;
+ if (arg && arg->type == VAL_LIST && arg->data.list.count == 1)
+ arg = list_get_view(arg, 0, &view);
+ int64_t value = eigs_int_arg(arg, "u64_to_string");
+ if (g_has_error) return make_null();
+ char text[21];
+ snprintf(text, sizeof(text), "%llu", (unsigned long long)(uint64_t)value);
+ return make_str(text);
+}
+/* fround of x — binary64 to binary32 to binary64, returning float kind */
+Value* builtin_fround(Value *arg) {
+ EigsView view;
+ if (arg && arg->type == VAL_LIST && arg->data.list.count == 1)
+ arg = list_get_view(arg, 0, &view);
+ double value = eigs_float_arg(arg, "fround");
+ if (g_has_error) return make_null();
+ /* Preserve int -> f64 -> f32 -> f64 double rounding. make_float keeps
+ * today's overflow guard and math flag until the IEEE round R2.2. */
+ return make_float((double)(float)value);
+}
diff --git a/src/builtins_internal.h b/src/builtins_internal.h
index 800481501..cf215b2ed 100644
--- a/src/builtins_internal.h
+++ b/src/builtins_internal.h
@@ -9,6 +9,85 @@
#include "eigenscript.h"
+/* #1758 N9 — builtins_int.c, shared table-driven width engine. */
+Value* builtin_i8_wrapping_add(Value *arg);
+Value* builtin_i8_wrapping_sub(Value *arg);
+Value* builtin_i8_wrapping_mul(Value *arg);
+Value* builtin_i8_checked_add(Value *arg);
+Value* builtin_i8_checked_sub(Value *arg);
+Value* builtin_i8_checked_mul(Value *arg);
+Value* builtin_i8_saturating_add(Value *arg);
+Value* builtin_i8_saturating_sub(Value *arg);
+Value* builtin_i8_saturating_mul(Value *arg);
+Value* builtin_i16_wrapping_add(Value *arg);
+Value* builtin_i16_wrapping_sub(Value *arg);
+Value* builtin_i16_wrapping_mul(Value *arg);
+Value* builtin_i16_checked_add(Value *arg);
+Value* builtin_i16_checked_sub(Value *arg);
+Value* builtin_i16_checked_mul(Value *arg);
+Value* builtin_i16_saturating_add(Value *arg);
+Value* builtin_i16_saturating_sub(Value *arg);
+Value* builtin_i16_saturating_mul(Value *arg);
+Value* builtin_i32_wrapping_add(Value *arg);
+Value* builtin_i32_wrapping_sub(Value *arg);
+Value* builtin_i32_wrapping_mul(Value *arg);
+Value* builtin_i32_checked_add(Value *arg);
+Value* builtin_i32_checked_sub(Value *arg);
+Value* builtin_i32_checked_mul(Value *arg);
+Value* builtin_i32_saturating_add(Value *arg);
+Value* builtin_i32_saturating_sub(Value *arg);
+Value* builtin_i32_saturating_mul(Value *arg);
+Value* builtin_i64_wrapping_add(Value *arg);
+Value* builtin_i64_wrapping_sub(Value *arg);
+Value* builtin_i64_wrapping_mul(Value *arg);
+Value* builtin_i64_checked_add(Value *arg);
+Value* builtin_i64_checked_sub(Value *arg);
+Value* builtin_i64_checked_mul(Value *arg);
+Value* builtin_i64_saturating_add(Value *arg);
+Value* builtin_i64_saturating_sub(Value *arg);
+Value* builtin_i64_saturating_mul(Value *arg);
+Value* builtin_u8_wrapping_add(Value *arg);
+Value* builtin_u8_wrapping_sub(Value *arg);
+Value* builtin_u8_wrapping_mul(Value *arg);
+Value* builtin_u8_checked_add(Value *arg);
+Value* builtin_u8_checked_sub(Value *arg);
+Value* builtin_u8_checked_mul(Value *arg);
+Value* builtin_u8_saturating_add(Value *arg);
+Value* builtin_u8_saturating_sub(Value *arg);
+Value* builtin_u8_saturating_mul(Value *arg);
+Value* builtin_u16_wrapping_add(Value *arg);
+Value* builtin_u16_wrapping_sub(Value *arg);
+Value* builtin_u16_wrapping_mul(Value *arg);
+Value* builtin_u16_checked_add(Value *arg);
+Value* builtin_u16_checked_sub(Value *arg);
+Value* builtin_u16_checked_mul(Value *arg);
+Value* builtin_u16_saturating_add(Value *arg);
+Value* builtin_u16_saturating_sub(Value *arg);
+Value* builtin_u16_saturating_mul(Value *arg);
+Value* builtin_u32_wrapping_add(Value *arg);
+Value* builtin_u32_wrapping_sub(Value *arg);
+Value* builtin_u32_wrapping_mul(Value *arg);
+Value* builtin_u32_checked_add(Value *arg);
+Value* builtin_u32_checked_sub(Value *arg);
+Value* builtin_u32_checked_mul(Value *arg);
+Value* builtin_u32_saturating_add(Value *arg);
+Value* builtin_u32_saturating_sub(Value *arg);
+Value* builtin_u32_saturating_mul(Value *arg);
+Value* builtin_u64_wrapping_add(Value *arg);
+Value* builtin_u64_wrapping_sub(Value *arg);
+Value* builtin_u64_wrapping_mul(Value *arg);
+Value* builtin_u64_checked_add(Value *arg);
+Value* builtin_u64_checked_sub(Value *arg);
+Value* builtin_u64_checked_mul(Value *arg);
+Value* builtin_u64_saturating_add(Value *arg);
+Value* builtin_u64_saturating_sub(Value *arg);
+Value* builtin_u64_saturating_mul(Value *arg);
+Value* builtin_u64_compare(Value *arg);
+Value* builtin_u64_div(Value *arg);
+Value* builtin_u64_rem(Value *arg);
+Value* builtin_u64_to_string(Value *arg);
+Value* builtin_fround(Value *arg);
+
/* Tensor builtins — implemented in builtins_tensor.c */
Value* builtin_tensor_add(Value *arg);
Value* builtin_tensor_subtract(Value *arg);
diff --git a/src/builtins_tensor.c b/src/builtins_tensor.c
index 10f7da2cc..4dd412f66 100644
--- a/src/builtins_tensor.c
+++ b/src/builtins_tensor.c
@@ -210,7 +210,7 @@ static Value* make_buffer_like(Value *a) { /* same count + shape as a */
/* ====================================================================
* TENSOR SUBSTRATE BUILTINS
* Expose tensor operations so the .eigs standard library can run natively.
- * Tensors are represented as nested VAL_LIST of VAL_NUM (1D or 2D).
+ * Tensors are nested numeric VAL_LIST values (1D or 2D), or float buffers.
* ==================================================================== */
/* --- Tensor helper: detect dimensions --- */
@@ -226,7 +226,7 @@ static int tensor_dims(Value *v, int *rows, int *cols) {
*rows = 1; *cols = v->data.buffer.count; return 1;
}
if (!v || v->type != VAL_LIST || v->data.list.count == 0) return 0;
- if (list_elem_type(v, 0) == VAL_NUM) {
+ if ((list_elem_type(v, 0) == VAL_FLOAT || list_elem_type(v, 0) == VAL_INT)) {
/* 1D tensor */
*rows = 1;
*cols = v->data.list.count;
@@ -254,7 +254,7 @@ static double* tensor_to_flat(Value *v, int *rows, int *cols,
* explicit EIGS_STRICT=0 path deliberately retains the old stand-in.
* Check even when the first element or row gives no usable shape. */
/* #1637: a bool element is refused in every strict mode. */
-#define FLAT_BAD(t) ((t) != VAL_NUM && (g_strict || (t) == VAL_BOOL))
+#define FLAT_BAD(t) (((t) != VAL_FLOAT && (t) != VAL_INT) && (g_strict || (t) == VAL_BOOL))
if (v && v->type == VAL_LIST) {
int bad = 0;
if (ndim == 1) {
@@ -288,15 +288,19 @@ static double* tensor_to_flat(Value *v, int *rows, int *cols,
return out;
}
if (ndim == 1) {
- for (int i = 0; i < *cols; i++)
- out[i] = (list_elem_type(v, i) == VAL_NUM) ? eigs_slot_num(list_slot(v, i), __func__) : 0.0;
+ for (int i = 0; i < *cols; i++) {
+ out[i] = ((list_elem_type(v, i) == VAL_FLOAT || list_elem_type(v, i) == VAL_INT)) ? eigs_slot_float(list_slot(v, i), who) : 0.0;
+ if (g_has_error) { free(out); return NULL; }
+ }
} else {
for (int r = 0; r < *rows; r++) {
/* list_get_ref is NULL for a number: test the type first. */
Value *row = (list_elem_type(v, r) == VAL_LIST) ? list_get_ref(v, r) : NULL;
int rc = row ? row->data.list.count : 0;
- for (int c = 0; c < *cols && c < rc; c++)
- out[r * (*cols) + c] = (list_elem_type(row, c) == VAL_NUM) ? eigs_slot_num(list_slot(row, c), __func__) : 0.0;
+ for (int c = 0; c < *cols && c < rc; c++) {
+ out[r * (*cols) + c] = ((list_elem_type(row, c) == VAL_FLOAT || list_elem_type(row, c) == VAL_INT)) ? eigs_slot_float(list_slot(row, c), who) : 0.0;
+ if (g_has_error) { free(out); return NULL; }
+ }
}
}
return out;
@@ -308,7 +312,7 @@ static Value* flat_to_tensor_2d(double *data, int rows, int cols) {
for (int r = 0; r < rows; r++) {
Value *row = make_list(cols);
for (int c = 0; c < cols; c++) {
- Value *item = make_num(data[r * cols + c]);
+ Value *item = make_float(data[r * cols + c]);
if (g_has_error) {
val_decref(item); val_decref(row); val_decref(outer);
return make_null();
@@ -324,7 +328,7 @@ static Value* flat_to_tensor_2d(double *data, int rows, int cols) {
static Value* flat_to_tensor_1d(double *data, int len) {
Value *out = make_list(len);
for (int i = 0; i < len; i++) {
- Value *item = make_num(data[i]);
+ Value *item = make_float(data[i]);
if (g_has_error) {
val_decref(item); val_decref(out);
return make_null();
@@ -356,7 +360,7 @@ static Value* flat_to_like(Value *src, double *data, int rows, int cols) {
/* --- Tensor helper: count total elements recursively --- */
static int tensor_total(Value *v) {
if (!v) return 0;
- if (v->type == VAL_NUM) return 1;
+ if (val_is_number(v)) return 1;
/* #1637: arithmetic on a bool raises. Other non-number elements keep
* their old (counted-as-absent) reading; a bool is the one that used to
* BE a number, so letting it vanish from `sum of [a < b, ...]` would turn
@@ -377,10 +381,10 @@ static int tensor_total(Value *v) {
* Return failure at the first raised read; callers own and free the workspace. */
static int tensor_flatten_recursive(Value *v, double *out, int *idx) {
if (!v) return 1;
- if (v->type == VAL_NUM) { out[(*idx)++] = eigs_num_arg(v, __func__); return 1; }
+ if (val_is_number(v)) { out[(*idx)++] = eigs_float_arg(v, __func__); return !g_has_error; }
if (v->type == VAL_BUFFER) { /* #1093 */
for (int i = 0; i < v->data.buffer.count; i++) {
- double x = buffer_read_num(v, i);
+ double x = buffer_read_f64(v, i);
if (g_has_error) return 0;
out[(*idx)++] = x;
}
@@ -436,9 +440,11 @@ static Value* buf_elementwise(Value *a, Value *b, BinOpFn fn) {
Value *out = make_shaped_buffer(rows, cols);
if (!out) return make_null();
for (int r = 0; r < rows; r++)
- for (int c = 0; c < cols; c++)
+ for (int c = 0; c < cols; c++) {
out->data.buffer.data[r * cols + c] =
fn(ad[r * cols + c], (bn == cols) ? bd[c] : bd[r]);
+ if (g_has_error) { val_decref(out); return make_null(); }
+ }
return out;
}
}
@@ -449,32 +455,42 @@ static Value* buf_elementwise(Value *a, Value *b, BinOpFn fn) {
Value *out = make_shaped_buffer(rows, cols);
if (!out) return make_null();
for (int r = 0; r < rows; r++)
- for (int c = 0; c < cols; c++)
+ for (int c = 0; c < cols; c++) {
out->data.buffer.data[r * cols + c] =
fn((an == cols) ? ad[c] : ad[r], bd[r * cols + c]);
+ if (g_has_error) { val_decref(out); return make_null(); }
+ }
return out;
}
}
if (an == bn) { /* same length: keep a's shape */
Value *out = make_buffer_like(a);
if (!out) return make_null();
- for (int i = 0; i < an; i++) out->data.buffer.data[i] = fn(ad[i], bd[i]);
+ for (int i = 0; i < an; i++) {
+ out->data.buffer.data[i] = fn(ad[i], bd[i]);
+ if (g_has_error) { val_decref(out); return make_null(); }
+ }
return out;
}
/* Mismatched lengths truncate to the shorter operand, as the list path does. */
int n = an < bn ? an : bn;
Value *out = make_shaped_buffer(0, n);
if (!out) return make_null();
- for (int i = 0; i < n; i++) out->data.buffer.data[i] = fn(ad[i], bd[i]);
+ for (int i = 0; i < n; i++) {
+ out->data.buffer.data[i] = fn(ad[i], bd[i]);
+ if (g_has_error) { val_decref(out); return make_null(); }
+ }
return out;
}
static Value* buf_scalar_elementwise(Value *buf, double sc, BinOpFn fn, int buf_left) {
Value *out = make_buffer_like(buf);
if (!out) return make_null();
- for (int i = 0; i < buf->data.buffer.count; i++)
+ for (int i = 0; i < buf->data.buffer.count; i++) {
out->data.buffer.data[i] = buf_left ? fn(buf->data.buffer.data[i], sc)
: fn(sc, buf->data.buffer.data[i]);
+ if (g_has_error) { val_decref(out); return make_null(); }
+ }
return out;
}
@@ -485,11 +501,33 @@ static Value* tensor_elementwise(Value *a, Value *b, BinOpFn fn) {
* `arg` is what the guard macros test for a bool (#1637): either operand. */
Value *arg = (a && a->type == VAL_BOOL) ? a : b;
ARG_GUARD(!a || !b, "add/subtract/multiply/divide/pow",
- "two tensor operands", make_num(0.0));
+ "two tensor operands", make_float(0.0));
/* scalar op scalar */
- if (a->type == VAL_NUM && b->type == VAL_NUM)
- return make_num(fn(eigs_num_arg(a, __func__), eigs_num_arg(b, __func__)));
+ if (val_is_number(a) && val_is_number(b)) {
+ const char *who = fn == op_add ? "add" : fn == op_sub ? "subtract" :
+ fn == op_mul ? "multiply" : "divide/pow";
+ EigsNum x = eigs_number_arg(a, who);
+ if (g_has_error) return make_null();
+ EigsNum y = eigs_number_arg(b, who);
+ if (g_has_error) return make_null();
+ if (x.is_int && y.is_int && (fn == op_add || fn == op_sub || fn == op_mul)) {
+ int64_t result;
+ int overflow = fn == op_add ? __builtin_add_overflow(x.value.i64, y.value.i64, &result) :
+ fn == op_sub ? __builtin_sub_overflow(x.value.i64, y.value.i64, &result) :
+ __builtin_mul_overflow(x.value.i64, y.value.i64, &result);
+ if (overflow) {
+ rt_error(EK_VALUE, 0, "%s: int64 overflow in '%s'", who,
+ fn == op_add ? "+" : fn == op_sub ? "-" : "*");
+ return make_null();
+ }
+ return make_int(result);
+ }
+ double xd = x.is_int ? (double)x.value.i64 : x.value.f64;
+ double yd = y.is_int ? (double)y.value.i64 : y.value.f64;
+ double result = fn(xd, yd);
+ return g_has_error ? make_null() : make_float(result);
+ }
/* #1093 buffers are flat numeric tensors. Buffer-only operands compute on
* the flat doubles and return a buffer; a buffer MIXED with a list is
@@ -497,10 +535,16 @@ static Value* tensor_elementwise(Value *a, Value *b, BinOpFn fn) {
* (the rule: a buffer out iff every tensor operand was a buffer). */
if (a->type == VAL_BUFFER && b->type == VAL_BUFFER)
return buf_elementwise(a, b, fn);
- if (a->type == VAL_BUFFER && b->type == VAL_NUM)
- return buf_scalar_elementwise(a, eigs_num_arg(b, __func__), fn, 1);
- if (a->type == VAL_NUM && b->type == VAL_BUFFER)
- return buf_scalar_elementwise(b, eigs_num_arg(a, __func__), fn, 0);
+ if (a->type == VAL_BUFFER && val_is_number(b)) {
+ double scalar = eigs_float_arg(b, __func__);
+ if (g_has_error) return make_null();
+ return buf_scalar_elementwise(a, scalar, fn, 1);
+ }
+ if (val_is_number(a) && b->type == VAL_BUFFER) {
+ double scalar = eigs_float_arg(a, __func__);
+ if (g_has_error) return make_null();
+ return buf_scalar_elementwise(b, scalar, fn, 0);
+ }
if (a->type == VAL_BUFFER && b->type == VAL_LIST) {
Value *al = buf_as_tensor_list(a);
if (g_has_error) { val_decref(al); return make_null(); }
@@ -517,7 +561,7 @@ static Value* tensor_elementwise(Value *a, Value *b, BinOpFn fn) {
}
/* scalar broadcast to list */
- if (a->type == VAL_NUM && b->type == VAL_LIST) {
+ if (val_is_number(a) && b->type == VAL_LIST) {
Value *out = make_list(b->data.list.count);
for (int i = 0; i < b->data.list.count; i++) {
Value *item = tensor_elementwise_slots(slot_from_heap(a), list_slot(b, i), fn);
@@ -528,7 +572,7 @@ static Value* tensor_elementwise(Value *a, Value *b, BinOpFn fn) {
}
return out;
}
- if (a->type == VAL_LIST && b->type == VAL_NUM) {
+ if (a->type == VAL_LIST && val_is_number(b)) {
Value *out = make_list(a->data.list.count);
for (int i = 0; i < a->data.list.count; i++) {
Value *item = tensor_elementwise_slots(list_slot(a, i), slot_from_heap(b), fn);
@@ -626,7 +670,7 @@ static Value* tensor_elementwise(Value *a, Value *b, BinOpFn fn) {
* function. That is the wrong-type case, and 0.0 was indistinguishable
* from a real elementwise result. */
ARG_GUARD(1, "add/subtract/multiply/divide/pow",
- "numbers, lists or buffers as operands", make_num(0.0));
+ "numbers, lists or buffers as operands", make_float(0.0));
}
static Value* tensor_elementwise_slots(EigsSlot a, EigsSlot b, BinOpFn fn) {
@@ -678,22 +722,41 @@ Value* builtin_tensor_pow(Value *arg) {
/* ---- Element-wise unary op ---- */
typedef double (*UnaryOpFn)(double);
+static double op_neg(double x);
static Value* tensor_unary(Value *v, UnaryOpFn fn) {
Value *arg = v; /* what the guard macros test for a bool (#1637) */
- if (v->type == VAL_NUM) return make_num(fn(eigs_num_arg(v, __func__)));
+ if (val_is_number(v)) {
+ EigsNum n = eigs_number_arg(v, fn == op_neg ? "negative" : "sqrt/exp/log");
+ if (g_has_error) return make_null();
+ if (fn == op_neg && n.is_int) {
+ if (n.value.i64 == INT64_MIN) {
+ rt_error(EK_VALUE, 0, "negative: int64 overflow in '-'");
+ return make_null();
+ }
+ return make_int(-n.value.i64);
+ }
+ double result = fn(n.is_int ? (double)n.value.i64 : n.value.f64);
+ return g_has_error ? make_null() : make_float(result);
+ }
/* #1093: a buffer is a flat numeric tensor — same kernel, buffer out. */
if (v->type == VAL_BUFFER) {
Value *out = make_buffer_like(v);
if (!out) return make_null();
- for (int i = 0; i < v->data.buffer.count; i++)
+ for (int i = 0; i < v->data.buffer.count; i++) {
out->data.buffer.data[i] = fn(v->data.buffer.data[i]);
+ if (g_has_error) { val_decref(out); return make_null(); }
+ }
return out;
}
if (v->type == VAL_LIST) {
Value *out = make_list(v->data.list.count);
for (int i = 0; i < v->data.list.count; i++) {
EIGS_VIEW(child_view);
- list_append_owned(out, tensor_unary(list_get_view(v, i, &child_view), fn));
+ Value *item = tensor_unary(list_get_view(v, i, &child_view), fn);
+ if (g_has_error) {
+ val_decref(item); val_decref(out); return make_null();
+ }
+ list_append_owned(out, item);
}
return out;
}
@@ -702,7 +765,7 @@ static Value* tensor_unary(Value *v, UnaryOpFn fn) {
* by sqrt/exp/log/negative and by its own recursion, so the guard names
* that surface rather than one call. */
ARG_GUARD(1, "sqrt/exp/log/negative", "a number, a list or a buffer",
- make_num(0.0));
+ make_float(0.0));
}
/* #865: `sqrt of -1` returns 0, which is indistinguishable from `sqrt of 0`.
@@ -794,7 +857,7 @@ Value* builtin_tensor_matmul(Value *arg) {
"result is not a number (NaN has no defined value)");
break;
}
- res->data.buffer.data[i] = SLOT_NUM_RAW(slot_null());
+ res->data.buffer.data[i] = SLOT_F64_RAW(slot_null());
}
}
return res;
@@ -823,7 +886,7 @@ Value* builtin_tensor_matmul(Value *arg) {
double *out = xcalloc((size_t)ar * bc, sizeof(double));
ne_matmul_buf(af, ar, ac, bf, bc, out);
/* #971: same NaN collapse as the buffer path, so the strict raise names
- * matmul instead of the bare num_guard backstop inside make_num. */
+ * matmul instead of the bare num_guard backstop inside make_float. */
for (int64_t i = 0; i < (int64_t)ar * bc; i++) {
if (out[i] != out[i]) out[i] = num_guard_named(out[i], "matmul");
if (g_has_error) { free(af); free(bf); free(out); return make_null(); }
@@ -992,7 +1055,7 @@ Value* builtin_tensor_scatter_add(Value *arg) {
rt_error(EK_TYPE, 0, "scatter_add: indices must be a list or buffer, got %s", val_type_name(indices->type));
return make_null();
}
- if (values->type != VAL_LIST && values->type != VAL_BUFFER && values->type != VAL_NUM) {
+ if (values->type != VAL_LIST && values->type != VAL_BUFFER && !val_is_number(values)) {
rt_error(EK_TYPE, 0, "scatter_add: values must be a buffer, a list of numbers, or a number, got %s", val_type_name(values->type));
return make_null();
}
@@ -1020,54 +1083,51 @@ Value* builtin_tensor_scatter_add(Value *arg) {
* so a raise leaves dst untouched instead of half-updated. */
for (int pass = 0; pass < 2; pass++) {
for (int i = 0; i < n; i++) {
- double di, v;
+ int64_t di;
+ double v;
if (indices->type == VAL_LIST) {
ValType it = list_elem_type(indices, i);
- if (it != VAL_NUM) {
+ if ((it != VAL_FLOAT && it != VAL_INT)) {
rt_error(EK_TYPE, 0, "scatter_add: index %d is %s (expected a number)", i, val_type_name(it));
return make_null();
}
- di = eigs_slot_num(list_slot(indices, i), __func__);
+ di = eigs_slot_int(list_slot(indices, i), "scatter_add");
+ if (g_has_error) return make_null();
} else {
- /* #1417: an index is a scalar read too. Guard before the
- * double-to-int conversion: casting a stored NaN is undefined
- * behavior, and must follow the same strict/lenient rule as
- * [] and the buffer accessors. */
- di = buffer_read_num(indices, i);
+ /* N5: require an exact finite index before bounds checking.
+ * Fractional and non-finite cells raise value in both modes. */
+ if (!buffer_index_at(indices, i, &di, "scatter_add")) return make_null();
if (g_has_error) return make_null();
}
if (values->type == VAL_LIST) {
ValType vt = list_elem_type(values, i);
- if (vt != VAL_NUM) {
+ if ((vt != VAL_FLOAT && vt != VAL_INT)) {
rt_error(EK_TYPE, 0, "scatter_add: value %d is %s (expected a number)", i, val_type_name(vt));
return make_null();
}
- v = eigs_slot_num(list_slot(values, i), __func__);
+ v = eigs_slot_float(list_slot(values, i), "scatter_add");
} else if (values->type == VAL_BUFFER) {
v = values->data.buffer.data[i];
} else {
- v = eigs_num_arg(values, __func__);
+ v = eigs_float_arg(values, "scatter_add");
}
- /* Check the double before converting it to int. In particular,
- * an out-of-range floating-to-integer conversion is undefined C
- * behavior, and indices can come from sandboxed bytecode. */
+ /* N5 has established int64 exactness; check bounds before narrowing. */
int index_limit = per_row ? cols : dst->data.buffer.count;
- int valid_index = isfinite(di) && di >= 0.0 && di < (double)index_limit;
+ if (g_has_error) return make_null();
+ int valid_index = di >= 0 && di < index_limit;
int idx = 0;
if (valid_index) {
- /* The range check makes this conversion representable. Check
- * both directions instead of calling trunc(), which is not
- * part of the freestanding runtime's mini-libm surface. */
+ /* Bounds prove that the storage offset fits int. */
idx = (int)di;
- valid_index = di >= (double)idx && di <= (double)idx;
+ valid_index = di == idx;
}
if (!valid_index) {
if (per_row) {
- rt_error(EK_INDEX, 0, "scatter_add: column index %.17g out of range for row %d (cols %d)",
- di, i, cols);
+ rt_error(EK_INDEX, 0, "scatter_add: column index %lld out of range for row %d (cols %d)",
+ (long long)di, i, cols);
} else {
- rt_error(EK_INDEX, 0, "scatter_add: index %.17g out of range (length %d)",
- di, index_limit);
+ rt_error(EK_INDEX, 0, "scatter_add: index %lld out of range (length %d)",
+ (long long)di, index_limit);
}
return make_null();
}
@@ -1088,7 +1148,7 @@ Value* builtin_tensor_scatter_add(Value *arg) {
Value* builtin_tensor_softmax(Value *arg) {
BOOL_REFUSE(arg, "softmax");
/* #632: softmax of a single element normalizes to 1.0. */
- if (arg && arg->type == VAL_NUM) return make_num(1.0);
+ if (arg && val_is_number(arg)) return make_float(1.0);
/* flat-buffer fast path (#973): row-wise on the shape, 1-D is one row;
* same ne_softmax_buf kernel as the list path, so byte-identical. */
if (arg && arg->type == VAL_BUFFER) {
@@ -1122,13 +1182,13 @@ Value* builtin_tensor_log_softmax(Value *arg) {
Value *tensor = arg;
if (arg && arg->type == VAL_LIST && arg->data.list.count == 2 &&
list_elem_type(arg, 0) == VAL_LIST &&
- list_elem_type(arg, 1) == VAL_NUM)
+ (list_elem_type(arg, 1) == VAL_FLOAT || list_elem_type(arg, 1) == VAL_INT))
tensor = list_get_ref(arg, 0); /* [tensor, dim] form */
/* #632: log(softmax(scalar)) = log(1) = 0. */
/* fs:ANSWER softmax of a single element is 1 and log(1) is 0, so 0.0 is
* the arithmetic result for a scalar argument — a NUMBER is a valid
* argument here, which is what makes this not a type guard (#632). */
- if (tensor && tensor->type == VAL_NUM) return make_num(0.0);
+ if (tensor && val_is_number(tensor)) return make_float(0.0);
/* flat-buffer fast path (#973): same kernel + the same #865 clamp. */
if (tensor && tensor->type == VAL_BUFFER) {
Value *res = make_buffer_like(tensor);
@@ -1162,9 +1222,9 @@ Value* builtin_tensor_log_softmax(Value *arg) {
Value* builtin_tensor_relu(Value *arg) {
BOOL_REFUSE(arg, "relu");
/* #632: a scalar is the degenerate element-wise case, like sqrt/exp/log. */
- if (arg && arg->type == VAL_NUM) {
- double x = eigs_num_arg(arg, __func__);
- return make_num(x < 0.0 ? 0.0 : x);
+ if (arg && val_is_number(arg)) {
+ double x = eigs_float_arg(arg, __func__);
+ return make_float(x < 0.0 ? 0.0 : x);
}
/* #1093: buffers go through the same flatten path and come back as
* buffers via flat_to_like — one implementation, not two. */
@@ -1183,9 +1243,9 @@ Value* builtin_tensor_relu(Value *arg) {
Value* builtin_tensor_leaky_relu(Value *arg) {
BOOL_REFUSE(arg, "leaky_relu");
/* #632: scalar is the degenerate element-wise case. */
- if (arg && arg->type == VAL_NUM) {
- double x = eigs_num_arg(arg, __func__);
- return make_num(x < 0.0 ? 0.01 * x : x);
+ if (arg && val_is_number(arg)) {
+ double x = eigs_float_arg(arg, __func__);
+ return make_float(x < 0.0 ? 0.01 * x : x);
}
/* flat-buffer fast path (#973), the twin of relu's. */
if (arg && arg->type == VAL_BUFFER) {
@@ -1216,14 +1276,14 @@ Value* builtin_tensor_mean(Value *arg) {
/* fs:EMPTY the mean over zero elements, as for `mean of []` below;
* the division would be 0/0. A buffer is a valid argument, so this
* is not a type guard and strict must not raise. */
- if (n == 0) return make_num(0.0);
+ if (n == 0) return make_float(0.0);
double s = 0.0;
for (int i = 0; i < n; i++) {
- double x = buffer_read_num(arg, i);
+ double x = buffer_read_f64(arg, i);
if (g_has_error) return make_null();
s = num_guard(s + x);
}
- return make_num(s / n);
+ return make_float(s / n);
}
/* Split from the empty case below. `tensor_total` answers 0 for an empty
* list AND for any non-tensor — a string, a dict, a function — so one
@@ -1233,14 +1293,14 @@ Value* builtin_tensor_mean(Value *arg) {
* tagged fs:EMPTY, which would have blessed the laundering permanently),
* so the type half is hoisted out. Non-strict is byte-identical: both
* halves still answer 0.0. Closes the main half of #1008. */
- ARG_GUARD(arg && arg->type != VAL_NUM && arg->type != VAL_LIST
+ ARG_GUARD(arg && !val_is_number(arg) && arg->type != VAL_LIST
&& arg->type != VAL_BUFFER, /* #1093 */
- "mean", "a number, a list of numbers or a buffer", make_num(0.0));
+ "mean", "a number, a list of numbers or a buffer", make_float(0.0));
int total = tensor_total(arg);
/* fs:EMPTY nothing to average, and the division below would be 0/0. The
* non-tensor case is gone (guarded above), so this line now carries one
* verdict only: `mean of []` is the empty mean and strict must not raise. */
- if (total == 0) return make_num(0.0);
+ if (total == 0) return make_float(0.0);
double *flat = xcalloc(total, sizeof(double));
int idx = 0;
if (!tensor_flatten_recursive(arg, flat, &idx)) {
@@ -1250,50 +1310,74 @@ Value* builtin_tensor_mean(Value *arg) {
double sum = 0.0;
for (int i = 0; i < total; i++) sum += flat[i];
free(flat);
- return make_num(sum / total);
+ return make_float(sum / total);
}
/* ==== BUILTIN: sum ==== */
/* sum / norm are association-unspecified reductions (see docs/SPEC.md
* "Reductions"): the summation order is not guaranteed, which lets an
* optimizing backend reassociate across SIMD lanes. no-NaN/Inf preserved. */
-Value* builtin_tensor_sum(Value *arg) {
- if (arg && arg->type == VAL_BUFFER) {
- int n = arg->data.buffer.count;
- double s = 0.0;
- for (int i = 0; i < n; i++) {
- double x = buffer_read_num(arg, i);
- if (g_has_error) return make_null();
- s = num_guard(s + x);
- }
- return make_num(s);
+/* C2/C4 and (A): decide the reduction kind before adding. A later float
+ * makes the whole reduction floating, so an int prefix must not overflow. */
+static int tensor_sum_has_float(Value *v) {
+ if (!v) return 0;
+ /* tensor_total previously checked every bool before any accumulation.
+ * Preserve that validation order while deciding the numeric kind. */
+ if (v->type == VAL_BOOL && !g_has_error)
+ rt_error(EK_TYPE, 0, "cannot use a bool as a number (branch on it: `if b:`)");
+ if (v->type == VAL_FLOAT || v->type == VAL_BUFFER) return 1;
+ if (v->type != VAL_LIST) return 0;
+ int has_float = 0;
+ for (int i = 0; i < v->data.list.count; i++) {
+ EIGS_VIEW(child);
+ has_float |= tensor_sum_has_float(list_get_view(v, i, &child));
}
- /* Split from the empty case below. `tensor_total` answers 0 for an empty
- * list AND for any non-tensor — a string, a dict, a function — so one
- * `total == 0` line was carrying two opposite verdicts: `sum of []`
- * is the identity, `sum of "hello"` is a laundered type mistake. The
- * mixed line could not be classified honestly (a blind review caught it
- * tagged fs:EMPTY, which would have blessed the laundering permanently),
- * so the type half is hoisted out. Non-strict is byte-identical: both
- * halves still answer 0.0. Closes the main half of #1008. */
- ARG_GUARD(arg && arg->type != VAL_NUM && arg->type != VAL_LIST
- && arg->type != VAL_BUFFER, /* #1093 */
- "sum", "a number, a list of numbers or a buffer", make_num(0.0));
- int total = tensor_total(arg);
- /* fs:EMPTY 0.0 is the additive identity this loop would accumulate over
- * zero elements. The non-tensor case is guarded above, so `sum of []` is
- * the only reading left here. */
- if (total == 0) return make_num(0.0);
- double *flat = xcalloc(total, sizeof(double));
- int idx = 0;
- if (!tensor_flatten_recursive(arg, flat, &idx)) {
- free(flat);
- return make_null();
+ return has_float;
+}
+
+static int tensor_sum_into(Value *v, EigsNum *sum) {
+ if (!v) return 1;
+ if (val_is_number(v)) {
+ EigsNum n = eigs_number_arg(v, "sum");
+ if (g_has_error) return 0;
+ if (sum->is_int) {
+ int64_t result;
+ if (__builtin_add_overflow(sum->value.i64, n.value.i64, &result)) {
+ rt_error(EK_VALUE, 0, "sum: int64 overflow in '+'");
+ return 0;
+ }
+ sum->value.i64 = result;
+ } else {
+ double d = n.is_int ? (double)n.value.i64 : n.value.f64;
+ sum->value.f64 = num_guard(sum->value.f64 + d);
+ if (g_has_error) return 0;
+ }
+ } else if (v->type == VAL_BUFFER) {
+ for (int i = 0; i < v->data.buffer.count; i++) {
+ double d = buffer_read_f64(v, i);
+ if (g_has_error) return 0;
+ sum->value.f64 = num_guard(sum->value.f64 + d);
+ if (g_has_error) return 0;
+ }
+ } else if (v->type == VAL_LIST) {
+ for (int i = 0; i < v->data.list.count; i++) {
+ EIGS_VIEW(child);
+ if (!tensor_sum_into(list_get_view(v, i, &child), sum)) return 0;
+ }
}
- double sum = 0.0;
- for (int i = 0; i < total; i++) sum = num_guard(sum + flat[i]);
- free(flat);
- return make_num(sum);
+ return 1;
+}
+
+Value* builtin_tensor_sum(Value *arg) {
+ ARG_GUARD(arg && !val_is_number(arg) && arg->type != VAL_LIST && arg->type != VAL_BUFFER,
+ "sum", "a number, a list of numbers or a buffer", make_float(0.0));
+ EigsNum sum = { .is_int = !tensor_sum_has_float(arg), .value = { .i64 = 0 } };
+ if (g_has_error) return make_null();
+ /* Explicitly initialize the active union arm rather than assuming all-bits
+ * zero is the floating identity on every target. */
+ if (!sum.is_int) sum.value.f64 = 0.0;
+ if (!tensor_sum_into(arg, &sum)) return make_null();
+ return sum.is_int ? make_int(sum.value.i64) : make_float(sum.value.f64);
}
/* norm of a → L2 (Euclidean) norm = sqrt(sum_i a[i]^2). Buffers and tensors. */
@@ -1302,11 +1386,11 @@ Value* builtin_tensor_norm(Value *arg) {
int n = arg->data.buffer.count;
double s = 0.0;
for (int i = 0; i < n; i++) {
- double x = buffer_read_num(arg, i);
+ double x = buffer_read_f64(arg, i);
if (g_has_error) return make_null();
s = num_guard(s + num_guard(x * x));
}
- return make_num(num_guard(sqrt(s)));
+ return make_float(num_guard(sqrt(s)));
}
/* Split from the empty case below. `tensor_total` answers 0 for an empty
* list AND for any non-tensor — a string, a dict, a function — so one
@@ -1316,13 +1400,13 @@ Value* builtin_tensor_norm(Value *arg) {
* tagged fs:EMPTY, which would have blessed the laundering permanently),
* so the type half is hoisted out. Non-strict is byte-identical: both
* halves still answer 0.0. Closes the main half of #1008. */
- ARG_GUARD(arg && arg->type != VAL_NUM && arg->type != VAL_LIST
+ ARG_GUARD(arg && !val_is_number(arg) && arg->type != VAL_LIST
&& arg->type != VAL_BUFFER, /* #1093 */
- "norm", "a number, a list of numbers or a buffer", make_num(0.0));
+ "norm", "a number, a list of numbers or a buffer", make_float(0.0));
int total = tensor_total(arg);
/* fs:EMPTY the L2 norm over zero elements is sqrt(0) = 0, exactly what the
* loop below would produce. The non-tensor case is guarded above. */
- if (total == 0) return make_num(0.0);
+ if (total == 0) return make_float(0.0);
double *flat = xcalloc(total, sizeof(double));
int idx = 0;
if (!tensor_flatten_recursive(arg, flat, &idx)) {
@@ -1332,7 +1416,7 @@ Value* builtin_tensor_norm(Value *arg) {
double s = 0.0;
for (int i = 0; i < total; i++) s = num_guard(s + num_guard(flat[i] * flat[i]));
free(flat);
- return make_num(num_guard(sqrt(s)));
+ return make_float(num_guard(sqrt(s)));
}
/* #292: bytes a list-of-fresh-numbers tensor of `n` elements costs — one Value
@@ -1353,8 +1437,9 @@ Value* builtin_tensor_zeros(Value *arg) {
* the list cost a Value per element on the VM and 12.4x on the AOT
* (ouroboros#170). make_shaped_buffer carries the sandbox charge, which
* is now 8 bytes/element instead of TENSOR_LIST_ELEM_BYTES. */
- if (arg->type == VAL_NUM) {
- int64_t n64 = (int64_t)eigs_num_arg(arg, __func__);
+ if (val_is_number(arg)) {
+ int64_t n64 = eigs_int_arg(arg, "zeros");
+ if (g_has_error) return make_null();
if (n64 < 0) n64 = 0;
if (n64 > EIGS_TENSOR_MAX_ELEMENTS) n64 = EIGS_TENSOR_MAX_ELEMENTS; /* #292: cap like fill/buffer (was uncapped → x_oom/abort) */
Value *out = make_shaped_buffer(0, (int)n64);
@@ -1362,10 +1447,11 @@ Value* builtin_tensor_zeros(Value *arg) {
}
/* zeros of [rows, cols] → 2D */
if (arg->type == VAL_LIST && arg->data.list.count >= 2
- && list_elem_type(arg, 0) == VAL_NUM
- && list_elem_type(arg, 1) == VAL_NUM) {
- int64_t rows64 = (int64_t)eigs_list_num(arg, 0, __func__);
- int64_t cols64 = (int64_t)eigs_list_num(arg, 1, __func__);
+ && (list_elem_type(arg, 0) == VAL_FLOAT || list_elem_type(arg, 0) == VAL_INT)
+ && (list_elem_type(arg, 1) == VAL_FLOAT || list_elem_type(arg, 1) == VAL_INT)) {
+ int64_t rows64 = eigs_list_int(arg, 0, "zeros");
+ int64_t cols64 = eigs_list_int(arg, 1, "zeros");
+ if (g_has_error) return make_null();
if (rows64 < 0) rows64 = 0;
if (cols64 < 0) cols64 = 0;
/* #292: bound each dim before multiplying (no int64 overflow), then the
@@ -1378,7 +1464,7 @@ Value* builtin_tensor_zeros(Value *arg) {
Value *outer = make_list(rows);
for (int r = 0; r < rows; r++) {
Value *row = make_list(cols);
- for (int c = 0; c < cols; c++) list_append_owned(row, make_num(0.0));
+ for (int c = 0; c < cols; c++) list_append_owned(row, make_float(0.0));
list_append_owned(outer, row);
}
return outer;
@@ -1393,7 +1479,7 @@ Value* builtin_tensor_zeros_like(Value *arg) {
/* fs:LITERAL a number is a valid argument and this IS the value being
* constructed — the zero of the same shape, which for a scalar is 0.0.
* It is the scalar leaf of the recursion below, not a guard. */
- if (arg->type == VAL_NUM) return make_num(0.0);
+ if (val_is_number(arg)) return make_float(0.0);
if (arg->type == VAL_LIST) {
Value *out = make_list(arg->data.list.count);
for (int i = 0; i < arg->data.list.count; i++) {
@@ -1410,12 +1496,11 @@ Value* builtin_tensor_zeros_like(Value *arg) {
}
/* Every shape zeros_like can mirror exits above, so `arg` is none of them
* (a string, a dict, a function). */
- ARG_GUARD(1, "zeros_like", "a number, a list or a buffer", make_num(0.0));
+ ARG_GUARD(1, "zeros_like", "a number, a list or a buffer", make_float(0.0));
}
-/* #1093: an index vector is a flat numeric tensor, so it may be a list or a
- * buffer. A non-numeric list element reads as -1, the out-of-range sentinel
- * the index loops already skip on. */
+/* Index vectors accept lists and buffers. N5 checks exactness before the
+ * caller applies its bounds policy; retain int64 until that check. */
static int flat_count(Value *v) {
if (!v) return 0;
if (v->type == VAL_LIST) return v->data.list.count;
@@ -1425,24 +1510,18 @@ static int flat_count(Value *v) {
static int flat_is_vector(Value *v) {
return v && (v->type == VAL_LIST || v->type == VAL_BUFFER);
}
-static int flat_index_at(Value *v, int i) {
+static int64_t flat_index_at(Value *v, int i, const char *who) {
if (v->type == VAL_LIST)
- return (list_elem_type(v, i) == VAL_NUM)
- ? (int)eigs_slot_num(list_slot(v, i), __func__) : -1;
- /* #1417: normalize before conversion, and let every caller propagate a
- * raised read before using its index or touching an output. Saturated
- * infinity is still outside the int domain and uses the existing invalid
- * index sentinel. Ordinary representable finite indices keep truncating. */
- double index = buffer_read_num(v, i);
- if (g_has_error) return -1;
- if (index < (double)INT_MIN || index > (double)INT_MAX) return -1;
- return (int)index;
+ return eigs_slot_int(list_slot(v, i), who);
+ int64_t index = 0;
+ if (!buffer_index_at(v, i, &index, who)) return 0;
+ return index;
}
/* #973: a non-numeric element of an index LIST has no index, and reporting it
* as "index -1 out of range" would name the wrong fault. Buffers hold doubles,
* so every element is a number by construction. */
static int flat_index_is_num(Value *v, int i) {
- return v->type != VAL_LIST || list_elem_type(v, i) == VAL_NUM;
+ return v->type != VAL_LIST || (list_elem_type(v, i) == VAL_FLOAT || list_elem_type(v, i) == VAL_INT);
}
/* Validate and copy selectors in one pass. Sparse writers and numerical-loss
@@ -1460,18 +1539,18 @@ static int snapshot_flat_indices(Value *indices, int limit, const char *name,
free(snapshot);
return 0;
}
- int index = flat_index_at(indices, i);
+ int64_t index = flat_index_at(indices, i, name);
if (g_has_error) {
free(snapshot);
return 0;
}
if (index < 0 || index >= limit) {
- rt_error(EK_INDEX, 0, "%s: %s index %d out of range (length %d)",
- name, axis, index, limit);
+ rt_error(EK_INDEX, 0, "%s: %s index %lld out of range (length %d)",
+ name, axis, (long long)index, limit);
free(snapshot);
return 0;
}
- snapshot[i] = index;
+ snapshot[i] = (int)index;
}
*snapshot_out = snapshot;
*count_out = count;
@@ -1537,14 +1616,15 @@ Value* builtin_tensor_gather(Value *arg) {
* row selects one element per row and yields a buffer; an unshaped (1-D)
* buffer with a scalar index yields that element. */
if (tensor->type == VAL_BUFFER) {
- if (indices->type == VAL_NUM && tensor->data.buffer.rows == 0) {
- int idx = (int)eigs_num_arg(indices, __func__);
+ if (val_is_number(indices) && tensor->data.buffer.rows == 0) {
+ int64_t idx = eigs_int_arg(indices, "gather");
+ if (g_has_error) return make_null();
if (idx < 0 || idx >= tensor->data.buffer.count) {
- rt_error(EK_INDEX, 0, "gather: index %d out of range (length %d)",
- idx, tensor->data.buffer.count);
+ rt_error(EK_INDEX, 0, "gather: index %lld out of range (length %d)",
+ (long long)idx, tensor->data.buffer.count);
return make_null();
}
- return make_num(buffer_read_num(tensor, idx));
+ return make_float(buffer_read_f64(tensor, idx));
} else if (flat_is_vector(indices)) {
int shaped = tensor->data.buffer.rows > 0;
int rows = shaped ? tensor->data.buffer.rows : tensor->data.buffer.count;
@@ -1561,13 +1641,13 @@ Value* builtin_tensor_gather(Value *arg) {
i, val_type_name(list_elem_type(indices, i)));
return make_null();
}
- int idx = flat_index_at(indices, i);
+ int64_t idx = flat_index_at(indices, i, "gather");
if (g_has_error) { val_decref(out); return make_null(); }
if (idx < 0 || idx >= cols) {
val_decref(out);
rt_error(EK_INDEX, 0,
- "gather: column index %d out of range for row %d (cols %d)",
- idx, i, cols);
+ "gather: column index %lld out of range for row %d (cols %d)",
+ (long long)idx, i, cols);
return make_null();
}
out->data.buffer.data[i] = tensor->data.buffer.data[(int64_t)i * cols + idx];
@@ -1587,46 +1667,48 @@ Value* builtin_tensor_gather(Value *arg) {
/* #1637: a bool where a row belongs raises, in every mode. */
if (row_t == VAL_BOOL) {
val_decref(out);
- eigs_num_arg_slow(list_get_ref(tensor, i), "gather");
+ eigs_float_arg(list_get_ref(tensor, i), "gather");
return make_null();
}
- list_append_owned(out, make_num(0.0));
+ list_append_owned(out, make_float(0.0));
continue;
}
Value *row = list_get_ref(tensor, i);
- if (list_elem_type(indices, i) != VAL_NUM) {
+ if ((list_elem_type(indices, i) != VAL_FLOAT && list_elem_type(indices, i) != VAL_INT)) {
val_decref(out);
rt_error(EK_TYPE, 0, "gather: index %d is %s (expected a number)",
i, val_type_name(list_elem_type(indices, i)));
return make_null();
}
- int idx = (int)eigs_slot_num(list_slot(indices, i), __func__);
+ int64_t idx = eigs_slot_int(list_slot(indices, i), "gather");
+ if (g_has_error) { val_decref(out); return make_null(); }
if (idx < 0 || idx >= row->data.list.count) {
val_decref(out);
rt_error(EK_INDEX, 0,
- "gather: column index %d out of range for row %d (cols %d)",
- idx, i, row->data.list.count);
+ "gather: column index %lld out of range for row %d (cols %d)",
+ (long long)idx, i, row->data.list.count);
return make_null();
}
/* #1637 round 4: the selected cell is read as a number -- a
* bool there raised nothing and read as 0 (critic r3). */
- double cell = eigs_slot_elem_num(list_slot(row, idx), "gather");
+ EigsNum cell = eigs_slot_elem_number(list_slot(row, (int)idx), "gather");
if (g_has_error) { val_decref(out); return make_null(); }
- list_append_owned(out, make_num(cell));
+ list_append_owned(out, cell.is_int ? make_int(cell.value.i64) : make_float(cell.value.f64));
}
return out;
}
/* 1D tensor, scalar index */
- if (tensor->type == VAL_LIST && indices->type == VAL_NUM) {
- int idx = (int)eigs_num_arg(indices, __func__);
+ if (tensor->type == VAL_LIST && val_is_number(indices)) {
+ int64_t idx = eigs_int_arg(indices, "gather");
+ if (g_has_error) return make_null();
if (idx < 0 || idx >= tensor->data.list.count) {
- rt_error(EK_INDEX, 0, "gather: index %d out of range (length %d)",
- idx, tensor->data.list.count);
+ rt_error(EK_INDEX, 0, "gather: index %lld out of range (length %d)",
+ (long long)idx, tensor->data.list.count);
return make_null();
}
- double cell = eigs_slot_elem_num(list_slot(tensor, idx), "gather"); /* #1637 round 4 */
+ EigsNum cell = eigs_slot_elem_number(list_slot(tensor, (int)idx), "gather"); /* #1637 round 4 */
if (g_has_error) return make_null();
- return make_num(cell);
+ return cell.is_int ? make_int(cell.value.i64) : make_float(cell.value.f64);
}
/* The fs:TODO #971 left here is resolved by the raise above: the two
* readings that shared this line are separated. Out-of-range no longer
@@ -1635,8 +1717,9 @@ Value* builtin_tensor_gather(Value *arg) {
* neither a vector nor a number — and it converts to ARG_GUARD like every
* other wrong-type case: a raise in strict mode (the default, #1361),
* 0.0 under EIGS_STRICT=0. */
- ARG_GUARD(1, "gather", "a tensor and an index or index vector", make_num(0.0));
- return make_null();
+ ARG_GUARD(1, "gather", "a tensor and an index or index vector", make_float(0.0));
+ return make_null(); /* unreachable: the guard above always returns, but
+ * -O0 does not fold its constant test (#1775) */
}
/* ==== Helper: call a user-defined EigenScript function from C ====
@@ -1726,59 +1809,44 @@ Value* call_eigs_fn(Value *fn, Value *arg) {
/* random_normal of [rows, cols, scale] → 2D, or random_normal of [len, scale] → 1D */
Value* builtin_random_normal(Value *arg) {
STRICT_LIST_MAX(arg, 3, "random_normal");
- BOOL_REFUSE(arg, "random_normal"); /* before the tape: a type error is not recorded */
- /* #1750: argument validation is the deterministic, fail-fast half of the
- * call and therefore belongs before the nondeterministic tape boundary.
- * eigs_list_num used to discover a bad dimension/scale after TAKE; the
- * live run then recorded the partially constructed stand-in and strict
- * replay returned that value instead of raising. Validate every element
- * the selected overload will read before a replay can consume a record. */
- if (arg && arg->type == VAL_LIST &&
- (arg->data.list.count == 2 || arg->data.list.count == 3)) {
- for (int i = 0; i < arg->data.list.count; i++) {
- if (list_elem_type(arg, i) != VAL_NUM) {
- (void)eigs_list_num(arg, i, __func__);
- return make_null();
- }
- }
+ /* #1750: every dimension and scale reader precedes the tape seam. */
+ BOOL_REFUSE(arg, "random_normal");
+ int argc = arg && arg->type == VAL_LIST ? arg->data.list.count : 0;
+ int rows = 0, cols = 0;
+ double scale = 1.0;
+ if (argc == 2 || argc == 3) {
+ int64_t r, c = 1;
+ if (!eigs_list_int_range(arg, 0, INT_MIN, INT_MAX, &r, "random_normal"))
+ return make_null();
+ if (argc == 3 && !eigs_list_int_range(arg, 1, INT_MIN, INT_MAX, &c, "random_normal"))
+ return make_null();
+ scale = eigs_list_float(arg, argc - 1, "random_normal");
+ if (g_has_error) return make_null();
+ rows = (int)r;
+ cols = (int)c;
}
TRACE_NONDET_TAKE("random_normal");
- if (!arg || arg->type != VAL_LIST) TRACE_NONDET_RECORD("random_normal", make_null());
- /* #960: draw from the shared drand48 stream that `seed_random` pins, not
- * libc generator (formerly seeded only by main from the clock, so a
- * randn-initialised tensor was unreproducible from script). After the TAKE
- * above: a replayed call serves its record without touching the stream. */
- int argc = arg->data.list.count;
if (argc == 3) {
- /* 2D: [rows, cols, scale] */
- int rows = (int)eigs_list_num(arg, 0, __func__);
- int cols = (int)eigs_list_num(arg, 1, __func__);
- double scale = eigs_list_num(arg, 2, __func__);
Value *outer = make_list(rows);
for (int r = 0; r < rows; r++) {
Value *row = make_list(cols);
for (int c = 0; c < cols; c++) {
- /* Box-Muller. 1 - drand48() lands in (0, 1]: drand48 can
- * return exactly 0, and log(0) is an infinity. */
double u1 = 1.0 - eigs_random_double();
double u2 = eigs_random_double();
double z = sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2);
- list_append_owned(row, make_num(z * scale));
+ list_append_owned(row, make_float(z * scale));
}
list_append_owned(outer, row);
}
TRACE_NONDET_RECORD("random_normal", outer);
}
if (argc == 2) {
- /* 1D: [len, scale] */
- int len = (int)eigs_list_num(arg, 0, __func__);
- double scale = eigs_list_num(arg, 1, __func__);
- Value *out = make_list(len);
- for (int i = 0; i < len; i++) {
- double u1 = 1.0 - eigs_random_double(); /* (0, 1] — see the 2D branch */
+ Value *out = make_list(rows);
+ for (int i = 0; i < rows; i++) {
+ double u1 = 1.0 - eigs_random_double();
double u2 = eigs_random_double();
double z = sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2);
- list_append_owned(out, make_num(z * scale));
+ list_append_owned(out, make_float(z * scale));
}
TRACE_NONDET_RECORD("random_normal", out);
}
@@ -1789,7 +1857,7 @@ Value* builtin_random_normal(Value *arg) {
/* shape of tensor → [rows, cols] for 2D, [len] for 1D */
Value* builtin_tensor_shape(Value *arg) {
if (!arg) return make_null();
- if (arg->type == VAL_NUM) {
+ if (val_is_number(arg)) {
Value *out = make_list(0);
return out; /* scalar: empty shape */
}
@@ -1797,18 +1865,18 @@ Value* builtin_tensor_shape(Value *arg) {
/* shaped buffer -> [rows, cols]; unshaped -> [count] */
if (arg->data.buffer.rows > 0) {
Value *out = make_list(2);
- list_append_owned(out, make_num(arg->data.buffer.rows));
- list_append_owned(out, make_num(arg->data.buffer.cols));
+ list_append_owned(out, make_int(arg->data.buffer.rows));
+ list_append_owned(out, make_int(arg->data.buffer.cols));
return out;
}
Value *out = make_list(1);
- list_append_owned(out, make_num(arg->data.buffer.count));
+ list_append_owned(out, make_int(arg->data.buffer.count));
return out;
}
if (arg->type != VAL_LIST) return make_null();
if (arg->data.list.count == 0) {
Value *out = make_list(1);
- list_append_owned(out, make_num(0));
+ list_append_owned(out, make_int(0));
return out;
}
if (list_elem_type(arg, 0) == VAL_LIST) {
@@ -1816,13 +1884,13 @@ Value* builtin_tensor_shape(Value *arg) {
Value *first = list_get_view(arg, 0, &first_view);
/* 2D */
Value *out = make_list(2);
- list_append_owned(out, make_num(arg->data.list.count));
- list_append_owned(out, make_num(first->data.list.count));
+ list_append_owned(out, make_int(arg->data.list.count));
+ list_append_owned(out, make_int(first->data.list.count));
return out;
}
/* 1D */
Value *out = make_list(1);
- list_append_owned(out, make_num(arg->data.list.count));
+ list_append_owned(out, make_int(arg->data.list.count));
return out;
}
@@ -1831,8 +1899,8 @@ static double numerical_loss(Value *loss_fn, Value *arg, const char *who,
int *loss_valid) {
if (!*loss_valid) return 0.0;
Value *loss = call_eigs_fn(loss_fn, arg);
- if (loss && loss->type == VAL_NUM) {
- double result = eigs_num_arg(loss, __func__);
+ if (loss && val_is_number(loss)) {
+ double result = eigs_float_arg(loss, who);
val_decref(loss);
return result;
}
@@ -1850,13 +1918,13 @@ static double numerical_loss(Value *loss_fn, Value *arg, const char *who,
* was just perturbed. For matrix rows, also validate the parent shape and
* identity. The caller retains container so restoration never targets a
* callback's replacement row. */
-/* Perturb one cell and return the value actually STORED there. list_set_num
+/* Perturb one cell and return the value actually STORED there. list_set_f64
* normalizes through num_guard (it clamps beyond +-EIGS_NUM_MAX), so the failure
* path must compare the cell with what was stored, not with old_val +- eps. */
static double gradient_perturb(Value *container, int index, double value) {
- list_set_num(container, index, value);
+ list_set_f64(container, index, value);
double stored = value;
- list_get_num(container, index, &stored);
+ list_get_f64(container, index, &stored);
return stored;
}
@@ -1880,8 +1948,8 @@ static int gradient_container_unchanged(Value *container, int expected_len,
double current_val;
if (container && container->type == VAL_LIST && index >= 0 &&
index < container->data.list.count &&
- list_get_num(container, index, ¤t_val) && current_val == written_val)
- list_set_num(container, index, old_val);
+ list_get_f64(container, index, ¤t_val) && current_val == written_val)
+ list_set_f64(container, index, old_val);
rt_error(EK_VALUE, 0, "param mutated during gradient");
return 0;
}
@@ -1908,12 +1976,12 @@ static int tensor_cells_numeric(const Value *v, const char *who) {
const Value *e = et == VAL_LIST ? list_get_ref(v, i) : NULL;
if (et == VAL_LIST) {
for (int c = 0; c < e->data.list.count; c++)
- if (list_elem_type(e, c) != VAL_NUM) {
- eigs_slot_num(list_slot(e, c), who);
+ if ((list_elem_type(e, c) != VAL_FLOAT && list_elem_type(e, c) != VAL_INT)) {
+ eigs_slot_float(list_slot(e, c), who);
return 0;
}
- } else if (et != VAL_NUM) {
- eigs_slot_num(list_slot(v, i), who);
+ } else if ((et != VAL_FLOAT && et != VAL_INT)) {
+ eigs_slot_float(list_slot(v, i), who);
return 0;
}
}
@@ -1930,7 +1998,7 @@ Value* builtin_numerical_grad(Value *arg) {
/* #1637: the optional eps: null keeps the default, a number sets it,
* anything else (a bool) raises instead of silently meaning the default. */
double eps = 0.001;
- eigs_slot_opt_num(list_slot(arg, 2), &eps, __func__);
+ eigs_slot_opt_float(list_slot(arg, 2), &eps, __func__);
if (g_has_error) return make_null();
if (!tensor_cells_numeric(param, "numerical_grad")) return make_null();
if (eps <= 0) eps = 0.001;
@@ -1966,7 +2034,7 @@ Value* builtin_numerical_grad(Value *arg) {
int len = param->data.list.count;
Value *grad = make_list(len);
for (int i = 0; i < len; i++) {
- double old_val = eigs_slot_num(list_slot(param, i), __func__);
+ double old_val = eigs_slot_float(list_slot(param, i), __func__);
double written = gradient_perturb(param, i, old_val + eps);
double lp = numerical_loss(loss_fn, nul, "numerical_grad", &loss_valid);
if (!gradient_container_unchanged(param, len, NULL, 0, -1, i, old_val, written))
@@ -1975,9 +2043,9 @@ Value* builtin_numerical_grad(Value *arg) {
double lm = numerical_loss(loss_fn, nul, "numerical_grad", &loss_valid);
if (!gradient_container_unchanged(param, len, NULL, 0, -1, i, old_val, written))
goto list_grad_failed;
- list_set_num(param, i, old_val);
+ list_set_f64(param, i, old_val);
/* Central difference */
- list_append_owned(grad, make_num((lp - lm) / (2.0 * eps)));
+ list_append_owned(grad, make_float((lp - lm) / (2.0 * eps)));
}
val_decref(nul);
if (!loss_valid) { val_decref(grad); return make_null(); }
@@ -1999,7 +2067,7 @@ Value* builtin_numerical_grad(Value *arg) {
Value *grad_row = make_list(cols);
unfinished_grad_row = grad_row;
for (int c = 0; c < cols; c++) {
- double old_val = eigs_slot_num(list_slot(row, c), __func__);
+ double old_val = eigs_slot_float(list_slot(row, c), __func__);
val_incref(row);
double written = gradient_perturb(row, c, old_val + eps);
double lp = numerical_loss(loss_fn, nul, "numerical_grad", &loss_valid);
@@ -2011,9 +2079,9 @@ Value* builtin_numerical_grad(Value *arg) {
if (!gradient_container_unchanged(row, cols, param, rows, r, c, old_val, written)) {
val_decref(row); goto matrix_grad_failed;
}
- list_set_num(row, c, old_val);
+ list_set_f64(row, c, old_val);
val_decref(row);
- list_append_owned(grad_row, make_num((lp - lm) / (2.0 * eps)));
+ list_append_owned(grad_row, make_float((lp - lm) / (2.0 * eps)));
}
list_append_owned(grad, grad_row);
unfinished_grad_row = NULL;
@@ -2040,7 +2108,7 @@ Value* builtin_sgd_update(Value *arg) {
/* #1637: the optional lr: null keeps the default, a number sets it,
* anything else (a bool) raises instead of silently meaning the default. */
double lr = 0.01;
- eigs_slot_opt_num(list_slot(arg, 2), &lr, __func__);
+ eigs_slot_opt_float(list_slot(arg, 2), &lr, __func__);
if (g_has_error) return make_null();
if (!tensor_cells_numeric(param, "sgd_update") || !tensor_cells_numeric(grad, "sgd_update")) return make_null();
@@ -2061,9 +2129,9 @@ Value* builtin_sgd_update(Value *arg) {
int len = param->data.list.count < grad->data.list.count
? param->data.list.count : grad->data.list.count;
for (int i = 0; i < len; i++) {
- double pv = eigs_slot_num(list_slot(param, i), __func__);
- double gv = eigs_slot_num(list_slot(grad, i), __func__);
- list_set_num(param, i, pv - lr * gv);
+ double pv = eigs_slot_float(list_slot(param, i), __func__);
+ double gv = eigs_slot_float(list_slot(grad, i), __func__);
+ list_set_f64(param, i, pv - lr * gv);
}
} else {
/* 2D */
@@ -2076,9 +2144,9 @@ Value* builtin_sgd_update(Value *arg) {
int cols = pr->data.list.count < gr->data.list.count
? pr->data.list.count : gr->data.list.count;
for (int c = 0; c < cols; c++) {
- double pv = eigs_slot_num(list_slot(pr, c), __func__);
- double gv = eigs_slot_num(list_slot(gr, c), __func__);
- list_set_num(pr, c, pv - lr * gv);
+ double pv = eigs_slot_float(list_slot(pr, c), __func__);
+ double gv = eigs_slot_float(list_slot(gr, c), __func__);
+ list_set_f64(pr, c, pv - lr * gv);
}
}
}
@@ -2102,7 +2170,7 @@ Value* builtin_numerical_grad_rows(Value *arg) {
/* #1637: the optional eps: null keeps the default, a number sets it,
* anything else (a bool) raises instead of silently meaning the default. */
double eps = 0.001;
- eigs_slot_opt_num(list_slot(arg, 3), &eps, __func__);
+ eigs_slot_opt_float(list_slot(arg, 3), &eps, __func__);
if (g_has_error) return make_null();
if (!tensor_cells_numeric(matrix, "numerical_grad_rows") || !tensor_cells_numeric(row_indices, "numerical_grad_rows")) return make_null();
if (eps <= 0) eps = 0.001;
@@ -2155,7 +2223,7 @@ Value* builtin_numerical_grad_rows(Value *arg) {
for (int r = 0; r < rows; r++) {
Value *grow = make_list(cols);
for (int c = 0; c < cols; c++)
- list_append_owned(grow, make_num(0.0));
+ list_append_owned(grow, make_float(0.0));
list_append_owned(grad, grow);
}
/* #1722: make_list caps capacity at its 8-slot fallback when the sandbox
@@ -2176,7 +2244,7 @@ Value* builtin_numerical_grad_rows(Value *arg) {
Value *grad_row = list_get_ref(grad, r);
for (int c = 0; c < cols && c < row->data.list.count; c++) {
- double old_val = eigs_slot_num(list_slot(row, c), __func__);
+ double old_val = eigs_slot_float(list_slot(row, c), __func__);
val_incref(row);
double written = gradient_perturb(row, c, old_val + eps);
double loss_plus = numerical_loss(loss_fn, nul, "numerical_grad_rows", &loss_valid);
@@ -2188,10 +2256,10 @@ Value* builtin_numerical_grad_rows(Value *arg) {
if (!gradient_container_unchanged(row, row_len, matrix, rows, r, c, old_val, written)) {
val_decref(row); goto rows_grad_failed;
}
- list_set_num(row, c, old_val);
+ list_set_f64(row, c, old_val);
val_decref(row);
/* gradient — release the zero placeholder this slot held */
- list_set_num(grad_row, c, (loss_plus - loss_minus) / (2.0 * eps));
+ list_set_f64(grad_row, c, (loss_plus - loss_minus) / (2.0 * eps));
}
}
free(row_snapshot);
@@ -2220,7 +2288,7 @@ Value* builtin_sgd_update_rows(Value *arg) {
/* #1637: the optional lr: null keeps the default, a number sets it,
* anything else (a bool) raises instead of silently meaning the default. */
double lr = 0.01;
- eigs_slot_opt_num(list_slot(arg, 3), &lr, __func__);
+ eigs_slot_opt_float(list_slot(arg, 3), &lr, __func__);
if (g_has_error) return make_null();
if (!tensor_cells_numeric(matrix, "sgd_update_rows") || !tensor_cells_numeric(grad, "sgd_update_rows") || !tensor_cells_numeric(row_indices, "sgd_update_rows")) return make_null();
@@ -2264,9 +2332,9 @@ Value* builtin_sgd_update_rows(Value *arg) {
int cols = mrow->data.list.count < grow->data.list.count
? mrow->data.list.count : grow->data.list.count;
for (int c = 0; c < cols; c++) {
- double pv = eigs_slot_num(list_slot(mrow, c), __func__);
- double gv = eigs_slot_num(list_slot(grow, c), __func__);
- list_set_num(mrow, c, pv - lr * gv);
+ double pv = eigs_slot_float(list_slot(mrow, c), __func__);
+ double gv = eigs_slot_float(list_slot(grow, c), __func__);
+ list_set_f64(mrow, c, pv - lr * gv);
}
}
free(row_snapshot);
@@ -2290,7 +2358,7 @@ Value* builtin_numerical_grad_cols(Value *arg) {
/* #1637: the optional eps: null keeps the default, a number sets it,
* anything else (a bool) raises instead of silently meaning the default. */
double eps = 0.001;
- eigs_slot_opt_num(list_slot(arg, 3), &eps, __func__);
+ eigs_slot_opt_float(list_slot(arg, 3), &eps, __func__);
if (g_has_error) return make_null();
if (!tensor_cells_numeric(matrix, "numerical_grad_cols") || !tensor_cells_numeric(col_indices, "numerical_grad_cols")) return make_null();
if (eps <= 0) eps = 0.001;
@@ -2346,7 +2414,7 @@ Value* builtin_numerical_grad_cols(Value *arg) {
for (int r = 0; r < rows; r++) {
Value *grow = make_list(cols);
for (int c = 0; c < cols; c++)
- list_append_owned(grow, make_num(0.0));
+ list_append_owned(grow, make_float(0.0));
list_append_owned(grad, grow);
}
/* #1722: see numerical_grad_rows — a refused make_list caps grad/grad_row
@@ -2363,7 +2431,7 @@ Value* builtin_numerical_grad_cols(Value *arg) {
if (!row || row->type != VAL_LIST || col >= row->data.list.count) continue;
int row_len = row->data.list.count;
- double old_val = eigs_slot_num(list_slot(row, col), __func__);
+ double old_val = eigs_slot_float(list_slot(row, col), __func__);
val_incref(row);
/* +eps */
double written = gradient_perturb(row, col, old_val + eps);
@@ -2378,11 +2446,11 @@ Value* builtin_numerical_grad_cols(Value *arg) {
val_decref(row); goto cols_grad_failed;
}
/* restore */
- list_set_num(row, col, old_val);
+ list_set_f64(row, col, old_val);
val_decref(row);
/* gradient — release the zero placeholder this slot held */
Value *grad_row = list_get_ref(grad, r);
- list_set_num(grad_row, col, (loss_plus - loss_minus) / (2.0 * eps));
+ list_set_f64(grad_row, col, (loss_plus - loss_minus) / (2.0 * eps));
}
}
free(col_snapshot);
@@ -2411,7 +2479,7 @@ Value* builtin_sgd_update_cols(Value *arg) {
/* #1637: the optional lr: null keeps the default, a number sets it,
* anything else (a bool) raises instead of silently meaning the default. */
double lr = 0.01;
- eigs_slot_opt_num(list_slot(arg, 3), &lr, __func__);
+ eigs_slot_opt_float(list_slot(arg, 3), &lr, __func__);
if (g_has_error) return make_null();
if (!tensor_cells_numeric(matrix, "sgd_update_cols") || !tensor_cells_numeric(grad, "sgd_update_cols") || !tensor_cells_numeric(col_indices, "sgd_update_cols")) return make_null();
@@ -2467,9 +2535,9 @@ Value* builtin_sgd_update_cols(Value *arg) {
if (!mrow || mrow->type != VAL_LIST || col >= mrow->data.list.count) continue;
if (!grow || grow->type != VAL_LIST || col >= grow->data.list.count) continue;
- double pv = eigs_slot_num(list_slot(mrow, col), __func__);
- double gv = eigs_slot_num(list_slot(grow, col), __func__);
- list_set_num(mrow, col, pv - lr * gv);
+ double pv = eigs_slot_float(list_slot(mrow, col), __func__);
+ double gv = eigs_slot_float(list_slot(grow, col), __func__);
+ list_set_f64(mrow, col, pv - lr * gv);
}
}
free(col_snapshot);
@@ -2590,8 +2658,8 @@ static void tensor_load_record_verdict(uint32_t rows, uint32_t cols) {
Value *verdict;
if (rows || cols) {
verdict = make_list(2);
- list_append_owned(verdict, make_num((double)rows));
- list_append_owned(verdict, make_num((double)cols));
+ list_append_owned(verdict, make_int((int64_t)rows));
+ list_append_owned(verdict, make_int((int64_t)cols));
} else verdict = make_null();
trace_nondet_value("tensor_load", verdict);
val_decref(verdict);
@@ -2601,7 +2669,7 @@ Value* builtin_tensor_load(Value *arg) {
if (!arg || arg->type != VAL_STR) return make_null();
if (__builtin_expect(g_replay_enabled, 0)) {
- /* Consume and structurally validate the v7 verdict so stream order and
+ /* Consume and structurally validate the v8 verdict so stream order and
* corrupt-tape diagnostics stay intact, but never use it as permission
* to open the payload: tensor bytes themselves were never taped. */
Value *verdict = NULL;
@@ -2610,8 +2678,8 @@ Value* builtin_tensor_load(Value *arg) {
int valid = taken && verdict &&
(verdict->type == VAL_NULL ||
(verdict->type == VAL_LIST && verdict->data.list.count == 2 &&
- list_elem_type(verdict, 0) == VAL_NUM &&
- list_elem_type(verdict, 1) == VAL_NUM));
+ list_elem_type(verdict, 0) == VAL_INT &&
+ list_elem_type(verdict, 1) == VAL_INT));
if (!valid && !g_has_error)
rt_error(EK_IO, 0, "tensor_load: malformed replay verdict");
val_decref(verdict);
@@ -2681,7 +2749,7 @@ Value* builtin_tensor_load(Value *arg) {
free(data); fclose(f); tensor_load_record_verdict(0, 0); return make_null();
}
/* #971: the file is untrusted bytes, so a NaN pattern is reachable
- * here. flat_to_tensor_* would collapse it through make_num anyway
+ * here. flat_to_tensor_* would collapse it through make_float anyway
* (same 0 + EIGS_MATH_INVALID); guarding first lets the strict raise
* name tensor_load. */
for (int i = 0; i < total; i++) {
diff --git a/src/chunk.c b/src/chunk.c
index 049d71e9c..c1b32c539 100644
--- a/src/chunk.c
+++ b/src/chunk.c
@@ -189,14 +189,12 @@ void chunk_patch_jump(EigsChunk *chunk, int offset) {
/* ---- Constant pool ---- */
/* ---- Constant-pool dedup index (#341) ----
- * Equality is EXACTLY the old linear scan's tests, so pools stay
- * byte-identical (the AOT oracle depends on that): NaN never equals
- * itself (each NaN appends), and -0.0 merges with +0.0 (they hash the
- * same and compare ==). Non-NUM/STR constants are never deduped. */
+ * N6 compares float identities by binary64 bits, retaining signed zero
+ * and NaN payloads. Integers and strings keep their existing value keys;
+ * numeric kinds stay distinct. */
static uint32_t const_hash_value(const Value *v) {
- if (v->type == VAL_NUM) {
- double d = VAL_NUM_RAW(v);
- if (d == 0.0) d = 0.0; /* fold -0.0 into +0.0, matching == */
+ if (v->type == VAL_FLOAT) {
+ double d = VAL_F64_RAW(v);
uint64_t bits;
memcpy(&bits, &d, sizeof bits);
bits ^= bits >> 33;
@@ -204,6 +202,13 @@ static uint32_t const_hash_value(const Value *v) {
bits ^= bits >> 33;
return (uint32_t)bits;
}
+ if (v->type == VAL_INT) {
+ uint64_t bits = (uint64_t)VAL_I64_RAW(v);
+ bits ^= bits >> 33;
+ bits *= 0xff51afd7ed558ccdULL;
+ bits ^= bits >> 33;
+ return (uint32_t)bits ^ 0x9e3779b9u; /* kind is part of a constant's key */
+ }
uint32_t h = 2166136261u; /* FNV-1a */
for (const char *p = v->data.str; *p; p++) {
h ^= (unsigned char)*p;
@@ -213,8 +218,13 @@ static uint32_t const_hash_value(const Value *v) {
}
static int const_equal(const Value *a, const Value *b) {
- if (a->type == VAL_NUM && b->type == VAL_NUM)
- return VAL_NUM_RAW(a) == VAL_NUM_RAW(b);
+ if (a->type == VAL_FLOAT && b->type == VAL_FLOAT) {
+ /* N6: constant identity preserves signed zero and every double bit. */
+ double av = VAL_F64_RAW(a), bv = VAL_F64_RAW(b);
+ return memcmp(&av, &bv, sizeof av) == 0;
+ }
+ if (a->type == VAL_INT && b->type == VAL_INT)
+ return VAL_I64_RAW(a) == VAL_I64_RAW(b);
if (a->type == VAL_STR && b->type == VAL_STR)
return strcmp(a->data.str, b->data.str) == 0;
return 0;
@@ -234,7 +244,7 @@ static void const_dedup_grow(EigsChunk *c) {
c->const_dedup_cap = new_cap;
for (int i = 0; i < c->const_count; i++) {
Value *v = c->constants[i];
- if (v->type == VAL_NUM || v->type == VAL_STR)
+ if (val_is_number(v) || v->type == VAL_STR)
const_dedup_insert(c, const_hash_value(v), i);
}
}
@@ -243,7 +253,7 @@ static int chunk_add_constant_ex(EigsChunk *chunk, Value *val, int dedup) {
/* Deduplicate numbers and strings via the hash index — the linear
* scan here was O(pool) per add: 92% of compile time on a 12k-name
* generated program (#341). */
- int hashable = (val->type == VAL_NUM || val->type == VAL_STR);
+ int hashable = (val_is_number(val) || val->type == VAL_STR);
uint32_t h = 0;
if (hashable) {
if ((chunk->const_count + 1) * 2 > chunk->const_dedup_cap)
@@ -340,7 +350,8 @@ const char *op_name(uint8_t op) {
switch ((OpCode)op) {
#define N(o) case o: return &(#o)[3];
N(OP_CONST) N(OP_NULL) N(OP_NUM_ZERO) N(OP_NUM_ONE) N(OP_TRUE) N(OP_FALSE)
- N(OP_ADD) N(OP_SUB) N(OP_MUL) N(OP_DIV) N(OP_MOD)
+ N(OP_INT_ZERO) N(OP_INT_ONE) N(OP_INT_CONST)
+ N(OP_ADD) N(OP_SUB) N(OP_MUL) N(OP_DIV) N(OP_MOD) N(OP_IDIV)
N(OP_BAND) N(OP_BOR) N(OP_BXOR) N(OP_SHL) N(OP_SHR)
N(OP_NEG) N(OP_NOT) N(OP_BNOT)
N(OP_EQ) N(OP_NE) N(OP_LT) N(OP_GT) N(OP_LE) N(OP_GE)
@@ -406,10 +417,15 @@ void chunk_disassemble(EigsChunk *chunk, const char *label) {
for (int k = 0; k < nops && i + 1 < chunk->code_len; k++) {
uint16_t arg = chunk->code[i] | (chunk->code[i + 1] << 8);
fprintf(stderr, " %d", arg);
- if (op == OP_CONST && arg < (uint16_t)chunk->const_count) {
+ if ((op == OP_CONST || op == OP_INT_CONST) && arg < (uint16_t)chunk->const_count) {
Value *v = chunk->constants[arg];
- if (v->type == VAL_NUM)
- fprintf(stderr, " (%.6g)", VAL_NUM_RAW(v));
+ if (v->type == VAL_FLOAT) {
+ char number[32];
+ eigs_num_text(number, sizeof number, VAL_F64_RAW(v));
+ fprintf(stderr, " (%s)", number);
+ }
+ else if (v->type == VAL_INT)
+ fprintf(stderr, " (%lld)", (long long)VAL_I64_RAW(v));
else if (v->type == VAL_STR)
fprintf(stderr, " (\"%s\")", v->data.str);
}
@@ -518,6 +534,7 @@ static StackEffect op_verify_stack_effect(uint8_t op8, int operand0) {
switch ((OpCode)op8) {
/* Pushes, consuming nothing. */
case OP_CONST: case OP_NULL: case OP_NUM_ZERO: case OP_NUM_ONE:
+ case OP_INT_ZERO: case OP_INT_ONE: case OP_INT_CONST:
case OP_TRUE: case OP_FALSE:
case OP_GET_LOCAL: case OP_GET_NAME: case OP_CLOSURE:
case OP_LOCAL_DOT_GET: case OP_LOCAL_IDX_GET: case OP_LOCAL_IDX_DOT_GET:
@@ -554,7 +571,7 @@ static StackEffect op_verify_stack_effect(uint8_t op8, int operand0) {
* DOT_GET, ITER_SETUP and the interrogatives. They still require their
* operands: see the frame-base note above — vm_pop()'s guard is the bottom
* of the whole VM stack, not the bottom of this frame's window. */
- case OP_DIV: case OP_MOD:
+ case OP_DIV: case OP_MOD: case OP_IDIV:
return EFF(2, 2, 1);
case OP_DOT_GET: case OP_ITER_SETUP:
case OP_INTERROGATE: case OP_INTERROGATE_NAMED:
@@ -671,7 +688,7 @@ static int chunk_verify_impl(EigsChunk *chunk, char *why, size_t whyn) {
if (i + 7 > n || code[i + 6] != OP_BINARY_LINE_END) { ok = 0; break; }
uint8_t bin = code[i + 5];
if (!((bin >= OP_ADD && bin <= OP_SHR) ||
- (bin >= OP_EQ && bin <= OP_GE))) { ok = 0; break; }
+ (bin >= OP_EQ && bin <= OP_GE) || bin == OP_IDIV)) { ok = 0; break; }
is_start[i + 5] = is_start[i + 6] = 2;
} else if (op == OP_BINARY_LINE_END && is_start[i] != 2) {
ok = 0; break;
@@ -712,6 +729,9 @@ static int chunk_verify_impl(EigsChunk *chunk, char *why, size_t whyn) {
if (operand >= chunk->const_count) {
WHY("constant index %d out of range at offset %d", operand, i);
ok = 0;
+ } else if (op == OP_INT_CONST && chunk->constants[operand]->type != VAL_INT) {
+ WHY("integer constant index %d is not an int at offset %d", operand, i);
+ ok = 0;
}
break;
case VR_NAME: if (operand >= chunk->const_count) {
@@ -1015,7 +1035,7 @@ void chunk_scan_leaf_accessor(EigsChunk *c) {
case OP_CONST: {
uint16_t idx = (uint16_t)(ip[0] | (ip[1] << 8));
if (idx >= (uint16_t)c->const_count) return;
- if (c->constants[idx]->type != VAL_NUM) return;
+ if (c->constants[idx]->type != VAL_FLOAT) return;
depth++;
break;
}
diff --git a/src/compiler.c b/src/compiler.c
index 0c92c1983..ef0a63abd 100644
--- a/src/compiler.c
+++ b/src/compiler.c
@@ -337,6 +337,7 @@ static int op_stack_effect(uint8_t op8) {
switch ((OpCode)op8) {
/* Push 1 */
case OP_CONST: case OP_NULL: case OP_NUM_ZERO: case OP_NUM_ONE:
+ case OP_INT_ZERO: case OP_INT_ONE: case OP_INT_CONST:
case OP_TRUE: case OP_FALSE:
case OP_GET_LOCAL: case OP_GET_NAME: case OP_DUP:
case OP_PREDICATE: case OP_LISTCOMP_BEGIN:
@@ -353,7 +354,7 @@ static int op_stack_effect(uint8_t op8) {
case OP_POP:
return -1;
/* Pop 2, push 1 = net -1 */
- case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: case OP_MOD:
+ case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: case OP_MOD: case OP_IDIV:
case OP_BAND: case OP_BOR: case OP_BXOR: case OP_SHL: case OP_SHR:
case OP_EQ: case OP_NE: case OP_LT: case OP_GT: case OP_LE: case OP_GE:
case OP_INDEX_GET:
@@ -639,12 +640,34 @@ static int add_string_constant(Compiler *c, const char *str) {
}
static int add_num_constant(Compiler *c, double num) {
- Value *v = make_num(num);
+ Value *v = make_float(num);
int idx = chunk_add_constant(c->chunk, v);
val_decref(v);
return check_const_index(idx);
}
+static int add_int_constant(Compiler *c, int64_t integer) {
+ Value *v = make_int(integer);
+ int idx = chunk_add_constant(c->chunk, v);
+ val_decref(v);
+ return check_const_index(idx);
+}
+
+/* A fused literal index is an exact u16 for either numeric kind. Check
+ * before casting: large int/float literals must take the generic path. */
+static int literal_index_u16(const ASTNode *node, int *out) {
+ EigsNum n = node->data.number;
+ if (n.is_int) {
+ if (n.value.i64 < 0 || n.value.i64 > 0xFFFF) return 0;
+ *out = (int)n.value.i64;
+ return 1;
+ }
+ double d = n.value.f64;
+ if (!(d >= 0.0 && d <= 65535.0) || floor(d) != d) return 0;
+ *out = (int)d;
+ return 1;
+}
+
/* ---- Local variable tracking ---- */
static int resolve_local(Compiler *c, const char *name, uint32_t hash) {
@@ -701,6 +724,7 @@ static uint8_t binop_to_opcode(const char *op) {
if (op[0] == '-' && op[1] == 0) return OP_SUB;
if (op[0] == '*' && op[1] == 0) return OP_MUL;
if (op[0] == '/' && op[1] == 0) return OP_DIV;
+ if (op[0] == '/' && op[1] == '/') return OP_IDIV;
if (op[0] == '%' && op[1] == 0) return OP_MOD;
if (op[0] == '&' && op[1] == 0) return OP_BAND;
if (op[0] == '|' && op[1] == 0) return OP_BOR;
@@ -2175,10 +2199,20 @@ static void compile_node_inner(Compiler *c, ASTNode *node) {
switch (node->type) {
case AST_NUM: {
- double v = node->data.num;
- if (v == 0.0) { emit(c, OP_NUM_ZERO, node->line); }
- else if (v == 1.0) { emit(c, OP_NUM_ONE, node->line); }
- else { emit_op_u16(c, OP_CONST, (uint16_t)add_num_constant(c, v), node->line); }
+ EigsNum number = node->data.number;
+ if (number.is_int) {
+ int64_t i = number.value.i64;
+ if (i == 0) emit(c, OP_INT_ZERO, node->line);
+ else if (i == 1) emit(c, OP_INT_ONE, node->line);
+ else emit_op_u16(c, OP_INT_CONST,
+ (uint16_t)add_int_constant(c, i), node->line);
+ } else {
+ double f = number.value.f64;
+ if (f == 0.0) emit(c, OP_NUM_ZERO, node->line);
+ else if (f == 1.0) emit(c, OP_NUM_ONE, node->line);
+ else emit_op_u16(c, OP_CONST,
+ (uint16_t)add_num_constant(c, f), node->line);
+ }
break;
}
@@ -2193,7 +2227,7 @@ static void compile_node_inner(Compiler *c, ASTNode *node) {
break;
case AST_BOOL: /* #1637 */
- emit(c, node->data.num != 0.0 ? OP_TRUE : OP_FALSE, node->line);
+ emit(c, node->data.boolean ? OP_TRUE : OP_FALSE, node->line);
break;
case AST_IDENT: {
@@ -3087,9 +3121,8 @@ static void compile_node_inner(Compiler *c, ASTNode *node) {
if (c->enclosing &&
node->data.index.target->type == AST_IDENT &&
node->data.index.index->type == AST_NUM) {
- double dv = node->data.index.index->data.num;
- int iv = (int)dv;
- if (iv == dv && iv >= 0 && iv <= 0xFFFF) {
+ int iv;
+ if (literal_index_u16(node->data.index.index, &iv)) {
const char *tname = node->data.index.target->data.ident.name;
uint32_t th = node->data.index.target->name_hash;
if (th == 0) th = env_hash_name(tname);
@@ -3143,9 +3176,8 @@ static void compile_node_inner(Compiler *c, ASTNode *node) {
ASTNode *idx_node = node->data.dot.target;
if (idx_node->data.index.target->type == AST_IDENT &&
idx_node->data.index.index->type == AST_NUM) {
- double dv = idx_node->data.index.index->data.num;
- int iv = (int)dv;
- if (iv == dv && iv >= 0 && iv <= 0xFFFF) {
+ int iv;
+ if (literal_index_u16(idx_node->data.index.index, &iv)) {
const char *tname = idx_node->data.index.target->data.ident.name;
uint32_t th = idx_node->data.index.target->name_hash;
if (th == 0) th = env_hash_name(tname);
@@ -3188,9 +3220,8 @@ static void compile_node_inner(Compiler *c, ASTNode *node) {
ASTNode *idx_node = node->data.dot_assign.target;
if (idx_node->data.index.target->type == AST_IDENT &&
idx_node->data.index.index->type == AST_NUM) {
- double dv = idx_node->data.index.index->data.num;
- int iv = (int)dv;
- if (iv == dv && iv >= 0 && iv <= 0xFFFF) {
+ int iv;
+ if (literal_index_u16(idx_node->data.index.index, &iv)) {
const char *tname = idx_node->data.index.target->data.ident.name;
uint32_t th = idx_node->data.index.target->name_hash;
if (th == 0) th = env_hash_name(tname);
diff --git a/src/container.h b/src/container.h
index e89a47121..c4291669c 100644
--- a/src/container.h
+++ b/src/container.h
@@ -3,13 +3,14 @@
/* Container slot boundary (#1665 arm D). List items and dict values are
* EigsSlot storage (B10, the flip). Every stored slot is one of:
- * - an immediate number, always num_guard'ed (the default x86 NaN
- * 0xFFF8... is SLOT_NULL_BITS, so an unguarded double could read back
- * as null);
+ * - an immediate number: a double, always num_guard'ed (the default x86
+ * NaN 0xFFF8... is SLOT_NULL_BITS, so an unguarded double could read
+ * back as null), or an SMI, an int in [-2^47, 2^47) (#1758 design 2.1);
* - SLOT_NULL_BITS or a bool encoding (the immortal singletons are never
* stored as heap pointers);
* - a TAG_HEAP pointer owning one counted reference to a Value whose type
- * is none of NUM / NULL / BOOL.
+ * is none of FLOAT / NULL / BOOL. A VAL_INT there is an int outside the
+ * SMI range, a box (eigs_container_slot below keeps it, since R1.3).
* All-zero bits are the number 0.0, not a vacancy: calloc'd storage reads as
* zeros, so only slots below `count` are meaningful. Nothing outside this
* header touches the storage arrays except the bulk alloc/grow/free code that
@@ -23,7 +24,13 @@ typedef struct { Value v; } EigsView;
* encodings, everything else is a heap pointer. C NULL encodes as null. */
static inline EigsSlot eigs_container_slot(Value *v) {
if (!v || v->type == VAL_NULL) return slot_null();
- if (v->type == VAL_NUM) return slot_from_num(num_guard(VAL_NUM_RAW(v)));
+ if (v->type == VAL_FLOAT) return slot_from_f64(num_guard(VAL_F64_RAW(v)));
+ /* #1758 (design 2.1): container storage is canonical -- a small int is
+ * an SMI, and only an int outside [-2^47, 2^47) stays its box. */
+ if (__builtin_expect(v->type == VAL_INT, 0)) {
+ int64_t i = VAL_I64_RAW(v);
+ return (i >= SLOT_SMI_MIN && i <= SLOT_SMI_MAX) ? slot_from_int(i) : slot_from_heap(v);
+ }
if (v->type == VAL_BOOL) return slot_from_bool(v->data.boolean);
return slot_from_heap(v);
}
@@ -37,18 +44,19 @@ static inline EigsSlot eigs_slot_adopt(Value *v) {
/* Normalise a slot for storage. Immediates are guarded; a heap slot that
* points at a NUM / NULL / BOOL Value (a VM-internal box or a singleton) is
* re-encoded as an immediate. _owned consumes the slot's reference, _borrow
- * takes a new one for the container. */
+ * takes a new one for the container. #1758: an SMI is already canonical
+ * storage and is returned as it is (it owns no reference). */
static inline EigsSlot eigs_slot_store_owned(EigsSlot s) {
- if (slot_is_num(s)) return slot_from_num(num_guard(SLOT_NUM_RAW(s)));
+ if (slot_is_f64(s)) return slot_from_f64(num_guard(SLOT_F64_RAW(s)));
if (slot_is_heap(s)) {
Value *v = slot_as_ptr(s);
- if (v->type == VAL_NUM || v->type == VAL_NULL || v->type == VAL_BOOL)
- return eigs_slot_adopt(v);
+ if (v->type == VAL_FLOAT || v->type == VAL_INT || v->type == VAL_NULL || v->type == VAL_BOOL)
+ return eigs_slot_adopt(v); /* #1758: a small-int box becomes its SMI */
}
return s;
}
static inline EigsSlot eigs_slot_store_borrow(EigsSlot s) {
- if (slot_is_num(s)) return slot_from_num(num_guard(SLOT_NUM_RAW(s)));
+ if (slot_is_f64(s)) return slot_from_f64(num_guard(SLOT_F64_RAW(s)));
if (slot_is_heap(s)) {
EigsSlot e = eigs_container_slot(slot_as_ptr(s));
slot_incref(e);
@@ -71,34 +79,26 @@ static inline EigsSlot dict_value_slot(const Value *dict, int i) {
return dict->data.dict.vals[i];
}
-/* Store an immediate number the caller has already normalised (a VM stack
- * slot is guarded at its producer), skipping the redundant num_guard on the
- * assignment hot path. A plain slot store: the previous occupant is
- * released after the store, so a destructor it runs sees the new value. */
-static inline int dict_value_rewrite_num(const Value *dict, int i, double d) {
- EigsSlot old = dict->data.dict.vals[i];
- dict->data.dict.vals[i] = slot_from_num(d);
- slot_decref(old);
- return 1;
-}
-static inline int list_rewrite_num(const Value *list, int i, double d) {
- EigsSlot old = list->data.list.items[i];
- list->data.list.items[i] = slot_from_num(d);
- slot_decref(old);
- return 1;
-}
-
-/* Return the logical type of a borrowed slot without allocating. */
+/* Return the logical type of a borrowed slot without allocating. #1758: an
+ * SMI's type is VAL_INT; any reserved tag refuses instead of being read
+ * as a pointer. */
static inline ValType slot_type(EigsSlot s) {
- if (slot_is_num(s)) return VAL_NUM;
+ if (slot_is_f64(s)) return VAL_FLOAT;
if (slot_is_null(s)) return VAL_NULL;
if (slot_is_bool(s)) return VAL_BOOL;
- return slot_as_ptr(s)->type;
-}
+ if (slot_is_heap(s)) return slot_as_ptr(s)->type;
+ if (slot_is_smi(s)) return VAL_INT;
+ eigs_slot_refuse("slot_type", s);
+}
+/* A slot's Value for a reader that wants a pointer: null and bool become
+ * their singletons, a heap slot its Value. #1758 R1.1: anything else (a
+ * double, an SMI, a reserved tag) is a broken caller contract -- every caller
+ * tests for a number first -- and aborts instead of being dereferenced. */
static inline Value *eigs_slot_ref(EigsSlot s) {
if (slot_is_null(s)) return make_null();
if (slot_is_bool(s)) return make_bool(slot_as_bool(s));
- return slot_as_ptr(s);
+ if (slot_is_heap(s)) return slot_as_ptr(s);
+ eigs_slot_refuse("eigs_slot_ref", s);
}
static inline ValType list_elem_type(const Value *list, int i) {
return slot_type(list_slot(list, i));
@@ -108,51 +108,38 @@ static inline ValType dict_value_type(const Value *dict, int i) {
}
/* Return the real heap pointer (including the immortal null/bool singleton),
- * or NULL when the element is numeric. */
+ * or NULL when the element is an immediate number: a double or an SMI. A
+ * boxed int (|i| >= 2^47, #1758 R1.3) is a heap slot, so it comes back as its
+ * non-NULL VAL_INT Value; type the element (list_elem_type) before reading. */
static inline Value *list_get_ref(const Value *list, int i) {
EigsSlot s = list_slot(list, i);
- return slot_is_num(s) ? NULL : eigs_slot_ref(s);
+ return (slot_is_f64(s) || slot_is_smi(s)) ? NULL : eigs_slot_ref(s);
}
static inline Value *dict_value_get_ref(const Value *dict, int i) {
EigsSlot s = dict_value_slot(dict, i);
- return slot_is_num(s) ? NULL : eigs_slot_ref(s);
-}
-
-/* Numeric readers have the same strict/bool behavior as the Value readers:
- * required raises for every non-number, element raises for bool or in strict
- * mode, and optional treats null as absent. The heap-VAL_NUM arms serve a
- * slot that did not come from container storage (a VM-internal box). */
-static inline double eigs_slot_num(EigsSlot s, const char *who) {
- if (slot_is_num(s)) return SLOT_NUM_RAW(s);
- if (slot_is_heap(s) && slot_as_ptr(s)->type == VAL_NUM)
- return eigs_num_arg(slot_as_ptr(s), who);
- return eigs_num_arg_slow(eigs_slot_ref(s), who);
-}
-static inline double eigs_slot_elem_num(EigsSlot s, const char *who) {
- if (slot_is_num(s)) return SLOT_NUM_RAW(s);
- Value *v = eigs_slot_ref(s);
- if (v && v->type == VAL_NUM) return eigs_num_arg(v, who);
- if (g_strict || (v && v->type == VAL_BOOL)) return eigs_num_arg_slow(v, who);
- return 0.0;
-}
-static inline int eigs_slot_opt_num(EigsSlot s, double *out, const char *who) {
- if (slot_is_null(s) || (slot_is_heap(s) && slot_as_ptr(s)->type == VAL_NULL)) return 0;
- if (slot_is_num(s)) { *out = SLOT_NUM_RAW(s); return 1; }
- if (slot_is_heap(s) && slot_as_ptr(s)->type == VAL_NUM) {
- *out = eigs_num_arg(slot_as_ptr(s), who); return 1;
- }
- (void)eigs_num_arg_slow(eigs_slot_ref(s), who);
- return 0;
+ return (slot_is_f64(s) || slot_is_smi(s)) ? NULL : eigs_slot_ref(s);
}
/* A numeric view is a caller-owned, allocation-free Value facade. Reference
* slots return their real pointer. The cleanup guard catches retaining or
- * decrefing a numeric facade: its sentinel refcount must be unchanged. */
+ * decrefing a numeric facade: its sentinel refcount must be unchanged.
+ * #1758: an SMI's facade preserves its exact int Value kind. */
static inline Value *slot_view(EigsSlot s, EigsView *view) {
- if (!slot_is_num(s) && slot_type(s) != VAL_NUM) return eigs_slot_ref(s);
- view->v.type = VAL_NUM;
- VAL_NUM_RAW(&view->v) = slot_is_num(s) ? SLOT_NUM_RAW(s)
- : eigs_num_arg(slot_as_ptr(s), "slot_view");
+ /* Float storage exits before any int classification (#1758 cost gate). */
+ if (!slot_is_f64(s)) {
+ if (slot_is_smi(s)) {
+ view->v.type = VAL_INT;
+ VAL_I64_RAW(&view->v) = slot_smi_val(s);
+ view->v.refcount = EIGS_VIEW_RC;
+ return &view->v;
+ }
+ /* A heap VAL_INT is a real Value, returned as itself. */
+ if (slot_type(s) != VAL_FLOAT) return eigs_slot_ref(s);
+ }
+ view->v.type = VAL_FLOAT;
+ /* The branch above proves float storage; retain raw facade semantics
+ * even for a direct-C nonfinite box, without a flag write or raise. */
+ (void)slot_f64_value(s, &VAL_F64_RAW(&view->v));
view->v.refcount = EIGS_VIEW_RC;
return &view->v;
}
@@ -163,8 +150,9 @@ static inline Value *dict_value_get_view(const Value *dict, int i, EigsView *vie
return slot_view(dict_value_slot(dict, i), view);
}
static inline void eigs_view_check(EigsView *view) {
- if (view->v.type == VAL_NUM && view->v.refcount != 0 &&
- view->v.refcount != EIGS_VIEW_RC) {
+ /* A valid facade (or an unused zero view) exits before kind checks. */
+ if (view->v.refcount != 0 && view->v.refcount != EIGS_VIEW_RC &&
+ (view->v.type == VAL_FLOAT || view->v.type == VAL_INT)) {
fprintf(stderr, "%s:%d: EigenScript FATAL: numeric view refcount moved (%d)\n",
__FILE__, __LINE__, view->v.refcount);
abort();
@@ -180,15 +168,105 @@ static inline void eigs_view_check(EigsView *view) {
* eigs_view_check explicitly before every exit from the view's scope. */
#define EIGS_VIEW_NOCHECK(name) EigsView name = {{0}}
+/* Final slot readers borrow a facade for the duration of the call. The
+ * facade is never retained, so SMI, boxed-int and raise paths own no refs. */
+__attribute__((unused))
+static inline EigsNum eigs_slot_number(EigsSlot s, const char *who) {
+ EIGS_VIEW(view); return eigs_number_arg(slot_view(s, &view), who);
+}
+__attribute__((unused))
+static inline EigsNum eigs_slot_elem_number(EigsSlot s, const char *who) {
+ EIGS_VIEW(view); return eigs_elem_number(slot_view(s, &view), who);
+}
+__attribute__((unused))
+static inline int eigs_slot_opt_number(EigsSlot s, EigsNum *out, const char *who) {
+ EIGS_VIEW(view); return eigs_opt_number(slot_view(s, &view), out, who);
+}
+__attribute__((unused))
+static inline double eigs_slot_float(EigsSlot s, const char *who) {
+ EIGS_VIEW(view); return eigs_float_arg(slot_view(s, &view), who);
+}
+__attribute__((unused))
+static inline double eigs_slot_elem_float(EigsSlot s, const char *who) {
+ EIGS_VIEW(view); return eigs_elem_float(slot_view(s, &view), who);
+}
+__attribute__((unused))
+static inline int eigs_slot_opt_float(EigsSlot s, double *out, const char *who) {
+ EIGS_VIEW(view); return eigs_opt_float(slot_view(s, &view), out, who);
+}
+__attribute__((unused))
+static inline int64_t eigs_slot_int(EigsSlot s, const char *who) {
+ EIGS_VIEW(view); return eigs_int_arg(slot_view(s, &view), who);
+}
+__attribute__((unused))
+static inline int64_t eigs_slot_elem_int(EigsSlot s, const char *who) {
+ EIGS_VIEW(view); return eigs_elem_int(slot_view(s, &view), who);
+}
+__attribute__((unused))
+static inline int eigs_slot_opt_int(EigsSlot s, int64_t *out, const char *who) {
+ EIGS_VIEW(view); return eigs_opt_int(slot_view(s, &view), out, who);
+}
+__attribute__((unused))
+static inline uint8_t eigs_slot_byte(EigsSlot s, const char *who) {
+ EIGS_VIEW(view); return eigs_byte_arg(slot_view(s, &view), who);
+}
+__attribute__((unused))
+static inline uint8_t eigs_slot_elem_byte(EigsSlot s, const char *who) {
+ EIGS_VIEW(view); return eigs_elem_byte(slot_view(s, &view), who);
+}
+__attribute__((unused))
+static inline int eigs_slot_opt_byte(EigsSlot s, uint8_t *out, const char *who) {
+ EIGS_VIEW(view); return eigs_opt_byte(slot_view(s, &view), out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_int_range(EigsSlot s, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ EIGS_VIEW(view); return eigs_int_arg_range(slot_view(s, &view), lo, hi, out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_elem_int_range(EigsSlot s, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ EIGS_VIEW(view); return eigs_elem_int_range(slot_view(s, &view), lo, hi, out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_opt_int_range(EigsSlot s, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ EIGS_VIEW(view); return eigs_opt_int_range(slot_view(s, &view), lo, hi, out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_bounded(EigsSlot s, double lo, double hi, int *out, const char *who) {
+ EIGS_VIEW(view); return eigs_bounded_arg(slot_view(s, &view), lo, hi, out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_elem_bounded(EigsSlot s, double lo, double hi, int *out, const char *who) {
+ EIGS_VIEW(view); return eigs_elem_bounded(slot_view(s, &view), lo, hi, out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_opt_bounded(EigsSlot s, double lo, double hi, int *out, const char *who) {
+ EIGS_VIEW(view); return eigs_opt_bounded(slot_view(s, &view), lo, hi, out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_magnitude(EigsSlot s, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ EIGS_VIEW(view); return eigs_magnitude_arg(slot_view(s, &view), lo, hi, out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_elem_magnitude(EigsSlot s, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ EIGS_VIEW(view); return eigs_elem_magnitude(slot_view(s, &view), lo, hi, out, who);
+}
+__attribute__((unused))
+static inline int eigs_slot_opt_magnitude(EigsSlot s, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ EIGS_VIEW(view); return eigs_opt_magnitude(slot_view(s, &view), lo, hi, out, who);
+}
+
/* Owned/return reads make numeric storage independent. Reference values are
* increfed by the owned form; return preserves the builtin direct-child
- * borrow protocol for references while numbers are always fresh. */
+ * borrow protocol for references while numbers are always fresh (#1758: an
+ * int's fresh Value comes from slot_to_value, the one place that boxes an
+ * SMI). */
static inline Value *list_get_owned(const Value *list, int i) {
return slot_to_value(list_slot(list, i));
}
static inline Value *list_get_return(const Value *list, int i) {
EigsSlot s = list_slot(list, i);
- return slot_is_num(s) ? make_num(SLOT_NUM_RAW(s)) : eigs_slot_ref(s);
+ if (slot_is_smi(s)) return slot_to_value(s);
+ return slot_is_f64(s) ? make_float(SLOT_F64_RAW(s)) : eigs_slot_ref(s);
}
static inline Value *dict_value_get_owned(const Value *dict, int i) {
return slot_to_value(dict_value_slot(dict, i));
@@ -196,7 +274,8 @@ static inline Value *dict_value_get_owned(const Value *dict, int i) {
__attribute__((unused))
static inline Value *dict_value_get_return(const Value *dict, int i) {
EigsSlot s = dict_value_slot(dict, i);
- return slot_is_num(s) ? make_num(SLOT_NUM_RAW(s)) : eigs_slot_ref(s);
+ if (slot_is_smi(s)) return slot_to_value(s);
+ return slot_is_f64(s) ? make_float(SLOT_F64_RAW(s)) : eigs_slot_ref(s);
}
/* Keyed reads (eigenscript.c). The slot is borrowed from the dict (for a
@@ -226,7 +305,8 @@ static inline void dict_value_set_owned(Value *dict, int i, Value *v) {
* append borrows it and therefore increfs. Every double path applies
* num_guard. */
static inline Value *eigs_slot_box(EigsSlot s) {
- if (slot_is_num(s)) return make_num(num_guard(SLOT_NUM_RAW(s)));
+ if (slot_is_f64(s)) return make_float(num_guard(SLOT_F64_RAW(s)));
+ if (slot_is_smi(s)) return slot_to_value(s); /* #1758: kind-preserving */
Value *v = eigs_slot_ref(s); val_incref(v); return v;
}
static inline __attribute__((always_inline)) void
@@ -247,19 +327,85 @@ static inline void list_append_slot(Value *list, EigsSlot s) {
if (!list_reserve_one(list)) return;
list->data.list.items[list->data.list.count++] = eigs_slot_store_borrow(s);
}
-static inline __attribute__((always_inline)) void
-list_set_num(Value *list, int i, double d) {
- list_rewrite_num(list, i, num_guard(d));
+/* Typed container writers own exactly one stored reference. _rewrite_f64
+ * receives an already-normalised double, matching the existing fast store.
+ * An int outside the SMI range owns the box slot_from_int creates. */
+__attribute__((unused))
+static inline int list_rewrite_f64(const Value *v, int i, double n) {
+ EigsSlot old = list_slot(v, i);
+ v->data.list.items[i] = slot_from_f64(n);
+ slot_decref(old); return 1;
}
-static inline void list_append_num(Value *list, double d) {
- d = num_guard(d);
- if (!list_reserve_one(list)) return;
- list->data.list.items[list->data.list.count++] = slot_from_num(d);
+__attribute__((unused))
+static inline void list_set_f64(Value *v, int i, double n) {
+ list_rewrite_f64(v, i, num_guard(n));
+}
+__attribute__((unused))
+static inline int list_get_f64(const Value *v, int i, double *out) {
+ return slot_f64_value(list_slot(v, i), out);
+}
+__attribute__((unused))
+static inline int dict_value_rewrite_f64(const Value *v, int i, double n) {
+ EigsSlot old = dict_value_slot(v, i);
+ v->data.dict.vals[i] = slot_from_f64(n);
+ slot_decref(old); return 1;
+}
+__attribute__((unused))
+static inline void dict_value_set_f64(Value *v, int i, double n) {
+ dict_value_rewrite_f64(v, i, num_guard(n));
+}
+__attribute__((unused))
+static inline int dict_value_get_f64(const Value *v, int i, double *out) {
+ return slot_f64_value(dict_value_slot(v, i), out);
+}
+__attribute__((unused))
+static inline void list_append_f64(Value *v, double n) {
+ n = num_guard(n);
+ if (!list_reserve_one(v)) return;
+ v->data.list.items[v->data.list.count++] = slot_from_f64(n);
+}
+__attribute__((unused))
+static inline int list_rewrite_int(const Value *v, int i, int64_t n) {
+ EigsSlot old = list_slot(v, i);
+ v->data.list.items[i] = slot_from_int(n);
+ slot_decref(old); return 1;
+}
+__attribute__((unused))
+static inline void list_set_int(Value *v, int i, int64_t n) {
+ list_rewrite_int(v, i, n);
+}
+/* The caller has proved this slot is an SMI with a nonnegative value
+ * below the SMI maximum. Incrementing the payload cannot carry into the tag and
+ * changes no reference ownership. ITER_NEXT additionally bounds it by
+ * idx < len <= INT_MAX. Keep element storage behind this header. */
+static inline void list_bump_smi(Value *v, int i) {
+ v->data.list.items[i].u++;
+}
+__attribute__((unused))
+static inline int list_get_int(const Value *v, int i, int64_t *out) {
+ return slot_int_value(list_slot(v, i), out);
+}
+__attribute__((unused))
+static inline int dict_value_rewrite_int(const Value *v, int i, int64_t n) {
+ EigsSlot old = dict_value_slot(v, i);
+ v->data.dict.vals[i] = slot_from_int(n);
+ slot_decref(old); return 1;
+}
+__attribute__((unused))
+static inline void dict_value_set_int(Value *v, int i, int64_t n) {
+ dict_value_rewrite_int(v, i, n);
}
__attribute__((unused))
-static inline void dict_value_set_num(Value *dict, int i, double d) {
- dict_value_rewrite_num(dict, i, num_guard(d));
+static inline int dict_value_get_int(const Value *v, int i, int64_t *out) {
+ return slot_int_value(dict_value_slot(v, i), out);
}
+__attribute__((unused))
+static inline void list_append_int(Value *v, int64_t n) {
+
+ if (!list_reserve_one(v)) return;
+ v->data.list.items[v->data.list.count++] = slot_from_int(n);
+}
+
/* Remove element i, shifting the tail down; the caller owns the result. */
static inline EigsSlot list_take_slot(Value *list, int i) {
EigsSlot s = list->data.list.items[i];
@@ -277,12 +423,13 @@ static inline EigsSlot dict_value_take_slot(Value *dict, int i) {
static inline void list_move(Value *list, int dst, int src, int n) {
memmove(&list->data.list.items[dst], &list->data.list.items[src], (size_t)n * sizeof(EigsSlot));
}
+__attribute__((unused))
static inline void dict_value_move(Value *dict, int dst, int src, int n) {
memmove(&dict->data.dict.vals[dst], &dict->data.dict.vals[src], (size_t)n * sizeof(EigsSlot));
}
/* Stable bottom-up mergesort: libc qsort has no tie-order guarantee.
* The comparator receives `const EigsSlot *` element pointers. Scratch
- * slots borrow the list's references; only their order changes. #1982. */
+ * slots borrow the list's references; only their order changes. #1758/#1982. */
static inline void list_sort(Value *list, int (*cmp)(const void *, const void *)) {
size_t n = (size_t)list->data.list.count;
if (n < 2) return;
@@ -308,20 +455,6 @@ static inline void list_sort(Value *list, int (*cmp)(const void *, const void *)
free(scratch);
}
-__attribute__((unused))
-static inline int list_get_num(const Value *list, int i, double *out) {
- EigsSlot s = list_slot(list, i);
- if (!slot_is_num(s)) return 0;
- *out = SLOT_NUM_RAW(s);
- return 1;
-}
-__attribute__((unused))
-static inline int dict_value_get_num(const Value *dict, int i, double *out) {
- EigsSlot s = dict_value_slot(dict, i);
- if (!slot_is_num(s)) return 0;
- *out = SLOT_NUM_RAW(s);
- return 1;
-}
__attribute__((unused))
static inline int list_iter_count(const Value *list) { return list->data.list.count; }
__attribute__((unused))
diff --git a/src/eigenlsp.c b/src/eigenlsp.c
index 56f185828..dc44565bb 100644
--- a/src/eigenlsp.c
+++ b/src/eigenlsp.c
@@ -1147,11 +1147,20 @@ static void handle_hover(int id, const char *params) {
if (!doc) { lsp_response_null(id); return; }
Token *tok = find_token_at(doc, line, col);
- if (!tok || !tok->str_val) { lsp_response_null(id); return; }
+ if (!tok || (!tok->str_val && tok->type != TOK_NUM)) {
+ lsp_response_null(id);
+ return;
+ }
const char *hover_text = NULL;
char hover_buf[1024];
+ /* #1758 R1.8: report the kind retained by the lexer/parser, rather
+ * than testing whether a double happens to be integral. In particular
+ * 3.0 is a float, while exact/hex-wrapped int64 literals stay ints. */
+ if (tok->type == TOK_NUM)
+ hover_text = tok->number.is_int ? "int" : "float";
+
/* A dotted token (`x.name`) is a member access — never a bare builtin,
* keyword, or document symbol, so those lookups are skipped for it and
* only the module-qualified stdlib branch below may claim it. Became
@@ -1163,7 +1172,7 @@ static void handle_hover(int id, const char *params) {
int dotted = (ti >= 1 && doc->tokens.tokens[ti - 1].type == TOK_DOT);
/* Check builtins */
- if (!dotted)
+ if (!hover_text && !dotted)
for (int i = 0; builtin_docs[i][0]; i++) {
if (strcmp(tok->str_val, builtin_docs[i][0]) == 0) {
hover_text = builtin_docs[i][1];
diff --git a/src/eigenscript.c b/src/eigenscript.c
index 64de814ee..c1973dadd 100644
--- a/src/eigenscript.c
+++ b/src/eigenscript.c
@@ -265,6 +265,8 @@ const char* tok_type_name(TokType t) {
case TOK_MINUS: return "'-'";
case TOK_STAR: return "'*'";
case TOK_SLASH: return "'/'";
+ case TOK_SLASH_SLASH: return "'//'";
+ case TOK_SLASH_SLASH_EQ: return "'//='";
case TOK_PERCENT: return "'%'";
case TOK_LT: return "'<'";
case TOK_GT: return "'>'";
@@ -325,7 +327,8 @@ const char* eigs_interrogative_word(int kind) {
const char* val_type_name(ValType t) {
switch (t) {
- case VAL_NUM: return "num";
+ case VAL_FLOAT: return "num";
+ case VAL_INT: return "num"; /* #1758 (B): one number type at the surface */
case VAL_STR: return "str";
case VAL_LIST: return "list";
case VAL_FN: return "fn";
@@ -372,7 +375,7 @@ static void env_intern_scope_remove(char *name);
/* #262 Phase-3 D: scalar entropy of a number, factored out so the slot
* observer can update from a raw immediate double without materializing a
* Value (the default path no longer promotes observed nums to tracked
- * pointers). Single source of truth for the VAL_NUM entropy formula. */
+ * pointers). Single source of truth for the VAL_FLOAT entropy formula. */
static double entropy_of_num(double num) {
/* #412: unity is the HORIZON, not a home point. The binary-entropy
* formula is smooth and maximal at |x| = 1 (p = 0.5, H = 1.0); the old
@@ -444,7 +447,9 @@ static double ent_child(Value *c) {
}
static double ent_child_slot(EigsSlot s) {
- if (slot_is_num(s)) return entropy_of_num(SLOT_NUM_RAW(s));
+ if (slot_is_f64(s)) return entropy_of_num(SLOT_F64_RAW(s));
+ /* #1758: an int is observed as its double until round 3 (design 7). */
+ if (slot_is_smi(s)) return entropy_of_num((double)slot_smi_val(s));
return ent_child(eigs_slot_ref(s));
}
@@ -452,7 +457,10 @@ static double compute_entropy_impl(Value *v) {
if (!v) return 0.0;
switch (v->type) {
case VAL_NULL: return 0.0;
- case VAL_NUM: return entropy_of_num(VAL_NUM_RAW(v));
+ case VAL_FLOAT: return entropy_of_num(VAL_F64_RAW(v));
+ /* #1758 R1.2a (design 7): an int's entropy is its double's; it joins
+ * the numeric channel when ints exist (R1.3). */
+ case VAL_INT: return entropy_of_num((double)VAL_I64_RAW(v));
case VAL_STR: return entropy_of_cstr(v->data.str);
case VAL_LIST: {
if (v->data.list.count == 0) return 0.0;
@@ -595,15 +603,33 @@ static int observer_eq_within_budget(Value *v, size_t *units, int depth) {
return 1;
}
+/* Shared structural reflexivity test (R1.6z-D2), defined with equality below. */
+static int equal_structure_reflexive(Value *root);
+
/* Equality used only by observer bookkeeping. Unlike language equality it
* cannot raise (notably num-vs-bool), set g_has_error, normalize buffers, or
* synchronize module namespaces. Observer snapshots are ordinary detached
* values, and unsupported/deep values are refused before reaching here. */
static int values_equal_for_observation(Value *a, Value *b, int depth) {
- if (a == b) return 1;
- if (!a || !b || a->type != b->type || depth > 64) return 0;
+ if (a == b) {
+ if (a && (a->type == VAL_LIST || a->type == VAL_DICT || a->type == VAL_BUFFER))
+ return equal_structure_reflexive(a);
+ return 1;
+ }
+ if (!a || !b || depth > 64) return 0;
+ if (a->type != b->type) {
+ /* N2 applies to numeric elements inside the equality snapshots too.
+ * Keep this observer-only comparison non-raising for every other
+ * kind pair, especially number/bool. */
+ if (a->type == VAL_INT && b->type == VAL_FLOAT)
+ return eigs_cmp_int_float(VAL_I64_RAW(a), VAL_F64_RAW(b)) == 0;
+ if (a->type == VAL_FLOAT && b->type == VAL_INT)
+ return eigs_cmp_int_float(VAL_I64_RAW(b), VAL_F64_RAW(a)) == 0;
+ return 0;
+ }
switch (a->type) {
- case VAL_NUM: return VAL_NUM_RAW(a) == VAL_NUM_RAW(b);
+ case VAL_FLOAT: return VAL_F64_RAW(a) == VAL_F64_RAW(b);
+ case VAL_INT: return VAL_I64_RAW(a) == VAL_I64_RAW(b);
case VAL_STR: return strcmp(a->data.str, b->data.str) == 0;
case VAL_NULL: return 1;
case VAL_BOOL: return a->data.boolean == b->data.boolean;
@@ -636,9 +662,9 @@ static int values_equal_for_observation(Value *a, Value *b, int depth) {
case VAL_BUFFER:
if (a->data.buffer.count != b->data.buffer.count) return 0;
for (int i = 0; i < a->data.buffer.count; i++) {
- double av = num_guard_value(a->data.buffer.data[i]);
- double bv = num_guard_value(b->data.buffer.data[i]);
- if (av != bv) return 0;
+ /* S10-D1 / R1.6z-D1: observe the stored IEEE values,
+ * never normalize the work area or alter math flags. */
+ if (a->data.buffer.data[i] != b->data.buffer.data[i]) return 0;
}
return 1;
case VAL_TEXT_BUILDER:
@@ -998,9 +1024,10 @@ static void observer_slot_update_e_locked(Env *e, int idx, double new_entropy) {
static inline void observer_slot_update_locked(Env *e, int idx, Value *newval,
double entropy) {
observer_slot_update_e_locked(e, idx, entropy);
- if (newval && newval->type == VAL_NUM) {
+ if (newval && val_is_number(newval)) {
ObserverSlot *s = env_obs_slot(e, idx);
- if (s) observer_slot_record_value(s, VAL_NUM_RAW(newval));
+ if (s) observer_slot_record_value(s, newval->type == VAL_INT ? (double)VAL_I64_RAW(newval)
+ : VAL_F64_RAW(newval));
} else {
ObserverSlot *s = env_obs_slot(e, idx);
if (s) {
@@ -1060,7 +1087,8 @@ static void observer_slot_update_value_num_shared(
env_shared_fold_lock();
observer_slot_update_e_locked(e, idx, entropy);
ObserverSlot *s = env_obs_slot(e, idx);
- if (s) observer_slot_record_value(s, VAL_NUM_RAW(newval));
+ if (s) observer_slot_record_value(s, newval->type == VAL_INT ? (double)VAL_I64_RAW(newval)
+ : VAL_F64_RAW(newval));
env_shared_fold_unlock();
}
@@ -1133,7 +1161,7 @@ void observer_slot_update(Env *e, int idx, Value *newval) {
if (!e || idx < 0) return;
if (idx >= e->obs_cap && !observer_obs_grow(e, idx)) return;
if (observer_fold_is_shared(e)) {
- if (newval && newval->type == VAL_NUM) {
+ if (newval && val_is_number(newval)) {
observer_slot_update_value_num_shared(e, idx, newval, entropy);
} else {
ObserverEqualitySnapshot eq = observer_equality_snapshot(newval);
@@ -1202,8 +1230,9 @@ void observer_slot_sample_num_gated(Env *e, int idx, double num) {
}
void observer_slot_sample_gated(Env *e, int idx, Value *newval) {
- if (newval && newval->type == VAL_NUM) {
- observer_slot_sample_num_gated(e, idx, VAL_NUM_RAW(newval));
+ if (newval && val_is_number(newval)) { /* #1758: an int box as its double */
+ observer_slot_sample_num_gated(e, idx, newval->type == VAL_INT ? (double)VAL_I64_RAW(newval)
+ : VAL_F64_RAW(newval));
return;
}
if (!e || idx < 0) return;
@@ -1672,9 +1701,21 @@ static int obs_entropy_stable(const ObserverSlot *s) {
return 1;
}
+/* The snapshot is detached and bounded by observer_eq_within_budget.
+ * Its caller owns the slot or holds the shared-env fold lock while querying. */
int observer_slot_stable(const ObserverSlot *s) {
if (obs_route_num(s)) return obs_num_stable(s);
- if (s && s->eq_seen) return observer_slot_eq_settled(s);
+ if (s && s->eq_seen) {
+ /* S10-D1 retains bank stability for a snapshot reaching a NaN
+ * buffer: its entropy can be stable while IEEE equality can never
+ * certify convergence. Finite snapshots keep #2048's equality
+ * route. Refusal clears bounded-window eligibility even when the
+ * older entropy window was already stable. */
+ if (obs_eq_count(s) < (size_t)observer_slot_window(s)) return 0;
+ if (s->eq_last && !equal_structure_reflexive(s->eq_last))
+ return obs_entropy_stable(s);
+ return observer_slot_eq_settled(s);
+ }
return obs_entropy_stable(s);
}
@@ -1774,9 +1815,12 @@ int observer_entropy_now(Env *e, int idx, double *out) {
env_dump_lock(e); /* exported #607 lock wrapper (defined below) */
if (idx < e->count) {
EigsSlot s = e->values[idx];
- if (slot_is_num(s)) {
+ if (slot_is_f64(s)) {
kind = 1;
- num = SLOT_NUM_RAW(s);
+ num = SLOT_F64_RAW(s);
+ } else if (slot_is_smi(s)) {
+ kind = 1; /* #1758: the numeric channel, as a double */
+ num = (double)slot_smi_val(s);
} else if (slot_is_bool(s)) {
held = make_bool(slot_as_bool(s)); /* #1637: immortal; incref no-op */
kind = 2;
@@ -1821,24 +1865,24 @@ Value *observer_slot_trajectory(const ObserverSlot *s) {
/* windows read newest-first (offset 0 = most recent); emit oldest-first
* so the lists read chronologically, like a history. */
for (int i = vcnt - 1; i >= 0; i--) {
- list_append_owned(rel, make_num(observer_slot_v_get(s, (size_t)i)));
- list_append_owned(raw, make_num(observer_slot_vr_get(s, (size_t)i)));
+ list_append_owned(rel, make_float(observer_slot_v_get(s, (size_t)i)));
+ list_append_owned(raw, make_float(observer_slot_vr_get(s, (size_t)i)));
}
for (int i = dcnt - 1; i >= 0; i--)
- list_append_owned(dh, make_num(observer_slot_window_get(s, (size_t)i)));
+ list_append_owned(dh, make_float(observer_slot_window_get(s, (size_t)i)));
dict_set_owned(out, "rel", rel);
dict_set_owned(out, "raw", raw);
dict_set_owned(out, "dh", dh);
- dict_set_owned(out, "entropy", make_num(s ? s->entropy : 0.0));
- dict_set_owned(out, "dH", make_num(s ? s->dH : 0.0));
- dict_set_owned(out, "last_entropy", make_num(s ? s->last_entropy : 0.0));
- dict_set_owned(out, "last_value", make_num((s && s->v_used) ? s->last_value : 0.0));
+ dict_set_owned(out, "entropy", make_float(s ? s->entropy : 0.0));
+ dict_set_owned(out, "dH", make_float(s ? s->dH : 0.0));
+ dict_set_owned(out, "last_entropy", make_float(s ? s->last_entropy : 0.0));
+ dict_set_owned(out, "last_value", make_float((s && s->v_used) ? s->last_value : 0.0));
dict_set_owned(out, "observed", make_bool(s && s->used != 0)); /* #1637 */
dict_set_owned(out, "numeric", make_bool(s && s->v_used != 0));
/* #1044: the depth this slot classifies over travels with the snapshot,
* so `classify of (trajectory of x)` agrees with `report of x` for a
* binding carrying a per-binding override. */
- dict_set_owned(out, "window", make_num((double)observer_slot_window(s)));
+ dict_set_owned(out, "window", make_int(observer_slot_window(s)));
return out;
}
@@ -1870,10 +1914,12 @@ int observer_slot_from_trajectory(ObserverSlot *out, Value *dict) {
EIGS_VIEW(w_view);
Value *w = dict_get_view_local(dict, "window", &w_view);
if (w) {
- if (w->type != VAL_NUM || VAL_NUM_RAW(w) < OBSERVER_WINDOW_MIN ||
- VAL_NUM_RAW(w) > OBSERVER_WINDOW_MAX || VAL_NUM_RAW(w) != (int)VAL_NUM_RAW(w))
+ /* #1758: a number of either kind, read through the bridge */
+ double wd = val_is_number(w) ? eigs_float_arg(w, __func__) : 0.0;
+ if (!val_is_number(w) || wd < OBSERVER_WINDOW_MIN ||
+ wd > OBSERVER_WINDOW_MAX || wd != (int)wd)
return 0;
- out->win_override = (uint8_t)(int)VAL_NUM_RAW(w);
+ out->win_override = (uint8_t)(int)wd;
}
}
const int N = observer_slot_window(out);
@@ -1887,13 +1933,14 @@ int observer_slot_from_trajectory(ObserverSlot *out, Value *dict) {
out->v_cap = out->dh_cap = (uint8_t)N;
for (int i = vstart; i < vcnt; i++) {
EigsSlot a = list_slot(rel, i), b = list_slot(raw, i);
- if (slot_type(a) != VAL_NUM || slot_type(b) != VAL_NUM) {
+ ValType ta = slot_type(a), tb = slot_type(b); /* #1758: either number kind */
+ if ((ta != VAL_FLOAT && ta != VAL_INT) || (tb != VAL_FLOAT && tb != VAL_INT)) {
free(out->v_window); free(out->vr_window);
memset(out, 0, sizeof *out);
return 0;
}
- out->v_window[out->v_window_count] = eigs_slot_num(a, __func__);
- out->vr_window[out->v_window_count] = eigs_slot_num(b, __func__);
+ out->v_window[out->v_window_count] = eigs_slot_float(a, __func__);
+ out->vr_window[out->v_window_count] = eigs_slot_float(b, __func__);
out->v_window_count++;
}
out->v_window_head = (uint8_t)(out->v_window_count % N);
@@ -1902,12 +1949,13 @@ int observer_slot_from_trajectory(ObserverSlot *out, Value *dict) {
out->dh_window = xcalloc((size_t)N, sizeof(double));
for (int i = dstart; i < dcnt; i++) {
EigsSlot a = list_slot(dh, i);
- if (slot_type(a) != VAL_NUM) {
+ ValType ta = slot_type(a);
+ if (ta != VAL_FLOAT && ta != VAL_INT) {
free(out->v_window); free(out->vr_window); free(out->dh_window);
memset(out, 0, sizeof *out);
return 0;
}
- out->dh_window[out->dh_window_count++] = eigs_slot_num(a, __func__);
+ out->dh_window[out->dh_window_count++] = eigs_slot_float(a, __func__);
}
out->dh_window_head = (uint8_t)(out->dh_window_count % N);
/* #1637: the scalar fields are numbers and the two flags are bools, as
@@ -1920,12 +1968,12 @@ int observer_slot_from_trajectory(ObserverSlot *out, Value *dict) {
EIGS_VIEW(v_view);
Value *v = dict_get_view_local(dict, nums[k], &v_view);
if (!v) continue;
- if (v->type != VAL_NUM) {
+ if (!val_is_number(v)) {
free(out->v_window); free(out->vr_window); free(out->dh_window);
memset(out, 0, sizeof *out);
return 0;
}
- *dsts[k] = VAL_NUM_RAW(v);
+ *dsts[k] = eigs_float_arg(v, __func__); /* #1758: an int converts (the bridge) */
}
EIGS_VIEW(fo_view); EIGS_VIEW(fn_view);
Value *fo = dict_get_view_local(dict, "observed", &fo_view);
@@ -2028,9 +2076,9 @@ void env_retain_intern_table(Env *env, EnvInternTable *table) {
void free_value(Value *v) {
if (!v) return;
env_intern_release_value(v);
- if (v->type == VAL_NUM) {
+ if (v->type == VAL_FLOAT) { /* the freelist recycles float boxes; an int box is freed */
#ifndef EIGS_POOL_OFF_NUM
- /* Route freed NUMs to freelist for reuse by make_num */
+ /* Route freed NUMs to freelist for reuse by make_float */
if (g_num_freelist_count < NUM_FREELIST_CAP) {
memcpy(EIGS_NUM_LINK(v), &g_num_freelist, sizeof(Value *));
g_num_freelist = v;
@@ -2109,7 +2157,8 @@ void free_value(Value *v) {
/* No owned memory. Enumerated rather than covered by a `default:` so
* that -Werror=switch (Makefile CFLAGS) makes a new ValType a build
* error here instead of a silent leak. */
- case VAL_NUM:
+ case VAL_FLOAT:
+ case VAL_INT: /* #1758 R1.2a: an int box owns nothing */
case VAL_BUILTIN:
case VAL_NULL:
case VAL_BOOL:
@@ -2118,7 +2167,8 @@ void free_value(Value *v) {
free(v);
}
-Value* make_num(double n) {
+static inline Value *make_float_pooled(double n) __attribute__((always_inline));
+static inline Value *make_float_pooled(double n) {
n = num_guard(n);
Value *v;
#ifndef EIGS_POOL_OFF_NUM
@@ -2130,19 +2180,32 @@ Value* make_num(double n) {
eigs_num_park_unpoison(v);
memset(v, 0, sizeof(Value));
} else
-#endif /* EIGS_POOL_OFF_NUM (#1665): never satisfy a make_num from the freelist —
+#endif /* EIGS_POOL_OFF_NUM (#1665): never satisfy a make_float from the freelist —
* every heap NUM is a fresh xcalloc. */
{
v = xcalloc(1, sizeof(Value));
}
- v->type = VAL_NUM;
- VAL_NUM_RAW(v) = n;
+ v->type = VAL_FLOAT;
+ VAL_F64_RAW(v) = n;
+ v->refcount = 1;
+ return v;
+}
+
+/* Float construction retains the existing pooling and normalization path. */
+Value *make_float(double n) { return make_float_pooled(n); }
+
+/* #1758 R1.3: large int slots and Value API boundaries own ordinary heap
+ * Values. Int boxes never enter the float freelist and own no GC edges. */
+Value* make_int(int64_t n) {
+ Value *v = xcalloc(1, sizeof(Value));
+ v->type = VAL_INT;
+ VAL_I64_RAW(v) = n;
v->refcount = 1;
return v;
}
void recycle_intermediate(Value *v) {
- if (!v || v->type != VAL_NUM || v->refcount > 1) return;
+ if (!v || v->type != VAL_FLOAT || v->refcount > 1) return;
#ifdef EIGS_POOL_OFF_NUM
/* #1665: no freelist — a discarded intermediate NUM is freed outright
* (the pooled path already free()s it when the cap is hit, so callers
@@ -2162,17 +2225,18 @@ void recycle_intermediate(Value *v) {
#endif
}
-/* Unpooled make_num: never touches the per-thread NUM freelist, so it is
+/* Unpooled make_float: never touches the per-thread NUM freelist, so it is
* safe without a current VM thread (C-level fixtures, tests/test_intern_owners.c). */
-Value* make_num_permanent(double n) {
+Value* make_float_permanent(double n) {
n = num_guard(n);
Value *v = xcalloc(1, sizeof(Value));
- v->type = VAL_NUM;
- VAL_NUM_RAW(v) = n;
+ v->type = VAL_FLOAT;
+ VAL_F64_RAW(v) = n;
v->refcount = 1;
return v;
}
+
/* Forward decl of the VAL_NULL singleton — defined below near make_null. */
static Value g_null_singleton;
/* #1637: the two VAL_BOOL singletons are immortal like null.
@@ -2197,30 +2261,465 @@ static const char *num_who(const char *who) {
return strncmp(who, "builtin_", 8) == 0 ? who + 8 : who;
}
-double eigs_num_arg_slow(const Value *v, const char *who) {
- if (v && v->type == VAL_NUM) return VAL_NUM_RAW(v);
+/* The final readers never own their argument. Keep the payload/type
+ * predicate here so all forms share it, including every refusal path. */
+static int eigs_number_read(const Value *v, EigsNum *out, const char *who) {
+ if (v && v->type == VAL_INT) {
+ out->is_int = 1; out->value.i64 = VAL_I64_RAW(v); return 1;
+ }
+ if (v && v->type == VAL_FLOAT) {
+ out->is_int = 0; out->value.f64 = VAL_F64_RAW(v); return 1;
+ }
if (!g_has_error)
rt_error(EK_TYPE, 0, "%s: expected a number, got %s", num_who(who),
v ? val_type_name(v->type) : "nothing");
- return 0.0;
+ return 0;
}
-double eigs_list_num(const Value *list, int i, const char *who) {
- if (!list || list->type != VAL_LIST || i < 0 || i >= list->data.list.count) {
+EigsNum eigs_number_arg(const Value *v, const char *who) {
+ EigsNum n = { .is_int = 0, .value = { .f64 = 0.0 } };
+ if (!eigs_number_read(v, &n, who)) return n;
+ if (!n.is_int && !isfinite(n.value.f64)) {
if (!g_has_error)
- rt_error(EK_TYPE, 0, "%s: expected a number as argument %d, got nothing",
- num_who(who), i);
- return 0.0;
+ rt_error(EK_VALUE, 0, "%s: expected a finite number", num_who(who));
+ n.value.f64 = 0.0;
+ }
+ return n;
+}
+
+double eigs_float_arg(const Value *v, const char *who) {
+ EigsNum n = eigs_number_arg(v, who);
+ return n.is_int ? (double)n.value.i64 : n.value.f64;
+}
+
+int64_t eigs_int_arg(const Value *v, const char *who) {
+ EigsNum n = { .is_int = 0, .value = { .f64 = 0.0 } };
+ if (!eigs_number_read(v, &n, who)) return 0;
+ if (n.is_int) return n.value.i64;
+ int64_t i;
+ if (num_to_i64_exact(n.value.f64, &i)) return i;
+ if (!g_has_error)
+ rt_error(EK_VALUE, 0, EIGS_NO_INT_REP_FMT, num_who(who), n.value.f64);
+ return 0;
+}
+
+int eigs_int_arg_range(const Value *v, int64_t lo, int64_t hi,
+ int64_t *out, const char *who) {
+ int64_t i = eigs_int_arg(v, who);
+ if (g_has_error) return 0;
+ if (i < lo || i > hi || lo > hi) {
+ rt_error(EK_VALUE, 0, "%s: integer %lld is outside [%lld, %lld]", num_who(who),
+ (long long)i, (long long)lo, (long long)hi);
+ return 0;
}
- return eigs_slot_num(list_slot(list, i), who);
+ *out = i;
+ return 1;
+}
+
+uint8_t eigs_byte_arg(const Value *v, const char *who) {
+ int64_t i;
+ return eigs_int_arg_range(v, 0, 255, &i, who) ? (uint8_t)i : 0;
+}
+
+/* Q7 clips complete continuous shapes before Q4 conversion. */
+/* Exact sums of up to six binary64 products. A product occupies bits
+ * 0..4195 in units of 2^-2148; six terms need at most bit4198. Fixed
+ * 66-word positive/negative accumulators retain cancellation and Q4 ties
+ * without assuming long double has a wider exponent range than double. */
+#define EIGS_GEOM_WORDS 66
+static double eigs_geom_products(const double terms[][2], int count, int *sign, int *exponent) {
+ uint64_t positive[EIGS_GEOM_WORDS]={0},negative[EIGS_GEOM_WORDS]={0};
+ for (int k=0;k>52)&0x7ff);
+ mant[j]=(bits[j]&UINT64_C(0xfffffffffffff)) | (e ? UINT64_C(0x10000000000000) : 0);
+ power[j]=e ? (int)e-1075 : -1074;
+ }
+ if (!mant[0] || !mant[1]) continue;
+ /* S10-D4: the freestanding rv32 target has no __int128. Keep the
+ * native path unchanged; the test-only macro exercises the same
+ * portable path on hosted compilers that do provide __int128. */
+#if defined(__SIZEOF_INT128__) && !defined(EIGS_TEST_FORCE_PORTABLE_GEOM)
+ unsigned __int128 product=(unsigned __int128)mant[0]*mant[1];
+ int shift=power[0]+power[1]+2148,word=shift/64,b=shift%64;
+ uint64_t lo=(uint64_t)product,hi=(uint64_t)(product>>64),part[3];
+#else
+ uint64_t a0=(uint32_t)mant[0],a1=mant[0]>>32;
+ uint64_t b0=(uint32_t)mant[1],b1=mant[1]>>32;
+ uint64_t p00=a0*b0,p01=a0*b1,p10=a1*b0,p11=a1*b1;
+ uint64_t middle=(p00>>32)+(uint32_t)p01+(uint32_t)p10;
+ uint64_t lo=(middle<<32)|(uint32_t)p00;
+ uint64_t hi=p11+(p01>>32)+(p10>>32)+(middle>>32),part[3];
+ int shift=power[0]+power[1]+2148,word=shift/64,b=shift%64;
+#endif
+ part[0]=lo<>(64-b)) : hi;
+ part[2]=b ? hi>>(64-b) : 0;
+ uint64_t *dest=((bits[0]^bits[1])>>63) ? negative : positive;
+#if defined(__SIZEOF_INT128__) && !defined(EIGS_TEST_FORCE_PORTABLE_GEOM)
+ unsigned __int128 carry=0;
+#else
+ uint64_t carry=0;
+#endif
+ for (int j=0;j<3 || carry;++j) {
+ if (word+j>=EIGS_GEOM_WORDS) abort(); /* internal six-product bound */
+#if defined(__SIZEOF_INT128__) && !defined(EIGS_TEST_FORCE_PORTABLE_GEOM)
+ unsigned __int128 sum=(unsigned __int128)dest[word+j]+(j<3 ? part[j] : 0)+carry;
+ dest[word+j]=(uint64_t)sum;carry=sum>>64;
+#else
+ uint64_t addend=j<3 ? part[j] : 0;
+ uint64_t sum=dest[word+j]+addend,overflow=sum=0;--i)
+ if (positive[i]!=negative[i]) { cmp=positive[i]>negative[i] ? 1 : -1;break; }
+ *sign=cmp;*exponent=0;if (!cmp) return 0;
+ uint64_t *large=cmp>0 ? positive : negative,*small=cmp>0 ? negative : positive;
+ unsigned borrow=0;
+ for (int i=0;i1023 ? 1023 : power < -1022 ? -1022 : power;
+ uint64_t bits=(uint64_t)(chunk+1023)<<52;double factor;
+ memcpy(&factor,&bits,sizeof factor);value*=factor;power-=chunk;
+ }
+ return value;
+}
+static double eigs_geom_intersection(const double *v,int edge_axis,double edge,int rounded) {
+ double x0=v[0],y0=v[1],x1=v[2],y1=v[3],a0,a1,terms[6][2];
+ if (!edge_axis) {
+ terms[0][0]=y1;terms[0][1]=edge;terms[1][0]=-y0;terms[1][1]=edge;
+ terms[2][0]=x1;terms[2][1]=y0;terms[3][0]=-x0;terms[3][1]=y1;
+ a0=x0;a1=x1;
+ } else {
+ terms[0][0]=x1;terms[0][1]=edge;terms[1][0]=-x0;terms[1][1]=edge;
+ terms[2][0]=-x1;terms[2][1]=y0;terms[3][0]=x0;terms[3][1]=y1;
+ a0=y0;a1=y1;
+ }
+ int sign,ne,de,ds;double nm=eigs_geom_products(terms,4,&sign,&ne);
+ double denominator[2][2]={{1,a1},{-1,a0}};
+ double dm=eigs_geom_products(denominator,2,&ds,&de);
+ double q=eigs_geom_scale((sign*ds)*nm/dm,ne-de);
+ if (!rounded || !isfinite(q)) return q;
+ double lower=floor(q),tie=lower+0.5;
+ terms[4][0]=-tie;terms[4][1]=a1;terms[5][0]=tie;terms[5][1]=a0;
+ (void)eigs_geom_products(terms,6,&sign,&ne);sign*=ds;
+ return sign>0 || (sign==0 && tie>=0) ? lower+1 : lower;
+}
+static double eigs_geom_min(double a,double b) { return ab ? a : b; }
+static int eigs_geom_line(const EigsRect *d, const double *v, EigsShape *out) {
+ double x0=v[0], y0=v[1], x1=v[2], y1=v[3];
+ double left=d->x, top=d->y, right=left+d->w, bottom=top+d->h;
+ if (eigs_geom_max(x0,x1)=right ||
+ eigs_geom_max(y0,y1)=bottom) return 0;
+ int dx=(x1>x0)-(x1y0)-(y1x=out->x2=(int)round(x0); out->y=out->y2=(int)round(y0);
+ return out->xyye || (xe==ye && xm>=ym) ? 0 : 1;
+ double direction=(axis ? dy : dx)>0 ? 1 : -1;
+ double enter=direction*(axis ? y0 : x0), leave=direction*(axis ? y1 : x1);
+ double ex=x0, ey=y0, lx=x1, ly=y1;
+ int enter_edge=-1,leave_edge=-1;
+ double bound[4]={left,right,top,bottom};
+ double p[4]={-dx,dx,-dy,dy};
+ double q[4]={x0-left,right-x0,y0-top,bottom-y0};
+ for (int k=0;k<4;++k) {
+ if (p[k]==0) { if (q[k]<0) return 0; continue; }
+ double cx,cy;
+ if (k<2) { cx=bound[k]; cy=eigs_geom_intersection(v,0,cx,0); }
+ else { cy=bound[k]; cx=eigs_geom_intersection(v,1,cy,0); }
+ double u=direction*(axis ? cy : cx);
+ if (p[k]<0) {
+ if (u>leave) return 0;
+ if (u>enter) { enter=u;ex=cx;ey=cy;enter_edge=k; }
+ } else {
+ if (u=0) {
+ if (enter_edge<2) ey=eigs_geom_intersection(v,0,bound[enter_edge],1);
+ else ex=eigs_geom_intersection(v,1,bound[enter_edge],1);
+ }
+ if (leave_edge>=0) {
+ if (leave_edge<2) ly=eigs_geom_intersection(v,0,bound[leave_edge],1);
+ else lx=eigs_geom_intersection(v,1,bound[leave_edge],1);
+ }
+ out->x=(int)round(eigs_geom_max(left,eigs_geom_min(right,ex)));
+ out->y=(int)round(eigs_geom_max(top,eigs_geom_min(bottom,ey)));
+ out->x2=(int)round(eigs_geom_max(left,eigs_geom_min(right,lx)));
+ out->y2=(int)round(eigs_geom_max(top,eigs_geom_min(bottom,ly)));
+ return 1;
+}
+
+int eigs_geom_shape(const Value *args, int first, EigsShapeKind kind,
+ const EigsRect *drawable, EigsShape *out, const char *who) {
+ memset(out, 0, sizeof *out);
+ out->empty = 1;
+ out->kind = kind;
+ int count = kind == EIGS_POINT ? 2 : kind == EIGS_CIRCLE ? 3 : kind == EIGS_RRECT ? 5 : 4;
+ for (int i = 0; i < count; ++i) {
+ out->raw[i] = eigs_list_float(args, first + i, who);
+ if (g_has_error) return 0;
+ }
+ if (!drawable || drawable->w <= 0 || drawable->h <= 0) return 1;
+ double left = drawable->x, top = drawable->y;
+ double right = left + drawable->w, bottom = top + drawable->h;
+ /* Drawable bounds themselves are C ints supplied by the renderer. */
+ if (left < INT_MIN || top < INT_MIN || right > INT_MAX || bottom > INT_MAX) {
+ rt_error(EK_VALUE, 0, "%s: drawable bounds do not fit a C int", num_who(who));
+ return 0;
+ }
+ double x = out->raw[0], y = out->raw[1], x2, y2;
+ if (kind == EIGS_POINT) {
+ if (x < left || x >= right || y < top || y >= bottom) return 1;
+ out->x = (int)round(x); out->y = (int)round(y);
+ if (out->x >= right || out->y >= bottom) return 1;
+ out->w = out->h = 1;
+ } else if (kind == EIGS_LINE) {
+ if (!eigs_geom_line(drawable,out->raw,out)) return 1;
+ } else {
+ if (kind == EIGS_CIRCLE) {
+ double r = out->raw[2];
+ if (r < 0) return 1;
+ x2 = x+r+1; y2 = y+r+1; x -= r; y -= r;
+ } else {
+ if (out->raw[2] <= 0 || out->raw[3] <= 0) return 1;
+ x2 = x+out->raw[2]; y2 = y+out->raw[3];
+ }
+ x = eigs_geom_max(left, x); y = eigs_geom_max(top, y);
+ x2 = eigs_geom_min(right, x2); y2 = eigs_geom_min(bottom, y2);
+ if (x >= x2 || y >= y2) return 1;
+ out->x = (int)round(x); out->y = (int)round(y);
+ out->w = (int)round(x2) - out->x;
+ out->h = (int)round(y2) - out->y;
+ if (out->w <= 0 || out->h <= 0) return 1;
+ }
+ out->empty = 0;
+ return 1;
+}
+
+int eigs_bounded_arg(const Value *v, double lo, double hi, int *out, const char *who) {
+ double x = eigs_float_arg(v, who);
+ if (g_has_error) return 0;
+ if (!isfinite(lo) || !isfinite(hi) || lo > hi) {
+ rt_error(EK_VALUE, 0, "%s: invalid bounded range", num_who(who)); return 0;
+ }
+ if (x < lo) x = lo;
+ if (x > hi) x = hi;
+ x = round(x); /* Q4: half away from zero, after clamping */
+ if (!(x >= (double)INT_MIN && x <= (double)INT_MAX)) {
+ rt_error(EK_VALUE, 0, "%s: bounded value does not fit a C int", num_who(who));
+ return 0;
+ }
+ *out = (int)x;
+ return 1;
+}
+
+int eigs_magnitude_arg(const Value *v, int64_t lo, int64_t hi,
+ int64_t *out, const char *who) {
+ EigsNum n = eigs_number_arg(v, who);
+ if (g_has_error) return 0;
+ int64_t i;
+ if (n.is_int) i = n.value.i64;
+ else if (!num_to_i64_exact(round(n.value.f64), &i)) {
+ rt_error(EK_VALUE, 0, "%s: magnitude has no integer representation", num_who(who));
+ return 0;
+ }
+ if (i < lo || i > hi || lo > hi) {
+ rt_error(EK_VALUE, 0, "%s: magnitude is outside [%lld, %lld]", num_who(who),
+ (long long)lo, (long long)hi);
+ return 0;
+ }
+ *out = i;
+ return 1;
+}
+
+/* Validate list shape once; readers below use borrowed views of its slot. */
+static const Value *eigs_reader_list_arg(const Value *list, int i,
+ EigsView *view, const char *who) {
+ if (list && list->type == VAL_LIST && i >= 0 && i < list->data.list.count)
+ return list_get_view(list, i, view);
+ if (!g_has_error)
+ rt_error(EK_TYPE, 0, "%s: expected a number as argument %d, got nothing",
+ num_who(who), i);
+ return NULL;
+}
+static int eigs_reader_elem_default(const Value *v) {
+ return !g_strict && (!v || (!val_is_number(v) && v->type != VAL_BOOL));
}
-double eigs_elem_num(const Value *v, const char *who) {
- return eigs_slot_elem_num(eigs_container_slot((Value *)v), who);
+EigsNum eigs_list_number(const Value *list, int i, const char *who) {
+ EIGS_VIEW(view);
+ const Value *v = eigs_reader_list_arg(list, i, &view, who);
+ return g_has_error ? (EigsNum){ .is_int = 0, .value = { .f64 = 0.0 } } : eigs_number_arg(v, who);
+}
+EigsNum eigs_elem_number(const Value *v, const char *who) {
+ return eigs_reader_elem_default(v) ? (EigsNum){ .is_int = 0, .value = { .f64 = 0.0 } } : eigs_number_arg(v, who);
+}
+int eigs_opt_number(const Value *v, EigsNum *out, const char *who) {
+ if (!v || v->type == VAL_NULL) return 0;
+ EigsNum n = eigs_number_arg(v, who);
+ if (g_has_error) return 0;
+ *out = n; return 1;
+}
+
+double eigs_list_float(const Value *list, int i, const char *who) {
+ EIGS_VIEW(view);
+ const Value *v = eigs_reader_list_arg(list, i, &view, who);
+ return g_has_error ? 0 : eigs_float_arg(v, who);
+}
+double eigs_elem_float(const Value *v, const char *who) {
+ return eigs_reader_elem_default(v) ? 0 : eigs_float_arg(v, who);
+}
+int eigs_opt_float(const Value *v, double *out, const char *who) {
+ if (!v || v->type == VAL_NULL) return 0;
+ double n = eigs_float_arg(v, who);
+ if (g_has_error) return 0;
+ *out = n; return 1;
+}
+
+int64_t eigs_list_int(const Value *list, int i, const char *who) {
+ EIGS_VIEW(view);
+ const Value *v = eigs_reader_list_arg(list, i, &view, who);
+ return g_has_error ? 0 : eigs_int_arg(v, who);
+}
+int64_t eigs_elem_int(const Value *v, const char *who) {
+ return eigs_reader_elem_default(v) ? 0 : eigs_int_arg(v, who);
+}
+int eigs_opt_int(const Value *v, int64_t *out, const char *who) {
+ if (!v || v->type == VAL_NULL) return 0;
+ int64_t n = eigs_int_arg(v, who);
+ if (g_has_error) return 0;
+ *out = n; return 1;
+}
+
+uint8_t eigs_list_byte(const Value *list, int i, const char *who) {
+ EIGS_VIEW(view);
+ const Value *v = eigs_reader_list_arg(list, i, &view, who);
+ return g_has_error ? 0 : eigs_byte_arg(v, who);
+}
+uint8_t eigs_elem_byte(const Value *v, const char *who) {
+ return eigs_reader_elem_default(v) ? 0 : eigs_byte_arg(v, who);
+}
+int eigs_opt_byte(const Value *v, uint8_t *out, const char *who) {
+ if (!v || v->type == VAL_NULL) return 0;
+ uint8_t n = eigs_byte_arg(v, who);
+ if (g_has_error) return 0;
+ *out = n; return 1;
+}
+
+int eigs_list_int_range(const Value *list, int i, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ EIGS_VIEW(view);
+ const Value *v = eigs_reader_list_arg(list, i, &view, who);
+ return g_has_error ? 0 : eigs_int_arg_range(v, lo, hi, out, who);
+}
+int eigs_elem_int_range(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ const Value zero = { .type = VAL_INT };
+ return eigs_int_arg_range(eigs_reader_elem_default(v) ? &zero : v, lo, hi, out, who);
+}
+int eigs_opt_int_range(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ if (!v || v->type == VAL_NULL) return 0;
+ return eigs_int_arg_range(v, lo, hi, out, who);
+}
+
+int eigs_list_bounded(const Value *list, int i, double lo, double hi, int *out, const char *who) {
+ EIGS_VIEW(view);
+ const Value *v = eigs_reader_list_arg(list, i, &view, who);
+ return g_has_error ? 0 : eigs_bounded_arg(v, lo, hi, out, who);
+}
+int eigs_elem_bounded(const Value *v, double lo, double hi, int *out, const char *who) {
+ const Value zero = { .type = VAL_INT };
+ return eigs_bounded_arg(eigs_reader_elem_default(v) ? &zero : v, lo, hi, out, who);
+}
+int eigs_opt_bounded(const Value *v, double lo, double hi, int *out, const char *who) {
+ if (!v || v->type == VAL_NULL) return 0;
+ return eigs_bounded_arg(v, lo, hi, out, who);
+}
+
+int eigs_list_magnitude(const Value *list, int i, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ EIGS_VIEW(view);
+ const Value *v = eigs_reader_list_arg(list, i, &view, who);
+ return g_has_error ? 0 : eigs_magnitude_arg(v, lo, hi, out, who);
+}
+int eigs_elem_magnitude(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ const Value zero = { .type = VAL_INT };
+ return eigs_magnitude_arg(eigs_reader_elem_default(v) ? &zero : v, lo, hi, out, who);
+}
+int eigs_opt_magnitude(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who) {
+ if (!v || v->type == VAL_NULL) return 0;
+ return eigs_magnitude_arg(v, lo, hi, out, who);
}
-int eigs_opt_num(const Value *v, double *out, const char *who) {
- return eigs_slot_opt_num(eigs_container_slot((Value *)v), out, who);
+/* buffer_read_f64 is the renamed data boundary; callers that need exact
+ * bytes or indices instead call the corresponding checked conversion. */
+double buffer_read_f64(const Value *buffer, int64_t index) {
+ double x = buffer->data.buffer.data[index];
+ /* #1417: a strict NaN scalar read retains the first NaN diagnostic and
+ * INVALID flag. Do this before the generic nonfinite refusal; callers
+ * must stop before reading a later infinity. The finite-only refusal in
+ * non-strict mode and the infinity refusal remain unchanged. */
+ if (g_strict && x != x && !g_has_error) return num_guard(x);
+ if (!isfinite(x)) {
+ if (!g_has_error)
+ rt_error(EK_VALUE, 0, "buffer_read_f64: expected a finite number");
+ return 0.0;
+ }
+ return num_guard(x);
+}
+int buffer_index_at(const Value *buffer, int64_t index, int64_t *out, const char *who) {
+ if (!buffer || buffer->type != VAL_BUFFER || index < 0 || index >= buffer->data.buffer.count) {
+ if (!g_has_error) rt_error(EK_INDEX, 0, "%s: buffer index is out of range", num_who(who));
+ return 0;
+ }
+ /* #1417: exact byte/index reads share the data boundary's first-NaN
+ * diagnostic and INVALID flag. Stop before converting its stand-in or
+ * reading a later cell; non-strict nonfinite inputs still refuse (N13). */
+ double cell = buffer_read_f64(buffer, index);
+ if (g_has_error) return 0;
+ Value v = { .type = VAL_FLOAT };
+ VAL_F64_RAW(&v) = cell;
+ int64_t i = eigs_int_arg(&v, who);
+ if (g_has_error) return 0;
+ *out = i; return 1;
+}
+int buffer_byte_at(const Value *buffer, int64_t index, uint8_t *out, const char *who) {
+ int64_t i;
+ if (!buffer_index_at(buffer, index, &i, who)) return 0;
+ if (i < 0 || i > 255) {
+ rt_error(EK_VALUE, 0, "%s: byte is outside [0, 255]", num_who(who)); return 0;
+ }
+ *out = (uint8_t)i; return 1;
}
/* Two levels: the argument, an element of the argument list, or an element
@@ -2246,7 +2745,7 @@ int eigs_arg_has_bool(const Value *arg) {
*
* slot_from_value: takes ownership of the input ref.
* - NULL or VAL_NULL singleton -> immediate null (input is borrowed)
- * - VAL_NUM -> immediate double; input released (#262 Step E: nums never
+ * - VAL_FLOAT -> immediate double; input released (#262 Step E: nums never
* carry observer state, so there is no tracked-pointer case)
* - any other type -> TAG_HEAP; ref transferred
*
@@ -2256,26 +2755,41 @@ EigsSlot slot_from_value(Value *v) {
if (v && v != &g_null_singleton) val_decref(v);
return slot_null();
}
- if (v->type == VAL_NUM) {
+ if (v->type == VAL_FLOAT) {
/* #262 Step E: a number never carries observer state (it lives on the
- * Env slot), so every VAL_NUM ships as an immediate double. TAG_TRACKED
+ * Env slot), so every VAL_FLOAT ships as an immediate double. TAG_TRACKED
* is dead. */
- double n = VAL_NUM_RAW(v);
+ double n = VAL_F64_RAW(v);
val_decref(v);
- return slot_from_num(n);
+ return slot_from_f64(n);
+ }
+ /* #1758 (design 2.1): a small int is stored as its SMI and its box is
+ * released; an int outside [-2^47, 2^47) keeps its box (a heap slot). */
+ if (v->type == VAL_INT) {
+ int64_t i = VAL_I64_RAW(v);
+ if (i >= SLOT_SMI_MIN && i <= SLOT_SMI_MAX) {
+ val_decref(v);
+ return slot_from_int(i);
+ }
+ return slot_from_heap(v);
}
if (v->type == VAL_BOOL) return slot_from_bool(v->data.boolean); /* singleton: borrowed */
return slot_from_heap(v);
}
/* slot_to_value: produces a Value* the caller owns a ref to.
- * - immediate number -> make_num
+ * - immediate double -> make_float
* - immediate null -> g_null_singleton
* - immediate bool -> the true/false singleton (#1637)
* - heap/tracked pointer -> incref and return
+ * - SMI (#1758) -> a fresh int Value. This is the ONE place that boxes an
+ * SMI (the lifts, slot_bridge_unwrap, eigs_slot_box and the *_get_return
+ * readers all route here).
+ * - any other tag is a broken contract and aborts (it used to read back
+ * as null, silently).
*/
Value* slot_to_value(EigsSlot s) {
- if (slot_is_num(s)) return make_num(SLOT_NUM_RAW(s));
+ if (slot_is_f64(s)) return make_float(SLOT_F64_RAW(s));
if (slot_is_null(s)) return &g_null_singleton;
if (slot_is_bool(s)) return make_bool(slot_as_bool(s));
if (slot_is_heap(s)) {
@@ -2283,8 +2797,15 @@ Value* slot_to_value(EigsSlot s) {
val_incref(v);
return v;
}
- /* unknown tag: fall back to null */
- return &g_null_singleton;
+ if (slot_is_smi(s)) return make_int(slot_smi_val(s));
+ eigs_slot_refuse("slot_to_value", s);
+}
+
+void eigs_slot_refuse(const char *who, EigsSlot s) {
+ fprintf(stderr, "EigenScript FATAL: %s: slot 0x%016llx (tag 0x%04llx) is not "
+ "a value this reader handles (#1758)\n", who,
+ (unsigned long long)s.u, (unsigned long long)(s.u >> 48));
+ abort();
}
Value* make_str(const char *s) {
@@ -2770,10 +3291,12 @@ static EigsSlot module_ns_project(Value *d, Env *e, const char *key, uint32_t h,
*found = 1;
return cur;
}
- if (slot_is_num(s) && slot_is_num(cur)) {
+ if (slot_is_f64(s) && slot_is_f64(cur)) {
/* Number over number: a plain slot store with nothing to
- * release, so no destructor runs under the hold. */
- dict_value_rewrite_num(d, di, SLOT_NUM_RAW(s));
+ * release, so no destructor runs under the hold. (#1758: an
+ * int takes the general path below; slot_to_value boxes it and
+ * the dict store re-canonicalizes it.) */
+ dict_value_rewrite_f64(d, di, SLOT_F64_RAW(s));
env_shared_unlock(e);
*found = 1;
return dict_value_slot(d, di);
@@ -3033,10 +3556,11 @@ static Value *chan_clone_rec(Value *v, int depth) {
* behavior) rather than overflow the stack. */
/* #1665: scalars are copied at any depth: a number may be a stack view,
* which must never be shared out of its frame. */
- if (depth > CHAN_CLONE_MAX_DEPTH && v->type != VAL_NUM && v->type != VAL_NULL &&
+ if (depth > CHAN_CLONE_MAX_DEPTH && v->type != VAL_FLOAT && v->type != VAL_INT && v->type != VAL_NULL &&
v->type != VAL_BOOL && v->type != VAL_STR) { val_incref(v); return v; }
switch (v->type) {
- case VAL_NUM: return make_num(VAL_NUM_RAW(v));
+ case VAL_FLOAT: return make_float(VAL_F64_RAW(v));
+ case VAL_INT: return make_int(VAL_I64_RAW(v));
case VAL_NULL: return make_null();
case VAL_BOOL: return make_bool(v->data.boolean);
case VAL_STR: return make_str(v->data.str);
@@ -3239,7 +3763,8 @@ int is_truthy(Value *v) {
if (!v) return 0;
switch (v->type) {
case VAL_NULL: return 0;
- case VAL_NUM: return VAL_NUM_RAW(v) != 0.0;
+ case VAL_FLOAT: return VAL_F64_RAW(v) != 0.0;
+ case VAL_INT: return VAL_I64_RAW(v) != 0; /* #1758 R1.2a */
case VAL_STR: return v->data.str && v->data.str[0] != '\0';
case VAL_LIST: return v->data.list.count > 0;
case VAL_FN: return 1;
@@ -3252,12 +3777,91 @@ int is_truthy(Value *v) {
return 0;
}
+/* R1.6z-D2: identity is sound only if every reachable element is reflexive.
+ * Before R2.2 only a NaN-holding buffer is non-reflexive; R2.2 must extend
+ * this predicate to NaN floats (including immediate container slots).
+ * The work stack avoids C recursion and the pointer set visits shared
+ * containers/buffers once, so DAGs stay linear and cycles terminate.
+ * All pointers are borrowed for this call. No scalar boundary, raise or
+ * g_math_flags write belongs in this scan. */
+typedef struct {
+ Value **todo, **seen;
+ size_t count, capacity, seen_count, seen_capacity;
+} EqualScan;
+
+static size_t equal_scan_hash(Value *v) {
+ uintptr_t x = (uintptr_t)v;
+ x ^= x >> 16; x *= 0x45d9f3bU; x ^= x >> 13;
+ return (size_t)x;
+}
+
+/* Return true if already visited; otherwise insert before following edges. */
+static int equal_scan_seen(EqualScan *scan, Value *v) {
+ if (!scan->seen_capacity || scan->seen_count * 2 >= scan->seen_capacity) {
+ size_t old_capacity = scan->seen_capacity;
+ Value **old = scan->seen;
+ scan->seen_capacity = old_capacity ? old_capacity * 2 : 64;
+ scan->seen = xcalloc_array(scan->seen_capacity, sizeof(*scan->seen));
+ for (size_t i = 0; i < old_capacity; i++) {
+ if (!old[i]) continue;
+ size_t slot = equal_scan_hash(old[i]) & (scan->seen_capacity - 1);
+ while (scan->seen[slot]) slot = (slot + 1) & (scan->seen_capacity - 1);
+ scan->seen[slot] = old[i];
+ }
+ free(old);
+ }
+ size_t slot = equal_scan_hash(v) & (scan->seen_capacity - 1);
+ while (scan->seen[slot]) {
+ if (scan->seen[slot] == v) return 1;
+ slot = (slot + 1) & (scan->seen_capacity - 1);
+ }
+ scan->seen[slot] = v;
+ scan->seen_count++;
+ return 0;
+}
+
+static void equal_scan_push(EqualScan *scan, Value *v) {
+ if (!v || (v->type != VAL_LIST && v->type != VAL_DICT && v->type != VAL_BUFFER))
+ return;
+ if (scan->count == scan->capacity) {
+ scan->capacity = scan->capacity ? scan->capacity * 2 : 32;
+ scan->todo = xrealloc_array(scan->todo, scan->capacity, sizeof(*scan->todo));
+ }
+ scan->todo[scan->count++] = v;
+}
+
+static int equal_structure_reflexive(Value *root) {
+ EqualScan scan = {0};
+ int reflexive = 1;
+ equal_scan_push(&scan, root);
+ while (scan.count && reflexive) {
+ Value *v = scan.todo[--scan.count];
+ if (equal_scan_seen(&scan, v)) continue;
+ if (v->type == VAL_BUFFER) {
+ for (int i = 0; i < v->data.buffer.count; i++) {
+ if (isnan(v->data.buffer.data[i])) { reflexive = 0; break; }
+ }
+ } else if (v->type == VAL_LIST) {
+ for (int i = 0; i < v->data.list.count; i++)
+ equal_scan_push(&scan, list_get_ref(v, i));
+ } else {
+ eigs_module_ns_sync(v); /* #1057 whole-dict reader */
+ DICT_FOR_EACH(v, i)
+ equal_scan_push(&scan, dict_value_get_ref(v, i));
+ }
+ }
+ free(scan.todo);
+ free(scan.seen);
+ return reflexive;
+}
+
/* Structural equality for `==` / `!=`.
* Scalars compare by value (numbers, strings, null); collections compare
* recursively by structure (lists element-wise, dicts by key/value
* order-independently, buffers/text-builders by contents). Functions,
* and builtins compare by identity. Mixed types are never equal
- * (no coercion — consistent with the comparison operators). The depth
+ * (no coercion — consistent with the comparison operators), except an int
+ * and a float, which are one type, `num`, and compare by value (#1758 N2). The depth
* guard prevents runaway recursion on self-referential containers; beyond
* it we fall back to identity. */
/* #1637 item 6: a bool met by a number — at any depth of the two values —
@@ -3269,13 +3873,23 @@ int is_truthy(Value *v) {
* the operator (`==`, `!=`) or the builtin, for the message. Every other
* mixed-type pair stays unequal. */
static int values_equal_impl(Value *a, Value *b, int depth, const char *op) {
- /* #1417: a buffer compared with itself still exposes its elements. Do
- * not bypass normalization (or a strict NaN raise) through identity. */
- if (a == b && (!a || a->type != VAL_BUFFER)) return 1;
+ /* #1417 / R1.6z-D2: identity cannot hide a reachable NaN buffer. */
+ if (a == b && (!a || a->type != VAL_BUFFER)) {
+ if (a && (a->type == VAL_LIST || a->type == VAL_DICT))
+ return equal_structure_reflexive(a);
+ return 1;
+ }
if (!a || !b) return 0;
if (a->type != b->type) {
- if ((a->type == VAL_BOOL && b->type == VAL_NUM) ||
- (a->type == VAL_NUM && b->type == VAL_BOOL)) {
+ /* #1758 N2 (design 7): an int and a float are both numbers and are
+ * equal exactly when their values are; no conversion rounds either
+ * side. */
+ if (a->type == VAL_INT && b->type == VAL_FLOAT)
+ return eigs_cmp_int_float(VAL_I64_RAW(a), VAL_F64_RAW(b)) == 0;
+ if (a->type == VAL_FLOAT && b->type == VAL_INT)
+ return eigs_cmp_int_float(VAL_I64_RAW(b), VAL_F64_RAW(a)) == 0;
+ if ((a->type == VAL_BOOL && val_is_number(b)) ||
+ (val_is_number(a) && b->type == VAL_BOOL)) {
if (op[0] == '=' || op[0] == '!')
rt_error(EK_TYPE, 0, "cannot compare %s and %s with '%s'",
val_type_name(a->type), val_type_name(b->type), op);
@@ -3289,7 +3903,8 @@ static int values_equal_impl(Value *a, Value *b, int depth, const char *op) {
* (a number may be a view, so pointer identity is meaningless). */
if (depth > 64 && (a->type == VAL_LIST || a->type == VAL_DICT)) return a == b;
switch (a->type) {
- case VAL_NUM: return VAL_NUM_RAW(a) == VAL_NUM_RAW(b);
+ case VAL_FLOAT: return VAL_F64_RAW(a) == VAL_F64_RAW(b);
+ case VAL_INT: return VAL_I64_RAW(a) == VAL_I64_RAW(b); /* #1758 R1.2a: both ints */
case VAL_STR: return strcmp(a->data.str, b->data.str) == 0;
case VAL_NULL: return 1;
case VAL_BOOL: return a->data.boolean == b->data.boolean;
@@ -3319,11 +3934,9 @@ static int values_equal_impl(Value *a, Value *b, int depth, const char *op) {
case VAL_BUFFER: {
if (a->data.buffer.count != b->data.buffer.count) return 0;
for (int i = 0; i < a->data.buffer.count; i++) {
- double av = buffer_read_num(a, i);
- if (g_has_error) return 0;
- double bv = buffer_read_num(b, i);
- if (g_has_error) return 0;
- if (av != bv) return 0;
+ /* R1.6z-D1: total IEEE equality on stored doubles. No
+ * scalar boundary, normalization, error or math-flag write. */
+ if (!(a->data.buffer.data[i] == b->data.buffer.data[i])) return 0;
}
return 1;
}
@@ -3356,23 +3969,120 @@ int values_equal_op(Value *a, Value *b, const char *op) { return values_equal_im
*
* Every producer of number text now calls this, so a fourth copy cannot
* appear with a fourth rule. Writes at most 32 bytes. */
+/* #1758 R1.2a: THE int->text rule: exact decimal digits, every int64. */
+void eigs_int_text(char *buf, size_t nbuf, int64_t i) {
+ snprintf(buf, nbuf, "%lld", (long long)i);
+}
+
void eigs_num_text(char *buf, size_t nbuf, double n) {
- /* Exact integers up to 2^53 (the largest integer all doubles represent
- * exactly) print without a decimal point or exponent. The magnitude test
- * runs BEFORE the cast — casting an out-of-range double is UB (#816). */
- if (fabs(n) < 9007199254740992.0 && n == (long long)n) {
- snprintf(buf, nbuf, "%lld", (long long)n);
+ if (nbuf == 0) return;
+ if (isnan(n)) { snprintf(buf, nbuf, "nan"); return; }
+ int neg = signbit(n) != 0;
+ if (isinf(n)) { snprintf(buf, nbuf, "%sinf", neg ? "-" : ""); return; }
+ if (n == 0.0) { snprintf(buf, nbuf, "%s0.0", neg ? "-" : ""); return; }
+ /* In Python's fixed range every integral float has its exact integer
+ * digits followed by .0. The range check makes the cast safe. */
+ if (fabs(n) < 1e16 && n == (int64_t)n) {
+ snprintf(buf, nbuf, "%lld.0", (long long)(int64_t)n);
return;
}
- /* Otherwise the shortest representation that round-trips: try 15..17
- * significant digits and stop at the first that parses back to the same
- * double. %.6g (the old `str of` default) silently truncated every float
- * to 6 figures — lossy for the numerical/STEM workloads this language
- * targets — and %.15g loses the 17th digit a double can need. */
- for (int prec = 15; prec <= 17; prec++) {
- snprintf(buf, nbuf, "%.*g", prec, n);
- if (strtod(buf, NULL) == n) return;
+
+ /* The first decimal precision that round-trips is the shortest. The
+ * correctly-rounded %e candidate is closest, with decimal half-even
+ * ties. At a binary power of two the rounding interval is asymmetric:
+ * its closest decimal can fail while the adjacent decimal succeeds
+ * (2^-24 needs 5.960464477539063e-08). Check the neighbors too. */
+ double magnitude = fabs(n);
+ uint64_t significand = 0;
+ int scale = 0;
+ /* Existence of a round-tripping decimal is monotone in precision:
+ * appending a zero preserves that decimal at the next precision.
+ * Most binary64 values need 16 or 17 digits; try 15 first, then 16/17.
+ * If 15 works, test 1 and binary-search the remaining smaller range. */
+ int low = 1, high = 17, precision = 15;
+ while (low <= high) {
+ char rounded[32];
+ snprintf(rounded, sizeof rounded, "%.*e", precision - 1, magnitude);
+ uint64_t nearest = 0;
+ const char *p = rounded;
+ while (*p != 'e' && *p != 'E') {
+ /* Ignore the locale's decimal separator. Candidate text below
+ * has an integer significand, so strtod needs no separator. */
+ if (*p >= '0' && *p <= '9') nearest = nearest * 10 + (unsigned)(*p - '0');
+ p++;
+ }
+ p++;
+ int exponent_negative = *p == '-';
+ if (*p == '+' || *p == '-') p++;
+ int exponent = 0;
+ while (*p >= '0' && *p <= '9') exponent = exponent * 10 + (*p++ - '0');
+ if (exponent_negative) exponent = -exponent;
+ int rounded_scale = exponent - precision + 1;
+ uint64_t minimum = 1;
+ for (int i = 1; i < precision; i++) minimum *= 10;
+ int found = 0;
+ for (int neighbor = 0; neighbor < 3; neighbor++) {
+ uint64_t candidate = neighbor == 0 ? nearest :
+ neighbor == 1 ? nearest - 1 : nearest + 1;
+ int candidate_scale = rounded_scale;
+ /* The predecessor of 1.00eE is 9.99e(E-1), where the decimal
+ * grid is ten times finer. Keep the actual p-digit neighbor. */
+ if (neighbor == 1 && nearest == minimum) {
+ candidate = nearest * 10 - 1;
+ candidate_scale--;
+ }
+ if (candidate == 0) continue;
+ /* uint64 digits + e + signed int32 exponent + NUL: at most 33. */
+ char decimal[40];
+ snprintf(decimal, sizeof decimal, "%llue%+d",
+ (unsigned long long)candidate, candidate_scale);
+ if (strtod(decimal, NULL) == magnitude) {
+ significand = candidate;
+ scale = candidate_scale;
+ found = 1;
+ break;
+ }
+ }
+ if (found) {
+ high = precision - 1;
+ precision = precision == 15 ? 1 : (low + high) / 2;
+ } else {
+ low = precision + 1;
+ precision = precision >= 15 ? precision + 1 : (low + high) / 2;
+ }
}
+
+ while (significand % 10 == 0) { significand /= 10; scale++; }
+ char digits[24], text[32];
+ snprintf(digits, sizeof digits, "%llu", (unsigned long long)significand);
+ int count = (int)strlen(digits);
+ int exponent = scale + count - 1;
+ int len = 0;
+ if (neg) text[len++] = '-';
+ if (exponent >= -4 && exponent < 16) {
+ int point = exponent + 1;
+ if (point <= 0) {
+ text[len++] = '0';
+ text[len++] = '.';
+ for (int i = 0; i < -point; i++) text[len++] = '0';
+ for (int i = 0; i < count; i++) text[len++] = digits[i];
+ } else {
+ for (int i = 0; i < point || i < count; i++) {
+ if (i == point) text[len++] = '.';
+ text[len++] = i < count ? digits[i] : '0';
+ }
+ if (point >= count) { text[len++] = '.'; text[len++] = '0'; }
+ }
+ text[len] = '\0';
+ } else {
+ text[len++] = digits[0];
+ if (count > 1) {
+ text[len++] = '.';
+ for (int i = 1; i < count; i++) text[len++] = digits[i];
+ }
+ snprintf(text + len, sizeof text - (size_t)len, "e%+03d", exponent);
+ }
+ snprintf(buf, nbuf, "%s", text);
}
char* value_to_string(Value *v) {
@@ -3381,8 +4091,12 @@ char* value_to_string(Value *v) {
char buf[256];
switch (v->type) {
case VAL_NULL: return xstrdup("null");
- case VAL_NUM: {
- eigs_num_text(buf, sizeof(buf), VAL_NUM_RAW(v));
+ case VAL_FLOAT: {
+ eigs_num_text(buf, sizeof(buf), VAL_F64_RAW(v));
+ return xstrdup(buf);
+ }
+ case VAL_INT: { /* #1758 R1.2a: an int prints its exact digits */
+ eigs_int_text(buf, sizeof(buf), VAL_I64_RAW(v));
return xstrdup(buf);
}
case VAL_STR: return xstrdup(v->data.str);
@@ -4756,7 +5470,8 @@ static int gc_value_is_node(Value *v) {
case VAL_FN:
return 1;
/* Leaves: no owned edges out, so no rows and never a cycle member. */
- case VAL_NUM:
+ case VAL_FLOAT:
+ case VAL_INT:
case VAL_STR:
case VAL_NULL:
case VAL_BUILTIN:
diff --git a/src/eigenscript.h b/src/eigenscript.h
index 41358418e..c006b50d8 100644
--- a/src/eigenscript.h
+++ b/src/eigenscript.h
@@ -14,7 +14,7 @@
* defines both; the default build (none defined) is the pooled baseline. The
* call sites below test the per-layer macros only — never EIGS_POOL_OFF
* directly — so this is the single point of truth for the union.
- * EIGS_POOL_OFF_NUM — Value NUM freelist (free_value/make_num/recycle_intermediate)
+ * EIGS_POOL_OFF_NUM — Value NUM freelist (free_value/make_float/recycle_intermediate)
* EIGS_POOL_OFF_CALLENV — call-env recycling (vm_park_call_env/vm_take_call_env) */
#ifdef EIGS_POOL_OFF
# ifndef EIGS_POOL_OFF_NUM
@@ -152,6 +152,13 @@ static inline int eigs_name_is_reserved(const char *name) {
#define MAX_PARAMS 16
#define MAX_MATCH_CASES 64
+/* #1758 R1.3: a source number retains its literal kind through lexing and
+ * parsing. Decimal integers outside int64 are floats; hex integers wrap. */
+typedef struct {
+ int is_int;
+ union { int64_t i64; double f64; } value;
+} EigsNum;
+
/* ---- Tokenizer ---- */
typedef enum {
@@ -180,12 +187,13 @@ typedef enum {
TOK_PLUS_EQ, TOK_MINUS_EQ, TOK_STAR_EQ, TOK_SLASH_EQ, TOK_PERCENT_EQ,
TOK_AMP_EQ, TOK_BITOR_EQ, TOK_CARET_EQ, TOK_SHL_EQ, TOK_SHR_EQ,
TOK_NEWLINE, TOK_INDENT, TOK_DEDENT,
- TOK_EOF
+ TOK_EOF,
+ TOK_SLASH_SLASH, TOK_SLASH_SLASH_EQ /* #1758 N4/Q6: append; existing token IDs stay fixed */
} TokType;
typedef struct {
TokType type;
- double num_val;
+ EigsNum number;
char *str_val;
int line;
int col; /* 0-based column offset */
@@ -224,7 +232,7 @@ typedef enum {
AST_TRY, AST_DICT, AST_DOT, AST_BREAK, AST_CONTINUE, AST_DOT_ASSIGN, AST_IMPORT,
AST_MATCH, AST_LAMBDA, AST_UNOBSERVED, AST_INDEX_ASSIGN, AST_LIST_PATTERN_ASSIGN,
AST_SLICE,
- AST_BOOL /* #1637: `true`/`false`; the value is data.num (1 or 0) */
+ AST_BOOL /* #1637: `true`/`false`; the value is data.boolean (1 or 0) */
} ASTType;
typedef struct ASTNode ASTNode;
@@ -241,7 +249,8 @@ struct ASTNode {
* parenthesized literal list is a single call
* argument, never spread (#355) */
union {
- double num;
+ EigsNum number;
+ int boolean;
char *str;
struct { char *name; } ident;
struct { char op[4]; ASTNode *left; ASTNode *right; uint8_t synth; } binop; /* synth: f-string `+` (#1425) */
@@ -279,25 +288,49 @@ struct ASTNode {
/* ---- Value types ---- */
typedef enum {
- VAL_NUM, VAL_STR, VAL_LIST, VAL_FN, VAL_BUILTIN, VAL_NULL,
- /* #1690: keep the surviving type tag numbers unchanged. */
+ /* #1758 R1.2a: a double. A number Value is a VAL_FLOAT or a
+ * VAL_INT; `type of` says "num" for both (decision (B)). */
+ VAL_FLOAT, VAL_STR, VAL_LIST, VAL_FN, VAL_BUILTIN, VAL_NULL,
+ /* #1690: preserve surviving tags; #1758 appends VAL_INT below. */
VAL_DICT = 7, VAL_BUFFER, VAL_TEXT_BUILDER,
/* #1637: a distinct boolean. Two immortal singletons (make_bool); the
* slot layer carries it as the TAG_BOOL immediate. Arithmetic on it and
* `==` against a number raise. */
- VAL_BOOL
+ VAL_BOOL,
+ /* #1758: an exact int64. Stack/container slots use an SMI when it fits;
+ * the Value API and out-of-SMI values use make_int (design 2.1). */
+ VAL_INT,
} ValType;
typedef struct Value Value;
typedef Value* (*BuiltinFn)(Value* arg);
+/* #1758 R1.2: a number Value is a VAL_FLOAT or a VAL_INT. THE test a number
+ * test uses when an int belongs on the same path as a float (a builtin's
+ * number test admits an int and reads it through the bridge, design 2.7). */
+#define val_is_number(v) ((v)->type == VAL_FLOAT || (v)->type == VAL_INT)
+/* #1758 N2 (design 7): an int and a float compare exactly, by value. Returns
+ * -1, 0 or 1 for i < f, i == f, i > f, and 2 when f is NaN (unordered: every
+ * comparison is false except !=). Lua's LTintfloat/EQintfloat give the same
+ * answers. Pure; neither operand is rounded through the other kind. */
+static inline int eigs_cmp_int_float(int64_t i, double f) {
+ if (f != f) return 2;
+ if (f >= 9223372036854775808.0) return -1; /* f above every int64 */
+ if (f < -9223372036854775808.0) return 1; /* f below every int64 */
+ double fl = floor(f); /* in [-2^63, 2^63): the cast is exact */
+ int64_t il = (int64_t)fl;
+ if (i != il) return i < il ? -1 : 1;
+ return f != fl ? -1 : 0; /* i == floor(f) < f when f has a fraction */
+}
/* #1637 round 4: the ONE raw way into a Value's number (an lvalue: also the
- * write). Only for a Value whose type is proven VAL_NUM, in a function listed
- * in tools/num_read_allowlist.txt with its use count. Everything else uses
- * eigs_num_arg / eigs_list_num / eigs_opt_num. VAL_NUM_OFFSET is the same
- * member for JIT-emitted loads and stores. */
-#define VAL_NUM_RAW(v) ((v)->data.num_)
-#define VAL_NUM_OFFSET ((int32_t)offsetof(Value, data.num_))
+ * write). Only for a Value whose type is proven (VAL_FLOAT for the double,
+ * VAL_INT for the int64), in a function listed in tools/num_read_allowlist.txt
+ * with its use count. Everything else uses eigs_float_arg / eigs_list_float /
+ * eigs_opt_float. VAL_F64_OFFSET is the double member for JIT-emitted loads and
+ * stores (#1758 R1.2). */
+#define VAL_F64_RAW(v) ((v)->data.f64_)
+#define VAL_I64_RAW(v) ((v)->data.i64_)
+#define VAL_F64_OFFSET ((int32_t)offsetof(Value, data.f64_))
/* EigsSlot union — full inline helpers in value_slot.h, which is
* included below after the Value struct is fully declared. We need the
@@ -439,13 +472,14 @@ struct Value {
ValType type;
union {
/* #1637 round 4: renamed so no unchecked read compiles. A Value's
- * number is read through the checking accessors (eigs_num_arg,
- * eigs_list_num, eigs_opt_num), which raise on a bool or any other
- * non-number, or through VAL_NUM_RAW where the type is already
- * proven -- and every VAL_NUM_RAW use sits in a function on the
+ * number is read through the checking accessors (eigs_float_arg,
+ * eigs_list_float, eigs_opt_float), which raise on a bool or any other
+ * non-number, or through VAL_F64_RAW where the type is already
+ * proven -- and every VAL_F64_RAW use sits in a function on the
* reviewed list tools/num_read_allowlist.txt, pinned by count
* (tools/num_read_check.sh). */
- double num_;
+ double f64_; /* VAL_FLOAT (#1758 R1.2a) */
+ int64_t i64_; /* VAL_INT (#1758) */
int boolean; /* VAL_BOOL: 0 or 1 (only the two singletons exist) */
/* VAL_STR payload: NUL-terminated bytes.
*
@@ -520,10 +554,10 @@ _Static_assert(sizeof(((Value *)0)->data.strv) <= sizeof(((Value *)0)->data.fn),
* compiler instruments for ASan; every other build expands these to nothing,
* so release objects are byte-identical.
*
- * Parked NUM (free_value / recycle_intermediate -> make_num): the whole Value
+ * Parked NUM (free_value / recycle_intermediate -> make_float): the whole Value
* is poisoned EXCEPT the link word, the sizeof(Value *) bytes the freelist
* threads its next pointer through (EIGS_NUM_LINK). Only the pool's own push,
- * pop (make_num) and drain (eigs_thread_drain_caches) touch those bytes. In
+ * pop (make_float) and drain (eigs_thread_drain_caches) touch those bytes. In
* every other build the link overlays the number itself (offset
* offsetof(Value, data)), which would leave a stale NUMBER read -- the
* commonest stale read of a recycled NUM -- undetectable; so under ASan the
@@ -1394,30 +1428,55 @@ extern __thread EigsThread *eigs_current;
* and false in every argument slot of every builtin must raise unless the
* slot is on its reviewed any-value list. */
int eigs_arg_has_bool(const Value *arg);
-/* #1637: THE checked number reads. C code turning a Value into a double goes
- * through one of these (tools/num_read_check.sh enforces it: a raw
- * `->data.num` read needs a VAL_NUM test in the same function, or a reviewed
- * allowlist entry). A bool -- or any other non-number -- raises a type error
- * naming `who` (a `builtin_` prefix is dropped) and reads as 0.0; the caller
- * checks g_has_error before acting on the value.
- * eigs_num_arg(v, who) v must be a number
- * eigs_list_num(l, i, who) element i of the argument list must exist and
- * be a number
- * eigs_opt_num(v, &d, who) optional: absent/null -> 0 (use the default);
- * a number -> 1 with *d set; anything else raises
- * and returns 0 */
-double eigs_num_arg_slow(const Value *v, const char *who);
-static inline double eigs_num_arg(const Value *v, const char *who) {
- if (__builtin_expect(v && v->type == VAL_NUM, 1)) return VAL_NUM_RAW(v);
- return eigs_num_arg_slow(v, who);
-}
-double eigs_list_num(const Value *list, int i, const char *who);
-/* #1637 round 4: an ELEMENT a builtin reads as a number (a tensor cell, a
- * byte). A bool raises in every strict mode; any other non-number raises
- * under EIGS_STRICT and reads as the documented 0.0 under EIGS_STRICT=0.
- * The caller checks g_has_error. */
-double eigs_elem_num(const Value *v, const char *who);
-int eigs_opt_num(const Value *v, double *out, const char *who);
+/* #1637/#1758: required, element and optional checked readers are split
+ * by numeric contract below. Raw payload macros require a reviewed kind
+ * proof in tools/num_read_allowlist.txt. Callers stop on g_has_error. */
+/* #1758 R1.6a: final numeric readers. All inputs are BORROWED, including
+ * on a raise: no reader allocates, retains, or releases its argument. Every
+ * floating input must be finite. Required/list reads reject non-numbers;
+ * element reads retain the legacy non-strict zero default (never for bool);
+ * optional reads return 0 for absent/null or a raise, leaving *out unchanged.
+ * A successful range/optional read returns 1. Callers stop on g_has_error. */
+EigsNum eigs_number_arg(const Value *v, const char *who);
+EigsNum eigs_list_number(const Value *list, int i, const char *who);
+EigsNum eigs_elem_number(const Value *v, const char *who);
+int eigs_opt_number(const Value *v, EigsNum *out, const char *who);
+double eigs_float_arg(const Value *v, const char *who);
+double eigs_list_float(const Value *list, int i, const char *who);
+double eigs_elem_float(const Value *v, const char *who);
+int eigs_opt_float(const Value *v, double *out, const char *who);
+int64_t eigs_int_arg(const Value *v, const char *who);
+int64_t eigs_list_int(const Value *list, int i, const char *who);
+int64_t eigs_elem_int(const Value *v, const char *who);
+int eigs_opt_int(const Value *v, int64_t *out, const char *who);
+uint8_t eigs_byte_arg(const Value *v, const char *who);
+uint8_t eigs_list_byte(const Value *list, int i, const char *who);
+uint8_t eigs_elem_byte(const Value *v, const char *who);
+int eigs_opt_byte(const Value *v, uint8_t *out, const char *who);
+int eigs_int_arg_range(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who);
+int eigs_list_int_range(const Value *list, int i, int64_t lo, int64_t hi, int64_t *out, const char *who);
+int eigs_elem_int_range(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who);
+int eigs_opt_int_range(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who);
+/* Q7 geometry: clipped pixel bounds plus borrowed continuous coordinates.
+ * CIRCLE/RRECT callers iterate only y..y+h and clip each span to x..x+w. */
+typedef enum { EIGS_POINT, EIGS_RECT, EIGS_LINE, EIGS_CIRCLE, EIGS_RRECT } EigsShapeKind;
+typedef struct { int x, y, w, h; } EigsRect;
+typedef struct {
+ EigsShapeKind kind;
+ int empty, x, y, w, h, x2, y2;
+ double raw[5];
+} EigsShape;
+int eigs_geom_shape(const Value *args, int first, EigsShapeKind kind,
+ const EigsRect *drawable, EigsShape *out, const char *who);
+int eigs_bounded_arg(const Value *v, double lo, double hi, int *out, const char *who);
+int eigs_list_bounded(const Value *list, int i, double lo, double hi, int *out, const char *who);
+int eigs_elem_bounded(const Value *v, double lo, double hi, int *out, const char *who);
+int eigs_opt_bounded(const Value *v, double lo, double hi, int *out, const char *who);
+int eigs_magnitude_arg(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who);
+int eigs_list_magnitude(const Value *list, int i, int64_t lo, int64_t hi, int64_t *out, const char *who);
+int eigs_elem_magnitude(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who);
+int eigs_opt_magnitude(const Value *v, int64_t lo, int64_t hi, int64_t *out, const char *who);
+
/* #1637: the builtin-call bool gate -- the structural half of "a bool is
* not a number". Every call of a C builtin goes through eigs_call_builtin
* (tools/num_read_check.sh refuses a bare `->data.builtin(` call). When the
@@ -1504,7 +1563,7 @@ void eigs_bool_gate_exempt(BuiltinFn fn); /* a host function: never gated */
* simple early return. The raise still returns, so the caller stops before
* consuming the coerced value.
*
- * These sites have no `return make_num(0)` to enumerate. A probe that
+ * These sites have no `return make_float(0)` to enumerate. A probe that
* stays silent under strict is what tools/strict_differential.sh catches. */
/* PLACEMENT IS LOAD-BEARING: this RETURNS, so it must sit BEFORE anything the
* function has allocated and still owns, or the raise abandons it. Put the
@@ -1791,7 +1850,9 @@ void strbuf_free(strbuf *b);
/* ---- Value constructors ---- */
-Value* make_num(double n);
+Value* make_float(double n);
+Value* make_float_permanent(double n);
+Value* make_int(int64_t n); /* owned, unpooled int box; slots canonicalize small ints */
void recycle_intermediate(Value *v);
Value* make_str(const char *s);
Value* make_str_len(const char *s, size_t n); /* #1183: caller knows strlen(s) */
@@ -1801,7 +1862,6 @@ Value* make_null(void);
/* #1637: the immortal true/false singletons (refcount no-ops, like null). */
Value* make_bool(int b);
Value* make_list(int capacity);
-Value* make_num_permanent(double n); /* unpooled make_num; needs no VM thread */
Value* make_list_heap(int capacity);
Value* make_text_builder(void);
Value* make_fn(const char *name, char **params, int param_count, Env *closure);
@@ -1897,10 +1957,11 @@ void free_value(Value *v);
* branch stays one call on a path a finite program never takes. */
void eigs_strict_nan_raise(const char *who);
-/* Side-effect-free VALUE mapping of num_guard, for readers such as observer
- * equality which must see language numbers without changing interpreter
- * state or raising in strict mode. Keep its branches in sync with
- * num_guard; tests/test_num_guard_value.c pins their bit-level results. */
+/* Legacy scalar VALUE mapping of num_guard, retained for the bit-level
+ * tests/test_num_guard_value.c fixture. Phase2 buffer/container equality uses
+ * the stored IEEE doubles directly (S10-D1), so runtime builds may not call
+ * this helper. Keep its mapping in sync with num_guard. */
+__attribute__((unused))
static inline double num_guard_value(double x) {
if (x != x) return 0.0;
if (x > EIGS_NUM_MAX) return EIGS_NUM_MAX;
@@ -1921,22 +1982,6 @@ static inline double num_guard(double x) {
return x;
}
-/* #1417: VAL_BUFFER storage is an unboxed numerical work area, so kernels
- * can transiently leave an IEEE NaN or infinity in it. The language's
- * finite-number invariant applies when an element crosses the buffer/scalar
- * boundary: every scalar read goes through this helper. Keeping the guard at
- * that boundary also lets bulk kernels retain their unboxed representation. */
-static inline double buffer_read_num(const Value *buffer, int64_t index) {
- return num_guard(buffer->data.buffer.data[index]);
-}
-
-/* Numeric byte consumers truncate and wrap modulo 256. Reduce in double
- * first: even a normalized finite number can be outside the C int range.
- * The caller must normalize the input and stop on a raised read first. */
-static inline unsigned char finite_num_to_byte(double x) {
- return (unsigned char)((int)fmod(x, 256.0) & 0xFF);
-}
-
/* #1740: a bitwise operand converts to int64 only if it is an EXACT integer
* in [-2^63, 2^63) -- Lua 5.4's rule (manual 3.4.3). Returns 0 for a
* fractional or out-of-range number; the caller raises `value`. Range test
@@ -1949,15 +1994,24 @@ static inline int num_to_i64_exact(double x, int64_t *out) {
*out = i;
return 1;
}
-#define EIGS_NO_INT_REP_FMT "'%s': number %.17g has no integer representation"
-
-__attribute__((unused))
-static inline unsigned char buffer_read_byte(const Value *buffer, int64_t index) {
- double x = buffer_read_num(buffer, index);
- if (g_has_error) return 0;
- return finite_num_to_byte(x);
+/* #1758 N8: Lua 5.4 shifts operate on an unsigned 64-bit pattern.
+ * Bound the signed count BEFORE negating it (INT64_MIN is valid input).
+ * Convert the result pattern without an out-of-range unsigned-to-signed cast. */
+static inline int64_t eigs_bit_shift(int64_t a, int64_t count, int right) {
+ if (count <= -64 || count >= 64) return 0;
+ if (count < 0) { count = -count; right = !right; }
+ uint64_t bits = right ? (uint64_t)a >> count : (uint64_t)a << count;
+ return bits <= INT64_MAX ? (int64_t)bits : -1 - (int64_t)(UINT64_MAX - bits);
}
+/* Final buffer readers refuse non-finite payloads. A finite data read
+ * retains the current scalar guard; byte/index reads also require exactness. */
+double buffer_read_f64(const Value *buffer, int64_t index);
+int buffer_byte_at(const Value *buffer, int64_t index, uint8_t *out, const char *who);
+int buffer_index_at(const Value *buffer, int64_t index, int64_t *out, const char *who);
+
+#define EIGS_NO_INT_REP_FMT "'%s': number %.17g has no integer representation"
+
/* #971: num_guard for a builtin whose result CAN be NaN on the current tree
* (`pow` of a negative base with a fractional exponent, `num of "nan"`,
* `f64_from_bytes` of a NaN bit pattern, `matmul`'s inf-inf accumulation,
@@ -2001,7 +2055,8 @@ static inline double num_guard_named(double x, const char *who) {
* inside on GC-enabled / not-collecting / single-threaded. */
void gc_note_possible_root(Value *v);
-static inline void val_incref(Value *v) {
+/* Keep reference acquisition inline as the numeric slow paths grow (#1758). */
+static inline __attribute__((always_inline)) void val_incref(Value *v) {
if (v && v->type != VAL_NULL && v->type != VAL_BOOL) {
if (__builtin_expect(g_vm_multithreaded, 0))
__atomic_add_fetch(&v->refcount, 1, __ATOMIC_RELAXED);
@@ -2130,7 +2185,7 @@ Value* env_get_hashed(Env *env, const char *name, uint32_t h);
Value* env_get_local_hashed(Env *env, const char *name, uint32_t h);
void env_set_local_hashed(Env *env, const char *name, uint32_t h, Value *val);
/* Slot-flavored fast paths: take/produce EigsSlot directly so immediates
- * never round-trip through make_num + val_decref. Reference-count
+ * never round-trip through make_float + val_decref. Reference-count
* semantics match the Value* variants: env *borrows* the input slot and
* incref's internally, *_get returns a slot the caller must slot_decref. */
/* #868/#908: how many assignments this binding has seen, for the `when `
@@ -2283,6 +2338,7 @@ char* value_to_string(Value *v);
* `str of`, all three JSON encoders, the SIGUSR1 observer dump — so a copy
* with a different precision cannot reappear. Needs 32 bytes. */
void eigs_num_text(char *buf, size_t nbuf, double n);
+void eigs_int_text(char *buf, size_t nbuf, int64_t i); /* #1758 R1.2a: exact digits */
void eigs_json_escape_string(strbuf *out, const char *s);
/* #880: decode a JSON string body (s[*pos] = first byte after the opening
* quote) into `out`, leaving *pos past the closing quote. Returns whether
@@ -2584,5 +2640,6 @@ void eigs_ensure_random_seeded(void);
* entry points runtime code may use, so seeding and draws share one lock. */
double eigs_random_double(void);
long eigs_random_long(void);
+void eigs_seed_random(long seed);
#endif /* EIGENSCRIPT_H */
diff --git a/src/eigs_embed.c b/src/eigs_embed.c
index 52cdab69a..d9e78de3b 100644
--- a/src/eigs_embed.c
+++ b/src/eigs_embed.c
@@ -369,7 +369,8 @@ EigsValue *eigs_get_global(const char *name) {
/* ---- Values ------------------------------------------------------- */
-EigsValue *eigs_value_new_num(double n) { return make_num(n); }
+EigsValue *eigs_value_new_num(double n) { return make_float(n); }
+EigsValue *eigs_value_new_int(int64_t n) { return make_int(n); }
EigsValue *eigs_value_new_string(const char *s) { return make_str(s ? s : ""); }
EigsValue *eigs_value_new_null(void) { return make_null(); }
EigsValue *eigs_value_new_bool(int b) { return make_bool(b != 0); }
@@ -382,7 +383,8 @@ void eigs_value_release(EigsValue *v) { if (v) val_decref(v); }
EigsValueType eigs_value_type(EigsValue *v) {
if (!v) return EIGS_TYPE_NULL;
switch (v->type) {
- case VAL_NUM: return EIGS_TYPE_NUM;
+ case VAL_FLOAT: return EIGS_TYPE_NUM;
+ case VAL_INT: return EIGS_TYPE_NUM; /* #1758 (Q3): one embed number type */
case VAL_STR: return EIGS_TYPE_STR;
case VAL_LIST: return EIGS_TYPE_LIST;
case VAL_DICT: return EIGS_TYPE_DICT;
@@ -427,11 +429,39 @@ int eigs_value_as_bool(EigsValue *v) {
* plausible false-as-zero. Other non-numbers keep the documented 0.0. Hosts
* test eigs_value_type first, or read a bool with eigs_value_as_bool. */
double eigs_value_as_num(EigsValue *v) {
- if (!v || v->type != VAL_NUM) {
- embed_value_error(EK_TYPE, "eigs_value_as_num: expected a number");
- return (v && v->type == VAL_BOOL) ? NAN : 0.0;
+ /* Phase 2 (Q3): an int is a number, converted like lua_tonumber; a float
+ * is read raw. Anything else reports through #1932's pending error
+ * (attached threads only), keeping #1637's NaN for a bool and 0.0
+ * otherwise. */
+ if (v && v->type == VAL_INT) return (double)VAL_I64_RAW(v);
+ if (v && v->type == VAL_FLOAT) return VAL_F64_RAW(v);
+ embed_value_error(EK_TYPE, "eigs_value_as_num: expected a number");
+ return (v && v->type == VAL_BOOL) ? NAN : 0.0;
+}
+
+int64_t eigs_value_as_int(EigsValue *v, int *ok) {
+ if (ok) *ok = 0;
+ if (v && v->type == VAL_INT) {
+ if (ok) *ok = 1;
+ return VAL_I64_RAW(v);
}
- return VAL_NUM_RAW(v);
+ if (v && v->type == VAL_FLOAT) {
+ /* The exactness comparison must see subnormals even in a host
+ * callback using DAZ. Preserve host flags as well as its controls. */
+ EmbedFenv fenv;
+ embed_fenv_enter(&fenv);
+ int64_t result = 0;
+ int exact = num_to_i64_exact(VAL_F64_RAW(v), &result);
+ embed_fenv_leave(&fenv);
+ if (ok) *ok = exact;
+ return exact ? result : 0;
+ }
+ embed_value_error(EK_TYPE, "eigs_value_as_int: expected a number");
+ return 0;
+}
+
+int eigs_value_is_int(EigsValue *v) {
+ return v && v->type == VAL_INT;
}
const char *eigs_value_as_string(EigsValue *v) {
@@ -500,7 +530,8 @@ double eigs_value_buffer_get(EigsValue *v, int i) {
i, v->data.buffer.count);
return 0.0;
}
- return buffer_read_num(v, i);
+ /* R1.6z-D1: the public C API exposes stored data, bit for bit. */
+ return v->data.buffer.data[i];
}
void eigs_value_buffer_set(EigsValue *v, int i, double x) {
@@ -632,7 +663,7 @@ void eigs_trace_record_nondet(const char *name, EigsValue *v) {
/* #1637: EIGS_KIND(EIGS_TYPE_X) bits -> the runtime's ValType bits. */
int eigs_trace_declare_kind(const char *name, unsigned kinds) {
static const struct { EigsValueType e; ValType v; } map[] = {
- {EIGS_TYPE_NULL, VAL_NULL}, {EIGS_TYPE_NUM, VAL_NUM}, {EIGS_TYPE_STR, VAL_STR},
+ {EIGS_TYPE_NULL, VAL_NULL}, {EIGS_TYPE_NUM, VAL_FLOAT}, {EIGS_TYPE_NUM, VAL_INT}, {EIGS_TYPE_STR, VAL_STR},
{EIGS_TYPE_LIST, VAL_LIST}, {EIGS_TYPE_DICT, VAL_DICT},
{EIGS_TYPE_BUFFER, VAL_BUFFER}, {EIGS_TYPE_BOOL, VAL_BOOL},
};
diff --git a/src/eigs_embed.h b/src/eigs_embed.h
index 492aba30f..9dccd17ee 100644
--- a/src/eigs_embed.h
+++ b/src/eigs_embed.h
@@ -29,6 +29,7 @@
#define EIGS_EMBED_H
#include
+#include
#ifdef __cplusplus
extern "C" {
@@ -148,7 +149,7 @@ EigsValue *eigs_get_global(const char *name);
typedef enum {
EIGS_TYPE_NULL = 0,
- EIGS_TYPE_NUM,
+ EIGS_TYPE_NUM, /* int and float, like Lua's LUA_TNUMBER */
EIGS_TYPE_STR,
EIGS_TYPE_LIST,
EIGS_TYPE_DICT,
@@ -159,6 +160,7 @@ typedef enum {
} EigsValueType;
EigsValue *eigs_value_new_num(double n);
+EigsValue *eigs_value_new_int(int64_t n); /* exact signed int64 */
EigsValue *eigs_value_new_string(const char *s);
EigsValue *eigs_value_new_null(void);
EigsValue *eigs_value_new_bool(int b); /* #1637: the true/false value */
@@ -169,6 +171,11 @@ void eigs_value_release(EigsValue *v);
EigsValueType eigs_value_type(EigsValue *v);
double eigs_value_as_num(EigsValue *v); /* wrong type: sentinel + error; bool sentinel is NaN */
+/* Like lua_tointegerx: exact int64 from int or integral float. Numeric
+ * refusal returns 0 with *ok=0 and sets no error; a non-number also sets a
+ * type error. ok may be NULL. Existing pending errors are never cleared. */
+int64_t eigs_value_as_int(EigsValue *v, int *ok);
+int eigs_value_is_int(EigsValue *v); /* kind only, like lua_isinteger */
int eigs_value_as_bool(EigsValue *v); /* wrong type: 0 + error */
const char *eigs_value_as_string(EigsValue *v); /* NULL if not str; borrowed */
@@ -196,6 +203,9 @@ EigsValue *eigs_value_new_buffer(int count); /* zeroed, 1-D (unshaped) */
* unchanged; inspect/clear the error through the error API above. A thread
* with no attached state has no error channel and gets the sentinel alone. */
int eigs_value_buffer_len(EigsValue *v); /* wrong type: 0 + error */
+/* Valid reads return the stored double exactly: NaN payloads, infinities and
+ * signed zero included. No normalization, math-flag write or raise. This
+ * mirrors the setter's raw double store; C callers can test isfinite(). */
double eigs_value_buffer_get(EigsValue *v, int i); /* OOB: 0.0 + error */
void eigs_value_buffer_set(EigsValue *v, int i, double x); /* OOB: no-op + error */
diff --git a/src/embed_smoke.c b/src/embed_smoke.c
index e87061e38..6b11fc4d7 100644
--- a/src/embed_smoke.c
+++ b/src/embed_smoke.c
@@ -43,6 +43,115 @@ static int old_task_ran = 0;
} \
} while (0)
+/* #1758 Q3: public C API, including boxed ints, exact conversion bounds,
+ * optional success flags, and the two distinct failure contracts. */
+static void test_embed_int(void) {
+ int before = failures, checks = 0;
+#define INT_CHECK(cond, msg) do { checks++; CHECK((cond), (msg)); } while (0)
+ const int64_t ints[] = {0, 3, -3, INT64_C(9007199254740993),
+ INT64_MIN, INT64_MAX};
+ for (size_t i = 0; i < sizeof ints / sizeof ints[0]; i++) {
+ EigsValue *v = eigs_value_new_int(ints[i]);
+ int ok = -1;
+ INT_CHECK(eigs_value_type(v) == EIGS_TYPE_NUM, "int has public number type");
+ INT_CHECK(eigs_value_is_int(v), "int predicate includes boxed int64");
+ INT_CHECK(eigs_value_as_int(v, &ok) == ints[i] && ok == 1,
+ "int round-trips all 64 bits");
+ INT_CHECK(eigs_value_as_int(v, NULL) == ints[i], "int permits NULL flag");
+ INT_CHECK(eigs_value_as_num(v) == (double)ints[i], "as_num converts int");
+ INT_CHECK(!eigs_has_error(), "valid int access leaves no error");
+ /* Exercise the owning container bridge with a genuinely boxed int. */
+ EigsValue *list = eigs_value_new_list(1);
+ eigs_value_list_append(list, v);
+ eigs_value_release(v);
+ v = eigs_value_list_get(list, 0);
+ INT_CHECK(eigs_value_as_int(v, &ok) == ints[i] && ok == 1,
+ "list bridge preserves exact int after caller releases");
+ eigs_value_release(v);
+ eigs_value_release(list);
+ }
+ const struct { double n; int ok; int64_t want; } floats[] = {
+ {3.0, 1, 3}, {-0.0, 1, 0}, {1.5, 0, 0},
+ {-0x1p63, 1, INT64_MIN}, {0x1p63, 0, 0},
+ {0x1.fffffffffffffp62, 1, INT64_MAX - 1023},
+ {-0x1.0000000000001p63, 0, 0}, {0x1p-1074, 0, 0}
+ };
+ for (size_t i = 0; i < sizeof floats / sizeof floats[0]; i++) {
+ EigsValue *v = eigs_value_new_num(floats[i].n);
+ int ok = -1;
+ INT_CHECK(eigs_value_type(v) == EIGS_TYPE_NUM, "float has public number type");
+ INT_CHECK(!eigs_value_is_int(v), "integral float is never int kind");
+ INT_CHECK(eigs_value_as_int(v, &ok) == floats[i].want && ok == floats[i].ok,
+ "float exact int64 conversion and refusal bounds");
+ INT_CHECK(eigs_value_as_int(v, NULL) == floats[i].want,
+ "float permits NULL success flag");
+ INT_CHECK(!eigs_has_error(), "numeric refusal sets no embed error");
+ eigs_value_release(v);
+ }
+ /* #1969 census: as_int's float arm is a conversion boundary. Raw
+ * storage also exercises nonfinite payloads constructors can refuse. */
+ const double raw[] = {0x1p-1074, 1.5, -0x1p63, 0x1p63, INFINITY, -INFINITY, NAN};
+ const int modes[] = {FE_TONEAREST, FE_UPWARD, FE_DOWNWARD, FE_TOWARDZERO};
+ fenv_t saved;
+ fegetenv(&saved);
+ for (size_t m = 0; m < sizeof modes / sizeof modes[0]; m++) {
+ for (size_t i = 0; i < sizeof raw / sizeof raw[0]; i++) {
+ EigsValue *v = eigs_value_new_num(0.0);
+ VAL_F64_RAW(v) = raw[i];
+ fesetround(modes[m]);
+ feclearexcept(FE_ALL_EXCEPT);
+ feraiseexcept(FE_DIVBYZERO);
+#if defined(__x86_64__)
+ _mm_setcsr(_mm_getcsr() | (1u << 15) | (1u << 6));
+ unsigned csr = _mm_getcsr();
+#endif
+ int flags = fetestexcept(FE_ALL_EXCEPT), ok = -1;
+ int64_t got = eigs_value_as_int(v, &ok);
+ INT_CHECK(ok == (i == 2) && got == (i == 2 ? INT64_MIN : 0),
+ "as_int exactness survives every host mode and DAZ");
+ INT_CHECK(fegetround() == modes[m] && fetestexcept(FE_ALL_EXCEPT) == flags,
+ "as_int restores host rounding and sticky flags");
+#if defined(__x86_64__)
+ INT_CHECK(_mm_getcsr() == csr, "as_int restores FTZ/DAZ and MXCSR flags");
+#endif
+ INT_CHECK(!eigs_has_error(), "raw numeric refusal sets no embed error");
+ fesetenv(&saved);
+ eigs_value_release(v);
+ }
+ }
+ fesetenv(&saved);
+ EigsValue *wrong[] = {NULL, eigs_value_new_null(), eigs_value_new_bool(1),
+ eigs_value_new_string("3"), eigs_value_new_list(0),
+ eigs_value_new_dict(0)};
+ for (size_t i = 0; i < sizeof wrong / sizeof wrong[0]; i++) {
+ int ok = -1;
+ INT_CHECK(!eigs_value_is_int(wrong[i]) && !eigs_has_error(),
+ "kind predicate is pure on non-numbers");
+ INT_CHECK(eigs_value_as_int(wrong[i], &ok) == 0 && ok == 0,
+ "non-number fails with zero success flag");
+ INT_CHECK(eigs_has_error() && strcmp(eigs_last_error_kind(), "type_mismatch") == 0
+ && strstr(eigs_last_error_message(), "eigs_value_as_int"),
+ "non-number reports embed type error naming accessor");
+ eigs_clear_error();
+ INT_CHECK(eigs_value_as_int(wrong[i], NULL) == 0 && eigs_has_error(),
+ "wrong type with NULL flag still reports error");
+ eigs_clear_error();
+ eigs_value_release(wrong[i]);
+ }
+ EigsValue *fraction = eigs_value_new_num(1.5);
+ eigs_value_as_int(NULL, NULL);
+ char pending[160];
+ snprintf(pending, sizeof pending, "%s", eigs_last_error_message());
+ int ok = -1;
+ INT_CHECK(eigs_value_as_int(fraction, &ok) == 0 && ok == 0 &&
+ strcmp(pending, eigs_last_error_message()) == 0,
+ "numeric refusal preserves an existing error");
+ eigs_clear_error();
+ eigs_value_release(fraction);
+ printf("gate.N7.embed-int: checks=%d failures=%d\n", checks, failures - before);
+#undef INT_CHECK
+}
+
static EigsValue *mark_old_task_ran(EigsValue *arg) {
(void)arg;
old_task_ran++;
@@ -112,7 +221,77 @@ static void check_fenv_eval(const char *mode_name, int mode, const char *witness
eigs_clear_error();
}
+/* #1969 / Phase 2 N6: exact binary64 payloads cross the public eval
+ * boundary and print Python repr text. Expected strings are from Python's
+ * struct.unpack(">d", bytes.fromhex(bits)); no decimal parser is the oracle. */
+static void check_fenv_n6_text(void) {
+ static const struct { const char *bits, *source, *expected; } rows[] = {
+ {"3fd3333333333334", "print of (f64_from_bytes of [63, 211, 51, 51, 51, 51, 51, 52])", "0.30000000000000004\n"},
+ {"bfd3333333333334", "print of (f64_from_bytes of [191, 211, 51, 51, 51, 51, 51, 52])", "-0.30000000000000004\n"},
+ {"0000000000000001", "print of (f64_from_bytes of [0, 0, 0, 0, 0, 0, 0, 1])", "5e-324\n"},
+ {"8000000000000001", "print of (f64_from_bytes of [128, 0, 0, 0, 0, 0, 0, 1])", "-5e-324\n"},
+ };
+ static const struct { const char *name; int rounding; int flush; } modes[] = {
+ {"FE_UPWARD", FE_UPWARD, 0},
+ {"FE_DOWNWARD", FE_DOWNWARD, 0},
+ {"FE_TOWARDZERO", FE_TOWARDZERO, 0},
+#if defined(__x86_64__)
+ {"FTZ/DAZ", FE_TONEAREST, 1},
+#endif
+ };
+ fenv_t saved;
+ CHECK(fegetenv(&saved) == 0, "N6 saves the host environment");
+ for (size_t m = 0; m < sizeof(modes) / sizeof(modes[0]); m++) {
+ for (size_t i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) {
+ CHECK(fesetenv(FE_DFL_ENV) == 0, "N6 initializes the host environment");
+ CHECK(fesetround(modes[m].rounding) == 0, "N6 sets the host rounding mode");
+ CHECK(feraiseexcept(FE_INVALID) == 0, "N6 seeds sticky flags");
+#if defined(__x86_64__)
+ if (modes[m].flush) _mm_setcsr(_mm_getcsr() | (1u << 15) | (1u << 6));
+ unsigned before = _mm_getcsr();
+#endif
+ int flags = fetestexcept(FE_ALL_EXCEPT);
+ FILE *capture = tmpfile();
+ int host_stdout = dup(STDOUT_FILENO);
+ CHECK(capture && host_stdout >= 0, "N6 opens stdout capture");
+ if (!capture || host_stdout < 0) {
+ if (capture) fclose(capture);
+ if (host_stdout >= 0) close(host_stdout);
+ continue;
+ }
+ fflush(stdout);
+ int redirected = dup2(fileno(capture), STDOUT_FILENO);
+ CHECK(redirected >= 0, "N6 redirects stdout");
+ if (redirected < 0) { fclose(capture); close(host_stdout); continue; }
+ EigsValue *v = eigs_eval_string(rows[i].source);
+ int restored = fegetround() == modes[m].rounding &&
+ fetestexcept(FE_ALL_EXCEPT) == flags;
+#if defined(__x86_64__)
+ restored = restored && _mm_getcsr() == before;
+#endif
+ fflush(stdout);
+ CHECK(dup2(host_stdout, STDOUT_FILENO) >= 0, "N6 restores stdout");
+ close(host_stdout);
+ rewind(capture);
+ char output[128];
+ size_t length = fread(output, 1, sizeof(output) - 1, capture);
+ output[length] = '\0';
+ CHECK(!ferror(capture), "N6 reads captured print output");
+ fclose(capture);
+ int ok = v && strcmp(output, rows[i].expected) == 0 && restored;
+ CHECK(ok, "N6 exact-bit repr and host fenv restoration");
+ printf("N6-FENV %s %s %s\n", modes[m].name, rows[i].bits, ok ? "PASS" : "FAIL");
+ if (!ok) fprintf(stderr, "N6 expected <%s> got <%s>; restored=%d\n",
+ rows[i].expected, output, restored);
+ eigs_value_release(v);
+ eigs_clear_error();
+ }
+ }
+ CHECK(fesetenv(&saved) == 0, "N6 restores the original host environment");
+}
+
static void test_fenv_boundary(void) {
+ check_fenv_n6_text();
EigsValue *v;
check_fenv_default_fast_path();
check_fenv_eval("FE_UPWARD", FE_UPWARD,
@@ -320,8 +499,8 @@ static void test_worker_exit_lifecycle(void) {
CHECK(handle && handle->type == VAL_DICT, "exit fixture owns join handle");
if (handle && handle->type == VAL_DICT) {
int found_id = 0, found_gen = 0;
- f.handle_id = (int)eigs_slot_num(dict_get_slot(handle, "_handle_id", &found_id), "embed_smoke");
- f.handle_gen = (uint32_t)eigs_slot_num(dict_get_slot(handle, "_handle_gen", &found_gen), "embed_smoke");
+ f.handle_id = (int)eigs_slot_float(dict_get_slot(handle, "_handle_id", &found_id), "embed_smoke");
+ f.handle_gen = (uint32_t)eigs_slot_float(dict_get_slot(handle, "_handle_gen", &found_gen), "embed_smoke");
CHECK(found_id && found_gen, "join handle carries its id and generation");
pthread_t requester;
int made = pthread_create(&requester, NULL, exit_after_join_claim, &f) == 0;
@@ -400,6 +579,18 @@ static void tape_sink(const char *bytes, size_t len, void *ud) {
g_tape_len += len;
}
+/* A tape assertion matches WHOLE lines. `want` is one or more complete lines,
+ * each ending in '\n', and it must begin at the start of a tape line. A bare
+ * strstr cannot tell `=100` from `=1000` or from v7's `=100.0`, so it passes
+ * on a v6 tape and on a v7 one alike. */
+static int tape_has_lines(const char *tape, const char *want) {
+ size_t wl = strlen(want);
+ if (wl == 0 || want[wl - 1] != '\n') return 0; /* not whole lines: refuse */
+ for (const char *p = tape; (p = strstr(p, want)) != NULL; p++)
+ if (p == tape || p[-1] == '\n') return 1;
+ return 0;
+}
+
/* Host function: adds two numbers. Multi-arg calls receive a VAL_LIST. */
static EigsValue *host_add(EigsValue *arg) {
if (eigs_value_type(arg) != EIGS_TYPE_LIST) return eigs_value_new_null();
@@ -479,18 +670,19 @@ int main(void) {
int line_save = g_trace_current_line;
g_trace_current_line = 10;
- SLOT_NUM_RAW(s) = 11.0; trace_assign(NM, s);
+ SLOT_F64_RAW(s) = 11.0; trace_assign(NM, s);
g_trace_current_line = 20;
- SLOT_NUM_RAW(s) = 22.0; trace_assign(NM, s);
+ SLOT_F64_RAW(s) = 22.0; trace_assign(NM, s);
- CHECK(trace_query_prev(NM, &out) && SLOT_NUM_RAW(out) == 11.0,
+ CHECK(trace_query_prev(NM, &out) && SLOT_F64_RAW(out) == 11.0,
"#830: prev of a name recorded by a non-compiler producer");
/* TEMPORAL-BACKWARD: at 15 the answer is the line-10 assignment. */
- CHECK(trace_query_at(0, NM, 15, &out) && SLOT_NUM_RAW(out) == 11.0,
+ CHECK(trace_query_at(0, NM, 15, &out) && SLOT_F64_RAW(out) == 11.0,
"#830: what-at from a non-compiler producer (line-10 value)");
- CHECK(trace_query_at(0, NM, 99, &out) && SLOT_NUM_RAW(out) == 22.0,
+ CHECK(trace_query_at(0, NM, 99, &out) && SLOT_F64_RAW(out) == 22.0,
"#830: what-at past the last non-compiler assignment");
- CHECK(trace_query_at(2, NM, 99, &out) && SLOT_NUM_RAW(out) == 2.0,
+ /* S9-K1: historical assignment counts are published as exact ints. */
+ CHECK(trace_query_at(2, NM, 99, &out) && slot_is_smi(out) && slot_smi_val(out) == 2,
"#830: when-at counts non-compiler assignments");
g_trace_current_line = line_save;
@@ -499,6 +691,7 @@ int main(void) {
/* #1969: every code-running boundary installs IEEE defaults and restores
* all host controls and sticky flags, including on errors/re-entry. */
test_fenv_boundary();
+ test_embed_int();
/* #1394: strictness is state-local embedder configuration. Reuse the
* one-shot state already keeping the process alive, then create a staged
@@ -786,9 +979,21 @@ int main(void) {
CHECK(eigs_has_error() && strcmp(eigs_last_error_kind(), "index_range") == 0,
"negative buffer set leaves index_range");
eigs_clear_error();
- eigs_value_buffer_set(buf, 2, INFINITY);
- CHECK(eigs_value_buffer_get(buf, 2) == EIGS_NUM_MAX,
- "buffer non-finite get is clamped");
+ /* R1.6z-D1 point 2 / N11: the C getter preserves stored bits and
+ * leaves both the pending-error channel and math flags alone. */
+ {
+ double stored = INFINITY;
+ unsigned saved_flags = g_math_flags;
+ eigs_value_buffer_set(buf, 2, stored);
+ g_math_flags = EIGS_MATH_UNDERFLOW;
+ double read_back = eigs_value_buffer_get(buf, 2);
+ CHECK(memcmp(&read_back, &stored, sizeof stored) == 0,
+ "R1.6z-D1 buffer non-finite get preserves stored bits");
+ CHECK(!eigs_has_error(), "R1.6z-D1 buffer non-finite get never raises");
+ CHECK(g_math_flags == EIGS_MATH_UNDERFLOW,
+ "R1.6z-D1 buffer non-finite get preserves math flags");
+ g_math_flags = saved_flags;
+ }
/* Host buffer visible to script — SAME object, no copy at the
* boundary: the script's buf_set is visible back in C. */
@@ -810,15 +1015,25 @@ int main(void) {
EigsValue *nf_raw = eigs_value_new_buffer(2);
EigsValue *nf_finite = eigs_value_new_buffer(2);
EigsValue *nf_later = eigs_value_new_buffer(2);
+ EigsValue *nf_finite3 = eigs_value_new_buffer(3);
+ EigsValue *nf_prefix = eigs_value_new_buffer(3);
eigs_value_buffer_set(nf_raw, 0, NAN);
eigs_value_buffer_set(nf_raw, 1, INFINITY);
eigs_value_buffer_set(nf_finite, 0, 3.0);
eigs_value_buffer_set(nf_finite, 1, 4.0);
eigs_value_buffer_set(nf_later, 0, INFINITY);
eigs_value_buffer_set(nf_later, 1, 4.0);
+ eigs_value_buffer_set(nf_finite3, 0, 3.0);
+ eigs_value_buffer_set(nf_finite3, 1, 4.0);
+ eigs_value_buffer_set(nf_finite3, 2, 5.0);
+ eigs_value_buffer_set(nf_prefix, 0, 3.0);
+ eigs_value_buffer_set(nf_prefix, 1, NAN);
+ eigs_value_buffer_set(nf_prefix, 2, INFINITY);
eigs_set_global("nf_raw", nf_raw);
eigs_set_global("nf_finite", nf_finite);
eigs_set_global("nf_later", nf_later);
+ eigs_set_global("nf_finite3", nf_finite3);
+ eigs_set_global("nf_prefix", nf_prefix);
/* #1926: f64_from_bytes refuses fewer than 8 bytes before reading
* any, so its row keeps the NaN-then-infinity prefix in a full
* 8-byte input. */
@@ -830,17 +1045,41 @@ int main(void) {
eigs_value_release(nf_raw);
eigs_value_release(nf_finite);
eigs_value_release(nf_later);
+ eigs_value_release(nf_finite3);
+ eigs_value_release(nf_prefix);
eigs_state_set_strict(st, 1);
r = eigs_eval_string("nf_shaped is reshape of [nf_raw, 1, 2]");
CHECK(r != NULL && !eigs_has_error(),
"#1417 prepare shaped materialization control");
eigs_value_release(r);
+ r = eigs_eval_string("num_type of (add of [9007199254740993, 1])");
+ CHECK(r && eigs_value_type(r) == EIGS_TYPE_STR &&
+ strcmp(eigs_value_as_string(r), "int") == 0,
+ "#1417 recursive failure prefix has a boxed int result");
+ eigs_value_release(r);
+ /* R1.6z-D1: equality never crosses the scalar data boundary. */
+ for (int mode = 0; mode <= 1; ++mode) {
+ eigs_state_set_strict(st, mode);
+ g_math_flags = 0;
+ r = eigs_eval_string("[nf_raw == nf_raw, nf_raw == nf_finite, "
+ "list_contains of [[nf_raw, nf_later], nf_finite], "
+ "list_index_of of [[nf_raw, nf_later], nf_finite]]");
+ CHECK(r && !eigs_has_error() && r->type == VAL_LIST &&
+ r->data.list.count == 4 &&
+ slot_is_bool(list_slot(r, 0)) && !slot_as_bool(list_slot(r, 0)),
+ "R1.6z-D1 NaN buffer equality is false without a raise");
+ CHECK(g_math_flags == 0, "R1.6z-D1 equality leaves math flags alone");
+ eigs_value_release(r);
+ eigs_clear_error();
+ }
+ eigs_state_set_strict(st, 1);
const char *nf_reads[] = {
- "nf_raw == nf_raw", "nf_raw == nf_finite",
- "list_contains of [[nf_raw, nf_later], nf_finite]",
- "list_index_of of [[nf_raw, nf_later], nf_finite]",
"sum of nf_raw", "mean of nf_raw", "norm of nf_raw",
"sum of ([nf_raw, nf_later])",
+ "sum of nf_shaped",
+ "sum of ([9223372036854775807, 1, nf_raw, nf_later])",
+ "sum of (negative of nf_raw)",
+ "sum of (negative of ([9007199254740993, nf_raw, nf_later]))",
"mean of ([nf_raw, nf_later])",
"norm of ([nf_raw, nf_later])",
"dot of [nf_raw, nf_finite]",
@@ -852,6 +1091,21 @@ int main(void) {
"add of [nf_raw, [0, 0]]", "add of [[0, 0], nf_raw]",
"subtract of [nf_raw, [0, 0]]", "subtract of [[0, 0], nf_raw]",
"multiply of [nf_raw, [0, 0]]", "multiply of [[0, 0], nf_raw]",
+ /* #1417: stop after a buffer kernel raises too, including both
+ * operand orders, shaped broadcasts, truncation and recursion.
+ * The nested int prefix owns a boxed result before the failure. */
+ "add of [nf_raw, nf_finite]", "add of [nf_finite, nf_raw]",
+ "subtract of [nf_raw, nf_finite]", "subtract of [nf_finite, nf_raw]",
+ "multiply of [nf_raw, nf_finite]", "multiply of [nf_finite, nf_raw]",
+ "add of [nf_raw, 1]", "add of [1, nf_raw]",
+ "subtract of [nf_raw, 1]", "subtract of [1, nf_raw]",
+ "multiply of [nf_raw, 1]", "multiply of [1, nf_raw]",
+ "add of [nf_shaped, nf_finite]", "add of [nf_finite, nf_shaped]",
+ "add of [nf_raw, nf_finite3]", "add of [nf_finite3, nf_raw]",
+ "add of [nf_prefix, nf_finite3]",
+ "add of [[9007199254740993, nf_raw, nf_later], [1, nf_finite, nf_finite]]",
+ "exp of nf_prefix",
+ "divide of [nf_raw, nf_finite]", "pow of [nf_raw, nf_finite]",
"divide of [nf_raw, [1, 1]]", "divide of [[1, 1], nf_raw]",
"pow of [nf_raw, [1, 1]]", "pow of [[1, 1], nf_raw]",
"add of [nf_shaped, [[0, 0]]]", "add of [[[0, 0]], nf_shaped]",
@@ -1238,10 +1492,11 @@ int main(void) {
{
static const char *const NM = "native_after_callback";
EigsSlot s;
- SLOT_NUM_RAW(s) = 1441.0;
+ SLOT_F64_RAW(s) = 1441.0;
trace_assign(NM, s);
g_tape[g_tape_len < sizeof g_tape ? g_tape_len : sizeof g_tape - 1] = 0;
- CHECK(strstr(g_tape, "S 0 0 0\nA 0 native_after_callback=1441\n") != NULL,
+ /* Tape v7 marks an integral float as a float: `1441.0`, never `1441`. */
+ CHECK(tape_has_lines(g_tape, "S 0 0 0\nA 0 native_after_callback=1441.0\n"),
"native assignment after callback carries native scope");
}
@@ -1261,9 +1516,9 @@ int main(void) {
eigs_set_trace_sink(NULL, NULL);
CHECK(g_tape_len > 0, "sink captured tape bytes");
g_tape[g_tape_len < sizeof g_tape ? g_tape_len : sizeof g_tape - 1] = 0;
- CHECK(strstr(g_tape, "N 0 host_sensor=100") != NULL, "tape has N record 100");
- CHECK(strstr(g_tape, "N 0 host_sensor=101") != NULL, "tape has N record 101");
- CHECK(strstr(g_tape, "A 0 s1=100") != NULL, "tape has assignment record");
+ CHECK(tape_has_lines(g_tape, "N 0 host_sensor=100.0\n"), "tape has N record 100.0");
+ CHECK(tape_has_lines(g_tape, "N 0 host_sensor=101.0\n"), "tape has N record 101.0");
+ CHECK(tape_has_lines(g_tape, "A 0 s1=100.0\n"), "tape has assignment record");
/* recording stopped: another live read advances but adds no bytes */
size_t tape_frozen = g_tape_len;
diff --git a/src/ext_db.c b/src/ext_db.c
index 509017983..82028755c 100644
--- a/src/ext_db.c
+++ b/src/ext_db.c
@@ -56,8 +56,14 @@ static int db_build_query(Value *arg, const char **sql, int *nparams,
types[i] = DB_PARAM_OID_BOOL;
} else if (v && v->type == VAL_STR) {
params[i] = v->data.str;
- } else if (v && v->type == VAL_NUM) {
- snprintf(numbuf[i], 64, "%g", eigs_num_arg(v, __func__));
+ } else if (v && v->type == VAL_INT) {
+ eigs_int_text(numbuf[i], 64, VAL_I64_RAW(v));
+ params[i] = numbuf[i];
+ } else if (v && v->type == VAL_FLOAT) {
+ /* N6/N14: exact round-trip parameter text, shared with display/JSON. */
+ double f = eigs_float_arg(v, __func__);
+ if (g_has_error) return 0;
+ eigs_num_text(numbuf[i], 64, f);
params[i] = numbuf[i];
} else {
rt_error(EK_TYPE, 0, "db: parameter %d is not a string, number or bool (got %s)",
@@ -173,11 +179,7 @@ static int db_require_conn(void) {
#define DB_OID_FLOAT8 701
#define DB_OID_NUMERIC 1700
-/* The largest integer a double represents exactly. A bigint past this cannot
- * round-trip through an EigenScript number. */
-#define DB_EXACT_INT_MAX 9007199254740992LL
-
-typedef enum { DBT_STR, DBT_NUM, DBT_BOOL } DbColType;
+typedef enum { DBT_STR, DBT_INT, DBT_FLOAT, DBT_BOOL } DbColType;
/* ONE classifier, shared by db_query_json and db_query_value, so the two
* cannot drift on what a column's type is.
@@ -196,57 +198,36 @@ typedef enum { DBT_STR, DBT_NUM, DBT_BOOL } DbColType;
* in to a number. int8 does NOT get that treatment: count(*), sum(int) and
* every serial key are int8, so leaving it a string would leave the filed
* bug unfixed for the majority of real numeric queries — instead it emits a
- * number and RAISES on the values a double cannot represent (below). */
+ * signed int64 without a conversion through double (N14, Q-DB253). */
static DbColType db_classify(unsigned int oid) {
switch (oid) {
case DB_OID_BOOL: return DBT_BOOL;
case DB_OID_INT2:
case DB_OID_INT4:
case DB_OID_OID:
- case DB_OID_INT8:
+ case DB_OID_INT8: return DBT_INT;
case DB_OID_FLOAT4:
- case DB_OID_FLOAT8: return DBT_NUM;
+ case DB_OID_FLOAT8: return DBT_FLOAT;
default: return DBT_STR; /* numeric, text, date, json, ... */
}
}
-/* An exact-integer column whose value is past 2^53. Raising beats rounding:
- * the value genuinely has no representation as an EigenScript number, and a
- * silently rounded primary key is unrecoverable downstream. The message names
- * the column and the fix. Returns 0 and raises; 1 when representable. */
-static int db_check_exact_int(unsigned int oid, const char *text, const char *colname) {
- if (oid != DB_OID_INT8 && oid != DB_OID_INT4 && oid != DB_OID_INT2 && oid != DB_OID_OID)
- return 1;
- /* strtoLL, not strtod: converting to a double FIRST rounds 9007199254740993
- * to exactly 2^53, so a strtod-based comparison cannot see the one thing
- * this check exists to detect. (It shipped that way to CI once — the live
- * postgres job is what caught it.) An out-of-int64 literal saturates at
- * LLONG_MAX, which is past the bound, so it still raises. */
- char *end = NULL;
- long long v = strtoll(text, &end, 10);
- if (end == text) return 1; /* not a plain integer literal; leave it */
- if (v > DB_EXACT_INT_MAX || v < -DB_EXACT_INT_MAX) {
- rt_error(EK_VALUE, 0,
- "db: column '%s' value %s exceeds the exact-integer range of a "
- "number (2^53); select it as text (%s::text) to keep the digits",
- colname ? colname : "?", text, colname ? colname : "col");
- return 0;
- }
- return 1;
-}
-
-/* Emit one column value as JSON. Returns 0 when it raised. */
-static int db_emit_json_value(strbuf *sb, unsigned int oid, int isnull,
- const char *text, const char *colname) {
- if (isnull) { strbuf_append(sb, "null"); return 1; }
+/* Emit one column value as JSON, retaining PostgreSQL's decimal text. */
+static void db_emit_json_value(strbuf *sb, unsigned int oid, int isnull,
+ const char *text) {
+ if (isnull) { strbuf_append(sb, "null"); return; }
if (!text) text = "";
switch (db_classify(oid)) {
case DBT_BOOL:
/* libpq text mode renders bool as exactly "t" or "f". */
strbuf_append(sb, text[0] == 't' ? "true" : "false");
- return 1;
- case DBT_NUM:
- if (!db_check_exact_int(oid, text, colname)) return 0;
+ return;
+ case DBT_INT:
+ /* PostgreSQL's integer types fit int64 and arrive as exact decimal
+ * digits. Preserve that text; the N14 JSON reader produces ints. */
+ strbuf_append(sb, text);
+ return;
+ case DBT_FLOAT:
/* float8/float4 can be NaN / Infinity / -Infinity, which JSON has
* no literal for. Emitting them as `null` would collide with SQL
* NULL — the very confusion this issue removes — so they stay
@@ -255,15 +236,14 @@ static int db_emit_json_value(strbuf *sb, unsigned int oid, int isnull,
if (text[0] == 'N' || text[0] == 'I' ||
(text[0] == '-' && text[1] == 'I')) {
eigs_json_escape_string(sb, text);
- return 1;
+ return;
}
strbuf_append(sb, text); /* libpq's decimal text is valid JSON */
- return 1;
+ return;
case DBT_STR:
break;
}
eigs_json_escape_string(sb, text);
- return 1;
}
/* ---- DB query builtins ---- */
@@ -293,17 +273,21 @@ Value* builtin_db_query_value(Value *arg) {
unsigned int oid = (unsigned int)PQftype(res, 0);
const char *text = PQgetvalue(res, 0, 0);
- const char *colname = PQfname(res, 0);
if (!text) text = "";
Value *result = NULL;
switch (db_classify(oid)) {
case DBT_BOOL:
result = make_bool(text[0] == 't'); /* #1637 */
break;
- case DBT_NUM:
- if (!db_check_exact_int(oid, text, colname)) { PQclear(res); return make_str(""); } /* fs:STRICT db_check_exact_int raised EK_VALUE for a past-2^53 integer (ext_db.c:211) */
+ case DBT_INT:
+ /* PostgreSQL validates its int2/int4/int8/oid text and range before
+ * sending it. Decode directly, including both int64 edges, rather
+ * than rounding through strtod (N14, Q-DB253). */
+ result = make_int((int64_t)strtoll(text, NULL, 10));
+ break;
+ case DBT_FLOAT:
/* Same rule as the JSON emitter: float4/float8 can be NaN /
- * Infinity / -Infinity, and make_num's num_guard would turn
+ * Infinity / -Infinity, and make_float's num_guard would turn
* those into 0 and 1e308 — a silently wrong number. Hand back
* the text so the caller sees what the column holds. */
if (text[0] == 'N' || text[0] == 'I' ||
@@ -311,7 +295,7 @@ Value* builtin_db_query_value(Value *arg) {
result = make_str(text);
break;
}
- result = make_num(strtod(text, NULL));
+ result = make_float(strtod(text, NULL));
break;
case DBT_STR:
result = make_str(text);
@@ -375,13 +359,8 @@ Value* builtin_db_query_json(Value *arg) {
if (c > 0) strbuf_append(&sb, ", ");
eigs_json_escape_string(&sb, PQfname(res, c));
strbuf_append(&sb, ": ");
- if (!db_emit_json_value(&sb, (unsigned int)PQftype(res, c),
- PQgetisnull(res, r, c), PQgetvalue(res, r, c),
- PQfname(res, c))) {
- strbuf_free(&sb);
- PQclear(res);
- return make_str("[]");
- }
+ db_emit_json_value(&sb, (unsigned int)PQftype(res, c),
+ PQgetisnull(res, r, c), PQgetvalue(res, r, c));
}
strbuf_append_char(&sb, '}');
}
diff --git a/src/ext_gfx.c b/src/ext_gfx.c
index f97f4cc13..e5d1b54aa 100644
--- a/src/ext_gfx.c
+++ b/src/ext_gfx.c
@@ -110,6 +110,7 @@ static int g_ctype_locale_warned = 0;
#endif
static SDL_Window *g_window = NULL;
static SDL_Renderer *g_renderer = NULL;
+static int (*p_SDL_GetRendererOutputSize)(SDL_Renderer*, int*, int*);
static int (*p_SDL_Init)(Uint32);
static void (*p_SDL_Quit)(void);
@@ -198,6 +199,7 @@ static int load_sdl2(void) {
LOAD(SDL_GetTicks); LOAD(SDL_Delay);
#undef LOAD
/* Optional symbols — NULL is fine */
+ p_SDL_GetRendererOutputSize = dlsym(g_sdl_lib, "SDL_GetRendererOutputSize");
p_SDL_GetModState = dlsym(g_sdl_lib, "SDL_GetModState");
p_SDL_GetMouseState = dlsym(g_sdl_lib, "SDL_GetMouseState");
p_SDL_RenderSetClipRect = dlsym(g_sdl_lib, "SDL_RenderSetClipRect");
@@ -374,6 +376,7 @@ static int g_ttf_font_count = 0;
* existing fixed-width layouts don't overflow (scale 2 → 14 px). */
static int ttf_scale_to_px(int scale) {
if (scale < 1) scale = 1;
+ if (scale > (INT_MAX - 2) / 6) return 0;
return 6 * scale + 2;
}
@@ -429,6 +432,7 @@ static int ttf_available(void) {
* (callers fall back to the bitmap path). */
static void* ttf_font_for_scale(int scale) {
int px = ttf_scale_to_px(scale);
+ if (!px) return NULL; /* Q7: use bitmap geometry when the font API cannot hold the size. */
for (int i = 0; i < g_ttf_font_count; i++)
if (g_ttf_fonts[i].px == px) return g_ttf_fonts[i].font;
if (g_ttf_font_count >= TTF_FONT_CACHE_MAX)
@@ -490,7 +494,7 @@ static const char* scancode_name(int sc) {
/* ---- Builtins ---- */
-/* #1007: the drawing surface reads RUNS of list elements as `eigs_num_arg(&(), __func__)`
+/* #1007: the drawing surface reads RUNS of list elements as `eigs_float_arg(&(), __func__)`
* with no type check. `Value`'s union overlaps `double num` with
* `char *str`, so an unchecked read reinterprets a pointer as a double
* and then `(int)`-casts it — the read itself is the defect, which is why
@@ -505,7 +509,7 @@ static int gfx_nums(Value *arg, int from, int to) {
int cnt = arg->data.list.count;
if (to > cnt) to = cnt;
for (int i = from; i < to; i++) {
- if (list_elem_type(arg, i) != VAL_NUM) return 0;
+ if ((list_elem_type(arg, i) != VAL_FLOAT && list_elem_type(arg, i) != VAL_INT)) return 0;
}
return 1;
}
@@ -518,7 +522,7 @@ static int gfx_nums(Value *arg, int from, int to) {
static int gfx_list_all_num(Value *l) {
if (!l || l->type != VAL_LIST) return 0;
for (int i = 0; i < l->data.list.count; i++) {
- if (list_elem_type(l, i) != VAL_NUM) return 0;
+ if ((list_elem_type(l, i) != VAL_FLOAT && list_elem_type(l, i) != VAL_INT)) return 0;
}
return 1;
}
@@ -548,16 +552,19 @@ Value* builtin_gfx_open(Value *arg) {
* same value. */
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 3,
"gfx_open", "[number width, number height, title]", make_bool(0));
- /* #1007: width and height were read as `eigs_num_arg(&(), __func__)` with no type check
+ /* #1007: width and height were read as `eigs_float_arg(&(), __func__)` with no type check
* while the title on the very next line WAS checked. Value's union
* overlaps `double num` with `char *str`, so gfx_open of ["800","600",t]
* reinterpreted a pointer as a double and int-cast it — in practice a
* tiny denormal, so the window opened 0x0 and gfx_open answered 1. */
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM,
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
"gfx_open", "[number width, number height, title]", make_bool(0));
- int w = (int)eigs_list_num(arg, 0, __func__);
- int h = (int)eigs_list_num(arg, 1, __func__);
+ int64_t width, height;
+ if (!eigs_list_magnitude(arg, 0, INT_MIN, INT_MAX, &width, "gfx_open") ||
+ !eigs_list_magnitude(arg, 1, INT_MIN, INT_MAX, &height, "gfx_open"))
+ return make_null();
+ int w = (int)width, h = (int)height;
const char *title = list_elem_type(arg, 2) == VAL_STR ? list_get_ref(arg, 2)->data.str : "EigenScript";
gfx_select_title_locale();
@@ -613,6 +620,62 @@ Value* builtin_gfx_close(Value *arg) {
return make_null(); /* fs:VOID gfx_close answers null on every path -- this is the return value, not a stand-in for a rejected argument */
}
+
+/* Q7 helpers shared by the geometry builtins below. Output dimensions are
+ * renderer pixels (not window coordinates, which can differ on HiDPI). */
+static EigsRect gfx_drawable(void) {
+ EigsRect area = {0, 0, 0, 0};
+ if (g_renderer && p_SDL_GetRendererOutputSize &&
+ p_SDL_GetRendererOutputSize(g_renderer, &area.w, &area.h) != 0)
+ area.w = area.h = 0;
+ return area;
+}
+
+static int gfx_colour(const Value *arg, int first, int alpha, int colour[4],
+ const char *who) {
+ colour[3] = 255;
+ for (int i = 0; i < 3; ++i)
+ if (!eigs_list_bounded(arg, first+i, 0, 255, &colour[i], who)) return 0;
+ return !alpha || arg->data.list.count <= first+3 ||
+ eigs_list_bounded(arg, first+3, 0, 255, &colour[3], who);
+}
+
+/* Derived edges may overflow outward when long double is binary64.
+ * Intersect edges separately, before subtracting to form a width: a huge
+ * circle can have left=-inf/right=+inf without ever forming -inf+inf.
+ * The stack list borrows only immediate float slots, and never escapes. */
+static int gfx_edges_ld(long double x, long double y, long double right, long double bottom,
+ const EigsRect *area, EigsShape *shape, const char *who) {
+ memset(shape, 0, sizeof *shape);
+ shape->empty = 1;
+ if (area->w <= 0 || area->h <= 0) return 1;
+ right = fminl((long double)area->x + area->w, right);
+ bottom = fminl((long double)area->y + area->h, bottom);
+ x = fmaxl(area->x, x); y = fmaxl(area->y, y);
+ if (x >= right || y >= bottom) return 1;
+ EigsSlot slots[4] = { slot_from_f64((double)x), slot_from_f64((double)y),
+ slot_from_f64((double)(right-x)), slot_from_f64((double)(bottom-y)) };
+ Value args = { .type = VAL_LIST, .data.list = { .items = slots, .count = 4, .capacity = 4 } };
+ return eigs_geom_shape(&args, 0, EIGS_RECT, area, shape, who);
+}
+
+/* Callers have finite offsets or offsets that overflow toward +inf, and
+ * positive extents. In particular, framebuffer w*scale may be +inf while
+ * its finite origin still clips safely through the complete rectangle. */
+static int gfx_rect_ld(long double x, long double y, long double w, long double h,
+ const EigsRect *area, EigsShape *shape, const char *who) {
+ if (w <= 0 || h <= 0) {
+ memset(shape, 0, sizeof *shape); shape->empty = 1; return 1;
+ }
+ return gfx_edges_ld(x, y, x+w, y+h, area, shape, who);
+}
+
+static void gfx_fill_shape(const EigsShape *shape) {
+ if (shape->empty) return;
+ SDL_Rect rect = {shape->x, shape->y, shape->w, shape->h};
+ p_SDL_RenderFillRect(g_renderer, &rect);
+}
+
/* gfx_clear of [r, g, b] */
Value* builtin_gfx_clear(Value *arg) {
STRICT_LIST_MAX(arg, 3, "gfx_clear");
@@ -633,16 +696,12 @@ Value* builtin_gfx_clear(Value *arg) {
* path of this builtin answers. */
ARG_GUARD(shaped && !gfx_nums(arg, 0, 3),
"gfx_clear", "[number r, number g, number b]", make_null());
- if (!g_renderer) return make_null(); /* fs:VOID no window open: gfx_clear answers null on every path -- this is the return value, not a stand-in for a rejected argument */
- int r = 0, g = 0, b = 0;
- if (shaped) {
- r = (int)eigs_list_num(arg, 0, __func__);
- g = (int)eigs_list_num(arg, 1, __func__);
- b = (int)eigs_list_num(arg, 2, __func__);
- }
- p_SDL_SetRenderDrawColor(g_renderer, r, g, b, 255);
+ int colour[4] = {0, 0, 0, 255};
+ if (shaped && !gfx_colour(arg, 0, 0, colour, __func__)) return make_null();
+ if (!g_renderer) return make_null(); /* fs:VOID no renderer; null is the result on every path. */
+ p_SDL_SetRenderDrawColor(g_renderer, colour[0], colour[1], colour[2], 255);
p_SDL_RenderClear(g_renderer);
- return make_null(); /* fs:VOID gfx_clear answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* gfx_rect of [x, y, w, h, r, g, b] or [x, y, w, h, r, g, b, a] */
@@ -653,19 +712,14 @@ Value* builtin_gfx_rect(Value *arg) {
"gfx_rect",
"[number x, number y, number w, number h, number r, number g, number b] and an optional number alpha",
make_null());
- if (!g_renderer) return make_null(); /* fs:VOID no window open: gfx_rect answers null on every path -- the return value, not a rejected-argument stand-in */
- SDL_Rect rect;
- rect.x = (int)eigs_list_num(arg, 0, __func__);
- rect.y = (int)eigs_list_num(arg, 1, __func__);
- rect.w = (int)eigs_list_num(arg, 2, __func__);
- rect.h = (int)eigs_list_num(arg, 3, __func__);
- int r = (int)eigs_list_num(arg, 4, __func__);
- int g = (int)eigs_list_num(arg, 5, __func__);
- int b = (int)eigs_list_num(arg, 6, __func__);
- int a = (arg->data.list.count >= 8) ? (int)eigs_list_num(arg, 7, __func__) : 255;
- p_SDL_SetRenderDrawColor(g_renderer, r, g, b, a);
- p_SDL_RenderFillRect(g_renderer, &rect);
- return make_null(); /* fs:VOID gfx_rect answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ int colour[4];
+ if (!gfx_colour(arg, 4, 1, colour, __func__)) return make_null();
+ EigsRect area = gfx_drawable(); EigsShape shape;
+ if (!eigs_geom_shape(arg, 0, EIGS_RECT, &area, &shape, __func__)) return make_null();
+ if (!g_renderer || shape.empty) return make_null(); /* fs:EMPTY empty visible intersection draws nothing. */
+ p_SDL_SetRenderDrawColor(g_renderer, colour[0], colour[1], colour[2], colour[3]);
+ gfx_fill_shape(&shape);
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* gfx_line of [x1, y1, x2, y2, r, g, b] */
@@ -676,17 +730,14 @@ Value* builtin_gfx_line(Value *arg) {
"gfx_line",
"[number x1, number y1, number x2, number y2, number r, number g, number b]",
make_null());
- if (!g_renderer) return make_null(); /* fs:VOID no window open: gfx_line answers null on every path -- the return value, not a rejected-argument stand-in */
- int x1 = (int)eigs_list_num(arg, 0, __func__);
- int y1 = (int)eigs_list_num(arg, 1, __func__);
- int x2 = (int)eigs_list_num(arg, 2, __func__);
- int y2 = (int)eigs_list_num(arg, 3, __func__);
- int r = (int)eigs_list_num(arg, 4, __func__);
- int g = (int)eigs_list_num(arg, 5, __func__);
- int b = (int)eigs_list_num(arg, 6, __func__);
- p_SDL_SetRenderDrawColor(g_renderer, r, g, b, 255);
- p_SDL_RenderDrawLine(g_renderer, x1, y1, x2, y2);
- return make_null(); /* fs:VOID gfx_line answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ int colour[4];
+ if (!gfx_colour(arg, 4, 0, colour, __func__)) return make_null();
+ EigsRect area = gfx_drawable(); EigsShape shape;
+ if (!eigs_geom_shape(arg, 0, EIGS_LINE, &area, &shape, __func__)) return make_null();
+ if (!g_renderer || shape.empty) return make_null(); /* fs:EMPTY no visible segment. */
+ p_SDL_SetRenderDrawColor(g_renderer, colour[0], colour[1], colour[2], 255);
+ p_SDL_RenderDrawLine(g_renderer, shape.x, shape.y, shape.x2, shape.y2);
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* gfx_point of [x, y, r, g, b] */
@@ -696,15 +747,14 @@ Value* builtin_gfx_point(Value *arg) {
|| !gfx_nums(arg, 0, 5),
"gfx_point", "[number x, number y, number r, number g, number b]",
make_null());
- if (!g_renderer) return make_null(); /* fs:VOID no window open: gfx_point answers null on every path -- the return value, not a rejected-argument stand-in */
- int x = (int)eigs_list_num(arg, 0, __func__);
- int y = (int)eigs_list_num(arg, 1, __func__);
- int r = (int)eigs_list_num(arg, 2, __func__);
- int g = (int)eigs_list_num(arg, 3, __func__);
- int b = (int)eigs_list_num(arg, 4, __func__);
- p_SDL_SetRenderDrawColor(g_renderer, r, g, b, 255);
- p_SDL_RenderDrawPoint(g_renderer, x, y);
- return make_null(); /* fs:VOID gfx_point answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ int colour[4];
+ if (!gfx_colour(arg, 2, 0, colour, __func__)) return make_null();
+ EigsRect area = gfx_drawable(); EigsShape shape;
+ if (!eigs_geom_shape(arg, 0, EIGS_POINT, &area, &shape, __func__)) return make_null();
+ if (!g_renderer || shape.empty) return make_null(); /* fs:EMPTY an outside point draws nothing. */
+ p_SDL_SetRenderDrawColor(g_renderer, colour[0], colour[1], colour[2], 255);
+ p_SDL_RenderDrawPoint(g_renderer, shape.x, shape.y);
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* gfx_circle of [cx, cy, radius, r, g, b] — filled circle via midpoint */
@@ -715,22 +765,28 @@ Value* builtin_gfx_circle(Value *arg) {
"gfx_circle",
"[number cx, number cy, number radius, number r, number g, number b] and an optional number alpha",
make_null());
- if (!g_renderer) return make_null(); /* fs:VOID no window open: gfx_circle answers null on every path -- the return value, not a rejected-argument stand-in */
- int cx = (int)eigs_list_num(arg, 0, __func__);
- int cy = (int)eigs_list_num(arg, 1, __func__);
- int radius = (int)eigs_list_num(arg, 2, __func__);
- int r = (int)eigs_list_num(arg, 3, __func__);
- int g = (int)eigs_list_num(arg, 4, __func__);
- int b = (int)eigs_list_num(arg, 5, __func__);
- int a = (arg->data.list.count >= 7) ? (int)eigs_list_num(arg, 6, __func__) : 255;
- p_SDL_SetRenderDrawColor(g_renderer, r, g, b, a);
- /* Filled circle via horizontal lines */
- for (int dy = -radius; dy <= radius; dy++) {
- int dx = (int)sqrt((double)(radius * radius - dy * dy));
- SDL_Rect row = { cx - dx, cy + dy, dx * 2 + 1, 1 };
- p_SDL_RenderFillRect(g_renderer, &row);
- }
- return make_null(); /* fs:VOID gfx_circle answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ int colour[4];
+ if (!gfx_colour(arg, 3, 1, colour, __func__)) return make_null();
+ EigsRect area = gfx_drawable(); EigsShape shape;
+ if (!eigs_geom_shape(arg, 0, EIGS_CIRCLE, &area, &shape, __func__)) return make_null();
+ if (!g_renderer || shape.empty) return make_null(); /* fs:EMPTY no visible bounding box. */
+ p_SDL_SetRenderDrawColor(g_renderer, colour[0], colour[1], colour[2], colour[3]);
+ long double cx = shape.raw[0], cy = shape.raw[1], radius = shape.raw[2];
+ for (int row = shape.y; row < shape.y + shape.h; ++row) {
+ /* Keep the visible row term after subtracting a far-away centre:
+ * cy==r==1e308 still has a wide chord at y=1, not a zero chord. */
+ long double near = cy >= row ? (radius-cy)+row : (radius+cy)-row;
+ if (near < 0) continue;
+ long double dy = fabsl((long double)row-cy);
+ /* The chord is <=radius, even when r+dy or r*r overflows.
+ * Keep near's cancellation-safe value and factor the square root. */
+ long double dx = radius == 0 ? 0 :
+ fminl(radius, sqrtl(near)*sqrtl(radius)*sqrtl(1+fminl(1,dy/radius)));
+ EigsShape span;
+ if (!gfx_edges_ld(cx-dx, row, cx+dx+1, row+1, &area, &span, __func__)) return make_null();
+ gfx_fill_shape(&span);
+ }
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* gfx_rrect of [x, y, w, h, radius, r, g, b] or [..., a]
@@ -742,41 +798,25 @@ Value* builtin_gfx_rrect(Value *arg) {
"gfx_rrect",
"[number x, number y, number w, number h, number radius, number r, number g, number b] and an optional number alpha",
make_null());
- if (!g_renderer) return make_null(); /* fs:VOID no window open: gfx_rrect answers null on every path -- the return value, not a rejected-argument stand-in */
- int x = (int)eigs_list_num(arg, 0, __func__);
- int y = (int)eigs_list_num(arg, 1, __func__);
- int w = (int)eigs_list_num(arg, 2, __func__);
- int h = (int)eigs_list_num(arg, 3, __func__);
- int rad = (int)eigs_list_num(arg, 4, __func__);
- int r = (int)eigs_list_num(arg, 5, __func__);
- int g = (int)eigs_list_num(arg, 6, __func__);
- int b = (int)eigs_list_num(arg, 7, __func__);
- int a = (arg->data.list.count >= 9) ? (int)eigs_list_num(arg, 8, __func__) : 255;
- if (w <= 0 || h <= 0) return make_null(); /* fs:EMPTY a rectangle with no width or height covers no pixels, so drawing nothing IS the answer -- the degenerate-geometry identity, not a laundered argument (lib/ui layout produces zero-size rects routinely) */
- /* Clamp radius to half the smaller dimension */
- if (rad > w / 2) rad = w / 2;
- if (rad > h / 2) rad = h / 2;
- if (rad < 0) rad = 0;
- p_SDL_SetRenderDrawColor(g_renderer, r, g, b, a);
- if (rad == 0) {
- SDL_Rect rect = { x, y, w, h };
- p_SDL_RenderFillRect(g_renderer, &rect);
- return make_null(); /* fs:VOID radius 0 took the plain-rect path and drew it; gfx_rrect answers null on every path -- this is the return value, not a stand-in for a rejected argument */
- }
- /* Center body (between top and bottom rounded bands) */
- SDL_Rect center = { x, y + rad, w, h - 2 * rad };
- p_SDL_RenderFillRect(g_renderer, ¢er);
- /* Top and bottom bands with rounded corners via scanlines */
- for (int dy = 0; dy < rad; dy++) {
- int dx = (int)sqrt((double)(rad * rad - (rad - dy) * (rad - dy)));
- /* Top band */
- SDL_Rect top_row = { x + rad - dx, y + dy, w - 2 * (rad - dx), 1 };
- p_SDL_RenderFillRect(g_renderer, &top_row);
- /* Bottom band */
- SDL_Rect bot_row = { x + rad - dx, y + h - 1 - dy, w - 2 * (rad - dx), 1 };
- p_SDL_RenderFillRect(g_renderer, &bot_row);
- }
- return make_null(); /* fs:VOID gfx_rrect answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ int colour[4];
+ if (!gfx_colour(arg, 5, 1, colour, __func__)) return make_null();
+ EigsRect area = gfx_drawable(); EigsShape shape;
+ if (!eigs_geom_shape(arg, 0, EIGS_RRECT, &area, &shape, __func__)) return make_null();
+ if (!g_renderer || shape.empty) return make_null(); /* fs:EMPTY no visible rounded rectangle. */
+ p_SDL_SetRenderDrawColor(g_renderer, colour[0], colour[1], colour[2], colour[3]);
+ long double x = shape.raw[0], y = shape.raw[1];
+ long double right = x+shape.raw[2], bottom = y+shape.raw[3];
+ long double rad = fminl(fmaxl(0, shape.raw[4]), fminl(shape.raw[2]/2.0L, shape.raw[3]/2.0L));
+ for (int row = shape.y; row < shape.y + shape.h; ++row) {
+ long double depth = row < y+rad ? row-y : row >= bottom-rad ? (bottom-1)-row : rad;
+ depth = fmaxl(0, fminl(rad, depth));
+ long double chord = fminl(rad, sqrtl(depth)*sqrtl(2*rad-depth));
+ long double inset = rad-chord;
+ EigsShape span;
+ if (!gfx_edges_ld(x+inset, row, right-inset, row+1, &area, &span, __func__)) return make_null();
+ gfx_fill_shape(&span);
+ }
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* gfx_clip of [x, y, w, h] — set render clip rectangle.
@@ -791,18 +831,15 @@ Value* builtin_gfx_clip(Value *arg) {
|| !gfx_nums(arg, 0, 4)),
"gfx_clip", "[number x, number y, number w, number h] or null",
make_null());
- if (!g_renderer || !p_SDL_RenderSetClipRect) return make_null(); /* fs:VOID no window / no SDL symbol: gfx_clip answers null on every path -- the return value, not a rejected-argument stand-in */
- if (clearing) {
- p_SDL_RenderSetClipRect(g_renderer, NULL);
- return make_null(); /* fs:VOID the clip was cleared -- gfx_clip's normal successful answer */
+ EigsRect area = gfx_drawable(); EigsShape shape;
+ if (!clearing && !eigs_geom_shape(arg, 0, EIGS_RECT, &area, &shape, __func__)) return make_null();
+ if (!g_renderer || !p_SDL_RenderSetClipRect) return make_null(); /* fs:VOID no renderer or optional clip API. */
+ if (clearing) p_SDL_RenderSetClipRect(g_renderer, NULL);
+ else {
+ SDL_Rect clip = {shape.x, shape.y, shape.empty ? 0 : shape.w, shape.empty ? 0 : shape.h};
+ p_SDL_RenderSetClipRect(g_renderer, &clip);
}
- SDL_Rect clip;
- clip.x = (int)eigs_list_num(arg, 0, __func__);
- clip.y = (int)eigs_list_num(arg, 1, __func__);
- clip.w = (int)eigs_list_num(arg, 2, __func__);
- clip.h = (int)eigs_list_num(arg, 3, __func__);
- p_SDL_RenderSetClipRect(g_renderer, &clip);
- return make_null(); /* fs:VOID gfx_clip answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* gfx_read of [x, y] — read back one rendered pixel as [r, g, b].
@@ -825,22 +862,25 @@ Value* builtin_gfx_read(Value *arg) {
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2
|| !gfx_nums(arg, 0, 2),
"gfx_read", "[number x, number y]", make_null());
+ /* Deterministic refusals stay above TAKE, including nonfinite numbers. */
+ (void)eigs_list_float(arg, 0, __func__);
+ if (g_has_error) return make_null();
+ (void)eigs_list_float(arg, 1, __func__);
+ if (g_has_error) return make_null();
TRACE_NONDET_TAKE("gfx_read");
if (!g_renderer || !p_SDL_RenderReadPixels)
TRACE_NONDET_RECORD("gfx_read", make_null());
- SDL_Rect r;
- r.x = (int)eigs_list_num(arg, 0, __func__);
- r.y = (int)eigs_list_num(arg, 1, __func__);
- r.w = 1;
- r.h = 1;
+ EigsRect area = gfx_drawable(); EigsShape shape;
+ if (!eigs_geom_shape(arg, 0, EIGS_POINT, &area, &shape, __func__)) return make_null();
+ if (shape.empty) TRACE_NONDET_RECORD("gfx_read", make_null());
+ SDL_Rect rect = {shape.x, shape.y, 1, 1};
Uint32 px = 0;
- if (p_SDL_RenderReadPixels(g_renderer, &r, MY_SDL_PIXELFORMAT_ARGB8888,
- &px, (int)sizeof(Uint32)) != 0)
+ if (p_SDL_RenderReadPixels(g_renderer, &rect, MY_SDL_PIXELFORMAT_ARGB8888, &px, sizeof px) != 0)
TRACE_NONDET_RECORD("gfx_read", make_null());
Value *out = make_list(3);
- list_append_owned(out, make_num((double)((px >> 16) & 0xFF)));
- list_append_owned(out, make_num((double)((px >> 8) & 0xFF)));
- list_append_owned(out, make_num((double)(px & 0xFF)));
+ list_append_owned(out, make_int((px >> 16) & 0xFF));
+ list_append_owned(out, make_int((px >> 8) & 0xFF));
+ list_append_owned(out, make_int(px & 0xFF));
TRACE_NONDET_RECORD("gfx_read", out);
}
@@ -883,39 +923,39 @@ Value* builtin_gfx_poll(Value *arg) {
case MY_SDL_KEYDOWN:
dict_set_owned(d, "type", make_str("keydown"));
dict_set_owned(d, "key", make_str(scancode_name(ev.key.keysym.scancode)));
- dict_set_owned(d, "scancode", make_num(ev.key.keysym.scancode));
+ dict_set_owned(d, "scancode", make_int(ev.key.keysym.scancode));
poll_set_mods(d, ev.key.keysym.mod);
break;
case MY_SDL_KEYUP:
dict_set_owned(d, "type", make_str("keyup"));
dict_set_owned(d, "key", make_str(scancode_name(ev.key.keysym.scancode)));
- dict_set_owned(d, "scancode", make_num(ev.key.keysym.scancode));
+ dict_set_owned(d, "scancode", make_int(ev.key.keysym.scancode));
poll_set_mods(d, ev.key.keysym.mod);
break;
case MY_SDL_MOUSEMOTION:
dict_set_owned(d, "type", make_str("mousemove"));
- dict_set_owned(d, "x", make_num(ev.motion.x));
- dict_set_owned(d, "y", make_num(ev.motion.y));
+ dict_set_owned(d, "x", make_int(ev.motion.x));
+ dict_set_owned(d, "y", make_int(ev.motion.y));
poll_set_mods(d, poll_mod_state());
break;
case MY_SDL_MOUSEBUTTONDOWN:
dict_set_owned(d, "type", make_str("mousedown"));
- dict_set_owned(d, "button", make_num(ev.button.button));
- dict_set_owned(d, "x", make_num(ev.button.x));
- dict_set_owned(d, "y", make_num(ev.button.y));
+ dict_set_owned(d, "button", make_int(ev.button.button));
+ dict_set_owned(d, "x", make_int(ev.button.x));
+ dict_set_owned(d, "y", make_int(ev.button.y));
poll_set_mods(d, poll_mod_state());
break;
case MY_SDL_MOUSEBUTTONUP:
dict_set_owned(d, "type", make_str("mouseup"));
- dict_set_owned(d, "button", make_num(ev.button.button));
- dict_set_owned(d, "x", make_num(ev.button.x));
- dict_set_owned(d, "y", make_num(ev.button.y));
+ dict_set_owned(d, "button", make_int(ev.button.button));
+ dict_set_owned(d, "x", make_int(ev.button.x));
+ dict_set_owned(d, "y", make_int(ev.button.y));
poll_set_mods(d, poll_mod_state());
break;
case MY_SDL_MOUSEWHEEL:
dict_set_owned(d, "type", make_str("wheel"));
- dict_set_owned(d, "x", make_num(ev.wheel.x));
- dict_set_owned(d, "y", make_num(ev.wheel.y));
+ dict_set_owned(d, "x", make_int(ev.wheel.x));
+ dict_set_owned(d, "y", make_int(ev.wheel.y));
/* #822: x/y are scroll DELTAS. Carry the pointer as mx/my so
* a consumer can zoom about the cursor without shadowing
* mousemove (stale before the first motion event). Queried
@@ -925,8 +965,8 @@ Value* builtin_gfx_poll(Value *arg) {
if (p_SDL_GetMouseState) {
int wmx = 0, wmy = 0;
p_SDL_GetMouseState(&wmx, &wmy);
- dict_set_owned(d, "mx", make_num(wmx));
- dict_set_owned(d, "my", make_num(wmy));
+ dict_set_owned(d, "mx", make_int(wmx));
+ dict_set_owned(d, "my", make_int(wmy));
}
poll_set_mods(d, poll_mod_state());
break;
@@ -934,8 +974,8 @@ Value* builtin_gfx_poll(Value *arg) {
/* SDL_WINDOWEVENT_SIZE_CHANGED = 6 (RESIZED is 5). */
if (ev.window.event == 6) {
dict_set_owned(d, "type", make_str("resize"));
- dict_set_owned(d, "w", make_num(ev.window.data1));
- dict_set_owned(d, "h", make_num(ev.window.data2));
+ dict_set_owned(d, "w", make_int(ev.window.data1));
+ dict_set_owned(d, "h", make_int(ev.window.data2));
} else {
/* #1007: `d` is already allocated. Returning without
* releasing it leaked 584 bytes / 6 allocations per
@@ -957,15 +997,17 @@ Value* builtin_gfx_ticks(Value *arg) {
(void)arg;
TRACE_NONDET_TAKE("gfx_ticks");
TRACE_NONDET_RECORD("gfx_ticks",
- make_num(g_sdl_lib ? (double)p_SDL_GetTicks() : 0.0));
+ make_int(g_sdl_lib ? (int64_t)p_SDL_GetTicks() : 0));
}
/* gfx_delay of ms */
Value* builtin_gfx_delay(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_NUM, "gfx_delay", "number milliseconds",
+ ARG_GUARD(!arg || !val_is_number(arg), "gfx_delay", "number milliseconds",
make_null());
- Uint32 ms = (Uint32)eigs_num_arg(arg, __func__);
- if (g_sdl_lib) p_SDL_Delay(ms);
+ int64_t milliseconds;
+ if (!eigs_magnitude_arg(arg, 0, UINT32_MAX, &milliseconds, "gfx_delay"))
+ return make_null();
+ if (g_sdl_lib) p_SDL_Delay((Uint32)milliseconds);
return make_null(); /* fs:VOID gfx_delay answers null on every path -- this is the return value, not a stand-in for a rejected argument */
}
@@ -1092,11 +1134,11 @@ Value* builtin_gfx_text(Value *arg) {
*
* THE OPTIONAL SCALE (slot 6) IS IN THIS GUARD ON PURPOSE, and that is
* why gfx_text and gfx_text_width answer a wrong-typed scale
- * DIFFERENTLY. gfx_text_width's slot carried `items[1]->type == VAL_NUM`
+ * DIFFERENTLY. gfx_text_width's slot carried `val_is_number(items[1])`
* before #1007, so its wrong-typed scale is a COERCION and keeps
* measuring at scale 1 (STRICT_REQUIRE there, byte-identical off the
* flag). This one did not:
- * int scale = (count >= 7) ? (int)eigs_num_arg(items[6], __func__) : 1;
+ * int scale = (count >= 7) ? (int)eigs_float_arg(items[6], __func__) : 1;
* reads the union unchecked, so a string scale drew the glyph from a
* reinterpreted `char *` — a subnormal that truncates to 0 and is then
* clamped to 1, which is why it LOOKED like scale 1 while being a pun.
@@ -1112,66 +1154,73 @@ Value* builtin_gfx_text(Value *arg) {
"gfx_text",
"[number x, number y, string text, number r, number g, number b] and an optional number scale",
make_null());
+ double x = eigs_list_float(arg, 0, __func__);
+ if (g_has_error) return make_null();
+ double y = eigs_list_float(arg, 1, __func__);
+ if (g_has_error) return make_null();
+ double scale = arg->data.list.count >= 7 ? eigs_list_float(arg, 6, __func__) : 1;
+ if (g_has_error) return make_null();
+ if (scale < 1) scale = 1;
+ int colour[4];
+ if (!gfx_colour(arg, 3, 0, colour, __func__)) return make_null();
+ const char *text = list_get_ref(arg, 2)->data.str;
if (__builtin_expect(g_replay_enabled, 0)) {
- rt_error(EK_IO, 0, "gfx_text: not replayable under "
- "EIGS_REPLAY (file-read boundary; see docs/TRACE.md)");
+ rt_error(EK_IO, 0, "gfx_text: not replayable under EIGS_REPLAY (file-read boundary; see docs/TRACE.md)");
return make_null();
}
- if (!g_renderer) return make_null(); /* fs:VOID no window open: gfx_text answers null on every path -- the return value, not a rejected-argument stand-in */
- int x = (int)eigs_list_num(arg, 0, __func__);
- int y = (int)eigs_list_num(arg, 1, __func__);
- const char *text = list_get_ref(arg, 2)->data.str;
- int r = (int)eigs_list_num(arg, 3, __func__);
- int g = (int)eigs_list_num(arg, 4, __func__);
- int b = (int)eigs_list_num(arg, 5, __func__);
- int scale = (arg->data.list.count >= 7) ? (int)eigs_list_num(arg, 6, __func__) : 1;
- if (scale < 1) scale = 1;
-
- if (*text && ttf_available() && p_SDL_CreateTextureFromSurface
- && p_SDL_FreeSurface && p_SDL_QueryTexture && p_SDL_DestroyTexture
- && p_SDL_RenderCopy) {
- void *font = ttf_font_for_scale(scale);
+ if (!g_renderer) return make_null(); /* fs:VOID no renderer. */
+ EigsRect area = gfx_drawable();
+ /* The integer TTF size API cannot express a fractional or enormous
+ * bitmap cell. Retain continuous geometry in the bitmap fallback. */
+ if (*text && scale <= (INT_MAX-2)/6 && scale == floor(scale) &&
+ ttf_available() && p_SDL_CreateTextureFromSurface && p_SDL_FreeSurface &&
+ p_SDL_QueryTexture && p_SDL_DestroyTexture && p_SDL_RenderCopy) {
+ void *font = ttf_font_for_scale((int)scale);
if (font) {
- SDL_Color col = { (Uint8)r, (Uint8)g, (Uint8)b, 255 };
+ SDL_Color col = {(Uint8)colour[0], (Uint8)colour[1], (Uint8)colour[2], 255};
void *surf = p_TTF_RenderUTF8_Blended(font, text, col);
if (surf) {
SDL_Texture *tex = p_SDL_CreateTextureFromSurface(g_renderer, surf);
p_SDL_FreeSurface(surf);
if (tex) {
int tw = 0, th = 0;
- p_SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
- SDL_Rect dst = { x, y, tw, th };
- p_SDL_RenderCopy(g_renderer, tex, NULL, &dst);
+ if (p_SDL_QueryTexture(tex, NULL, NULL, &tw, &th) == 0 && tw > 0 && th > 0) {
+ EigsShape shape;
+ if (!gfx_rect_ld(x, y, tw, th, &area, &shape, __func__)) {
+ p_SDL_DestroyTexture(tex); return make_null();
+ }
+ if (!shape.empty) {
+ SDL_Rect dst = {shape.x, shape.y, shape.w, shape.h};
+ SDL_Rect src = {(int)((long double)shape.x-roundl(x)),
+ (int)((long double)shape.y-roundl(y)), shape.w, shape.h};
+ p_SDL_RenderCopy(g_renderer, tex, &src, &dst);
+ }
+ p_SDL_DestroyTexture(tex);
+ return make_null(); /* fs:VOID TTF rendering completed, including an empty intersection. */
+ }
p_SDL_DestroyTexture(tex);
- return make_null(); /* fs:VOID the TTF path rendered the string; gfx_text answers null on every path -- this is the return value, not a stand-in for a rejected argument */
}
}
}
- /* fall through to the bitmap path on any failure */
}
-
- p_SDL_SetRenderDrawColor(g_renderer, r, g, b, 255);
- int cx = x;
- for (const char *p = text; *p; p++) {
+ p_SDL_SetRenderDrawColor(g_renderer, colour[0], colour[1], colour[2], 255);
+ long double cx = x;
+ for (const char *p = text; *p; ++p) {
int ch = (unsigned char)*p;
if (ch < 32 || ch > 126) ch = '?';
- const unsigned char *glyph = font5x7[ch - 32];
- for (int row = 0; row < 7; row++) {
- unsigned char bits = glyph[row];
- for (int col = 0; col < 5; col++) {
- if (bits & (0x10 >> col)) {
- if (scale == 1) {
- p_SDL_RenderDrawPoint(g_renderer, cx + col, y + row);
- } else {
- SDL_Rect pr = { cx + col * scale, y + row * scale, scale, scale };
- p_SDL_RenderFillRect(g_renderer, &pr);
- }
+ const unsigned char *glyph = font5x7[ch-32];
+ for (int row = 0; row < 7; ++row)
+ for (int col = 0; col < 5; ++col)
+ if (glyph[row] & (0x10 >> col)) {
+ EigsShape cell;
+ if (!gfx_rect_ld(cx+(long double)col*scale, (long double)y+(long double)row*scale,
+ scale, scale, &area, &cell, __func__)) return make_null();
+ gfx_fill_shape(&cell);
}
- }
- }
- cx += (5 + 1) * scale; /* 5 pixel width + 1 pixel gap */
+ cx += 6*(long double)scale;
+ if (cx >= (long double)area.x + area.w) break;
}
- return make_null(); /* fs:VOID gfx_text answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* gfx_text_width of [text, scale?] (or of "text") — pixel width of `text`
@@ -1190,16 +1239,19 @@ Value* builtin_gfx_text_width(Value *arg) {
&& list_elem_type(arg, 0) == VAL_STR) {
text = list_get_ref(arg, 0)->data.str;
if (arg->data.list.count >= 2) {
- if (list_elem_type(arg, 1) == VAL_NUM)
- scale = (int)eigs_list_num(arg, 1, __func__);
- else
+ if ((list_elem_type(arg, 1) == VAL_FLOAT || list_elem_type(arg, 1) == VAL_INT)) {
+ int64_t rounded;
+ if (!eigs_list_magnitude(arg, 1, INT_MIN, INT_MAX, &rounded,
+ "gfx_text_width")) return make_null();
+ scale = (int)rounded;
+ } else
bad_scale = 1;
}
}
/* #1007: converted out of its #971 deferral marker. The 0 stays the non-strict
* stand-in — tests/test_gfx_text.eigs pins `gfx_text_width of 5` at 0. */
ARG_GUARD(!text, "gfx_text_width",
- "string text or [string text, number scale]", make_num(0));
+ "string text or [string text, number scale]", make_int(0));
/* A wrong-typed SCALE is the COERCION shape, not a stand-in one: the
* width for scale 1 is still computed and returned. STRICT_REQUIRE
* leaves the default path byte-identical by construction. */
@@ -1207,16 +1259,21 @@ Value* builtin_gfx_text_width(Value *arg) {
if (__builtin_expect(g_replay_enabled, 0)) {
rt_error(EK_IO, 0, "gfx_text_width: not replayable under EIGS_REPLAY "
"(font file-read boundary; see docs/TRACE.md)");
- return make_num(0);
+ return make_int(0);
}
if (scale < 1) scale = 1;
if (*text && ttf_available()) {
void *font = ttf_font_for_scale(scale);
int w = 0, h = 0;
if (font && p_TTF_SizeUTF8(font, text, &w, &h) == 0)
- return make_num((double)w);
+ return make_int(w);
+ }
+ size_t length = strlen(text);
+ if (length > (uint64_t)INT64_MAX / 6 / (uint64_t)scale) {
+ rt_error(EK_VALUE, 0, "gfx_text_width: pixel width does not fit an int64");
+ return make_null();
}
- return make_num((double)strlen(text) * 6.0 * (double)scale);
+ return make_int((int64_t)length * 6 * scale);
}
/* gfx_text_height of scale? (number, [scale], or null → 1) — pixel line
@@ -1229,27 +1286,33 @@ Value* builtin_gfx_text_height(Value *arg) {
* 1 and the caller was told the height it did not ask for. There is no
* stand-in to name (the height for scale 1 is still returned), so this
* raises under strict and does nothing otherwise. */
- STRICT_REQUIRE(arg && arg->type != VAL_NULL && arg->type != VAL_NUM
+ STRICT_REQUIRE(arg && arg->type != VAL_NULL && !val_is_number(arg)
&& !(arg->type == VAL_LIST && arg->data.list.count >= 1
- && list_elem_type(arg, 0) == VAL_NUM),
+ && (list_elem_type(arg, 0) == VAL_FLOAT || list_elem_type(arg, 0) == VAL_INT)),
"gfx_text_height", "number scale, [number scale] or null");
+ if (arg && val_is_number(arg)) {
+ int64_t rounded;
+ if (!eigs_magnitude_arg(arg, INT_MIN, INT_MAX, &rounded, "gfx_text_height"))
+ return make_null();
+ scale = (int)rounded;
+ } else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 1
+ && (list_elem_type(arg, 0) == VAL_FLOAT || list_elem_type(arg, 0) == VAL_INT)) {
+ int64_t rounded;
+ if (!eigs_list_magnitude(arg, 0, INT_MIN, INT_MAX, &rounded, "gfx_text_height"))
+ return make_null();
+ scale = (int)rounded;
+ }
if (__builtin_expect(g_replay_enabled, 0)) {
rt_error(EK_IO, 0, "gfx_text_height: not replayable under EIGS_REPLAY "
"(font file-read boundary; see docs/TRACE.md)");
- return make_num(0);
- }
- if (arg && arg->type == VAL_NUM) {
- scale = (int)eigs_num_arg(arg, __func__);
- } else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 1
- && list_elem_type(arg, 0) == VAL_NUM) {
- scale = (int)eigs_list_num(arg, 0, __func__);
+ return make_int(0);
}
if (scale < 1) scale = 1;
if (ttf_available()) {
void *font = ttf_font_for_scale(scale);
- if (font) return make_num((double)p_TTF_FontHeight(font));
+ if (font) return make_int(p_TTF_FontHeight(font));
}
- return make_num(7.0 * (double)scale);
+ return make_int(INT64_C(7) * scale);
}
/* ---- Audio Builtins ---- */
@@ -1304,60 +1367,38 @@ static void audio_mix_callback(void *ud, uint8_t *stream, int len) {
/* Convert samples (floats -1..1) to an owned int16 buffer. Accepts a
* VAL_LIST or a VAL_BUFFER (#578 — the buffer is the type the language
* recommends for bulk samples and what a DAW render produces; forcing a
- * buffer→list conversion per play meant millions of appends + VAL_NUM
+ * buffer→list conversion per play meant millions of appends + VAL_FLOAT
* allocations just to feed the mixer). Returns NULL on bad shape or an
* over-64MB clip. */
static int16_t* audio_convert_samples(Value *samples, int *out_n) {
if (!samples) return NULL;
- if (samples->type == VAL_BUFFER) {
- int n = samples->data.buffer.count;
- if (n <= 0 || (double)n * sizeof(int16_t) > 64.0 * 1024.0 * 1024.0) return NULL;
- int16_t *buf = xmalloc_array(n, sizeof(int16_t));
- for (int i = 0; i < n; i++) {
- double s = buffer_read_num(samples, i);
- if (g_has_error) { free(buf); return NULL; }
- if (s > 1.0) s = 1.0;
- if (s < -1.0) s = -1.0;
- buf[i] = (int16_t)(s * 32767);
- }
- *out_n = n;
- return buf;
- }
- if (samples->type != VAL_LIST) return NULL;
- int n = samples->data.list.count;
- if (n <= 0 || (double)n * sizeof(int16_t) > 64.0 * 1024.0 * 1024.0) return NULL;
- int16_t *buf = xmalloc_array(n, sizeof(int16_t));
- for (int i = 0; i < n; i++) {
- EigsSlot sample = list_slot(samples, i);
- /* #1007: a non-number used to be COERCED to 0 here, so a wrong-typed
- * sample list produced a valid buffer of silence and the caller's
- * `if (!buf)` guard never saw it — audio_play answered with a real
- * channel id and played nothing. Coercion is the one shape that
- * cannot be caught downstream, so it is rejected at the element. */
- if (slot_type(sample) != VAL_NUM) {
- /* Raise only under strict, exactly as ARG_GUARD does — this macro
- * cannot be used here because the function answers int16_t*, not
- * Value*. An unconditional raise turned a call that used to answer
- * a channel id into a hard error in the DEFAULT build, which is
- * the one thing the #971 reform promises not to do.
- * Rejecting (rather than coercing) is unconditional, because the
- * coerced result was the defect: the caller then answers 0, which
- * is what BUILTINS.md already documents for audio_play ("0 on bad
- * args") and what the old code could not produce. */
- if (g_strict) {
- rt_error(EK_TYPE, 0, "audio samples: element %d is %s, expected a number\n",
- i, val_type_name(slot_type(sample)));
+ int n=samples->type==VAL_BUFFER ? samples->data.buffer.count :
+ samples->type==VAL_LIST ? samples->data.list.count : 0;
+ if (n<=0 || (size_t)n>64u*1024u*1024u/sizeof(int16_t)) return NULL;
+ int16_t *buf=xmalloc_array(n,sizeof *buf);
+ for (int i=0;itype==VAL_BUFFER) {
+ /* R1.6d-D1: stored data crosses the final finite boundary once. */
+ s=buffer_read_f64(samples,i);
+ } else {
+ EigsSlot sample=list_slot(samples,i);
+ if ((slot_type(sample)!=VAL_FLOAT && slot_type(sample)!=VAL_INT)) {
+ if (g_strict) rt_error(EK_TYPE,0,"audio samples: element %d is %s, expected a number\n",i,val_type_name(slot_type(sample)));
+ free(buf);return NULL;
}
- free(buf);
- return NULL;
+ s=eigs_slot_float(sample,__func__);
}
- double s = eigs_slot_num(sample, __func__);
- if (s > 1.0) s = 1.0;
- if (s < -1.0) s = -1.0;
- buf[i] = (int16_t)(s * 32767);
+ if (g_has_error) { free(buf);return NULL; }
+ /* Clamp before multiplying so even a finite 1e308 sample cannot
+ * overflow its conversion. Q7/Q4 then round the PCM integer. */
+ if (s>1) s=1;
+ if (s< -1) s= -1;
+ int pcm;
+ if (!eigs_slot_bounded(slot_from_f64(s*32767),-32767,32767,&pcm,__func__)) { free(buf);return NULL; }
+ buf[i]=(int16_t)pcm;
}
- *out_n = n;
- return buf;
+ *out_n=n;return buf;
}
/* Install a clip on a free (else oldest-finite, else oldest) channel.
@@ -1435,31 +1476,34 @@ Value* builtin_audio_open(Value *arg) {
&& !(arg->type == VAL_LIST && arg->data.list.count >= 2),
"audio_open", "[number freq, number channels] or null");
if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
- /* The same unchecked `eigs_num_arg(&(), __func__)` type-pun as gfx_open. That it is an
+ /* The same unchecked `eigs_float_arg(&(), __func__)` type-pun as gfx_open. That it is an
* oversight rather than a convention is settled 180 lines down:
- * audio_stream_open guards this identical pair with `type == VAL_NUM`
+ * audio_stream_open guards this identical pair with `(type == VAL_FLOAT || type == VAL_INT)`
* before reading it. */
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM,
- "audio_open", "[number freq, number channels]", make_num(0));
-
- /* #1821: generator refusal must not depend on an untaped device rate.
- * At every accepted integer rate, even the longest allowed generator
- * duration has an int-sized sample count. Check the double before
- * the cast below as well, so an enormous positive
- * argument cannot invoke an out-of-range floating-to-integer conversion. */
- double requested_rate = eigs_list_num(arg, 0, __func__);
- if (!(requested_rate <= (double)AUDIO_MAX_DEVICE_RATE)) {
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
+ "audio_open", "[number freq, number channels]", make_int(0));
+ }
+ /* R1.6e-D1: channels are exact counts in 1..255. Validate all
+ * deterministic arguments before SDL, device changes or a tape record. */
+ int64_t freq = 44100, channels = 1;
+ if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
+ if (!eigs_list_int_range(arg, 0, INT_MIN, INT_MAX, &freq, "audio_open")
+ || !eigs_list_int_range(arg, 1, 1, 255, &channels, "audio_open"))
+ return make_int(0);
+ /* #1821: generator refusal must not depend on an untaped device
+ * rate. N5 checked the rate exactly before this domain bound. */
+ if (freq > AUDIO_MAX_DEVICE_RATE) {
rt_error(EK_VALUE, 0,
"audio_open: rate exceeds %d Hz (30-second generator limit)",
AUDIO_MAX_DEVICE_RATE);
return make_null();
}
}
- if (!g_sdl_lib) { if (!load_sdl2()) return make_num(0); } /* fs:ANSWER 0 is not a device id -- line 1095 returns the real one; libSDL2 unavailable is environment state, not an argument */
+ if (!g_sdl_lib) { if (!load_sdl2()) return make_int(0); } /* fs:ANSWER 0 is not a device id -- line 1095 returns the real one; libSDL2 unavailable is environment state, not an argument */
if (!p_SDL_OpenAudioDevice) {
fprintf(stderr, "audio_open: SDL2 audio symbols not available\n");
- return make_num(0); /* fs:ANSWER SDL2 audio symbols missing -- same no-device-id 0, detail on stderr above */
+ return make_int(0); /* fs:ANSWER SDL2 audio symbols missing -- same no-device-id 0, detail on stderr above */
}
p_SDL_Init(MY_SDL_INIT_AUDIO);
@@ -1470,18 +1514,15 @@ Value* builtin_audio_open(Value *arg) {
want.samples = 1024;
want.callback = audio_mix_callback;
- if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
- /* Type-checked at the top of the function, before the SDL load. */
- want.freq = (int)eigs_list_num(arg, 0, __func__);
- want.channels = (int)eigs_list_num(arg, 1, __func__);
- }
+ want.freq = (int)freq;
+ want.channels = (Uint8)channels;
g_audio_device = p_SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
- if (g_audio_device < 2) { g_audio_device = 0; return make_num(0); } /* fs:ANSWER SDL_OpenAudioDevice failed; valid ids are >= 2, so 0 is the no-device result and g_audio_device is reset to match */
+ if (g_audio_device < 2) { g_audio_device = 0; return make_int(0); } /* fs:ANSWER SDL_OpenAudioDevice failed; valid ids are >= 2, so 0 is the no-device result and g_audio_device is reset to match */
g_audio_freq = have.freq;
g_audio_channels = have.channels;
p_SDL_PauseAudioDevice(g_audio_device, 0); /* start playing */
- return make_num(g_audio_device);
+ return make_int(g_audio_device);
}
/* audio_close of null */
@@ -1500,11 +1541,14 @@ Value* builtin_audio_pause(Value *arg) {
/* #1007, COERCION shape: a wrong-typed flag fell back to 1 (pause), so
* `audio_pause of "off"` PAUSED the device and said nothing. Above the
* device check, the [135] rule. */
- STRICT_REQUIRE(arg && arg->type != VAL_NULL && arg->type != VAL_NUM,
+ STRICT_REQUIRE(arg && arg->type != VAL_NULL && !val_is_number(arg),
"audio_pause", "number flag (1 = pause, 0 = unpause) or null");
+ int64_t pause = 1;
+ if (arg && val_is_number(arg)
+ && !eigs_int_arg_range(arg, INT_MIN, INT_MAX, &pause, "audio_pause"))
+ return make_null();
if (!g_audio_device) return make_null(); /* fs:VOID no device open: audio_pause answers null on every path -- the return value, not a rejected-argument stand-in */
- int pause = (arg && arg->type == VAL_NUM) ? (int)eigs_num_arg(arg, __func__) : 1;
- p_SDL_PauseAudioDevice(g_audio_device, pause);
+ p_SDL_PauseAudioDevice(g_audio_device, (int)pause);
return make_null(); /* fs:VOID audio_pause answers null on every path -- this is the return value, not a stand-in for a rejected argument */
}
@@ -1592,17 +1636,25 @@ Value* builtin_audio_capture_open(Value *arg) {
&& !(arg->type == VAL_LIST && arg->data.list.count >= 2),
"audio_capture_open", "[number freq, number channels] or null");
if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM,
- "audio_capture_open", "[number freq, number channels]", make_num(0));
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
+ "audio_capture_open", "[number freq, number channels]", make_int(0));
+ }
+ /* R1.6e-D1: channels are exact counts in 1..255. Validate all
+ * deterministic arguments before SDL, device changes or a tape record. */
+ int64_t freq = 44100, channels = 1;
+ if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
+ if (!eigs_list_int_range(arg, 0, INT_MIN, INT_MAX, &freq, "audio_capture_open")
+ || !eigs_list_int_range(arg, 1, 1, 255, &channels, "audio_capture_open"))
+ return make_int(0);
}
TRACE_NONDET_TAKE("audio_capture_open");
if (!g_sdl_lib) {
- if (!load_sdl2()) TRACE_NONDET_RECORD("audio_capture_open", make_num(0));
+ if (!load_sdl2()) TRACE_NONDET_RECORD("audio_capture_open", make_int(0));
}
if (!p_SDL_OpenAudioDevice || !p_SDL_DequeueAudio) {
fprintf(stderr, "audio_capture_open: SDL2 capture symbols not available\n");
- TRACE_NONDET_RECORD("audio_capture_open", make_num(0));
+ TRACE_NONDET_RECORD("audio_capture_open", make_int(0));
}
p_SDL_Init(MY_SDL_INIT_AUDIO);
@@ -1613,11 +1665,8 @@ Value* builtin_audio_capture_open(Value *arg) {
want.samples = 1024;
want.callback = NULL; /* queue mode: drain via SDL_DequeueAudio */
- if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
- /* Type-checked at the top of the function, before the SDL load. */
- want.freq = (int)eigs_list_num(arg, 0, __func__);
- want.channels = (int)eigs_list_num(arg, 1, __func__);
- }
+ want.freq = (int)freq;
+ want.channels = (Uint8)channels;
if (g_capture_device) {
p_SDL_CloseAudioDevice(g_capture_device);
@@ -1626,10 +1675,10 @@ Value* builtin_audio_capture_open(Value *arg) {
g_capture_device = p_SDL_OpenAudioDevice(NULL, 1, &want, &have, 0);
if (g_capture_device < 2) {
g_capture_device = 0;
- TRACE_NONDET_RECORD("audio_capture_open", make_num(0));
+ TRACE_NONDET_RECORD("audio_capture_open", make_int(0));
}
p_SDL_PauseAudioDevice(g_capture_device, 0); /* start recording */
- TRACE_NONDET_RECORD("audio_capture_open", make_num(g_capture_device));
+ TRACE_NONDET_RECORD("audio_capture_open", make_int(g_capture_device));
}
/* audio_capture_read of null — drain accumulated samples since the
@@ -1721,14 +1770,22 @@ Value* builtin_audio_stream_open(Value *arg) {
&& !(arg->type == VAL_LIST && arg->data.list.count >= 2),
"audio_stream_open", "[number freq, number channels] or null");
if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM,
- "audio_stream_open", "[number freq, number channels]", make_num(0));
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
+ "audio_stream_open", "[number freq, number channels]", make_int(0));
+ }
+ /* R1.6e-D1: channels are exact counts in 1..255. Validate all
+ * deterministic arguments before SDL, device changes or a tape record. */
+ int64_t freq = 44100, channels = 1;
+ if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2) {
+ if (!eigs_list_int_range(arg, 0, INT_MIN, INT_MAX, &freq, "audio_stream_open")
+ || !eigs_list_int_range(arg, 1, 1, 255, &channels, "audio_stream_open"))
+ return make_int(0);
}
- if (!g_sdl_lib) { if (!load_sdl2()) return make_num(0); } /* fs:ANSWER BUILTINS.md audio_stream_open: "0 when SDL/audio is unavailable"; libSDL2 absent is environment state */
+ if (!g_sdl_lib) { if (!load_sdl2()) return make_int(0); } /* fs:ANSWER BUILTINS.md audio_stream_open: "0 when SDL/audio is unavailable"; libSDL2 absent is environment state */
if (!p_SDL_OpenAudioDevice || !p_SDL_QueueAudio) {
fprintf(stderr, "audio_stream_open: SDL2 audio symbols not available\n");
- return make_num(0); /* fs:ANSWER SDL2 queue-audio symbols missing -- the same documented 0-when-unavailable result */
+ return make_int(0); /* fs:ANSWER SDL2 queue-audio symbols missing -- the same documented 0-when-unavailable result */
}
p_SDL_Init(MY_SDL_INIT_AUDIO);
@@ -1739,23 +1796,19 @@ Value* builtin_audio_stream_open(Value *arg) {
want.samples = 1024;
want.callback = NULL; /* queue mode: feed via SDL_QueueAudio */
- if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2
- && list_elem_type(arg, 0) == VAL_NUM
- && list_elem_type(arg, 1) == VAL_NUM) {
- want.freq = (int)eigs_list_num(arg, 0, __func__);
- want.channels = (int)eigs_list_num(arg, 1, __func__);
- }
+ want.freq = (int)freq;
+ want.channels = (Uint8)channels;
if (g_stream_device) {
p_SDL_CloseAudioDevice(g_stream_device);
g_stream_device = 0;
}
g_stream_device = p_SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
- if (g_stream_device < 2) { g_stream_device = 0; return make_num(0); } /* fs:ANSWER SDL_OpenAudioDevice failed; valid ids are >= 2, so 0 is the documented unavailable result and g_stream_device is reset */
+ if (g_stream_device < 2) { g_stream_device = 0; return make_int(0); } /* fs:ANSWER SDL_OpenAudioDevice failed; valid ids are >= 2, so 0 is the documented unavailable result and g_stream_device is reset */
g_stream_freq = have.freq;
g_stream_channels = have.channels;
p_SDL_PauseAudioDevice(g_stream_device, 0); /* start playing */
- return make_num(g_stream_device);
+ return make_int(g_stream_device);
}
/* audio_stream_push of samples — queue a block of float samples [-1, 1]
@@ -1799,10 +1852,10 @@ Value* builtin_audio_stream_queued(Value *arg) {
(void)arg;
TRACE_NONDET_TAKE("audio_stream_queued");
if (!g_stream_device || !p_SDL_GetQueuedAudioSize)
- TRACE_NONDET_RECORD("audio_stream_queued", make_num(0));
+ TRACE_NONDET_RECORD("audio_stream_queued", make_int(0));
Uint32 bytes = p_SDL_GetQueuedAudioSize(g_stream_device);
TRACE_NONDET_RECORD("audio_stream_queued",
- make_num((double)(bytes / sizeof(int16_t))));
+ make_int((int64_t)(bytes / sizeof(int16_t))));
}
/* audio_stream_clear of null — drop any buffered audio on the live
@@ -1838,16 +1891,19 @@ Value* builtin_audio_music_play(Value *arg) {
/* A wrong-typed `loops` is the COERCION shape: it fell back to -1
* (forever), so a typo made the track loop rather than play once. */
STRICT_REQUIRE(arg->data.list.count >= 2
- && list_elem_type(arg, 1) != VAL_NUM,
+ && (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
"audio_music_play", "[string path, number loops]");
+ int64_t loops = -1;
+ if (arg->data.list.count >= 2
+ && (list_elem_type(arg, 1) == VAL_FLOAT || list_elem_type(arg, 1) == VAL_INT)
+ && !eigs_list_int_range(arg, 1, INT_MIN, INT_MAX, &loops, "audio_music_play"))
+ return make_bool(0);
if (__builtin_expect(g_replay_enabled, 0)) {
rt_error(EK_IO, 0, "audio_music_play: not replayable under EIGS_REPLAY "
"(audio file-read boundary; see docs/TRACE.md)");
return make_bool(0);
}
const char *path = list_get_ref(arg, 0)->data.str;
- int loops = (arg->data.list.count >= 2 && list_elem_type(arg, 1) == VAL_NUM)
- ? (int)eigs_list_num(arg, 1, __func__) : -1;
if (!load_sdl2()) return make_bool(0); /* fs:ANSWER the header's documented "0 on failure (missing mixer lib ...)" -- libSDL2 absent is environment state, not an argument */
p_SDL_Init(MY_SDL_INIT_AUDIO); /* ensure the audio subsystem is up */
if (!load_sdl_mixer()) return make_bool(0); /* fs:ANSWER SDL2_mixer not loadable -- the documented missing-mixer-lib 0 */
@@ -1869,7 +1925,7 @@ Value* builtin_audio_music_play(Value *arg) {
p_SDL_GetError ? p_SDL_GetError() : "?");
return make_bool(0); /* fs:ANSWER Mix_LoadMUS failed -- the documented unreadable/undecodable-file 0 */
}
- if (p_Mix_PlayMusic(g_music, loops) < 0) {
+ if (p_Mix_PlayMusic(g_music, (int)loops) < 0) {
fprintf(stderr, "audio_music: play failed: %s\n",
p_SDL_GetError ? p_SDL_GetError() : "?");
return make_bool(0); /* fs:ANSWER Mix_PlayMusic failed -- documented 0; line 1378 returns 1 only when the track is actually playing */
@@ -1882,18 +1938,18 @@ Value* builtin_audio_music_volume(Value *arg) {
STRICT_LIST_MAX(arg, 1, "audio_music_volume");
/* #1007, COERCION shape: a wrong-typed volume fell through to v = 0 and
* MUTED the music. Above the mixer-state check, the [135] rule. */
- STRICT_REQUIRE(!(arg && arg->type == VAL_NUM)
+ STRICT_REQUIRE(!(arg && val_is_number(arg))
&& !(arg && arg->type == VAL_LIST && arg->data.list.count >= 1
- && list_elem_type(arg, 0) == VAL_NUM),
+ && (list_elem_type(arg, 0) == VAL_FLOAT || list_elem_type(arg, 0) == VAL_INT)),
"audio_music_volume", "number volume 0..128 or [number volume]");
- if (!g_mixer_open || !p_Mix_VolumeMusic) return make_null(); /* fs:VOID no mixer open: audio_music_volume answers null on every path -- the return value, not a rejected-argument stand-in */
- int v = 0;
- if (arg && arg->type == VAL_NUM) v = (int)eigs_num_arg(arg, __func__);
- else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 1
- && list_elem_type(arg, 0) == VAL_NUM)
- v = (int)eigs_list_num(arg, 0, __func__);
- if (v < 0) v = 0;
- if (v > MY_MIX_MAX_VOLUME) v = MY_MIX_MAX_VOLUME;
+ int v=0;
+ if (arg && val_is_number(arg)) {
+ if (!eigs_bounded_arg(arg,0,MY_MIX_MAX_VOLUME,&v,"audio_music_volume")) return make_null();
+ } else if (arg && arg->type==VAL_LIST && arg->data.list.count>=1 &&
+ (list_elem_type(arg,0)==VAL_FLOAT || list_elem_type(arg,0)==VAL_INT)) {
+ if (!eigs_list_bounded(arg,0,0,MY_MIX_MAX_VOLUME,&v,"audio_music_volume")) return make_null();
+ }
+ if (!g_mixer_open || !p_Mix_VolumeMusic) return make_null(); /* fs:VOID no music mixer */
p_Mix_VolumeMusic(v);
return make_null(); /* fs:VOID audio_music_volume answers null on every path -- this is the return value, not a stand-in for a rejected argument */
}
@@ -1914,16 +1970,16 @@ Value* builtin_audio_play(Value *arg) {
* -- so the guard would exist only where nothing runs it. */
STRICT_REQUIRE(gfx_bad_samples(arg), "audio_play",
"a list or buffer of samples, or null");
- if (!g_audio_device) return make_num(0); /* fs:ANSWER BUILTINS.md audio_play: "0 on ... closed device"; channel ids are slot+1 >= 1 (line 1057), so 0 is not a channel */
+ if (!g_audio_device) return make_int(0); /* fs:ANSWER BUILTINS.md audio_play: "0 on ... closed device"; channel ids are slot+1 >= 1 (line 1057), so 0 is not a channel */
int n = 0;
int16_t *buf = audio_convert_samples(arg, &n);
if (!buf) {
/* fs:CHANNEL post-raise placeholder for a wrong-typed sample list. */
- if (g_has_error) return make_num(0);
+ if (g_has_error) return make_int(0);
/* fs:ANSWER an empty or over-64MB clip queues nothing. */
- return make_num(0);
+ return make_int(0);
}
- return make_num(audio_install_channel(buf, n, 1));
+ return make_int(audio_install_channel(buf, n, 1));
}
/* audio_play_loop of [samples, loops] — play `samples` `loops` times on
@@ -1937,19 +1993,13 @@ Value* builtin_audio_play_loop(Value *arg) {
* soft, and it moves BELOW the guard so the guard is reachable on a
* machine with no audio device (the [135] rule). */
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2
- || list_elem_type(arg, 1) != VAL_NUM,
- "audio_play_loop", "[samples, number loops]", make_num(0));
- /* #152: NaN/huge casts are UB; -1 is the one negative with meaning. */
- double loops_d = eigs_list_num(arg, 1, __func__);
- int loops;
- if (loops_d == -1.0) loops = -1;
- else {
- ARG_GUARD(isnan(loops_d) || loops_d < 1.0 || loops_d > 10000.0,
- "audio_play_loop",
- "[samples, number loops] with loops == -1 or 1..10000",
- make_num(0));
- loops = (int)loops_d;
- }
+ || (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
+ "audio_play_loop", "[samples, number loops]", make_int(0));
+ int64_t exact_loops;
+ if (!eigs_list_int_range(arg,1,INT_MIN,INT_MAX,&exact_loops,"audio_play_loop")) return make_int(0);
+ int loops=(int)exact_loops;
+ ARG_GUARD(loops != -1 && (loops<1 || loops>10000),"audio_play_loop",
+ "[samples, number loops] with loops == -1 or 1..10000",make_int(0));
/* #1007 round 3: the samples slot, beside the loops slot and above the
* device check for the same reachability reason. */
/* #1637: a non-number sample element is refused here too, above the
@@ -1960,16 +2010,16 @@ Value* builtin_audio_play_loop(Value *arg) {
|| (list_elem_type(arg, 0) == VAL_LIST
&& !gfx_list_all_num(samples)),
"audio_play_loop", "[list or buffer of samples, number loops]");
- if (!g_audio_device) return make_num(0); /* fs:ANSWER BUILTINS.md audio_play_loop: "0 on ... closed device"; channel ids are slot+1 >= 1, so 0 is not a channel */
+ if (!g_audio_device) return make_int(0); /* fs:ANSWER BUILTINS.md audio_play_loop: "0 on ... closed device"; channel ids are slot+1 >= 1, so 0 is not a channel */
int n = 0;
int16_t *buf = audio_convert_samples(samples, &n);
if (!buf) {
/* fs:CHANNEL post-raise placeholder for a wrong-typed sample list. */
- if (g_has_error) return make_num(0);
+ if (g_has_error) return make_int(0);
/* fs:ANSWER an empty or over-64MB clip queues nothing. */
- return make_num(0);
+ return make_int(0);
}
- return make_num(audio_install_channel(buf, n, loops));
+ return make_int(audio_install_channel(buf, n, loops));
}
/* audio_volume of [channel, vol] — live per-channel volume, 0.0..4.0
@@ -1978,56 +2028,59 @@ Value* builtin_audio_volume(Value *arg) {
STRICT_LIST_MAX(arg, 2, "audio_volume");
/* #1007: the mixed condition split, as its #971 deferral marker asked. */
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2
- || list_elem_type(arg, 0) != VAL_NUM
- || list_elem_type(arg, 1) != VAL_NUM,
- "audio_volume", "[number channel, number volume]", make_num(0));
- if (!g_audio_device) return make_num(0); /* fs:ANSWER 0 means "that channel is not playing", and with no device open no channel is */
- int c = (int)eigs_slot_num(list_slot(arg, 0), "audio_volume") - 1;
- if (c < 0 || c >= AUDIO_MAX_CHANNELS) return make_num(0); /* fs:ANSWER 0 means "that channel is not playing" -- the same value line 1452 returns for an inactive in-range channel; an out-of-range id is definitionally inactive */
- double vol = eigs_slot_num(list_slot(arg, 1), "audio_volume");
- if (isnan(vol) || vol < 0.0) vol = 0.0;
- if (vol > 4.0) vol = 4.0;
+ || (list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT)
+ || (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
+ "audio_volume", "[number channel, number volume]", make_int(0));
+ int64_t channel;
+ if (!eigs_list_int_range(arg,0,INT_MIN,INT_MAX,&channel,"audio_volume")) return make_int(0);
+ int vol;
+ if (!eigs_list_bounded(arg,1,0,4,&vol,"audio_volume")) return make_int(0);
+ if (!g_audio_device || channel<1 || channel>AUDIO_MAX_CHANNELS) return make_int(0); /* fs:ANSWER inactive channel */
+ int c=(int)channel-1;
p_SDL_LockAudioDevice(g_audio_device);
int ok = g_audio_ch[c].active;
if (ok) g_audio_ch[c].volume = vol;
p_SDL_UnlockAudioDevice(g_audio_device);
- return make_num(ok);
+ return make_int(ok);
}
/* audio_stop of channel — stop one mixer channel. Returns 1, or 0 on a
* bad/inactive channel. */
Value* builtin_audio_stop(Value *arg) {
/* #1007: the mixed condition split, as its #971 deferral marker asked. */
- ARG_GUARD(!arg || arg->type != VAL_NUM, "audio_stop", "number channel",
- make_num(0));
- if (!g_audio_device) return make_num(0); /* fs:ANSWER 0 means "the channel was not active", and with no device open none is */
- int c = (int)eigs_num_arg(arg, __func__) - 1;
- if (c < 0 || c >= AUDIO_MAX_CHANNELS) return make_num(0); /* fs:ANSWER 0 means "the channel was not active" -- the same value line 1465 returns for an inactive in-range channel */
+ ARG_GUARD(!arg || !val_is_number(arg), "audio_stop", "number channel",
+ make_int(0));
+ int64_t channel = 0;
+ if (!eigs_int_arg_range(arg, INT_MIN, INT_MAX, &channel, "audio_stop"))
+ return make_int(0);
+ if (!g_audio_device) return make_int(0); /* fs:ANSWER 0 means "the channel was not active", and with no device open none is */
+ if (channel < 1 || channel > AUDIO_MAX_CHANNELS) return make_int(0); /* fs:ANSWER 0 means "the channel was not active" -- an out-of-range id is inactive */
+ int c = (int)channel - 1;
p_SDL_LockAudioDevice(g_audio_device);
int ok = g_audio_ch[c].active;
g_audio_ch[c].active = 0;
p_SDL_UnlockAudioDevice(g_audio_device);
- return make_num(ok);
+ return make_int(ok);
}
/* audio_queue_size of null — bytes queued */
Value* builtin_audio_queue_size(Value *arg) {
(void)arg;
- if (!g_audio_device) return make_num(0); /* fs:ANSWER 0 bytes queued: with no device no channel holds unplayed samples, which is exactly the total lines 1477-1485 sum for an idle device */
+ if (!g_audio_device) return make_int(0); /* fs:ANSWER 0 bytes queued: with no device no channel holds unplayed samples, which is exactly the total lines 1477-1485 sum for an idle device */
/* Mixer equivalent of the old queued-bytes count: unplayed bytes
* summed over active FINITE channels (infinite loops would be
* infinity; they are excluded so refill-style polls terminate). */
- double bytes = 0;
+ int64_t bytes = 0;
p_SDL_LockAudioDevice(g_audio_device);
for (int c = 0; c < AUDIO_MAX_CHANNELS; c++) {
AudioChannel *ch = &g_audio_ch[c];
if (!ch->active || ch->loops < 0) continue;
- double rem = (double)(ch->count - ch->pos)
- + (double)(ch->loops - 1) * (double)ch->count;
- bytes += rem * (double)sizeof(int16_t);
+ int64_t rem = (int64_t)(ch->count - ch->pos)
+ + ((int64_t)ch->loops - 1) * (int64_t)ch->count;
+ bytes += rem * (int64_t)sizeof(int16_t);
}
p_SDL_UnlockAudioDevice(g_audio_device);
- return make_num(bytes);
+ return make_int(bytes);
}
/* audio_clear of null — stop all mixer channels and free their clips */
@@ -2047,10 +2100,11 @@ static int audio_generator_sample_count(double duration, int rate,
rt_error(EK_VALUE, 0, "%s: duration exceeds the 30 second limit", who);
return 0;
}
- if (!(samples <= (double)INT_MAX)) {
- rt_error(EK_VALUE, 0, "%s: duration produces a sample count that does not fit in int", who);
+ int64_t rounded;
+ if (!eigs_slot_magnitude(slot_from_f64(samples), INT_MIN, INT_MAX, &rounded, who))
return 0;
- }
+ /* RULINGS B5: Q7 validates the rounded count here; conversion itself
+ * remains truncation until R2.6's #1746 residue migration. */
if (samples <= 0.0) {
*count = 0;
return 1;
@@ -2082,13 +2136,14 @@ Value* builtin_audio_sine(Value *arg) {
* Seven builtins share the shape; the list was derived by running each
* candidate twice and diffing (a per-run difference IS the disclosure),
* not by reading names — audio_gain was missed by the reading. */
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM ||
- list_elem_type(arg, 2) != VAL_NUM,
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT) ||
+ (list_elem_type(arg, 2) != VAL_FLOAT && list_elem_type(arg, 2) != VAL_INT),
"audio_sine", "[number freq, number duration, number amplitude]", make_list(0));
- double freq = eigs_list_num(arg, 0, __func__);
- double dur = eigs_list_num(arg, 1, __func__);
- double amp = eigs_list_num(arg, 2, __func__);
+ double freq = eigs_list_float(arg, 0, __func__);
+ double dur = eigs_list_float(arg, 1, __func__);
+ double amp = eigs_list_float(arg, 2, __func__);
+ if (g_has_error) return make_null();
int rate = g_audio_freq > 0 ? g_audio_freq : 44100;
int n;
if (!audio_generator_sample_count(dur, rate, "audio_sine", &n)) return make_null();
@@ -2098,7 +2153,7 @@ Value* builtin_audio_sine(Value *arg) {
for (int i = 0; i < n; i++) {
double t = (double)i / rate;
double s = sin(2.0 * M_PI * freq * t) * amp;
- list_append_owned(list, make_num(s));
+ list_append_owned(list, make_float(s));
}
return list;
}
@@ -2118,13 +2173,14 @@ Value* builtin_audio_saw(Value *arg) {
* pointer value, disclosed into script-visible data and varying per run.
* All six generators share the shape; guarding one would have left the
* class open. */
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM ||
- list_elem_type(arg, 2) != VAL_NUM,
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT) ||
+ (list_elem_type(arg, 2) != VAL_FLOAT && list_elem_type(arg, 2) != VAL_INT),
"audio_saw", "[number freq, number duration, number amplitude]", make_list(0));
- double freq = eigs_list_num(arg, 0, __func__);
- double dur = eigs_list_num(arg, 1, __func__);
- double amp = eigs_list_num(arg, 2, __func__);
+ double freq = eigs_list_float(arg, 0, __func__);
+ double dur = eigs_list_float(arg, 1, __func__);
+ double amp = eigs_list_float(arg, 2, __func__);
+ if (g_has_error) return make_null();
int rate = g_audio_freq > 0 ? g_audio_freq : 44100;
int n;
if (!audio_generator_sample_count(dur, rate, "audio_saw", &n)) return make_null();
@@ -2134,7 +2190,7 @@ Value* builtin_audio_saw(Value *arg) {
for (int i = 0; i < n; i++) {
double t = (double)i / rate;
double s = 2.0 * (t * freq - floor(t * freq + 0.5)) * amp;
- list_append_owned(list, make_num(s));
+ list_append_owned(list, make_float(s));
}
return list;
}
@@ -2154,13 +2210,14 @@ Value* builtin_audio_square(Value *arg) {
* pointer value, disclosed into script-visible data and varying per run.
* All six generators share the shape; guarding one would have left the
* class open. */
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM ||
- list_elem_type(arg, 2) != VAL_NUM,
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT) ||
+ (list_elem_type(arg, 2) != VAL_FLOAT && list_elem_type(arg, 2) != VAL_INT),
"audio_square", "[number freq, number duration, number amplitude]", make_list(0));
- double freq = eigs_list_num(arg, 0, __func__);
- double dur = eigs_list_num(arg, 1, __func__);
- double amp = eigs_list_num(arg, 2, __func__);
+ double freq = eigs_list_float(arg, 0, __func__);
+ double dur = eigs_list_float(arg, 1, __func__);
+ double amp = eigs_list_float(arg, 2, __func__);
+ if (g_has_error) return make_null();
int rate = g_audio_freq > 0 ? g_audio_freq : 44100;
int n;
if (!audio_generator_sample_count(dur, rate, "audio_square", &n)) return make_null();
@@ -2170,7 +2227,7 @@ Value* builtin_audio_square(Value *arg) {
for (int i = 0; i < n; i++) {
double t = (double)i / rate;
double s = (sin(2.0 * M_PI * freq * t) >= 0 ? 1.0 : -1.0) * amp;
- list_append_owned(list, make_num(s));
+ list_append_owned(list, make_float(s));
}
return list;
}
@@ -2191,17 +2248,18 @@ Value* builtin_audio_sweep(Value *arg) {
* pointer value, disclosed into script-visible data and varying per run.
* All six generators share the shape; guarding one would have left the
* class open. */
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM ||
- list_elem_type(arg, 2) != VAL_NUM ||
- list_elem_type(arg, 3) != VAL_NUM ||
- list_elem_type(arg, 4) != VAL_NUM,
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT) ||
+ (list_elem_type(arg, 2) != VAL_FLOAT && list_elem_type(arg, 2) != VAL_INT) ||
+ (list_elem_type(arg, 3) != VAL_FLOAT && list_elem_type(arg, 3) != VAL_INT) ||
+ (list_elem_type(arg, 4) != VAL_FLOAT && list_elem_type(arg, 4) != VAL_INT),
"audio_sweep", "[number freq_start, number freq_end, number duration, number amplitude, number waveform]", make_list(0));
- double f0 = eigs_list_num(arg, 0, __func__);
- double f1 = eigs_list_num(arg, 1, __func__);
- double dur = eigs_list_num(arg, 2, __func__);
- double amp = eigs_list_num(arg, 3, __func__);
- int wave = (int)eigs_list_num(arg, 4, __func__);
+ double f0 = eigs_list_float(arg, 0, __func__);
+ double f1 = eigs_list_float(arg, 1, __func__);
+ double dur = eigs_list_float(arg, 2, __func__);
+ double amp = eigs_list_float(arg, 3, __func__);
+ int64_t wave = eigs_list_int(arg, 4, "audio_sweep");
+ if (g_has_error) return make_null();
int rate = g_audio_freq > 0 ? g_audio_freq : 44100;
int n;
if (!audio_generator_sample_count(dur, rate, "audio_sweep", &n)) return make_null();
@@ -2219,7 +2277,7 @@ Value* builtin_audio_sweep(Value *arg) {
s = sin(2.0 * M_PI * phase) * amp;
else
s = (2.0 * phase - 1.0) * amp;
- list_append_owned(list, make_num(s));
+ list_append_owned(list, make_float(s));
}
return list;
}
@@ -2303,11 +2361,12 @@ Value* builtin_audio_noise(Value *arg) {
* pointer value, disclosed into script-visible data and varying per run.
* All six generators share the shape; guarding one would have left the
* class open. */
- ARG_GUARD(list_elem_type(arg, 0) != VAL_NUM ||
- list_elem_type(arg, 1) != VAL_NUM,
+ ARG_GUARD((list_elem_type(arg, 0) != VAL_FLOAT && list_elem_type(arg, 0) != VAL_INT) ||
+ (list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
"audio_noise", "[number duration, number amplitude]", make_list(0));
- double dur = eigs_list_num(arg, 0, __func__);
- double amp = eigs_list_num(arg, 1, __func__);
+ double dur = eigs_list_float(arg, 0, __func__);
+ double amp = eigs_list_float(arg, 1, __func__);
+ if (g_has_error) return make_null();
int rate = g_audio_freq > 0 ? g_audio_freq : 44100;
int n;
/* #1767's refusal (a duration over 30 s, or a sample count that does not
@@ -2338,7 +2397,7 @@ Value* builtin_audio_noise(Value *arg) {
list = make_list(n);
for (int i = 0; i < n; i++) {
double s = (eigs_random_double() * 2.0 - 1.0) * amp;
- list_append_owned(list, make_num(s));
+ list_append_owned(list, make_float(s));
}
}
/* Every live answer is recorded, the empty one included: replay takes a
@@ -2370,12 +2429,13 @@ Value* builtin_audio_mix(Value *arg) {
Value *out = make_list(n);
for (int i = 0; i < n; i++) {
- double sa = (i < a->data.list.count && list_elem_type(a, i) == VAL_NUM) ? eigs_slot_num(list_slot(a, i), __func__) : 0;
- double sb = (i < b->data.list.count && list_elem_type(b, i) == VAL_NUM) ? eigs_slot_num(list_slot(b, i), __func__) : 0;
+ double sa = (i < a->data.list.count && (list_elem_type(a, i) == VAL_FLOAT || list_elem_type(a, i) == VAL_INT)) ? eigs_list_float(a, i, "audio_mix") : 0;
+ double sb = (i < b->data.list.count && (list_elem_type(b, i) == VAL_FLOAT || list_elem_type(b, i) == VAL_INT)) ? eigs_list_float(b, i, "audio_mix") : 0;
+ if (g_has_error) { val_decref(out); return make_list(0); }
double mixed = sa + sb;
if (mixed > 1.0) mixed = 1.0;
if (mixed < -1.0) mixed = -1.0;
- list_append_owned(out, make_num(mixed));
+ list_append_owned(out, make_float(mixed));
}
return out;
}
@@ -2401,18 +2461,20 @@ Value* builtin_audio_gain(Value *arg) {
* `char *` into script-visible data exactly as audio_sweep did. Note the
* element read on the next line was ALREADY type-checked — the argument
* beside it was not, which is the same next-line asymmetry as gfx_open. */
- ARG_GUARD(list_elem_type(arg, 1) != VAL_NUM,
+ ARG_GUARD((list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT),
"audio_gain", "[samples, number volume]", make_list(0));
- double vol = eigs_list_num(arg, 1, __func__);
+ double vol = eigs_list_float(arg, 1, "audio_gain");
+ if (g_has_error) return make_list(0);
int n = samples->data.list.count;
Value *out = make_list(n);
for (int i = 0; i < n; i++) {
- double s = (list_elem_type(samples, i) == VAL_NUM) ? eigs_slot_num(list_slot(samples, i), __func__) : 0;
+ double s = ((list_elem_type(samples, i) == VAL_FLOAT || list_elem_type(samples, i) == VAL_INT)) ? eigs_list_float(samples, i, "audio_gain") : 0;
+ if (g_has_error) { val_decref(out); return make_list(0); }
s *= vol;
if (s > 1.0) s = 1.0;
if (s < -1.0) s = -1.0;
- list_append_owned(out, make_num(s));
+ list_append_owned(out, make_float(s));
}
return out;
}
@@ -2432,10 +2494,10 @@ Value* builtin_audio_envelope(Value *arg) {
* pointer value, disclosed into script-visible data and varying per run.
* All six generators share the shape; guarding one would have left the
* class open. */
- ARG_GUARD(list_elem_type(arg, 1) != VAL_NUM ||
- list_elem_type(arg, 2) != VAL_NUM ||
- list_elem_type(arg, 3) != VAL_NUM ||
- list_elem_type(arg, 4) != VAL_NUM,
+ ARG_GUARD((list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT) ||
+ (list_elem_type(arg, 2) != VAL_FLOAT && list_elem_type(arg, 2) != VAL_INT) ||
+ (list_elem_type(arg, 3) != VAL_FLOAT && list_elem_type(arg, 3) != VAL_INT) ||
+ (list_elem_type(arg, 4) != VAL_FLOAT && list_elem_type(arg, 4) != VAL_INT),
"audio_envelope", "[samples, number attack, number decay, number sustain, number release]", make_list(0));
ARG_GUARD(list_elem_type(arg, 0) != VAL_LIST, "audio_envelope",
"[list samples, number attack, number decay, number sustain, number release]",
@@ -2446,18 +2508,25 @@ Value* builtin_audio_envelope(Value *arg) {
* nothing. */
STRICT_REQUIRE(!gfx_list_all_num(samples), "audio_envelope",
"[list of numbers, number attack, number decay, number sustain, number release]");
- double attack = eigs_list_num(arg, 1, __func__);
- double decay = eigs_list_num(arg, 2, __func__);
- double sustain = eigs_list_num(arg, 3, __func__);
- double release = eigs_list_num(arg, 4, __func__);
+ double attack = eigs_list_float(arg, 1, __func__);
+ double decay = eigs_list_float(arg, 2, __func__);
+ double sustain = eigs_list_float(arg, 3, __func__);
+ double release = eigs_list_float(arg, 4, __func__);
int n = samples->data.list.count;
int rate = g_audio_freq > 0 ? g_audio_freq : 44100;
- int a_samples = (int)(attack * rate);
- int d_samples = (int)(decay * rate);
- int r_samples = (int)(release * rate);
- int s_start = a_samples + d_samples;
- int r_start = n - r_samples;
+ if (g_has_error) return make_null();
+ int64_t a_samples, d_samples, r_samples;
+ if (!eigs_slot_magnitude(slot_from_f64(attack * rate), INT_MIN, INT_MAX,
+ &a_samples, "audio_envelope") ||
+ !eigs_slot_magnitude(slot_from_f64(decay * rate), INT_MIN, INT_MAX,
+ &d_samples, "audio_envelope") ||
+ !eigs_slot_magnitude(slot_from_f64(release * rate), INT_MIN, INT_MAX,
+ &r_samples, "audio_envelope")) return make_null();
+ /* Wider intermediates keep two valid C-int durations from overflowing
+ * while locating the sustain/release boundaries. */
+ int64_t s_start = a_samples + d_samples;
+ int64_t r_start = (int64_t)n - r_samples;
if (r_start < s_start) r_start = s_start;
Value *out = make_list(n);
@@ -2478,13 +2547,22 @@ Value* builtin_audio_envelope(Value *arg) {
double frac = (r_samples > 0) ? (double)(i - r_start) / r_samples : 1.0;
env = sustain * (1.0 - frac);
}
- double s = (list_elem_type(samples, i) == VAL_NUM) ? eigs_slot_num(list_slot(samples, i), __func__) : 0;
+ double s = ((list_elem_type(samples, i) == VAL_FLOAT || list_elem_type(samples, i) == VAL_INT)) ? eigs_list_float(samples, i, __func__) : 0;
+ if (g_has_error) { val_decref(out); return make_null(); }
s *= env;
- list_append_owned(out, make_num(s));
+ list_append_owned(out, make_float(s));
}
return out;
}
+/* The complete caller census is gfx_fb and ppu_render_frame (round e).
+ * Every call tests g_has_error before publishing the byte or its effects. */
+static uint8_t gfx_byte_at(const Value *buffer, int64_t index, const char *who) {
+ uint8_t byte = 0;
+ (void)buffer_byte_at(buffer, index, &byte, who);
+ return byte;
+}
+
/* gfx_fb of [buffer, width, height, x, y, scale]
* Blit a framebuffer (VAL_BUFFER of width*height palette indices 0-3) to the
* renderer as a scaled texture. One C call replaces width*height draw calls.
@@ -2501,58 +2579,81 @@ Value* builtin_gfx_fb(Value *arg) {
"gfx_fb",
"[buffer fb, number w, number h, number x, number y, number scale]",
make_null());
- Value *buf = list_get_ref(arg, 0);
- int w = (int)eigs_list_num(arg, 1, __func__);
- int h = (int)eigs_list_num(arg, 2, __func__);
- int dx = (int)eigs_list_num(arg, 3, __func__);
- int dy = (int)eigs_list_num(arg, 4, __func__);
- int sc = (int)eigs_list_num(arg, 5, __func__);
- /* A non-positive dimension is DEGENERATE geometry (nothing to blit),
- * the same verdict gfx_rrect gives a zero-size rectangle. A buffer
- * SHORTER than w * h is an argument mismatch and is loud -- still above
- * the SDL check so it is reachable without libSDL2 (the [135] rule);
- * the reads it needs are type-checked by the guard above. */
- if (w <= 0 || h <= 0 || sc <= 0) return make_null(); /* fs:EMPTY a zero or negative width/height/scale covers no pixels, so drawing nothing IS the answer -- the same degenerate-geometry identity gfx_rrect gives */
- ARG_GUARD(buf->data.buffer.count < w * h,
- "gfx_fb",
- "[buffer fb, number w, number h, number x, number y, number scale] with len of fb >= w * h",
- make_null());
+ Value *buf = list_get_ref(arg, 0);
+ int64_t wi, hi;
+ if (!eigs_list_int_range(arg, 1, INT_MIN, INT_MAX, &wi, __func__) ||
+ !eigs_list_int_range(arg, 2, INT_MIN, INT_MAX, &hi, __func__)) return make_null();
+ double dx = eigs_list_float(arg, 3, __func__);
+ if (g_has_error) return make_null();
+ double dy = eigs_list_float(arg, 4, __func__);
+ if (g_has_error) return make_null();
+ double scale = eigs_list_float(arg, 5, __func__);
+ if (g_has_error) return make_null();
+ if (wi <= 0 || hi <= 0 || scale <= 0) return make_null(); /* fs:EMPTY degenerate framebuffer geometry. */
+ int64_t total = wi*hi;
+ ARG_GUARD(total > buf->data.buffer.count, "gfx_fb", "len of fb >= w * h", make_null());
if (!g_renderer || !p_SDL_CreateTexture || !p_SDL_UpdateTexture || !p_SDL_RenderCopy)
- return make_null(); /* fs:VOID no window / no SDL texture symbols: gfx_fb answers null on every path -- the return value, not a rejected-argument stand-in */
-
- /* Recreate texture if size changed */
- if (!g_fb_texture || g_fb_w != w || g_fb_h != h) {
- if (g_fb_texture) p_SDL_DestroyTexture(g_fb_texture);
- g_fb_texture = p_SDL_CreateTexture(g_renderer,
- MY_SDL_PIXELFORMAT_ARGB8888, MY_SDL_TEXTUREACCESS_STREAMING, w, h);
- if (!g_fb_texture) return make_null(); /* fs:ANSWER SDL_CreateTexture failed -- environment/driver state, not an argument; nothing can be blitted so null is the result */
- g_fb_w = w;
- g_fb_h = h;
- }
-
- /* GB shade palette: index → ARGB */
- static const Uint32 palette[4] = {
- 0xFFFFFFFF, /* 0 = white */
- 0xFFAAAAAA, /* 1 = light */
- 0xFF555555, /* 2 = dark */
- 0xFF000000 /* 3 = black */
- };
-
- /* Convert buffer → ARGB pixel array */
- int total = w * h;
- Uint32 *pixels = xmalloc_array(total, sizeof(Uint32));
- for (int i = 0; i < total; i++) {
- int idx = buffer_read_byte(buf, i);
+ return make_null(); /* fs:VOID no renderer or optional texture API. */
+ int w = (int)wi, h = (int)hi;
+ /* Preserve the source layout and validate every source byte, even if
+ * clipping later hides it. Q1 requires an exact byte at the data boundary. */
+ static const Uint32 palette[4] = {0xFFFFFFFF, 0xFFAAAAAA, 0xFF555555, 0xFF000000};
+ Uint32 *pixels = xmalloc_array((size_t)total, sizeof *pixels);
+ for (int64_t i = 0; i < total; ++i) {
+ int idx = gfx_byte_at(buf, i, __func__);
if (g_has_error) { free(pixels); return make_null(); }
pixels[i] = palette[idx & 3];
}
-
- p_SDL_UpdateTexture(g_fb_texture, NULL, pixels, w * (int)sizeof(Uint32));
+ EigsRect area = gfx_drawable(); EigsShape shape;
+ if (!gfx_rect_ld(dx, dy, wi*(long double)scale, hi*(long double)scale, &area, &shape, __func__)) {
+ free(pixels); return make_null();
+ }
+ if (shape.empty) { free(pixels); return make_null(); } /* fs:EMPTY source is valid but destination has no visible pixels. */
+ long double rx = roundl(dx), ry = roundl(dy);
+ long double rw = roundl((long double)dx+wi*(long double)scale)-rx;
+ long double rh = roundl((long double)dy+hi*(long double)scale)-ry;
+ if (scale == floor(scale) && rx >= area.x && ry >= area.y &&
+ rx+rw <= (long double)area.x+area.w && ry+rh <= (long double)area.y+area.h &&
+ rw > 0 && rh > 0 && rw <= INT_MAX && rh <= INT_MAX && wi <= INT_MAX/(int)sizeof *pixels) {
+ if (!g_fb_texture || g_fb_w != w || g_fb_h != h) {
+ if (g_fb_texture) p_SDL_DestroyTexture(g_fb_texture);
+ g_fb_texture = p_SDL_CreateTexture(g_renderer, MY_SDL_PIXELFORMAT_ARGB8888,
+ MY_SDL_TEXTUREACCESS_STREAMING, w, h);
+ if (!g_fb_texture) { free(pixels); return make_null(); } /* fs:ANSWER SDL texture allocation failed. */
+ g_fb_w = w; g_fb_h = h;
+ }
+ p_SDL_UpdateTexture(g_fb_texture, NULL, pixels, w*(int)sizeof *pixels);
+ SDL_Rect dst = {(int)rx, (int)ry, (int)rw, (int)rh};
+ p_SDL_RenderCopy(g_renderer, g_fb_texture, NULL, &dst);
+ } else {
+ /* A destination outside SDL's integer rectangle domain is sampled
+ * only over visible pixels. Inverse source mapping avoids clipping
+ * and stretching the complete source into the visible destination.
+ * ceil(pixel+0.5)-1 selects the source cell whose independently
+ * rounded right edge contains this pixel, including exact Q4 ties. */
+ for (int y = shape.y; y < shape.y+shape.h; ++y) {
+ long double sy = ceill(((long double)y+0.5L-dy)/scale)-1;
+ if (sy < 0 || sy >= hi) continue;
+ int x = shape.x;
+ while (x < shape.x+shape.w) {
+ long double sx = ceill(((long double)x+0.5L-dx)/scale)-1;
+ if (sx < 0 || sx >= wi) { ++x; continue; }
+ Uint32 pixel = pixels[(int64_t)sy*wi+(int64_t)sx];
+ int end = x+1;
+ while (end < shape.x+shape.w) {
+ long double next = ceill(((long double)end+0.5L-dx)/scale)-1;
+ if (next < 0 || next >= wi || pixels[(int64_t)sy*wi+(int64_t)next] != pixel) break;
+ ++end;
+ }
+ p_SDL_SetRenderDrawColor(g_renderer, (pixel>>16)&255, (pixel>>8)&255, pixel&255, 255);
+ SDL_Rect run = {x, y, end-x, 1};
+ p_SDL_RenderFillRect(g_renderer, &run);
+ x = end;
+ }
+ }
+ }
free(pixels);
-
- SDL_Rect dst = { dx, dy, w * sc, h * sc };
- p_SDL_RenderCopy(g_renderer, g_fb_texture, NULL, &dst);
- return make_null(); /* fs:VOID gfx_fb answers null on every path -- this is the return value, not a stand-in for a rejected argument */
+ return make_null(); /* fs:VOID graphics commands return null. */
}
/* ================================================================
@@ -2582,7 +2683,7 @@ Value* builtin_ppu_render_frame(Value *arg) {
double *fb = fb_v->data.buffer.data;
- int lcdc = buffer_read_byte(mem_v, 0xFF40);
+ int lcdc = gfx_byte_at(mem_v, 0xFF40, __func__);
if (g_has_error) return make_null();
if (!(lcdc & 0x80)) {
/* LCD off — blank */
@@ -2590,19 +2691,19 @@ Value* builtin_ppu_render_frame(Value *arg) {
return make_null(); /* fs:VOID the LCD is off, so the frame was blanked; ppu_render_frame answers null on every path -- this is the return value, not a stand-in for a rejected argument */
}
- int scy = buffer_read_byte(mem_v, 0xFF42);
+ int scy = gfx_byte_at(mem_v, 0xFF42, __func__);
if (g_has_error) return make_null();
- int scx = buffer_read_byte(mem_v, 0xFF43);
+ int scx = gfx_byte_at(mem_v, 0xFF43, __func__);
if (g_has_error) return make_null();
- int bgp_raw = buffer_read_byte(mem_v, 0xFF47);
+ int bgp_raw = gfx_byte_at(mem_v, 0xFF47, __func__);
if (g_has_error) return make_null();
- int obp0_raw = buffer_read_byte(mem_v, 0xFF48);
+ int obp0_raw = gfx_byte_at(mem_v, 0xFF48, __func__);
if (g_has_error) return make_null();
- int obp1_raw = buffer_read_byte(mem_v, 0xFF49);
+ int obp1_raw = gfx_byte_at(mem_v, 0xFF49, __func__);
if (g_has_error) return make_null();
- int wy = buffer_read_byte(mem_v, 0xFF4A);
+ int wy = gfx_byte_at(mem_v, 0xFF4A, __func__);
if (g_has_error) return make_null();
- int wx = buffer_read_byte(mem_v, 0xFF4B);
+ int wx = gfx_byte_at(mem_v, 0xFF4B, __func__);
if (g_has_error) return make_null();
/* Decode palettes */
@@ -2646,7 +2747,7 @@ Value* builtin_ppu_render_frame(Value *arg) {
if (tile_x != prev_tile_x) {
prev_tile_x = tile_x;
- int tile_num = buffer_read_byte(mem_v, bg_map_base + map_row + tile_x);
+ int tile_num = gfx_byte_at(mem_v, bg_map_base + map_row + tile_x, __func__);
if (g_has_error) return make_null();
int tile_addr;
if (unsigned_mode)
@@ -2655,9 +2756,9 @@ Value* builtin_ppu_render_frame(Value *arg) {
tile_addr = (tile_num >= 128)
? 0x8800 + (tile_num - 128) * 16 + tile_row * 2
: 0x9000 + tile_num * 16 + tile_row * 2;
- tile_lo = buffer_read_byte(mem_v, tile_addr);
+ tile_lo = gfx_byte_at(mem_v, tile_addr, __func__);
if (g_has_error) return make_null();
- tile_hi = buffer_read_byte(mem_v, tile_addr + 1);
+ tile_hi = gfx_byte_at(mem_v, tile_addr + 1, __func__);
if (g_has_error) return make_null();
}
@@ -2688,7 +2789,7 @@ Value* builtin_ppu_render_frame(Value *arg) {
int wtx = win_x >> 3;
if (wtx != prev_wtx) {
prev_wtx = wtx;
- int wtn = buffer_read_byte(mem_v, win_map_base + win_map_row + wtx);
+ int wtn = gfx_byte_at(mem_v, win_map_base + win_map_row + wtx, __func__);
if (g_has_error) return make_null();
int wta;
if (unsigned_mode)
@@ -2697,9 +2798,9 @@ Value* builtin_ppu_render_frame(Value *arg) {
wta = (wtn >= 128)
? 0x8800 + (wtn - 128) * 16 + win_row * 2
: 0x9000 + wtn * 16 + win_row * 2;
- wtile_lo = buffer_read_byte(mem_v, wta);
+ wtile_lo = gfx_byte_at(mem_v, wta, __func__);
if (g_has_error) return make_null();
- wtile_hi = buffer_read_byte(mem_v, wta + 1);
+ wtile_hi = gfx_byte_at(mem_v, wta + 1, __func__);
if (g_has_error) return make_null();
}
int wbit = 7 - (win_x & 7);
@@ -2718,16 +2819,16 @@ Value* builtin_ppu_render_frame(Value *arg) {
for (int i = 0; i < 40 && spr_count < 10; i++) {
int oam = 0xFE00 + i * 4;
- int sy = buffer_read_byte(mem_v, oam) - 16;
+ int sy = gfx_byte_at(mem_v, oam, __func__) - 16;
if (g_has_error) return make_null();
- int sx = buffer_read_byte(mem_v, oam + 1) - 8;
+ int sx = gfx_byte_at(mem_v, oam + 1, __func__) - 8;
if (g_has_error) return make_null();
if (ly >= sy && ly < sy + sprite_h) {
sprites[spr_count].sx = sx;
sprites[spr_count].sy = sy;
- sprites[spr_count].tile = buffer_read_byte(mem_v, oam + 2);
+ sprites[spr_count].tile = gfx_byte_at(mem_v, oam + 2, __func__);
if (g_has_error) return make_null();
- sprites[spr_count].flags = buffer_read_byte(mem_v, oam + 3);
+ sprites[spr_count].flags = gfx_byte_at(mem_v, oam + 3, __func__);
if (g_has_error) return make_null();
sprites[spr_count].oam_idx = i;
spr_count++;
@@ -2764,9 +2865,9 @@ Value* builtin_ppu_render_frame(Value *arg) {
}
int taddr = 0x8000 + tile * 16 + row * 2;
- int slo = buffer_read_byte(mem_v, taddr);
+ int slo = gfx_byte_at(mem_v, taddr, __func__);
if (g_has_error) return make_null();
- int shi = buffer_read_byte(mem_v, taddr + 1);
+ int shi = gfx_byte_at(mem_v, taddr + 1, __func__);
if (g_has_error) return make_null();
for (int col = 0; col < 8; col++) {
diff --git a/src/ext_http.c b/src/ext_http.c
index ccfda9242..c5526be02 100644
--- a/src/ext_http.c
+++ b/src/ext_http.c
@@ -586,25 +586,37 @@ Value* builtin_http_early_bind(Value *arg) {
}
arg = list_get_view(arg, 0, &port_view);
}
- if (arg && arg->type != VAL_NULL && arg->type != VAL_NUM) {
+ if (arg && arg->type != VAL_NULL && !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "http_early_bind: port must be a number or null");
return make_null();
}
+ int64_t port_value = 5000;
+ if (arg && arg->type != VAL_NULL
+ && !eigs_int_arg_range(arg, 0, 65535, &port_value, "http_early_bind")) return make_null();
+ int port = (int)port_value;
if (g_server.early_bind_fd >= 0 || g_server.serving || g_server.response_header_rejected) {
rt_error(EK_VALUE, 0, "http_early_bind: server already bound/serving or http_response_header rejected");
return make_null();
}
- int port = 5000;
- double port_d = 0;
- if (eigs_opt_num(arg, &port_d, "http_serve")) port = (int)port_d; /* #1637: a bool raises */
- if (g_has_error) return make_null();
+
const char *env_port = getenv("PORT");
- if (env_port && atoi(env_port) > 0) {
- port = atoi(env_port);
+ char *port_end = NULL;
+ errno = 0;
+ long override = env_port ? strtol(env_port, &port_end, 10) : 0;
+ if (env_port && port_end != env_port && override > 0) {
+ if (errno == ERANGE || override > 65535) {
+ rt_error(EK_VALUE, 0, "http_early_bind: PORT outside [0, 65535]");
+ return make_null();
+ }
+ port = (int)override;
printf("[deploy] PORT env=%s, binding port %d\n", env_port, port);
} else {
printf("[deploy] No PORT env, using default %d\n", port);
}
+ if (port < 0 || port > 65535) {
+ rt_error(EK_VALUE, 0, "http_early_bind: port outside [0, 65535]");
+ return make_null();
+ }
char cwd[512];
if (getcwd(cwd, sizeof(cwd))) {
printf("[deploy] cwd=%s\n", cwd);
@@ -651,6 +663,25 @@ Value* builtin_http_early_bind(Value *arg) {
}
Value* builtin_http_serve(Value *arg) {
+ int64_t port_value = 5000;
+ if (arg && arg->type != VAL_NULL
+ && !eigs_int_arg_range(arg, 0, 65535, &port_value, "http_serve")) return make_null();
+ int port = (int)port_value;
+ const char *env_port = getenv("PORT");
+ char *port_end = NULL;
+ errno = 0;
+ long override = env_port ? strtol(env_port, &port_end, 10) : 0;
+ if (env_port && port_end != env_port && override > 0) {
+ if (errno == ERANGE || override > 65535) {
+ rt_error(EK_VALUE, 0, "http_serve: PORT outside [0, 65535]");
+ return make_null();
+ }
+ port = (int)override;
+ }
+ if (port < 0 || port > 65535) {
+ rt_error(EK_VALUE, 0, "http_serve: port outside [0, 65535]");
+ return make_null();
+ }
if (__builtin_expect(g_replay_enabled, 0)) {
rt_error(EK_IO, 0, "http_serve: not replayable under EIGS_REPLAY "
"(server/static-file boundary; see docs/TRACE.md)");
@@ -667,14 +698,7 @@ Value* builtin_http_serve(Value *arg) {
rt_error(EK_VALUE, 0, "http_response_header: rejected configuration; http_serve cannot start");
return make_null();
}
- int port = 5000;
- double port_d = 0;
- if (eigs_opt_num(arg, &port_d, "http_serve")) port = (int)port_d; /* #1637: a bool raises */
- if (g_has_error) return make_null();
- const char *env_port = getenv("PORT");
- if (env_port && atoi(env_port) > 0) {
- port = atoi(env_port);
- }
+
printf("Starting HTTP server on port %d...\n", port);
fflush(stdout);
http_serve_blocking(port);
@@ -951,7 +975,8 @@ Value* builtin_shared_set(Value *arg) {
* as 0 (so the first incr inserts the key with `delta` as its value);
* an existing non-numeric value is a usage error, returns null without
* mutating. Subject to the same byte cap as shared_set. Returns the new
- * value on success, null on bad args / over-cap / type mismatch. */
+ * value on success, null on bad args / over-cap / type mismatch. Ints add
+ * exactly and int64 overflow raises without mutating the counter (A). */
Value* builtin_shared_incr(Value *arg) {
STRICT_LIST_MAX(arg, 2, "shared_incr");
ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2,
@@ -959,39 +984,60 @@ Value* builtin_shared_incr(Value *arg) {
Value *key_v = list_get_view(arg, 0, &(EigsView){{0}});
Value *delta_v = list_get_view(arg, 1, &(EigsView){{0}});
ARG_GUARD(key_v->type != VAL_STR, "shared_incr", "a string key", make_null());
- ARG_GUARD(delta_v->type != VAL_NUM, "shared_incr", "a number delta", make_null());
+ ARG_GUARD(!val_is_number(delta_v), "shared_incr", "a number delta", make_null());
Server *s = eigs_http_active;
if (!s) return make_null();
- double delta = eigs_num_arg(delta_v, __func__);
+ EigsNum delta = eigs_number_arg(delta_v, __func__);
+ if (g_has_error) return make_null();
long cap = shared_max_bytes();
pthread_mutex_lock(&s->shared_mu);
int idx = shared_find(s, key_v->data.str);
- double cur = 0;
+ EigsNum cur = { .is_int = 1, .value = { .i64 = 0 } };
if (idx >= 0) {
int pos = 0;
Value *parsed = eigs_json_parse_root(s->shared[idx].json, &pos); /* #777 */
/* Read the number out BEFORE dropping the ref — decref may free it —
* and drop it on the mismatch path too, which is the one an early
* return makes easy to miss. */
- int bad = (!parsed || parsed->type != VAL_NUM);
- if (!bad) cur = eigs_num_arg(parsed, __func__);
+ int bad = (!parsed || !val_is_number(parsed));
+ if (!bad) cur = eigs_number_arg(parsed, __func__);
val_decref(parsed);
- if (bad) {
+ if (bad || g_has_error) {
+ pthread_mutex_unlock(&s->shared_mu);
+ return make_null();
+ }
+ }
+ Value *new_v;
+ if (cur.is_int && delta.is_int) {
+ int64_t new_val;
+ if (__builtin_add_overflow(cur.value.i64, delta.value.i64, &new_val)) {
pthread_mutex_unlock(&s->shared_mu);
+ rt_error(EK_VALUE, 0, "shared_incr: integer overflow in '+'");
return make_null();
}
+ new_v = make_int(new_val);
+ } else {
+ double cur_f = cur.is_int ? (double)cur.value.i64 : cur.value.f64;
+ double delta_f = delta.is_int ? (double)delta.value.i64 : delta.value.f64;
+ new_v = make_float(cur_f + delta_f);
+ }
+ if (g_has_error) {
+ val_decref(new_v);
+ pthread_mutex_unlock(&s->shared_mu);
+ return make_null();
}
- double new_val = cur + delta;
- /* eigs_json_encode borrows its argument, so the Value handed to it is
- * ours to release. */
- Value *new_v = make_num(new_val);
+ /* eigs_json_encode borrows new_v. Keep its owned ref for the return value
+ * on success and release it on every refusal before leaving the lock. */
char *new_json = eigs_json_encode(new_v);
- val_decref(new_v);
/* A number can't exceed the depth bound, but don't leave a bare strlen on
* a documented-nullable return. */
- if (!new_json) { pthread_mutex_unlock(&s->shared_mu); return make_null(); }
+ if (!new_json) {
+ val_decref(new_v);
+ pthread_mutex_unlock(&s->shared_mu);
+ return make_null();
+ }
long new_json_len = (long)strlen(new_json);
long key_len = (long)val_str_len(key_v);
long old_bytes = (idx >= 0) ? (long)strlen(s->shared[idx].json) : 0;
@@ -1000,6 +1046,7 @@ Value* builtin_shared_incr(Value *arg) {
if (s->shared_bytes + delta_bytes > cap) {
pthread_mutex_unlock(&s->shared_mu);
free(new_json);
+ val_decref(new_v);
return make_null();
}
if (idx >= 0) {
@@ -1017,7 +1064,7 @@ Value* builtin_shared_incr(Value *arg) {
}
s->shared_bytes += delta_bytes;
pthread_mutex_unlock(&s->shared_mu);
- return make_num(new_val);
+ return new_v;
}
Value* builtin_shared_get(Value *arg) {
@@ -1046,9 +1093,9 @@ Value* builtin_shared_has(Value *arg) {
}
Value* builtin_shared_delete(Value *arg) {
- ARG_GUARD(!arg || arg->type != VAL_STR, "shared_delete", "a string key", make_num(0));
+ ARG_GUARD(!arg || arg->type != VAL_STR, "shared_delete", "a string key", make_int(0));
Server *s = eigs_http_active;
- if (!s) return make_num(0); /* fs:ANSWER 0 means "nothing removed" -- the same value line 761 returns via `removed` when the key is absent */
+ if (!s) return make_int(0); /* fs:ANSWER 0 means "nothing removed" -- the same value line 761 returns via `removed` when the key is absent */
pthread_mutex_lock(&s->shared_mu);
int idx = shared_find(s, arg->data.str);
int removed = 0;
@@ -1064,7 +1111,7 @@ Value* builtin_shared_delete(Value *arg) {
removed = 1;
}
pthread_mutex_unlock(&s->shared_mu);
- return make_num(removed);
+ return make_int(removed);
}
Value* builtin_shared_keys(Value *arg) {
@@ -1083,11 +1130,11 @@ Value* builtin_shared_keys(Value *arg) {
Value* builtin_shared_size(Value *arg) {
(void)arg;
Server *s = eigs_http_active;
- if (!s) return make_num(0); /* fs:ANSWER 0 is the store's key count, the same quantity line 784 returns; with no active server the store is empty */
+ if (!s) return make_int(0); /* fs:ANSWER 0 is the store's key count, the same quantity line 784 returns; with no active server the store is empty */
pthread_mutex_lock(&s->shared_mu);
int n = s->shared_count;
pthread_mutex_unlock(&s->shared_mu);
- return make_num(n);
+ return make_int(n);
}
Value* builtin_shared_clear(Value *arg) {
diff --git a/src/ext_net.c b/src/ext_net.c
index def69a3fb..63d713288 100644
--- a/src/ext_net.c
+++ b/src/ext_net.c
@@ -74,16 +74,15 @@ static Value* net_bytes_to_buffer(const unsigned char *bytes, int n) {
* opens named a different connection's fd and read/wrote it silently. The
* packing is `slot + gen * HANDLE_TABLE_SIZE`: slot is 1..255, gen starts at
* 1, so every live handle is >= 257 and a bare small integer (a forged or
- * hand-computed id) decodes to gen 0, which no live slot has. Doubles carry
- * 53 bits exactly, so the packed form is exact until a single slot has been
- * reused 2^45 times. */
-static double net_pack(int idx, uint32_t gen) {
- return (double)idx + (double)gen * (double)HANDLE_TABLE_SIZE;
+ * hand-computed id) decodes to gen 0, which no live slot has. The packed
+ * integer fits int64 for every uint32 generation. */
+static int64_t net_pack(int idx, uint32_t gen) {
+ return (int64_t)idx + (int64_t)gen * HANDLE_TABLE_SIZE;
}
-static EigsNetSock* net_unpack(double packed, int *out_idx, uint32_t *out_gen) {
- if (!(packed > 0) || packed > 9007199254740992.0) return NULL;
- long long p = (long long)packed;
+static EigsNetSock* net_unpack(int64_t packed, int *out_idx, uint32_t *out_gen) {
+ if (packed <= 0 || packed / HANDLE_TABLE_SIZE > UINT32_MAX) return NULL;
+ int64_t p = packed;
int idx = (int)(p % HANDLE_TABLE_SIZE);
uint32_t gen = (uint32_t)(p / HANDLE_TABLE_SIZE);
if (out_idx) *out_idx = idx;
@@ -91,7 +90,7 @@ static EigsNetSock* net_unpack(double packed, int *out_idx, uint32_t *out_gen) {
return (EigsNetSock*)handle_lookup(idx, gen, HANDLE_NET, NULL);
}
-static double net_register_sock(int fd, EigsNetSockKind kind) {
+static int64_t net_register_sock(int fd, EigsNetSockKind kind) {
EigsNetSock *s = xmalloc(sizeof(EigsNetSock));
s->fd = fd;
s->kind = kind;
@@ -103,14 +102,13 @@ static double net_register_sock(int fd, EigsNetSockKind kind) {
rt_error(EK_LIMIT, 0,
"net: handle table full (max %d live threads/channels/"
"tasks/sockets)", HANDLE_TABLE_SIZE - 1);
- return -1.0;
+ return -1;
}
return net_pack(id, gen);
}
-static EigsNetSock* net_lookup(Value *v, EigsNetSockKind kind) {
- if (!v || v->type != VAL_NUM) return NULL;
- EigsNetSock *s = net_unpack(eigs_num_arg(v, __func__), NULL, NULL);
+static EigsNetSock* net_lookup(int64_t handle, EigsNetSockKind kind) {
+ EigsNetSock *s = net_unpack(handle, NULL, NULL);
if (!s || s->kind != kind) return NULL;
return s;
}
@@ -139,13 +137,22 @@ static int net_require_list(Value *arg, int min, int max, const char *fn) {
return 1;
}
-static int net_num_at(Value *arg, int idx, int fallback) {
- if (!arg || arg->type != VAL_LIST || idx >= arg->data.list.count)
- return fallback;
- /* #1637: null keeps the fallback, a number is read, anything else (a
- * bool) raises; callers check g_has_error after their reads. */
- double d = 0;
- return eigs_slot_opt_num(list_slot(arg, idx), &d, "net") ? (int)d : fallback;
+/* Optional integer counts and continuous poll durations have different
+ * contracts. Both validate before a tape take or any socket operation. */
+static int net_count_at(Value *arg, int idx, int fallback, const char *who) {
+ if (idx >= arg->data.list.count) return fallback;
+ int64_t value = fallback;
+ eigs_opt_int_range(list_get_view(arg, idx, &(EigsView){{0}}),
+ INT_MIN, INT_MAX, &value, who);
+ return (int)value;
+}
+
+static int net_timeout_at(Value *arg, int idx, const char *who) {
+ if (idx >= arg->data.list.count) return -1;
+ int64_t value = -1;
+ eigs_opt_magnitude(list_get_view(arg, idx, &(EigsView){{0}}),
+ INT_MIN, INT_MAX, &value, who);
+ return (int)value;
}
/* ---- builtins ----------------------------------------------------- */
@@ -155,14 +162,13 @@ static int net_num_at(Value *arg, int idx, int fallback) {
* ephemeral port — read it back with net_port. Binds 0.0.0.0, matching
* ext_http's server posture (SECURITY.md: the script is trusted). */
Value* builtin_net_listen(Value *arg) {
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "net_listen: expected a port number");
return make_null();
}
+ int64_t port;
+ if (!eigs_int_arg_range(arg, 0, 65535, &port, "net_listen")) return make_null();
TRACE_NONDET_TAKE("net_listen");
- int port = (int)eigs_num_arg(arg, __func__);
- if (port < 0 || port > 65535)
- TRACE_NONDET_RECORD("net_listen", make_null());
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) TRACE_NONDET_RECORD("net_listen", make_null());
int one = 1;
@@ -176,27 +182,29 @@ Value* builtin_net_listen(Value *arg) {
close(fd);
TRACE_NONDET_RECORD("net_listen", make_null());
}
- double id = net_register_sock(fd, NET_SOCK_LISTENER);
+ int64_t id = net_register_sock(fd, NET_SOCK_LISTENER);
if (id < 0) TRACE_NONDET_RECORD("net_listen", make_null());
- TRACE_NONDET_RECORD("net_listen", make_num(id));
+ TRACE_NONDET_RECORD("net_listen", make_int(id));
}
/* net_port of listener_id → the locally bound port (the kernel's pick
* for a port-0 listen), or null on a bad/closed handle. Kernel-chosen,
* so it rides the tape like every other environment outcome. */
Value* builtin_net_port(Value *arg) {
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "net_port: expected a listener handle");
return make_null();
}
+ int64_t handle = eigs_int_arg(arg, "net_port");
+ if (g_has_error) return make_null();
TRACE_NONDET_TAKE("net_port");
- EigsNetSock *s = net_lookup(arg, NET_SOCK_LISTENER);
+ EigsNetSock *s = net_lookup(handle, NET_SOCK_LISTENER);
if (!s) TRACE_NONDET_RECORD("net_port", make_null());
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
if (getsockname(s->fd, (struct sockaddr*)&addr, &len) < 0)
TRACE_NONDET_RECORD("net_port", make_null());
- TRACE_NONDET_RECORD("net_port", make_num(ntohs(addr.sin_port)));
+ TRACE_NONDET_RECORD("net_port", make_int(ntohs(addr.sin_port)));
}
/* net_accept of listener_id
@@ -208,25 +216,27 @@ Value* builtin_net_accept(Value *arg) {
int timeout_ms = -1;
if (arg && arg->type == VAL_LIST) {
if (!net_require_list(arg, 2, 2, "net_accept")) return make_null();
- timeout_ms = net_num_at(arg, 1, -1);
+ timeout_ms = net_timeout_at(arg, 1, "net_accept");
if (g_has_error) return make_null(); /* #1637: a bool timeout raised */
arg = list_get_view(arg, 0, &listener_view);
}
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "net_accept: expected a listener handle");
return make_null();
}
+ int64_t handle = eigs_int_arg(arg, "net_accept");
+ if (g_has_error) return make_null();
TRACE_NONDET_TAKE("net_accept");
- EigsNetSock *s = net_lookup(arg, NET_SOCK_LISTENER);
+ EigsNetSock *s = net_lookup(handle, NET_SOCK_LISTENER);
if (!s) TRACE_NONDET_RECORD("net_accept", make_null());
if (!net_wait_readable(s->fd, timeout_ms))
TRACE_NONDET_RECORD("net_accept", make_null());
int cfd;
do { cfd = accept(s->fd, NULL, NULL); } while (cfd < 0 && errno == EINTR);
if (cfd < 0) TRACE_NONDET_RECORD("net_accept", make_null());
- double id = net_register_sock(cfd, NET_SOCK_CONN);
+ int64_t id = net_register_sock(cfd, NET_SOCK_CONN);
if (id < 0) TRACE_NONDET_RECORD("net_accept", make_null());
- TRACE_NONDET_RECORD("net_accept", make_num(id));
+ TRACE_NONDET_RECORD("net_accept", make_int(id));
}
/* net_dial of [host, port]
@@ -241,14 +251,14 @@ Value* builtin_net_dial(Value *arg) {
rt_error(EK_TYPE, 0, "net_dial: expected [host, port] with a string host");
return make_null();
}
- int port = net_num_at(arg, 1, -1);
- int timeout_ms = net_num_at(arg, 2, -1);
+ int64_t port;
+ if (!eigs_list_int_range(arg, 1, 0, 65535, &port, "net_dial")) return make_null();
+ int timeout_ms = net_timeout_at(arg, 2, "net_dial");
if (g_has_error) return make_null(); /* #1637: before the tape boundary */
TRACE_NONDET_TAKE("net_dial");
- if (port < 0 || port > 65535) TRACE_NONDET_RECORD("net_dial", make_null());
char portstr[8];
- snprintf(portstr, sizeof(portstr), "%d", port);
+ snprintf(portstr, sizeof(portstr), "%d", (int)port);
struct addrinfo hints = {0}, *res = NULL;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
@@ -277,9 +287,9 @@ Value* builtin_net_dial(Value *arg) {
TRACE_NONDET_RECORD("net_dial", make_null());
}
fcntl(fd, F_SETFL, flags);
- double id = net_register_sock(fd, NET_SOCK_CONN);
+ int64_t id = net_register_sock(fd, NET_SOCK_CONN);
if (id < 0) TRACE_NONDET_RECORD("net_dial", make_null());
- TRACE_NONDET_RECORD("net_dial", make_num(id));
+ TRACE_NONDET_RECORD("net_dial", make_int(id));
}
/* net_recv of [conn_id, max_bytes]
@@ -291,8 +301,10 @@ Value* builtin_net_dial(Value *arg) {
Value* builtin_net_recv(Value *arg) {
if (!net_require_list(arg, 2, 3, "net_recv")) return make_null();
Value *conn = list_get_view(arg, 0, &(EigsView){{0}});
- int max = net_num_at(arg, 1, 0);
- int timeout_ms = net_num_at(arg, 2, -1);
+ int64_t handle = eigs_int_arg(conn, "net_recv");
+ if (g_has_error) return make_null();
+ int max = net_count_at(arg, 1, 0, "net_recv");
+ int timeout_ms = net_timeout_at(arg, 2, "net_recv");
if (g_has_error) return make_null(); /* #1637 */
/* A zero-byte recv() returns 0 — indistinguishable from EOF — so a
* senseless max is a shape error, decided before the tape boundary. */
@@ -301,7 +313,7 @@ Value* builtin_net_recv(Value *arg) {
return make_null();
}
TRACE_NONDET_TAKE("net_recv");
- EigsNetSock *s = net_lookup(conn, NET_SOCK_CONN);
+ EigsNetSock *s = net_lookup(handle, NET_SOCK_CONN);
if (!s) TRACE_NONDET_RECORD("net_recv", make_null());
if (max > NET_RECV_MAX) max = NET_RECV_MAX;
if (!net_wait_readable(s->fd, timeout_ms))
@@ -321,18 +333,18 @@ Value* builtin_net_recv(Value *arg) {
* for proc_write). */
/* Borrow the already converted bytes. The caller owns any workspace across
* the early replay return as well as every live send outcome. */
-static Value* net_send_bytes(Value *conn, const unsigned char *bytes, size_t len) {
+static Value* net_send_bytes(int64_t handle, const unsigned char *bytes, size_t len) {
TRACE_NONDET_TAKE("net_send");
- EigsNetSock *s = net_lookup(conn, NET_SOCK_CONN);
- if (!s) TRACE_NONDET_RECORD("net_send", make_num(-1));
+ EigsNetSock *s = net_lookup(handle, NET_SOCK_CONN);
+ if (!s) TRACE_NONDET_RECORD("net_send", make_int(-1));
size_t sent = 0;
while (sent < len) {
ssize_t n = send(s->fd, bytes + sent, len - sent, MSG_NOSIGNAL);
if (n < 0 && errno == EINTR) continue;
- if (n <= 0) TRACE_NONDET_RECORD("net_send", make_num(-1));
+ if (n <= 0) TRACE_NONDET_RECORD("net_send", make_int(-1));
sent += (size_t)n;
}
- TRACE_NONDET_RECORD("net_send", make_num((double)sent));
+ TRACE_NONDET_RECORD("net_send", make_int((int64_t)sent));
}
Value* builtin_net_send(Value *arg) {
@@ -344,6 +356,8 @@ Value* builtin_net_send(Value *arg) {
rt_error(EK_TYPE, 0, "net_send: data must be a string, buffer, or byte list");
return make_null();
}
+ int64_t handle = eigs_int_arg(conn, "net_send");
+ if (g_has_error) return make_null();
/* Read errors are deterministic argument errors: stop before the tape
* boundary in both capture and replay, before any socket work. */
const unsigned char *bytes;
@@ -357,17 +371,21 @@ Value* builtin_net_send(Value *arg) {
: data->data.list.count;
owned = xmalloc(n > 0 ? (size_t)n : 1);
for (int i = 0; i < n; i++) {
- double dv = data->type == VAL_BUFFER
- ? buffer_read_num(data, i)
- : eigs_slot_elem_num(list_slot(data, i), "net_send"); /* #1637 round 4 */
- if (g_has_error) { free(owned); return make_null(); }
- owned[i] = finite_num_to_byte(dv);
+ if (data->type == VAL_BUFFER) {
+ if (!buffer_byte_at(data, i, &owned[i], "net_send")) {
+ free(owned);
+ return make_null();
+ }
+ } else {
+ owned[i] = eigs_elem_byte(list_get_view(data, i, &(EigsView){{0}}), "net_send");
+ if (g_has_error) { free(owned); return make_null(); }
+ }
}
bytes = owned;
len = (size_t)n;
}
- Value *result = net_send_bytes(conn, bytes, len);
+ Value *result = net_send_bytes(handle, bytes, len);
free(owned);
return result;
}
@@ -376,13 +394,15 @@ Value* builtin_net_send(Value *arg) {
* idempotent bookkeeping, and under EIGS_REPLAY no handle exists so it
* is a natural no-op (the audio_capture_close shape). */
Value* builtin_net_close(Value *arg) {
- if (!arg || arg->type != VAL_NUM) {
+ if (!arg || !val_is_number(arg)) {
rt_error(EK_TYPE, 0, "net_close: expected a socket handle");
return make_null();
}
int nidx = 0;
uint32_t ngen = 0;
- EigsNetSock *s = net_unpack(eigs_num_arg(arg, __func__), &nidx, &ngen);
+ int64_t handle = eigs_int_arg(arg, "net_close");
+ if (g_has_error) return make_null();
+ EigsNetSock *s = net_unpack(handle, &nidx, &ngen);
if (s) {
close(s->fd);
free(s);
diff --git a/src/ext_store.c b/src/ext_store.c
index a5e6cf12b..ca7d4abf5 100644
--- a/src/ext_store.c
+++ b/src/ext_store.c
@@ -6,6 +6,7 @@
#include "ext_store_internal.h"
#include "builtins_internal.h"
#include "trace.h"
+#include
#if EIGENSCRIPT_FREESTANDING
/* The page store is file-backed end to end — carved out of the
@@ -154,14 +155,14 @@ static int store_nonfinite_sentinel(const char *s, double *out) {
return 0;
}
-/* Buffer elements use the 17-significant-digit form, as trace.c's buffer tape
- * encoding does: %.17g is the shortest width that round-trips a double
- * bit-exactly, and a buffer holding samples or weights would otherwise come
- * back subtly altered. (VAL_NUM's own %d/%.15g encoding is untouched.) */
+/* N6/N14: all finite record numbers share the one shortest writer.
+ * The existing non-finite buffer sentinels stay strings until N13. */
static void store_json_encode_buf_elem(strbuf *out, double d) {
if (isnan(d)) { strbuf_append(out, "\"nan\""); return; }
if (isinf(d)) { strbuf_append(out, d < 0 ? "\"-inf\"" : "\"inf\""); return; }
- strbuf_append_fmt(out, "%.17g", d);
+ char nb[32];
+ eigs_num_text(nb, sizeof nb, d);
+ strbuf_append(out, nb);
}
static void store_json_encode(Value *v, strbuf *out);
@@ -171,12 +172,20 @@ static void store_json_encode(Value *v, strbuf *out);
* inline number; format it through the same branch and format strings used by
* the historical boxed-number path so persisted bytes remain identical. */
static void store_json_encode_slot(EigsSlot slot, strbuf *out) {
- if (slot_is_num(slot)) {
- double n = eigs_slot_num(slot, __func__);
- if (fabs(n) < 2147483648.0 && n == (int)n)
- strbuf_append_fmt(out, "%d", (int)n);
- else
- strbuf_append_fmt(out, "%.15g", n);
+ if (slot_is_f64(slot)) {
+ /* This is persisted float data, not a finite builtin argument.
+ * Keep the existing NaN/inf spelling until N13 (R2.3). */
+ double n = SLOT_F64_RAW(slot);
+ char nb[32];
+ eigs_num_text(nb, sizeof nb, n);
+ strbuf_append(out, nb);
+ return;
+ }
+ if (slot_is_smi(slot)) {
+ /* #1758: an int is written as its exact digits (design 7). */
+ char nb[24];
+ eigs_int_text(nb, sizeof nb, slot_smi_val(slot));
+ strbuf_append(out, nb);
return;
}
store_json_encode(eigs_slot_ref(slot), out);
@@ -188,18 +197,19 @@ static void store_json_encode(Value *v, strbuf *out) {
return;
}
switch (v->type) {
- case VAL_NUM: {
- double n = eigs_num_arg(v, __func__);
- /* #816: magnitude BEFORE the narrowing cast (same class as
- * #695) — converting a double beyond int's range is UB, and
- * the old `n == (int)n && fabs(n) < 1e15` order ran the cast
- * first. The bound is int's own range: integral values beyond
- * it never took the %d path anyway (the equality failed), so
- * output is unchanged and the cast is now always defined. */
- if (fabs(n) < 2147483648.0 && n == (int)n)
- strbuf_append_fmt(out, "%d", (int)n);
- else
- strbuf_append_fmt(out, "%.15g", n);
+ case VAL_FLOAT: {
+ /* The switch proves VAL_FLOAT; serialization also accepts the
+ * historical non-finite payloads until N13 (R2.3). */
+ double n = VAL_F64_RAW(v);
+ char nb[32];
+ eigs_num_text(nb, sizeof nb, n);
+ strbuf_append(out, nb);
+ break;
+ }
+ case VAL_INT: { /* #1758 R1.2a: an int encodes as its exact digits */
+ char nb[24];
+ eigs_int_text(nb, sizeof(nb), VAL_I64_RAW(v));
+ strbuf_append(out, nb);
break;
}
case VAL_STR: {
@@ -319,11 +329,23 @@ static Value* store_json_parse_string(const char *s, int *pos) {
}
static Value* store_json_parse_number(const char *s, int *pos) {
+ const char *start = s + *pos;
char *end = NULL;
- double val = strtod(s + *pos, &end);
- if (end == s + *pos) return NULL; /* no digits consumed */
+ double val = strtod(start, &end);
+ if (end == start) return NULL; /* no digits consumed */
*pos = (int)(end - s);
- return make_num(val);
+
+ /* #1758 N14: decimal integer text within int64 decodes exactly. A
+ * decimal point or exponent keeps the float kind even for a whole value.
+ * Keep strtod's existing token span and float fallback (including the
+ * guard in make_float); float text itself changes in R1.7, not here. */
+ char *int_end = NULL;
+ errno = 0;
+ long long integer = strtoll(start, &int_end, 10);
+ if (int_end == end && errno != ERANGE &&
+ integer >= INT64_MIN && integer <= INT64_MAX)
+ return make_int((int64_t)integer);
+ return make_float(val);
}
static Value* store_json_parse_array(const char *s, int *pos) {
@@ -359,21 +381,21 @@ static Value* store_buffer_from_tag(Value *dict) {
Value *rv = list_get_view(shape, 0, &(EigsView){{0}});
Value *cv = list_get_view(shape, 1, &(EigsView){{0}});
- if (!rv || rv->type != VAL_NUM || !cv || cv->type != VAL_NUM) return NULL;
-
- /* Range-gate BEFORE narrowing. A stored shape is whatever double the file
- * holds, and (int)d is undefined when d is outside int's range (C11
- * 6.3.1.4p1) — reachable here with e.g. [1e300, 0]. The negated form also
- * rejects a NaN, which fails every comparison. Only once the value is
- * known in-range is the exact `==` below meaningful: it is an INTEGRALITY
- * test (is this dimension a whole number?), so exact equality is the
- * correct operator and a tolerance would be the bug — 2.0000000000000004
- * is not a row count. This also establishes rows/cols >= 0. */
- double rd = eigs_num_arg(rv, __func__), cd = eigs_num_arg(cv, __func__);
- if (!(rd >= 0 && rd <= (double)INT_MAX)) return NULL;
- if (!(cd >= 0 && cd <= (double)INT_MAX)) return NULL;
- int rows = (int)rd, cols = (int)cd;
- if (rd != (double)rows || cd != (double)cols) return NULL;
+ if (!rv || !val_is_number(rv) || !cv || !val_is_number(cv)) return NULL;
+
+ /* This is an untrusted codec tag, so an invalid dimension leaves it a
+ * user dict, as before. Read integer text without passing through double;
+ * float dimensions must convert exactly before the C int range check. */
+ EigsNum rn = eigs_number_arg(rv, __func__);
+ EigsNum cn = eigs_number_arg(cv, __func__);
+ if (g_has_error) return NULL;
+ int64_t ri, ci;
+ if (rn.is_int) ri = rn.value.i64;
+ else if (!num_to_i64_exact(rn.value.f64, &ri)) return NULL;
+ if (cn.is_int) ci = cn.value.i64;
+ else if (!num_to_i64_exact(cn.value.f64, &ci)) return NULL;
+ if (ri < 0 || ri > INT_MAX || ci < 0 || ci > INT_MAX) return NULL;
+ int rows = (int)ri, cols = (int)ci;
/* Accept exactly the (rows, cols, count) triples the buffer constructors
* can produce, no more. rows>0 is a real 2-D shape, so rows*cols must be
@@ -389,7 +411,7 @@ static Value* store_buffer_from_tag(Value *dict) {
for (int i = 0; i < count; i++) {
Value *e = list_get_view(body, i, &(EigsView){{0}});
if (!e) return NULL;
- if (e->type == VAL_NUM) continue;
+ if (val_is_number(e)) continue;
if (e->type == VAL_STR && store_nonfinite_sentinel(e->data.str, NULL)) continue;
return NULL;
}
@@ -404,7 +426,7 @@ static Value* store_buffer_from_tag(Value *dict) {
for (int i = 0; i < count; i++) {
Value *e = list_get_view(body, i, &(EigsView){{0}});
double d = 0;
- if (e->type == VAL_NUM) d = eigs_num_arg(e, __func__);
+ if (val_is_number(e)) d = eigs_float_arg(e, __func__);
else store_nonfinite_sentinel(e->data.str, &d);
buf->data.buffer.data[i] = d;
}
@@ -742,12 +764,17 @@ static int store_load_catalog(Store *store) {
}
Value *rv = store_dict_get_view(col, "root", &(EigsView){{0}});
Value *nv = store_dict_get_view(col, "next_id", &(EigsView){{0}});
- if (!rv || rv->type != VAL_NUM || !nv || nv->type != VAL_NUM) {
+ if (!rv || !val_is_number(rv) || !nv || !val_is_number(nv)) {
fprintf(stderr, "store_open: collection '%s' missing root or next_id\n", name);
free(visited);
return -1;
}
- uint32_t root = (uint32_t)eigs_num_arg(rv, __func__);
+ int64_t root_id;
+ if (!eigs_int_arg_range(rv, 0, UINT32_MAX, &root_id, "store_open")) {
+ free(visited);
+ return -1;
+ }
+ uint32_t root = (uint32_t)root_id;
if (root == 0 || root >= store->page_count) {
fprintf(stderr, "store_open: collection '%s' has invalid root %u\n", name, root);
free(visited);
@@ -809,15 +836,21 @@ static int store_load_catalog(Store *store) {
* store's slot; a forged handle (tests/test_handle_forge.eigs) carries no
* generation and reads as gen 0, which no live slot ever has. `out_id` is set
* even on failure so the refusal can name the slot. */
-static Store* get_store_why(Value *v, int *why, int *out_id) {
+/* R1.6f-D1: this reader owns all nine store_arg callers, including the
+ * already-b key/record operations. Refuse numeric fields before handle lookup. */
+static Store* get_store_why(Value *v, int *why, int *out_id, const char *who) {
*why = HANDLE_CLAIM_BADVALUE;
*out_id = 0;
if (!v || v->type != VAL_DICT) return NULL;
Value *sv = store_dict_get_view(v, "_store_id", &(EigsView){{0}});
- if (!sv || sv->type != VAL_NUM) return NULL;
- *out_id = (int)eigs_num_arg(sv, __func__);
+ if (!sv || !val_is_number(sv)) return NULL;
+ int64_t id, generation = 0;
+ if (!eigs_int_arg_range(sv, INT_MIN, INT_MAX, &id, who)) return NULL;
+ *out_id = (int)id;
Value *gv = store_dict_get_view(v, "_store_gen", &(EigsView){{0}});
- uint32_t gen = (gv && gv->type == VAL_NUM) ? (uint32_t)eigs_num_arg(gv, __func__) : 0;
+ if (gv && val_is_number(gv)
+ && !eigs_int_arg_range(gv, 0, UINT32_MAX, &generation, who)) return NULL;
+ uint32_t gen = (uint32_t)generation;
return (Store*)handle_lookup(*out_id, gen, HANDLE_STORE, why);
}
@@ -831,8 +864,8 @@ static Store* get_store_why(Value *v, int *why, int *out_id) {
* tenth cannot re-open the hole by forgetting to check. */
static Store* store_arg(Value *v, const char *who) {
int why = 0, id = 0;
- Store *st = get_store_why(v, &why, &id);
- if (!st) handle_raise_unresolved(who, "store", id, why, "closed");
+ Store *st = get_store_why(v, &why, &id, who);
+ if (!st && !g_has_error) handle_raise_unresolved(who, "store", id, why, "closed");
return st;
}
@@ -955,8 +988,8 @@ static Value* builtin_store_open(Value *arg) {
return make_null();
}
Value *handle = make_dict(8);
- dict_set_owned(handle, "_store_id", make_num((double)hid));
- dict_set_owned(handle, "_store_gen", make_num((double)sgen));
+ dict_set_owned(handle, "_store_id", make_int(hid));
+ dict_set_owned(handle, "_store_gen", make_int(sgen));
dict_set_owned(handle, "_type", make_str("eigenstore"));
return handle;
}
@@ -969,10 +1002,12 @@ static Value* builtin_store_close(Value *arg) {
/* Release handle BEFORE freeing memory to prevent use-after-free
* if another thread calls get_store() concurrently. */
Value *id_val = (arg && arg->type == VAL_DICT) ? store_dict_get_view(arg, "_store_id", &(EigsView){{0}}) : NULL;
- int hid = (id_val && id_val->type == VAL_NUM) ? (int)eigs_num_arg(id_val, __func__) : -1;
+ int64_t hid;
+ if (!eigs_int_arg_range(id_val, INT_MIN, INT_MAX, &hid, "store_close")) return make_null();
Value *g_val = (arg && arg->type == VAL_DICT) ? store_dict_get_view(arg, "_store_gen", &(EigsView){{0}}) : NULL;
- uint32_t hgen = (g_val && g_val->type == VAL_NUM) ? (uint32_t)eigs_num_arg(g_val, __func__) : 0;
- handle_release(hid, hgen);
+ int64_t hgen;
+ if (!eigs_int_arg_range(g_val, 0, UINT32_MAX, &hgen, "store_close")) return make_null();
+ handle_release((int)hid, (uint32_t)hgen);
store->dirty = 1; /* Force flush */
store_flush_catalog(store);
fclose(store->fp);
@@ -1017,8 +1052,8 @@ static Value* builtin_store_put(Value *arg) {
root_page = store_alloc_page(store);
next_id = 1;
col_info = make_dict(4);
- dict_set_owned(col_info, "root", make_num(root_page));
- dict_set_owned(col_info, "next_id", make_num(next_id));
+ dict_set_owned(col_info, "root", make_int(root_page));
+ dict_set_owned(col_info, "next_id", make_int(next_id));
dict_set_owned(store->catalog, collection, col_info); /* adopt make_dict ref */
store->dirty = 1;
@@ -1032,8 +1067,13 @@ static Value* builtin_store_put(Value *arg) {
} else {
Value *rv = store_dict_get_view(col_info, "root", &(EigsView){{0}});
Value *nv = store_dict_get_view(col_info, "next_id", &(EigsView){{0}});
- root_page = (uint32_t)(rv ? eigs_num_arg(rv, __func__) : 0);
- next_id = (int)(nv ? eigs_num_arg(nv, __func__) : 1);
+ int64_t root = 0, next = 1;
+ if (rv && !eigs_int_arg_range(rv, 0, UINT32_MAX, &root, "store_put"))
+ return make_null();
+ if (nv && !eigs_int_arg_range(nv, INT_MIN, INT_MAX, &next, "store_put"))
+ return make_null();
+ root_page = (uint32_t)root;
+ next_id = (int)next;
}
/* Determine key */
@@ -1045,7 +1085,7 @@ static Value* builtin_store_put(Value *arg) {
} else {
snprintf(key_buf, STORE_MAX_KEY_LEN, "%d", next_id);
next_id++;
- dict_set_owned(col_info, "next_id", make_num(next_id));
+ dict_set_owned(col_info, "next_id", make_int(next_id));
store->dirty = 1;
}
@@ -1169,8 +1209,10 @@ static Value* builtin_store_get(Value *arg) {
if (key_val->type == VAL_STR) {
strncpy(key_buf, key_val->data.str, STORE_MAX_KEY_LEN - 1);
key_buf[STORE_MAX_KEY_LEN - 1] = '\0';
- } else if (key_val->type == VAL_NUM) {
- snprintf(key_buf, STORE_MAX_KEY_LEN, "%d", (int)eigs_num_arg(key_val, __func__));
+ } else if (val_is_number(key_val)) {
+ int64_t key = eigs_int_arg(key_val, "store_get");
+ if (g_has_error) return make_null();
+ eigs_int_text(key_buf, sizeof(key_buf), key);
} else {
return make_null();
}
@@ -1179,7 +1221,9 @@ static Value* builtin_store_get(Value *arg) {
if (!col_info || col_info->type != VAL_DICT) return make_null();
Value *rv = store_dict_get_view(col_info, "root", &(EigsView){{0}});
if (!rv) return make_null();
- uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__);
+ int64_t root;
+ if (!eigs_int_arg_range(rv, 0, UINT32_MAX, &root, "store_get")) return make_null();
+ uint32_t pg = (uint32_t)root;
size_t target_key_len = strlen(key_buf);
@@ -1237,7 +1281,9 @@ static int store_locate(Store *store, const char *collection,
if (!col_info || col_info->type != VAL_DICT) return 0;
Value *rv = store_dict_get_view(col_info, "root", &(EigsView){{0}});
if (!rv) return 0;
- uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__);
+ int64_t root;
+ if (!eigs_int_arg_range(rv, 0, UINT32_MAX, &root, "store_locate")) return 0;
+ uint32_t pg = (uint32_t)root;
size_t target_key_len = strlen(key_buf);
for (int _hops = 0; pg != 0 && _hops < STORE_MAX_PAGE_HOPS; _hops++) {
@@ -1333,8 +1379,10 @@ static Value* builtin_store_delete(Value *arg) {
if (key_val->type == VAL_STR) {
strncpy(key_buf, key_val->data.str, STORE_MAX_KEY_LEN - 1);
key_buf[STORE_MAX_KEY_LEN - 1] = '\0';
- } else if (key_val->type == VAL_NUM) {
- snprintf(key_buf, STORE_MAX_KEY_LEN, "%d", (int)eigs_num_arg(key_val, __func__));
+ } else if (val_is_number(key_val)) {
+ int64_t key = eigs_int_arg(key_val, "store_delete");
+ if (g_has_error) return make_bool(0);
+ eigs_int_text(key_buf, sizeof(key_buf), key);
} else {
/* Reaching the else arm IS the rejection: the key is neither a string
* nor a number. Guarded in place with a constant condition so the
@@ -1380,7 +1428,9 @@ static Value* builtin_store_query(Value *arg) {
if (!col_info || col_info->type != VAL_DICT) return make_list(0);
Value *rv = store_dict_get_view(col_info, "root", &(EigsView){{0}});
if (!rv) return make_list(0);
- uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__);
+ int64_t root;
+ if (!eigs_int_arg_range(rv, 0, UINT32_MAX, &root, "store_query")) return make_list(0);
+ uint32_t pg = (uint32_t)root;
Value *results = make_list(16);
@@ -1415,7 +1465,7 @@ static Value* builtin_store_count(Value *arg) {
/* fs:CHANNEL the arity/type failure is already signalled by the
* unconditional rt_error above (EK_TYPE latch, catchable — asserted by
* CV2-71); this return is the post-raise placeholder, not a count. */
- return make_num(0);
+ return make_int(0);
}
Store *store = store_arg(list_get_view(arg, 0, &(EigsView){{0}}), "store_count");
/* fs:CHANNEL store_arg is resolve-OR-RAISE (#1146): it has already
@@ -1423,24 +1473,26 @@ static Value* builtin_store_count(Value *arg) {
* than unwinding, so this return is the post-raise placeholder, not a
* count of 0. Distinct from the two documented-answer zeros below, which
* are store_count's real result for an unknown collection. */
- if (!store) return make_num(0);
- if (replay_blocks("store_count", "store")) return make_num(0);
+ if (!store) return make_int(0);
+ if (replay_blocks("store_count", "store")) return make_int(0);
Value *col_val = list_get_view(arg, 1, &(EigsView){{0}});
ARG_GUARD(!col_val || col_val->type != VAL_STR,
- "store_count", "[store handle, string collection]", make_num(0));
+ "store_count", "[store handle, string collection]", make_int(0));
const char *collection = col_val->data.str;
Value *col_info = store_dict_get_view(store->catalog, collection, &(EigsView){{0}});
/* fs:ANSWER a collection that is not in the catalog holds no records, so
* its count is 0 — the documented result for an unknown collection. */
- if (!col_info || col_info->type != VAL_DICT) return make_num(0);
+ if (!col_info || col_info->type != VAL_DICT) return make_int(0);
Value *rv = store_dict_get_view(col_info, "root", &(EigsView){{0}});
/* fs:ANSWER a catalog entry with no root page has no record pages, so the
* count is 0. */
- if (!rv) return make_num(0);
- uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__);
+ if (!rv) return make_int(0);
+ int64_t page_id;
+ if (!eigs_int_arg_range(rv, 0, UINT32_MAX, &page_id, "store_count")) return make_null();
+ uint32_t pg = (uint32_t)page_id;
- int count = 0;
+ int64_t count = 0;
for (int _hops = 0; pg != 0 && _hops < STORE_MAX_PAGE_HOPS; _hops++) {
Page page;
if (store_read_page(store, pg, &page) != 0) break;
@@ -1449,13 +1501,13 @@ static Value* builtin_store_count(Value *arg) {
int offset = 0;
for (int i = 0; i < (int)page.count; i++) {
StoreRecord rec;
- if (store_record_next(&page, &offset, &rec) != 0) return make_num(count);
+ if (store_record_next(&page, &offset, &rec) != 0) return make_int(count);
if (rec.key_len == 0) continue; /* deleted */
count++;
}
pg = page.next_page; if (pg >= store->page_count) break;
}
- return make_num(count);
+ return make_int(count);
}
/* store_update([handle, collection, key, record]) -> 1 or 0 */
@@ -1489,8 +1541,10 @@ static Value* builtin_store_update(Value *arg) {
if (key_val->type == VAL_STR) {
strncpy(key_buf, key_val->data.str, STORE_MAX_KEY_LEN - 1);
key_buf[STORE_MAX_KEY_LEN - 1] = '\0';
- } else if (key_val->type == VAL_NUM) {
- snprintf(key_buf, STORE_MAX_KEY_LEN, "%d", (int)eigs_num_arg(key_val, __func__));
+ } else if (val_is_number(key_val)) {
+ int64_t key = eigs_int_arg(key_val, "store_update");
+ if (g_has_error) return make_bool(0);
+ eigs_int_text(key_buf, sizeof(key_buf), key);
} else {
/* Reaching the else arm IS the rejection: the key is neither a string
* nor a number. Guarded in place with a constant condition so the
@@ -1631,7 +1685,9 @@ static Value* builtin_store_drop(Value *arg) {
/* Mark all pages in chain as free */
Value *rv = store_dict_get_view(col_info, "root", &(EigsView){{0}});
if (rv) {
- uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__);
+ int64_t page_id;
+ if (!eigs_int_arg_range(rv, 0, UINT32_MAX, &page_id, "store_drop")) return make_null();
+ uint32_t pg = (uint32_t)page_id;
for (int _hops = 0; pg != 0 && _hops < STORE_MAX_PAGE_HOPS; _hops++) {
Page page;
if (store_read_page(store, pg, &page) != 0) break;
diff --git a/src/freestanding/mini_fmt.c b/src/freestanding/mini_fmt.c
index e369699a5..5de34b6b2 100644
--- a/src/freestanding/mini_fmt.c
+++ b/src/freestanding/mini_fmt.c
@@ -258,6 +258,8 @@ static void fmt_f(Sink *s, const Spec *sp, const BigDec *b) {
emit_padded(s, &sp2, sign, "", body, n, 0);
}
+/* N6 uses this correctly-rounded %e operation to select shortest decimal
+ * candidates. Keep printf semantics here; eigs_num_text owns language text. */
static void fmt_e(Sink *s, const Spec *sp, const BigDec *b, int upper) {
int P = (sp->prec >= 0) ? sp->prec : 6;
int is_zero;
diff --git a/src/jit.c b/src/jit.c
index 6913a5af2..71f54976f 100644
--- a/src/jit.c
+++ b/src/jit.c
@@ -745,8 +745,7 @@ static int jit_supported_prefix(const struct EigsChunk *chunk,
} else if (op == OP_DUP2) {
i += 1; ops++; non_line_ops++;
} else if (op == OP_ADD || op == OP_SUB ||
- op == OP_MUL || op == OP_DIV ||
- op == OP_MOD) {
+ op == OP_MUL || op == OP_DIV) {
i += 1; ops++; non_line_ops++;
*has_bail_op = 1;
/* Stage 5e: the tracked-operand loaders + commit decrefs
@@ -761,13 +760,7 @@ static int jit_supported_prefix(const struct EigsChunk *chunk,
*extra_size += 320; /* Stage 5e loaders + decrefs */
/* #1637: the result is the false/true TAG_BOOL immediate, so
* OP_POP after a comparison remains a no-op peephole. */
- } else if (op == OP_BAND || op == OP_BOR || op == OP_BXOR ||
- op == OP_SHL || op == OP_SHR) {
- i += 1; ops++; non_line_ops++;
- *has_bail_op = 1;
- /* Result is a double (whole-number-valued), so a subsequent
- * OP_POP stays a no-op peephole. */
- } else if (op == OP_NEG || op == OP_NOT || op == OP_BNOT) {
+ } else if (op == OP_NEG || op == OP_NOT) {
i += 1; ops++; non_line_ops++;
*has_bail_op = 1;
} else if (op == OP_JUMP || op == OP_JUMP_BACK) {
@@ -1737,18 +1730,6 @@ static uint8_t *emit_cvttsd2si_xmm0_r8d(uint8_t *w) {
static uint8_t *emit_cvtsi2sd_r8d_xmm1(uint8_t *w) {
*w++ = 0xF2; *w++ = 0x41; *w++ = 0x0F; *w++ = 0x2A; *w++ = 0xC8; return w;
}
-/* cvttsd2si %xmm0, %edx (4 bytes) — 32-bit, mirrors `(int)val_s.d`.
- * No longer used by OP_INDEX_SET (buffer stores keep the raw double now)
- * but kept for future int-conversion fast paths. */
-__attribute__((unused))
-static uint8_t *emit_cvttsd2si_xmm0_edx(uint8_t *w) {
- *w++ = 0xF2; *w++ = 0x0F; *w++ = 0x2C; *w++ = 0xD0; return w;
-}
-/* cvtsi2sd %edx, %xmm0 (4 bytes) — 32-bit source. */
-__attribute__((unused))
-static uint8_t *emit_cvtsi2sd_edx_xmm0(uint8_t *w) {
- *w++ = 0xF2; *w++ = 0x0F; *w++ = 0x2A; *w++ = 0xC2; return w;
-}
/* jp rel32 (6 bytes): 0F 8A — jump if PF=1 (unordered ucomisd). */
static uint8_t *emit_jp_rel32(uint8_t *w, uint8_t **patch) {
*w++ = 0x0F; *w++ = 0x8A; *patch = w;
@@ -2357,9 +2338,9 @@ static uint8_t *emit_mov_disp32_rdi_to_rdx(uint8_t *w, int32_t disp) {
/* Stage 5e: numeric-operand loader for the arith/compare templates.
*
- * Accepts what the interpreter's slot_as_double accepts MINUS the
+ * Accepts what the interpreter's slot_f64_value accepts MINUS the
* NUM_REUSE candidates: an immediate double, or a heap/tracked pointer
- * to a VAL_NUM with refcount >= 2 (refcount 1 would make the
+ * to a VAL_FLOAT with refcount >= 2 (refcount 1 would make the
* interpreter's NUM_REUSE in-place branch fire, so it routes to the
* interpreter to keep observer-value identity semantics exact).
*
@@ -2383,17 +2364,17 @@ static uint8_t *emit_load_numeric_operand(uint8_t *w, int is_b,
/* Tag 0xFFFD+ (reserved/sentinel): not a pointer we may chase. */
w = emit_cmp_imm32_esi(w, 0xFFFD);
w = emit_jae_rel32(w, &lp[*lp_n]); (*lp_n)++;
- /* Heap or tracked pointer: must be a non-REUSE VAL_NUM. */
+ /* Heap or tracked pointer: must be a non-REUSE VAL_FLOAT. */
w = is_b ? emit_mov_rax_rdi(w) : emit_mov_rdx_rdi(w);
w = emit_shl_16_rdi(w);
w = emit_shr_16_rdi(w);
w = emit_cmpl_imm32_disp32_rdi(w, (int32_t)offsetof(Value, type),
- (uint32_t)VAL_NUM);
+ (uint32_t)VAL_FLOAT);
w = emit_jne_rel32(w, &lp[*lp_n]); (*lp_n)++;
w = emit_cmpl_imm32_disp32_rdi(w, (int32_t)offsetof(Value, refcount), 2);
w = emit_jl_rel32(w, &lp[*lp_n]); (*lp_n)++;
- w = is_b ? emit_mov_disp32_rdi_to_rax(w, (int32_t)VAL_NUM_OFFSET)
- : emit_mov_disp32_rdi_to_rdx(w, (int32_t)VAL_NUM_OFFSET);
+ w = is_b ? emit_mov_disp32_rdi_to_rax(w, (int32_t)VAL_F64_OFFSET)
+ : emit_mov_disp32_rdi_to_rdx(w, (int32_t)VAL_F64_OFFSET);
/* .imm: bits (immediate or loaded data.num) → xmm. */
{
int rel = (int)(w - imm_after);
@@ -2847,10 +2828,10 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk,
Value *v = chunk->constants[idx];
uint64_t bits;
int needs_incref = 0;
- if (v->type == VAL_NUM) {
+ if (v->type == VAL_FLOAT) {
/* Immediate-num: same shape as NUM_ZERO. (#262 Step E: a
* constant num never carries observer state.) */
- memcpy(&bits, &VAL_NUM_RAW(v), 8);
+ memcpy(&bits, &VAL_F64_RAW(v), 8);
} else {
/* Heap slot: TAG_HEAP | payload. */
bits = 0xFFFB000000000000ULL |
@@ -3438,7 +3419,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk,
} else if (op == OP_SET_LOCAL) {
/* Stage 5e: mirror CASE(SET_LOCAL) exactly, including its
* in-place branch — writing an immediate into a slot whose
- * existing Value is an exclusive non-observed VAL_NUM
+ * existing Value is an exclusive non-observed VAL_FLOAT
* rewrites data.num instead of swapping pointers. This is
* load-bearing beyond speed: the observer's g_last_observer
* can point at that Value, and the swap path would free it
@@ -3447,9 +3428,9 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk,
* tos imm? no → .swap
* old slot ptr? no → .plain (imm/sentinel old:
* incref+decref both no-op)
- * old is VAL_NUM && rc==1 && obs_age==0?
+ * old is VAL_FLOAT && rc==1 && obs_age==0?
* no → .swap
- * in-place: VAL_NUM_RAW(old) = tos bits; → .done
+ * in-place: VAL_F64_RAW(old) = tos bits; → .done
* .plain: values[slot] = tos; → .done
* .swap: values[slot] = tos; incref tos; decref old
* .done: */
@@ -3490,13 +3471,13 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk,
w = emit_shl_16_rdi(w);
w = emit_shr_16_rdi(w);
w = emit_cmpl_imm32_disp32_rdi(w, (int32_t)offsetof(Value, type),
- (uint32_t)VAL_NUM);
+ (uint32_t)VAL_FLOAT);
w = emit_jne_rel32(w, &swap_p[swap_n]); swap_n++;
w = emit_cmpl_imm32_disp32_rdi(w, (int32_t)offsetof(Value, refcount), 1);
w = emit_jne_rel32(w, &swap_p[swap_n]); swap_n++;
/* #262 Step E: obs_age guard removed — nums are never tracked. */
- /* in-place: VAL_NUM_RAW(existing) = tos bits. */
- w = emit_mov_rax_to_disp32_rdi(w, (int32_t)VAL_NUM_OFFSET);
+ /* in-place: VAL_F64_RAW(existing) = tos bits. */
+ w = emit_mov_rax_to_disp32_rdi(w, (int32_t)VAL_F64_OFFSET);
w = emit_jmp_rel32(w, &done_p[done_n]); done_n++;
/* .plain: store only (both refcount sides are no-ops). */
for (int k = 0; k < plain_n; k++) patch_rel32(plain_p[k], w);
@@ -3563,7 +3544,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk,
if (ab) {
JIT_BAIL_AND_RETURN();
}
- /* Divisor zero check. The VM compares `VAL_NUM_RAW(b) != 0.0`,
+ /* Divisor zero check. The VM compares `VAL_F64_RAW(b) != 0.0`,
* which under IEEE-754 treats +0.0 and -0.0 as equal — so
* we must also bail when b == -0.0 (bits = 0x8000…0).
* Strategy: copy b's bits to %rsi (dead scratch after the
@@ -3614,7 +3595,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk,
} else if (op == OP_ADD || op == OP_SUB ||
op == OP_MUL || op == OP_DIV) {
/* Stage 5e: operands may be immediates OR heap/tracked
- * VAL_NUM pointers with refcount >= 2 (the loader bails
+ * VAL_FLOAT pointers with refcount >= 2 (the loader bails
* the rc==1 case so the interpreter's NUM_REUSE branch
* keeps its in-place semantics). All guard failures route
* through one per-op trampoline so the epilogue patch
@@ -3679,7 +3660,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk,
i += 1;
} else if (op == OP_EQ || op == OP_NE || op == OP_LT ||
op == OP_GT || op == OP_LE || op == OP_GE) {
- /* Stage 5e: tracked/heap VAL_NUM operands accepted via the
+ /* Stage 5e: tracked/heap VAL_FLOAT operands accepted via the
* shared loader (rc==1 bails — NUM_CMP/EQ/NE all have the
* same NUM_REUSE branch as ARITH_FAST). One trampoline
* slot. NaN inputs would give incorrect comparison results
@@ -4461,7 +4442,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk,
uint16_t idx = (uint16_t)(chunk->code[op_start + 1] |
((uint16_t)chunk->code[op_start + 2] << 8));
Value *v = chunk->constants[idx];
- last_imm = (v && v->type == VAL_NUM);
+ last_imm = (v && v->type == VAL_FLOAT);
break;
}
case OP_LINE: case OP_BINARY_LINE: case OP_BINARY_LINE_END:
diff --git a/src/jit_smoke.c b/src/jit_smoke.c
index b6be2bb6b..c56fe2fe9 100644
--- a/src/jit_smoke.c
+++ b/src/jit_smoke.c
@@ -100,7 +100,7 @@ int jit_helper_local_idx_get(int slot, int idx) {
VM *vm = eigs_current->vm;
index_receipt.calls++;
if (slot != 0 || idx != 0 || vm->sp != 0) index_receipt.bad++;
- vm->stack[vm->sp++] = index_receipt.status ? slot_null() : slot_from_num(42);
+ vm->stack[vm->sp++] = index_receipt.status ? slot_null() : slot_from_f64(42);
return index_receipt.status;
}
@@ -135,22 +135,22 @@ int jit_helper_iter_next(void) {
VM *vm = eigs_current->vm;
reader_receipt.calls++;
if (reader_receipt.kind != 1 || vm->sp != 1 ||
- SLOT_NUM_RAW(vm->stack[0]) != 0 || vm->frames[0].ip != reader_receipt.ip ||
+ SLOT_F64_RAW(vm->stack[0]) != 0 || vm->frames[0].ip != reader_receipt.ip ||
vm->current_line != 71)
reader_receipt.bad++;
if (reader_receipt.status != 1)
- vm->stack[vm->sp++] = slot_from_num(reader_receipt.status ? 0 : 42);
+ vm->stack[vm->sp++] = slot_from_f64(reader_receipt.status ? 0 : 42);
return reader_receipt.status;
}
int jit_helper_index_get(void) {
VM *vm = eigs_current->vm;
reader_receipt.calls++;
if (reader_receipt.kind != 0 || vm->sp != 2 ||
- SLOT_NUM_RAW(vm->stack[0]) != 0 || SLOT_NUM_RAW(vm->stack[1]) != 1 ||
+ SLOT_F64_RAW(vm->stack[0]) != 0 || SLOT_F64_RAW(vm->stack[1]) != 1 ||
vm->frames[0].ip != reader_receipt.ip || vm->current_line != 71)
reader_receipt.bad++;
vm->sp -= 2;
- vm->stack[vm->sp++] = slot_from_num(reader_receipt.status ? 0 : 42);
+ vm->stack[vm->sp++] = slot_from_f64(reader_receipt.status ? 0 : 42);
return reader_receipt.status;
}
/* Remaining Stage 4q/4v helpers are linker-immediate stubs. */
@@ -171,11 +171,11 @@ static struct {
void jit_helper_set_name(struct EigsChunk *chunk, int idx) {
store_receipt.calls++;
if (chunk != store_receipt.chunk || idx != 0 || g_vm.sp != 3 ||
- SLOT_NUM_RAW(g_vm.stack[0]) != 42 || SLOT_NUM_RAW(g_vm.stack[1]) != 0 ||
- SLOT_NUM_RAW(g_vm.stack[2]) != 1 || g_vm.frames[0].ip != store_receipt.ip)
+ SLOT_F64_RAW(g_vm.stack[0]) != 42 || SLOT_F64_RAW(g_vm.stack[1]) != 0 ||
+ SLOT_F64_RAW(g_vm.stack[2]) != 1 || g_vm.frames[0].ip != store_receipt.ip)
store_receipt.bad = 1;
for (int i = 0; i < 3; i++) {
- if (SLOT_NUM_RAW(store_receipt.values[i]) != 10 + i ||
+ if (SLOT_F64_RAW(store_receipt.values[i]) != 10 + i ||
store_receipt.counts[i] != 17)
store_receipt.bad = 1;
}
@@ -328,7 +328,7 @@ static int run_store_cases(void) {
for (size_t c = 0; c < sizeof cases / sizeof cases[0]; c++) {
row = cases[c].name;
for (int i = 0; i < 3; i++) {
- values[i] = slot_from_num(10 + i);
+ values[i] = slot_from_f64(10 + i);
counts[i] = 17;
}
env[0].parent = cases[c].parent < 0 ? NULL : &env[cases[c].parent];
@@ -342,7 +342,7 @@ static int run_store_cases(void) {
ic.walk_depth = cases[c].depth;
ic.slot_idx = 0;
vm->sp = 1;
- vm->stack[0] = slot_from_num(42);
+ vm->stack[0] = slot_from_f64(42);
vm->stack[1] = vm->stack[2] = slot_null();
vm->frame_count = 1;
vm->frames[0].chunk = &chunk;
@@ -359,15 +359,15 @@ static int run_store_cases(void) {
STORE_ASSERT(store_receipt.calls == cases[c].helper);
STORE_ASSERT(store_receipt.bad == 0);
STORE_ASSERT(vm->sp == 3);
- STORE_ASSERT(SLOT_NUM_RAW(vm->stack[0]) == 42 && SLOT_NUM_RAW(vm->stack[1]) == 0);
- STORE_ASSERT(SLOT_NUM_RAW(vm->stack[2]) == 1);
+ STORE_ASSERT(SLOT_F64_RAW(vm->stack[0]) == 42 && SLOT_F64_RAW(vm->stack[1]) == 0);
+ STORE_ASSERT(SLOT_F64_RAW(vm->stack[2]) == 1);
STORE_ASSERT(vm->frames[0].ip == store_receipt.ip);
STORE_ASSERT((osr ? chunk.jit_osr[0].advance : chunk.jit_advance) == 5);
STORE_ASSERT((osr ? chunk.jit_advance : chunk.jit_osr[0].advance) == -99);
STORE_ASSERT(state->builtin_env == &env[2]);
for (int i = 0; i < 3; i++) {
int stored = !cases[c].helper && i == target;
- STORE_ASSERT(SLOT_NUM_RAW(values[i]) == (stored ? 1 : 10 + i));
+ STORE_ASSERT(SLOT_F64_RAW(values[i]) == (stored ? 1 : 10 + i));
STORE_ASSERT(counts[i] == 17 + stored);
}
helpers += store_receipt.calls;
@@ -443,7 +443,7 @@ static int run_index_bail_cases(void) {
advance == expected && index_receipt.calls == 1 &&
index_receipt.bad == 0 && vm->sp == (status ? 1 : 2) &&
(status ? slot_is_null(vm->stack[0]) :
- SLOT_NUM_RAW(vm->stack[0]) == 42 && SLOT_NUM_RAW(vm->stack[1]) == 1);
+ SLOT_F64_RAW(vm->stack[0]) == 42 && SLOT_F64_RAW(vm->stack[1]) == 1);
rows++;
if (!good) {
fprintf(stderr, "FAIL: index-bail %s status=%d r13=%" PRIx64
@@ -535,12 +535,12 @@ static int run_reader_bail_cases(void) {
int expected_sp = kind ? (status ? 2 : 4) : (status ? 1 : 2);
int expected_line = status ? 71 : 72;
int stack_ok = kind
- ? SLOT_NUM_RAW(vm->stack[0]) == 0 &&
- (status ? SLOT_NUM_RAW(vm->stack[1]) == 0 :
- SLOT_NUM_RAW(vm->stack[1]) == 42 && SLOT_NUM_RAW(vm->stack[2]) == 1 &&
- SLOT_NUM_RAW(vm->stack[3]) == 0)
- : (status ? SLOT_NUM_RAW(vm->stack[0]) == 0 :
- SLOT_NUM_RAW(vm->stack[0]) == 42 && SLOT_NUM_RAW(vm->stack[1]) == 1);
+ ? SLOT_F64_RAW(vm->stack[0]) == 0 &&
+ (status ? SLOT_F64_RAW(vm->stack[1]) == 0 :
+ SLOT_F64_RAW(vm->stack[1]) == 42 && SLOT_F64_RAW(vm->stack[2]) == 1 &&
+ SLOT_F64_RAW(vm->stack[3]) == 0)
+ : (status ? SLOT_F64_RAW(vm->stack[0]) == 0 :
+ SLOT_F64_RAW(vm->stack[0]) == 42 && SLOT_F64_RAW(vm->stack[1]) == 1);
int good = saved_r13 == UINT64_C(0x13579bdf2468ace0) &&
advance == expected && reader_receipt.calls == 1 &&
reader_receipt.bad == 0 && vm->sp == expected_sp && stack_ok &&
@@ -600,8 +600,8 @@ static int run_exit_cases(void) {
vm->owner = thread;
eigs_current = thread;
state->jit_entry_threshold = state->jit_iter_threshold = 1;
- Value limit = {.type = VAL_NUM};
- VAL_NUM_RAW(&limit) = 4;
+ Value limit = {.type = VAL_FLOAT};
+ VAL_F64_RAW(&limit) = 4;
Value *constants[] = {&limit};
uint8_t code[] = {OP_NULL, OP_POP, OP_NULL, OP_POP,
OP_NUM_ONE, OP_ADD, OP_DUP, OP_CONST, 0, 0,
@@ -627,7 +627,7 @@ static int run_exit_cases(void) {
thread->exit_scope = stop ? &stopped : &open;
state->exit_scope = row == 2 ? &stopped : &open;
vm->sp = vm->frame_count = 1;
- vm->stack[0] = slot_from_num(0);
+ vm->stack[0] = slot_from_f64(0);
vm->frames[0].chunk = &chunk;
vm->frames[0].ip = chunk.code + entry;
chunk.jit_advance = chunk.jit_osr[0].advance = -99;
@@ -636,12 +636,12 @@ static int run_exit_cases(void) {
int advance = osr ? chunk.jit_osr[0].advance : chunk.jit_advance;
int other = osr ? chunk.jit_advance : chunk.jit_osr[0].advance;
int want_advance = (stop ? 10 : 13) + (osr ? 2 : 0);
- int ok = vm->sp == 1 && SLOT_NUM_RAW(vm->stack[0]) == (stop ? 1 : 4) &&
+ int ok = vm->sp == 1 && SLOT_F64_RAW(vm->stack[0]) == (stop ? 1 : 4) &&
advance == want_advance && other == -99 &&
vm->frames[0].ip == chunk.code + entry;
if (!ok) {
fprintf(stderr, "FAIL native_exit %s row=%d sp=%d value=%g advance=%d other=%d\n",
- osr ? "osr" : "entry", row, vm->sp, SLOT_NUM_RAW(vm->stack[0]), advance, other);
+ osr ? "osr" : "entry", row, vm->sp, SLOT_F64_RAW(vm->stack[0]), advance, other);
rc = 1;
}
if (ok && stop) bailed++;
diff --git a/src/lexer.c b/src/lexer.c
index 2672d8436..2b20b7109 100644
--- a/src/lexer.c
+++ b/src/lexer.c
@@ -32,14 +32,14 @@ static void lexer_error_at(const char *caret_source, int line, int col,
eigs_print_caret_src(stderr, caret_source, line, col);
}
-static void tok_add(TokenList *tl, TokType type, double num, const char *str, int line, int col) {
+static void tok_add(TokenList *tl, TokType type, EigsNum number, const char *str, int line, int col) {
if (tl->count >= tl->capacity) {
tl->capacity *= 2;
tl->tokens = xrealloc_array(tl->tokens, tl->capacity, sizeof(Token));
}
Token *t = &tl->tokens[tl->count++];
t->type = type;
- t->num_val = num;
+ t->number = number;
t->str_val = str ? xstrdup(str) : NULL;
t->line = line;
t->col = col;
@@ -170,7 +170,7 @@ static const char *fstr_interp_end(const char *p, int nesting, int *too_deep) {
* reports the post-`}` column, since a zero length would read as
* "column unknown" to the error recorder. */
static void tok_add_synth(TokenList *tl, TokType type, const char *str, int line, int col) {
- tok_add(tl, type, 0, str, line, col);
+ tok_add(tl, type, (EigsNum){0}, str, line, col);
tl->tokens[tl->count - 1].synth = 1;
}
@@ -260,7 +260,7 @@ static TokType keyword_type(const char *word) {
}
int tok_base_string_id_count(void) {
- return (int)TOK_EOF + 1;
+ return (int)TOK_SLASH_SLASH_EQ + 1;
}
/* Recognised multi-char operators. Each operator is spelled once, as
@@ -268,6 +268,7 @@ int tok_base_string_id_count(void) {
#define LEX_MULTI_OP_MAP(X2, X3) \
X3(TOK_SHL_EQ, '<', '<', '=') \
X3(TOK_SHR_EQ, '>', '>', '=') \
+ X3(TOK_SLASH_SLASH_EQ, '/', '/', '=') \
X2(TOK_EQ, '=', '=') \
X2(TOK_NE, '!', '=') \
X2(TOK_LE, '<', '=') \
@@ -278,6 +279,7 @@ int tok_base_string_id_count(void) {
X2(TOK_MINUS_EQ, '-', '=') \
X2(TOK_STAR_EQ, '*', '=') \
X2(TOK_SLASH_EQ, '/', '=') \
+ X2(TOK_SLASH_SLASH, '/', '/') \
X2(TOK_PERCENT_EQ, '%', '=') \
X2(TOK_AMP_EQ, '&', '=') \
X2(TOK_BITOR_EQ, '|', '=') \
@@ -394,6 +396,8 @@ const char* tok_base_string(TokType t) {
case TOK_MINUS: return "- ";
case TOK_STAR: return "* ";
case TOK_SLASH: return "/ ";
+ case TOK_SLASH_SLASH: return "// ";
+ case TOK_SLASH_SLASH_EQ: return "//= ";
case TOK_PERCENT: return "% ";
case TOK_LT: return "< ";
case TOK_GT: return "> ";
@@ -478,7 +482,7 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
snprintf(msg, sizeof(msg), "f-string nesting too deep (max %d levels)",
MAX_TOKENIZE_DEPTH);
lexer_error_at(caret_source, initial_line, initial_col, msg);
- tok_add(&tl, TOK_EOF, 0, NULL, initial_line, initial_col);
+ tok_add(&tl, TOK_EOF, (EigsNum){0}, NULL, initial_line, initial_col);
return tl;
}
g_tokenize_depth++;
@@ -531,12 +535,12 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
} else {
indent_top++;
indent_stack[indent_top] = spaces;
- tok_add(&tl, TOK_INDENT, 0, NULL, line, col);
+ tok_add(&tl, TOK_INDENT, (EigsNum){0}, NULL, line, col);
}
} else {
while (indent_top > 0 && spaces < indent_stack[indent_top]) {
indent_top--;
- tok_add(&tl, TOK_DEDENT, 0, NULL, line, col);
+ tok_add(&tl, TOK_DEDENT, (EigsNum){0}, NULL, line, col);
}
if (spaces != indent_stack[indent_top]) {
fprintf(stderr, "Syntax error line %d: indentation does not match any outer level\n", line);
@@ -571,7 +575,7 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
if (tl.count > 0 && tl.tokens[tl.count-1].type != TOK_NEWLINE
&& tl.tokens[tl.count-1].type != TOK_INDENT
&& tl.tokens[tl.count-1].type != TOK_DEDENT) {
- tok_add(&tl, TOK_NEWLINE, 0, NULL, line, col);
+ tok_add(&tl, TOK_NEWLINE, (EigsNum){0}, NULL, line, col);
}
at_line_start = 1;
}
@@ -694,7 +698,7 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
* the post-`}` column sent LSP rename edits onto
* the closing quote). */
Token *it = &inner.tokens[ti];
- tok_add(&tl, it->type, it->num_val, it->str_val, it->line, it->col);
+ tok_add(&tl, it->type, it->number, it->str_val, it->line, it->col);
tl.tokens[tl.count - 1].len = it->len;
tl.tokens[tl.count - 1].synth = it->synth;
}
@@ -760,7 +764,7 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
fprintf(stderr, "Syntax error line %d: unterminated string\n", tok_line);
lexer_error_at(caret_source, tok_line, tok_col, "unterminated string");
}
- tok_add(&tl, TOK_STR, 0, buf.data, tok_line, tok_col);
+ tok_add(&tl, TOK_STR, (EigsNum){0}, buf.data, tok_line, tok_col);
tl.tokens[tl.count - 1].len = (int)(p - str_start); /* true source span */
if (layout) literal_span_add(&tl, (size_t)(str_start - source),
(size_t)(p - source));
@@ -770,41 +774,58 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
if (isdigit(*p) || (*p == '.' && isdigit(*(p+1)))) {
const char *num_start = p;
- /* Hex integer literals are lexed HERE, not via strtod: the
- * freestanding mini_strtod has no hex path (documented
- * divergence — hex used to break on EigenOS), and C99 strtod
- * also accepts hex-FLOAT forms (0x1p4, 0xA.8) the language
- * does not mean. Digits accumulate in a double, so values
- * past 2^53 round exactly like long decimal literals do. */
+ /* Hex is parsed here rather than by strtod, whose hex-float
+ * grammar is wider than ours and absent from mini_strtod. N1:
+ * accumulate modulo 2^64, then interpret the signed int64 value
+ * without an implementation-defined uint64-to-int64 cast. */
if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) {
if (isxdigit((unsigned char)p[2])) {
- double num = 0;
+ uint64_t bits = 0;
p += 2; col += 2;
while (isxdigit((unsigned char)*p)) {
int d = *p <= '9' ? *p - '0' : (*p | 32) - 'a' + 10;
- num = num * 16 + d;
+ bits = bits * UINT64_C(16) + (uint64_t)d;
p++; col++;
}
- tok_add(&tl, TOK_NUM, num, NULL, line, tok_col);
+ int64_t value = bits <= INT64_MAX ? (int64_t)bits
+ : -1 - (int64_t)(UINT64_MAX - bits);
+ EigsNum number = { .is_int = 1, .value.i64 = value };
+ tok_add(&tl, TOK_NUM, number, NULL, line, tok_col);
tl.tokens[tl.count - 1].len = (int)(p - num_start);
continue;
}
- /* A 0x prefix with no hex digit (bare `0x`, or the
- * hex-FLOAT fraction form `0x.8`): lex just the `0` so
- * the rest re-lexes as an identifier/dot and the parse
- * fails loudly. strtod must NEVER see a hex prefix —
- * glibc would parse `0x.8` as 0.5 while mini_strtod
- * (no hex) reads 0, a silent profile divergence. */
- tok_add(&tl, TOK_NUM, 0, NULL, line, tok_col);
+ /* A hex prefix without a digit lexes as `0` followed by
+ * invalid suffix tokens, on hosted and freestanding builds. */
+ EigsNum zero = { .is_int = 1, .value.i64 = 0 };
+ tok_add(&tl, TOK_NUM, zero, NULL, line, tok_col);
tl.tokens[tl.count - 1].len = 1;
p++; col++;
continue;
}
char *end;
- double num = strtod(p, &end);
+ double f = strtod(p, &end);
+ EigsNum number = { .is_int = 0, .value.f64 = f };
+ /* strtod determines the existing decimal grammar/span. Only a
+ * digits-only token can be an int: `3.0` and `1e3` stay floats.
+ * Parse the integer independently, so no double rounding occurs
+ * above 2^53, and fall back to the float on decimal overflow. */
+ int64_t integer = 0;
+ int fits = 1;
+ for (const char *q = p; q < end; q++) {
+ if (*q < '0' || *q > '9' ||
+ integer > (INT64_MAX - (*q - '0')) / 10) {
+ fits = 0;
+ break;
+ }
+ integer = integer * 10 + (*q - '0');
+ }
+ if (fits) {
+ number.is_int = 1;
+ number.value.i64 = integer;
+ }
col += (int)(end - p);
p = end;
- tok_add(&tl, TOK_NUM, num, NULL, line, tok_col);
+ tok_add(&tl, TOK_NUM, number, NULL, line, tok_col);
tl.tokens[tl.count - 1].len = (int)(end - num_start);
continue;
}
@@ -822,7 +843,7 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
col++;
}
TokType tt = keyword_type(buf.data);
- tok_add(&tl, tt, 0, buf.data, line, tok_col);
+ tok_add(&tl, tt, (EigsNum){0}, buf.data, line, tok_col);
strbuf_free(&buf);
continue;
}
@@ -832,7 +853,7 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
TokType op_ty = 0;
int op_n = lexer_operator_len(p, &op_ty);
if (op_n > 1) {
- tok_add(&tl, op_ty, 0, NULL, line, tok_col);
+ tok_add(&tl, op_ty, (EigsNum){0}, NULL, line, tok_col);
p += op_n;
col += op_n;
continue;
@@ -840,32 +861,32 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
}
switch (*p) {
- case '+': tok_add(&tl, TOK_PLUS, 0, NULL, line, tok_col); p++; col++; break;
- case '-': tok_add(&tl, TOK_MINUS, 0, NULL, line, tok_col); p++; col++; break;
- case '*': tok_add(&tl, TOK_STAR, 0, NULL, line, tok_col); p++; col++; break;
- case '/': tok_add(&tl, TOK_SLASH, 0, NULL, line, tok_col); p++; col++; break;
- case '%': tok_add(&tl, TOK_PERCENT, 0, NULL, line, tok_col); p++; col++; break;
- case '(': tok_add(&tl, TOK_LPAREN, 0, NULL, line, tok_col); p++; col++; bracket_depth++; break;
- case ')': tok_add(&tl, TOK_RPAREN, 0, NULL, line, tok_col); p++; col++; if (bracket_depth > 0) bracket_depth--; break;
- case '[': tok_add(&tl, TOK_LBRACKET, 0, NULL, line, tok_col); p++; col++; bracket_depth++; break;
- case ']': tok_add(&tl, TOK_RBRACKET, 0, NULL, line, tok_col); p++; col++; if (bracket_depth > 0) bracket_depth--; break;
- case '{': tok_add(&tl, TOK_LBRACE, 0, NULL, line, tok_col); p++; col++; bracket_depth++; break;
- case '}': tok_add(&tl, TOK_RBRACE, 0, NULL, line, tok_col); p++; col++; if (bracket_depth > 0) bracket_depth--; break;
- case ',': tok_add(&tl, TOK_COMMA, 0, NULL, line, tok_col); p++; col++; break;
- case ':': tok_add(&tl, TOK_COLON, 0, NULL, line, tok_col); p++; col++; break;
- case '.': tok_add(&tl, TOK_DOT, 0, NULL, line, tok_col); p++; col++; break;
- case '<': tok_add(&tl, TOK_LT, 0, NULL, line, tok_col); p++; col++; break;
- case '>': tok_add(&tl, TOK_GT, 0, NULL, line, tok_col); p++; col++; break;
+ case '+': tok_add(&tl, TOK_PLUS, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '-': tok_add(&tl, TOK_MINUS, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '*': tok_add(&tl, TOK_STAR, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '/': tok_add(&tl, TOK_SLASH, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '%': tok_add(&tl, TOK_PERCENT, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '(': tok_add(&tl, TOK_LPAREN, (EigsNum){0}, NULL, line, tok_col); p++; col++; bracket_depth++; break;
+ case ')': tok_add(&tl, TOK_RPAREN, (EigsNum){0}, NULL, line, tok_col); p++; col++; if (bracket_depth > 0) bracket_depth--; break;
+ case '[': tok_add(&tl, TOK_LBRACKET, (EigsNum){0}, NULL, line, tok_col); p++; col++; bracket_depth++; break;
+ case ']': tok_add(&tl, TOK_RBRACKET, (EigsNum){0}, NULL, line, tok_col); p++; col++; if (bracket_depth > 0) bracket_depth--; break;
+ case '{': tok_add(&tl, TOK_LBRACE, (EigsNum){0}, NULL, line, tok_col); p++; col++; bracket_depth++; break;
+ case '}': tok_add(&tl, TOK_RBRACE, (EigsNum){0}, NULL, line, tok_col); p++; col++; if (bracket_depth > 0) bracket_depth--; break;
+ case ',': tok_add(&tl, TOK_COMMA, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case ':': tok_add(&tl, TOK_COLON, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '.': tok_add(&tl, TOK_DOT, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '<': tok_add(&tl, TOK_LT, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '>': tok_add(&tl, TOK_GT, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
case '!':
fprintf(stderr, "Syntax error line %d: expected '!=' after '!'\n", line);
lexer_error_at(caret_source, line, tok_col, "expected '!=' after '!'");
p++; col++;
break;
- case '=': tok_add(&tl, TOK_ASSIGN, 0, NULL, line, tok_col); p++; col++; break;
- case '|': tok_add(&tl, TOK_BITOR, 0, NULL, line, tok_col); p++; col++; break;
- case '&': tok_add(&tl, TOK_AMP, 0, NULL, line, tok_col); p++; col++; break;
- case '^': tok_add(&tl, TOK_CARET, 0, NULL, line, tok_col); p++; col++; break;
- case '~': tok_add(&tl, TOK_TILDE, 0, NULL, line, tok_col); p++; col++; break;
+ case '=': tok_add(&tl, TOK_ASSIGN, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '|': tok_add(&tl, TOK_BITOR, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '&': tok_add(&tl, TOK_AMP, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '^': tok_add(&tl, TOK_CARET, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
+ case '~': tok_add(&tl, TOK_TILDE, (EigsNum){0}, NULL, line, tok_col); p++; col++; break;
default:
{
/* Spell the offending byte, never echo it (#1048). A byte
@@ -892,14 +913,14 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init
}
while (indent_top > 0) {
- tok_add(&tl, TOK_DEDENT, 0, NULL, line, col);
+ tok_add(&tl, TOK_DEDENT, (EigsNum){0}, NULL, line, col);
indent_top--;
}
if (tl.count > 0 && tl.tokens[tl.count-1].type != TOK_NEWLINE) {
- tok_add(&tl, TOK_NEWLINE, 0, NULL, line, col);
+ tok_add(&tl, TOK_NEWLINE, (EigsNum){0}, NULL, line, col);
}
- tok_add(&tl, TOK_EOF, 0, NULL, line, col);
+ tok_add(&tl, TOK_EOF, (EigsNum){0}, NULL, line, col);
g_tokenize_depth--;
if (layout) resolve_comment_depths(&tl);
diff --git a/src/lsp_builtin_index.h b/src/lsp_builtin_index.h
index 11d933bd2..f45b26e41 100644
--- a/src/lsp_builtin_index.h
+++ b/src/lsp_builtin_index.h
@@ -105,6 +105,7 @@ static const char *builtin_docs[][2] = {
{"fill", "fill of [count, value] — create a list of `count` elements all set to `value`."},
{"floor", "floor — builtin; see docs/BUILTINS.md"},
{"flush", "flush of null — flush stdout"},
+ {"fround", "fround of x — binary64 to binary32 to binary64, returning float kind"},
{"gather", "gather of [tensor, indices, dim] -> select one element per row by index."},
{"get_at", "get_at of [list, index] or get_at of [list, row, col]"},
{"get_observer_thresholds", "get_observer_thresholds — builtin; see docs/BUILTINS.md"},
@@ -141,6 +142,42 @@ static const char *builtin_docs[][2] = {
{"http_serve", "http_serve — http extension builtin; see docs/BUILTINS.md"},
{"http_session_id", "http_session_id — http_request extension builtin; see docs/BUILTINS.md"},
{"http_static", "http_static — http extension builtin; see docs/BUILTINS.md"},
+ {"i16_checked_add", "i16_checked_add of [a, b] — i16 checked add; N5 operands, int result"},
+ {"i16_checked_mul", "i16_checked_mul of [a, b] — i16 checked mul; N5 operands, int result"},
+ {"i16_checked_sub", "i16_checked_sub of [a, b] — i16 checked sub; N5 operands, int result"},
+ {"i16_saturating_add", "i16_saturating_add of [a, b] — i16 saturating add; N5 operands, int result"},
+ {"i16_saturating_mul", "i16_saturating_mul of [a, b] — i16 saturating mul; N5 operands, int result"},
+ {"i16_saturating_sub", "i16_saturating_sub of [a, b] — i16 saturating sub; N5 operands, int result"},
+ {"i16_wrapping_add", "i16_wrapping_add of [a, b] — i16 wrapping add; N5 operands, int result"},
+ {"i16_wrapping_mul", "i16_wrapping_mul of [a, b] — i16 wrapping mul; N5 operands, int result"},
+ {"i16_wrapping_sub", "i16_wrapping_sub of [a, b] — i16 wrapping sub; N5 operands, int result"},
+ {"i32_checked_add", "i32_checked_add of [a, b] — i32 checked add; N5 operands, int result"},
+ {"i32_checked_mul", "i32_checked_mul of [a, b] — i32 checked mul; N5 operands, int result"},
+ {"i32_checked_sub", "i32_checked_sub of [a, b] — i32 checked sub; N5 operands, int result"},
+ {"i32_saturating_add", "i32_saturating_add of [a, b] — i32 saturating add; N5 operands, int result"},
+ {"i32_saturating_mul", "i32_saturating_mul of [a, b] — i32 saturating mul; N5 operands, int result"},
+ {"i32_saturating_sub", "i32_saturating_sub of [a, b] — i32 saturating sub; N5 operands, int result"},
+ {"i32_wrapping_add", "i32_wrapping_add of [a, b] — i32 wrapping add; N5 operands, int result"},
+ {"i32_wrapping_mul", "i32_wrapping_mul of [a, b] — i32 wrapping mul; N5 operands, int result"},
+ {"i32_wrapping_sub", "i32_wrapping_sub of [a, b] — i32 wrapping sub; N5 operands, int result"},
+ {"i64_checked_add", "i64_checked_add of [a, b] — i64 checked add; N5 operands, int result"},
+ {"i64_checked_mul", "i64_checked_mul of [a, b] — i64 checked mul; N5 operands, int result"},
+ {"i64_checked_sub", "i64_checked_sub of [a, b] — i64 checked sub; N5 operands, int result"},
+ {"i64_saturating_add", "i64_saturating_add of [a, b] — i64 saturating add; N5 operands, int result"},
+ {"i64_saturating_mul", "i64_saturating_mul of [a, b] — i64 saturating mul; N5 operands, int result"},
+ {"i64_saturating_sub", "i64_saturating_sub of [a, b] — i64 saturating sub; N5 operands, int result"},
+ {"i64_wrapping_add", "i64_wrapping_add of [a, b] — i64 wrapping add; N5 operands, int result"},
+ {"i64_wrapping_mul", "i64_wrapping_mul of [a, b] — i64 wrapping mul; N5 operands, int result"},
+ {"i64_wrapping_sub", "i64_wrapping_sub of [a, b] — i64 wrapping sub; N5 operands, int result"},
+ {"i8_checked_add", "i8_checked_add of [a, b] — i8 checked add; N5 operands, int result"},
+ {"i8_checked_mul", "i8_checked_mul of [a, b] — i8 checked mul; N5 operands, int result"},
+ {"i8_checked_sub", "i8_checked_sub of [a, b] — i8 checked sub; N5 operands, int result"},
+ {"i8_saturating_add", "i8_saturating_add of [a, b] — i8 saturating add; N5 operands, int result"},
+ {"i8_saturating_mul", "i8_saturating_mul of [a, b] — i8 saturating mul; N5 operands, int result"},
+ {"i8_saturating_sub", "i8_saturating_sub of [a, b] — i8 saturating sub; N5 operands, int result"},
+ {"i8_wrapping_add", "i8_wrapping_add of [a, b] — i8 wrapping add; N5 operands, int result"},
+ {"i8_wrapping_mul", "i8_wrapping_mul of [a, b] — i8 wrapping mul; N5 operands, int result"},
+ {"i8_wrapping_sub", "i8_wrapping_sub of [a, b] — i8 wrapping sub; N5 operands, int result"},
{"index_of", "index_of of [haystack, needle] → first index, or -1. Non-string operands"},
{"inflate", "inflate — builtin; see docs/BUILTINS.md"},
{"is_dir", "is_dir of path — true if path names a directory, false for a plain file, a"},
@@ -194,6 +231,7 @@ static const char *builtin_docs[][2] = {
{"norm", "norm of a → L2 (Euclidean) norm = sqrt(sum_i a[i]^2). Buffers and tensors."},
{"num", "num — builtin; see docs/BUILTINS.md"},
{"num_copy", "num_copy of val → fresh heap-allocated copy of a numeric Value."},
+ {"num_type", "num_type — builtin; see docs/BUILTINS.md"},
{"numerical_grad", "numerical_grad of [loss_fn, param, eps]"},
{"numerical_grad_cols", "numerical_grad_cols of [loss_fn, matrix, col_indices, eps]"},
{"numerical_grad_rows", "numerical_grad_rows of [loss_fn, matrix, row_indices, eps]"},
@@ -218,7 +256,7 @@ static const char *builtin_docs[][2] = {
{"read_text", "read_text of \"path\" → file contents as string; I/O failure raises EK_IO."},
{"record_history", "record_history of flag — enable (nonzero) or disable (0) per-assignment"},
{"recv", "recv — builtin; see docs/BUILTINS.md"},
- {"recv_timeout", "recv_timeout of [channel, ms] — bounded wait. Returns the value if one"},
+ {"recv_timeout", "recv_timeout of [channel, ms] — milliseconds rounded half away from zero"},
{"regex_find", "regex_find — builtin; see docs/BUILTINS.md"},
{"regex_match", "regex_match — builtin; see docs/BUILTINS.md"},
{"regex_replace", "regex_replace of [string, pattern, replacement] -> string"},
@@ -305,6 +343,46 @@ static const char *builtin_docs[][2] = {
{"try_parse", "try_parse of string → true if valid EigenScript syntax, false if not."},
{"try_recv", "try_recv of channel — non-blocking receive, returns null if empty"},
{"type", "type — builtin; see docs/BUILTINS.md"},
+ {"u16_checked_add", "u16_checked_add of [a, b] — u16 checked add; N5 operands, int result"},
+ {"u16_checked_mul", "u16_checked_mul of [a, b] — u16 checked mul; N5 operands, int result"},
+ {"u16_checked_sub", "u16_checked_sub of [a, b] — u16 checked sub; N5 operands, int result"},
+ {"u16_saturating_add", "u16_saturating_add of [a, b] — u16 saturating add; N5 operands, int result"},
+ {"u16_saturating_mul", "u16_saturating_mul of [a, b] — u16 saturating mul; N5 operands, int result"},
+ {"u16_saturating_sub", "u16_saturating_sub of [a, b] — u16 saturating sub; N5 operands, int result"},
+ {"u16_wrapping_add", "u16_wrapping_add of [a, b] — u16 wrapping add; N5 operands, int result"},
+ {"u16_wrapping_mul", "u16_wrapping_mul of [a, b] — u16 wrapping mul; N5 operands, int result"},
+ {"u16_wrapping_sub", "u16_wrapping_sub of [a, b] — u16 wrapping sub; N5 operands, int result"},
+ {"u32_checked_add", "u32_checked_add of [a, b] — u32 checked add; N5 operands, int result"},
+ {"u32_checked_mul", "u32_checked_mul of [a, b] — u32 checked mul; N5 operands, int result"},
+ {"u32_checked_sub", "u32_checked_sub of [a, b] — u32 checked sub; N5 operands, int result"},
+ {"u32_saturating_add", "u32_saturating_add of [a, b] — u32 saturating add; N5 operands, int result"},
+ {"u32_saturating_mul", "u32_saturating_mul of [a, b] — u32 saturating mul; N5 operands, int result"},
+ {"u32_saturating_sub", "u32_saturating_sub of [a, b] — u32 saturating sub; N5 operands, int result"},
+ {"u32_wrapping_add", "u32_wrapping_add of [a, b] — u32 wrapping add; N5 operands, int result"},
+ {"u32_wrapping_mul", "u32_wrapping_mul of [a, b] — u32 wrapping mul; N5 operands, int result"},
+ {"u32_wrapping_sub", "u32_wrapping_sub of [a, b] — u32 wrapping sub; N5 operands, int result"},
+ {"u64_checked_add", "u64_checked_add of [a, b] — u64 checked add; N5 operands, int result"},
+ {"u64_checked_mul", "u64_checked_mul of [a, b] — u64 checked mul; N5 operands, int result"},
+ {"u64_checked_sub", "u64_checked_sub of [a, b] — u64 checked sub; N5 operands, int result"},
+ {"u64_compare", "u64_compare of [a, b] — unsigned ordering of int64 bit patterns"},
+ {"u64_div", "u64_div of [a, b] — unsigned division; zero raises value"},
+ {"u64_rem", "u64_rem of [a, b] — unsigned remainder; zero raises value"},
+ {"u64_saturating_add", "u64_saturating_add of [a, b] — u64 saturating add; N5 operands, int result"},
+ {"u64_saturating_mul", "u64_saturating_mul of [a, b] — u64 saturating mul; N5 operands, int result"},
+ {"u64_saturating_sub", "u64_saturating_sub of [a, b] — u64 saturating sub; N5 operands, int result"},
+ {"u64_to_string", "u64_to_string of x — unsigned decimal text from an int64 bit pattern"},
+ {"u64_wrapping_add", "u64_wrapping_add of [a, b] — u64 wrapping add; N5 operands, int result"},
+ {"u64_wrapping_mul", "u64_wrapping_mul of [a, b] — u64 wrapping mul; N5 operands, int result"},
+ {"u64_wrapping_sub", "u64_wrapping_sub of [a, b] — u64 wrapping sub; N5 operands, int result"},
+ {"u8_checked_add", "u8_checked_add of [a, b] — u8 checked add; N5 operands, int result"},
+ {"u8_checked_mul", "u8_checked_mul of [a, b] — u8 checked mul; N5 operands, int result"},
+ {"u8_checked_sub", "u8_checked_sub of [a, b] — u8 checked sub; N5 operands, int result"},
+ {"u8_saturating_add", "u8_saturating_add of [a, b] — u8 saturating add; N5 operands, int result"},
+ {"u8_saturating_mul", "u8_saturating_mul of [a, b] — u8 saturating mul; N5 operands, int result"},
+ {"u8_saturating_sub", "u8_saturating_sub of [a, b] — u8 saturating sub; N5 operands, int result"},
+ {"u8_wrapping_add", "u8_wrapping_add of [a, b] — u8 wrapping add; N5 operands, int result"},
+ {"u8_wrapping_mul", "u8_wrapping_mul of [a, b] — u8 wrapping mul; N5 operands, int result"},
+ {"u8_wrapping_sub", "u8_wrapping_sub of [a, b] — u8 wrapping sub; N5 operands, int result"},
{"usleep", "usleep of microseconds — pause execution"},
{"values", "values — builtin; see docs/BUILTINS.md"},
{"vm_run_bytecode", "vm_run_bytecode of — assemble a chunk (and its nested"},
diff --git a/src/main.c b/src/main.c
index 7259a8c08..1cf438a17 100644
--- a/src/main.c
+++ b/src/main.c
@@ -112,6 +112,24 @@ int main(int argc, char **argv) {
if (getenv("EIGS_TSAN_MISORDERED_PUBLISH_CONTROL"))
return tsan_misordered_publish_control();
#endif
+ /* EIGS_SEED_RANDOM= (#2120): seed the shared random stream before
+ * any program code runs, through seed_random's own path, so every run with
+ * one value draws one sequence (differential testing, reproduction). A
+ * malformed value is a startup error here: never a silent fallback to the
+ * clock, and never an exit from inside the runtime, which embedders share. */
+ {
+ const char *seed_env = getenv("EIGS_SEED_RANDOM");
+ if (seed_env && *seed_env) { /* empty means unset, as for EIGS_* flags */
+ char *end = NULL;
+ errno = 0;
+ long seed = strtol(seed_env, &end, 10);
+ if (errno == ERANGE || end == seed_env || *end != '\0') {
+ fprintf(stderr, "EIGS_SEED_RANDOM: invalid integer: %s\n", seed_env);
+ return 2;
+ }
+ eigs_seed_random(seed);
+ }
+ }
/* #413: a bundled binary (appended archive, trailer magic) extracts
* itself and rewrites argv to run the extracted script — before ANY
* flag handling, because in bundle mode the arguments belong to the
@@ -239,7 +257,8 @@ int main(int argc, char **argv) {
}
/* Machine-readable lexer fingerprint used by the formatter safety gate.
- * Values are hex bytes, so quotes, newlines and CR are unambiguous. */
+ * String values are hex bytes; numbers carry an i/f kind prefix followed
+ * by exact decimal int digits or an exact hexadecimal float. */
if (argc >= 2 && strcmp(argv[1], "--dump-tokens") == 0) {
if (argc != 3) {
fprintf(stderr, "Usage: eigenscript --dump-tokens file.eigs\n");
@@ -259,8 +278,12 @@ int main(int argc, char **argv) {
if (!rc) for (int i = 0; i < tl.count; i++) {
Token *t = &tl.tokens[i];
printf("%d\t", (int)t->type);
- if (t->type == TOK_NUM) printf("%a", t->num_val);
- else if (t->str_val) {
+ if (t->type == TOK_NUM) {
+ if (t->number.is_int)
+ printf("i%lld", (long long)t->number.value.i64);
+ else
+ printf("f%a", t->number.value.f64);
+ } else if (t->str_val) {
const unsigned char *p = (const unsigned char *)t->str_val;
while (*p) printf("%02x", *p++);
}
diff --git a/src/model_infer.c b/src/model_infer.c
index b60810d46..136d4942d 100644
--- a/src/model_infer.c
+++ b/src/model_infer.c
@@ -536,6 +536,23 @@ Value* builtin_eigen_generate(Value *arg) {
* It takes either outcome before consulting the model, which may be absent
* or replaced. Refusal messages carry recorded lengths, not live config.
* Deterministic argument validation precedes the tape bracket. */
+ /* N5 argument refusals consume no tape record. Validate without consulting
+ * model state: replay still serves recorded outcomes on an unloaded model. */
+ double temperature = 0.1;
+ int max_tokens = 80;
+ /* Optional 4th arg: top_p. Absent (or outside (0,1)) keeps the historical
+ * fixed top-k=40 path, so every existing caller and recorded number is
+ * unchanged -- decoding policy is opt-in, not a silent default flip. */
+ double top_p = 0.0;
+ if (arg && arg->type == VAL_LIST && arg->data.list.count >= 3) {
+ /* #1637: optional numbers -- null keeps the default, a bool raises. */
+ int64_t mt = max_tokens;
+ eigs_slot_opt_float(list_slot(arg, 1), &temperature, "eigen_generate");
+ eigs_slot_opt_int_range(list_slot(arg, 2), INT_MIN, INT_MAX, &mt, "eigen_generate");
+ if (arg->data.list.count >= 4) eigs_slot_opt_float(list_slot(arg, 3), &top_p, "eigen_generate");
+ if (g_has_error) return make_null();
+ max_tokens = (int)mt;
+ }
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) {
fprintf(stderr, "eigen_generate: requires [prompt_ids, temperature, max_tokens]\n");
TRACE_NONDET_RECORD("eigen_generate", make_list(0));
@@ -547,19 +564,6 @@ Value* builtin_eigen_generate(Value *arg) {
TRACE_NONDET_RECORD("eigen_generate", make_list(0));
}
- double temperature = 0.1;
- int max_tokens = 80;
- /* Optional 4th arg: top_p. Absent (or outside (0,1)) keeps the historical
- * fixed top-k=40 path, so every existing caller and recorded number is
- * unchanged -- decoding policy is opt-in, not a silent default flip. */
- double top_p = 0.0;
- /* #1637: optional numbers -- null keeps the default, a bool raises. */
- double mt = max_tokens;
- eigs_slot_opt_num(list_slot(arg, 1), &temperature, __func__);
- eigs_slot_opt_num(list_slot(arg, 2), &mt, __func__);
- if (arg->data.list.count >= 4) eigs_slot_opt_num(list_slot(arg, 3), &top_p, __func__);
- if (g_has_error) return make_null();
- max_tokens = (int)mt;
/* Validate prompt and token count */
int prompt_len = prompt_list->data.list.count;
@@ -578,7 +582,10 @@ Value* builtin_eigen_generate(Value *arg) {
int *prompt_ids = xcalloc(prompt_len, sizeof(int));
for (int i = 0; i < prompt_len; i++) {
- prompt_ids[i] = (int)eigs_slot_num(list_slot(prompt_list, i), "eigen_generate"); /* #1637: a bool raises */
+ int64_t token;
+ if (!eigs_slot_int_range(list_slot(prompt_list, i), INT_MIN, INT_MAX,
+ &token, "eigen_generate")) { free(prompt_ids); return make_null(); }
+ prompt_ids[i] = (int)token; /* #1637: a bool raises */
}
if (g_has_error) { free(prompt_ids); return make_null(); }
@@ -650,7 +657,7 @@ Value* builtin_eigen_generate(Value *arg) {
Value *result = make_list(out_len);
for (int i = 0; i < out_len; i++) {
- list_append_owned(result, make_num((double)output_ids[i]));
+ list_append_owned(result, make_int(output_ids[i]));
}
free(output_ids);
TRACE_NONDET_RECORD("eigen_generate", result);
@@ -681,24 +688,27 @@ Value* builtin_eigen_eval_loss(Value *arg) {
* provably unchanged. This is a grader: a laundered -1 loss is the
* exact shape #975 exists to stop, since a caller cannot tell it from
* a real score. */
- ARG_GUARD(1, "eigen_eval_loss", "[prompt_ids, target_id]", make_num(-1.0));
+ ARG_GUARD(1, "eigen_eval_loss", "[prompt_ids, target_id]", make_float(-1.0));
}
EIGS_VIEW(prompt_list_view);
Value *prompt_list = list_get_view(arg, 0, &prompt_list_view);
if (prompt_list->type != VAL_LIST) {
fprintf(stderr, "eigen_eval_loss: prompt_ids must be a list\n");
- ARG_GUARD(1, "eigen_eval_loss", "a list of prompt ids", make_num(-1.0));
+ ARG_GUARD(1, "eigen_eval_loss", "a list of prompt ids", make_float(-1.0));
}
- if (list_elem_type(arg, 1) != VAL_NUM) {
+ if ((list_elem_type(arg, 1) != VAL_FLOAT && list_elem_type(arg, 1) != VAL_INT)) {
fprintf(stderr, "eigen_eval_loss: target_id must be a number\n");
- ARG_GUARD(1, "eigen_eval_loss", "a numeric target_id", make_num(-1.0));
+ ARG_GUARD(1, "eigen_eval_loss", "a numeric target_id", make_float(-1.0));
}
- int target_id = (int)eigs_list_num(arg, 1, __func__);
+ int64_t target;
+ if (!eigs_list_int_range(arg, 1, INT_MIN, INT_MAX, &target, "eigen_eval_loss"))
+ return make_null();
+ int target_id = (int)target;
/* fs:ANSWER no model is loaded, so there is no loss to compute. Model
* state, not an argument mistake — the identical call scores fine once a
* checkpoint is loaded. */
- if (!g_model.loaded) return make_num(-1.0);
+ if (!g_model.loaded) return make_float(-1.0);
int vocab_size = g_model.config.vocab_size;
if (target_id < 0 || target_id >= vocab_size) {
rt_error(EK_INDEX, 0, "eigen_eval_loss: target_id %d out of range [0,%d)",
@@ -711,7 +721,7 @@ Value* builtin_eigen_eval_loss(Value *arg) {
fprintf(stderr, "eigen_eval_loss: prompt must be non-empty\n");
/* fs:EMPTY an empty prompt has no logits to score, so there is no
* loss; the list's TYPE was already accepted above. */
- return make_num(-1.0);
+ return make_float(-1.0);
}
if (prompt_len > g_model.config.max_seq_len) {
rt_error(EK_VALUE, 0,
@@ -722,7 +732,10 @@ Value* builtin_eigen_eval_loss(Value *arg) {
int *prompt_ids = xcalloc(prompt_len, sizeof(int));
for (int i = 0; i < prompt_len; i++) {
- int t = (int)eigs_slot_num(list_slot(prompt_list, i), "eigen_eval_loss"); /* #1637: a bool raises */
+ int64_t token;
+ if (!eigs_slot_int_range(list_slot(prompt_list, i), INT_MIN, INT_MAX,
+ &token, "eigen_eval_loss")) { free(prompt_ids); return make_null(); }
+ int t = (int)token; /* #1637: a bool raises */
if (!g_has_error && (t < 0 || t >= vocab_size)) {
rt_error(EK_INDEX, 0,
"eigen_eval_loss: token id %d at prompt index %d out of range [0,%d)",
@@ -749,8 +762,8 @@ Value* builtin_eigen_eval_loss(Value *arg) {
/* fs:ANSWER the loss computed to NaN/Inf from a degenerate logit row —
* a numeric outcome of this model on this input, not a bad argument. */
- if (isnan(loss) || isinf(loss)) return make_num(-1.0);
- return make_num(loss);
+ if (isnan(loss) || isinf(loss)) return make_float(-1.0);
+ return make_float(loss);
}
Value* builtin_eigen_model_info(Value *arg) {
diff --git a/src/model_io.c b/src/model_io.c
index 9be74175f..16dd37c6a 100644
--- a/src/model_io.c
+++ b/src/model_io.c
@@ -64,21 +64,32 @@ static void replay_model_load_banner(Value *status) {
Value *layers = dict_get_view(root, "n_layers", &layers_view);
Value *d_model = dict_get_view(root, "d_model", &d_model_view);
Value *d_ff = dict_get_view(root, "d_ff", &d_ff_view);
- if (vocab && vocab->type == VAL_NUM && layers && layers->type == VAL_NUM &&
- d_model && d_model->type == VAL_NUM && d_ff && d_ff->type == VAL_NUM) {
- g_model.config.vocab_size = (int)eigs_num_arg(vocab, __func__);
- g_model.config.n_layers = (int)eigs_num_arg(layers, __func__);
- g_model.config.d_model = (int)eigs_num_arg(d_model, __func__);
- g_model.config.d_ff = (int)eigs_num_arg(d_ff, __func__);
+ if (vocab && val_is_number(vocab) && layers && val_is_number(layers) &&
+ d_model && val_is_number(d_model) && d_ff && val_is_number(d_ff)) {
+ int64_t vs, nl, dm, df, ms = INT_MAX;
+ /* The status carries dimensions, not continuous values. Apply N5
+ * before the C conversion, with the loader's supported bounds. Do
+ * not install a partially decoded configuration after a refusal. */
+ int valid = eigs_int_arg_range(vocab, 1, VOCAB_SIZE, &vs,
+ "eigen_model_load replay vocab_size") &&
+ eigs_int_arg_range(layers, 1, MAX_LAYERS, &nl,
+ "eigen_model_load replay n_layers") &&
+ eigs_int_arg_range(d_model, 1, MAX_D_MODEL, &dm,
+ "eigen_model_load replay d_model") &&
+ eigs_int_arg_range(d_ff, 1, MAX_D_FF, &df,
+ "eigen_model_load replay d_ff");
/* max_seq_len is added to statuses below. Older statuses still carry
* the vocabulary; defer their context decision to the taped outcome. */
EIGS_VIEW(max_seq_view);
Value *max_seq = dict_get_view(root, "max_seq_len", &max_seq_view);
- if (max_seq && max_seq->type == VAL_NUM)
- g_model.config.max_seq_len = (int)eigs_num_arg(max_seq, __func__);
- else
- g_model.config.max_seq_len = INT_MAX;
- g_model.loaded = 1;
+ if (valid && max_seq && val_is_number(max_seq))
+ valid = eigs_int_arg_range(max_seq, 1, MAX_SEQ_LEN, &ms,
+ "eigen_model_load replay max_seq_len");
+ if (valid) {
+ ModelConfig cfg = { (int)vs, (int)dm, (int)nl, (int)df, (int)ms };
+ g_model.config = cfg;
+ g_model.loaded = 1;
+ }
}
val_decref(root);
}
diff --git a/src/model_train.c b/src/model_train.c
index e361e71f5..2a9676296 100644
--- a/src/model_train.c
+++ b/src/model_train.c
@@ -1460,9 +1460,10 @@ Value* builtin_native_train_step(Value *arg) {
}
float lr = 0.001f;
- if (list_elem_type(arg, 2) == VAL_BOOL) { eigs_slot_num(list_slot(arg, 2), __func__); return make_null(); } /* #1637 */
- if (list_elem_type(arg, 2) == VAL_NUM) lr = eigs_list_num(arg, 2, __func__);
+ if (list_elem_type(arg, 2) == VAL_BOOL) { eigs_slot_float(list_slot(arg, 2), "native_train_step"); return make_null(); } /* #1637 */
+ if ((list_elem_type(arg, 2) == VAL_FLOAT || list_elem_type(arg, 2) == VAL_INT)) lr = eigs_list_float(arg, 2, "native_train_step");
else if (list_elem_type(arg, 2) == VAL_STR) lr = strtod(list_get_ref(arg, 2)->data.str, NULL);
+ if (g_has_error) return make_null();
if (lr <= 0 || lr > 1) lr = 0.001f;
int input_len = in_list->data.list.count;
@@ -1477,7 +1478,12 @@ Value* builtin_native_train_step(Value *arg) {
int *input_ids = xcalloc(input_len > 0 ? input_len : 1, sizeof(int));
int *output_ids = xcalloc(output_len > 0 ? output_len : 1, sizeof(int));
for (int i = 0; i < input_len; i++) {
- input_ids[i] = (int)eigs_slot_num(list_slot(in_list, i), "native_train_step"); /* #1637: a bool raises */
+ int64_t token;
+ if (!eigs_slot_int_range(list_slot(in_list, i), INT_MIN, INT_MAX,
+ &token, "native_train_step")) {
+ free(input_ids); free(output_ids); return make_null();
+ }
+ input_ids[i] = (int)token; /* #1637: a bool raises */
if (!g_has_error && (input_ids[i] < 0 || input_ids[i] >= g_model.config.vocab_size)) {
rt_error(EK_INDEX, 0,
"native_train_step: token id %d at input index %d out of range [0,%d)",
@@ -1486,7 +1492,12 @@ Value* builtin_native_train_step(Value *arg) {
}
}
for (int i = 0; i < output_len && !g_has_error; i++) {
- output_ids[i] = (int)eigs_slot_num(list_slot(out_list, i), "native_train_step");
+ int64_t token;
+ if (!eigs_slot_int_range(list_slot(out_list, i), INT_MIN, INT_MAX,
+ &token, "native_train_step")) {
+ free(input_ids); free(output_ids); return make_null();
+ }
+ output_ids[i] = (int)token;
if (!g_has_error && (output_ids[i] < 0 || output_ids[i] >= g_model.config.vocab_size)) {
rt_error(EK_INDEX, 0,
"native_train_step: token id %d at output index %d out of range [0,%d)",
diff --git a/src/opcode_operands.h b/src/opcode_operands.h
index a60373f06..c8678f7b5 100644
--- a/src/opcode_operands.h
+++ b/src/opcode_operands.h
@@ -18,7 +18,7 @@ typedef enum {
* switch is the single operand-role table used by every bytecode walker. */
static inline int op_verify_operands(uint8_t op8, VerifyRole roles[3]) {
switch ((OpCode)op8) {
- case OP_CONST:
+ case OP_CONST: case OP_INT_CONST:
roles[0] = VR_CONST; return 1;
case OP_GET_NAME: case OP_SET_NAME: case OP_SET_NAME_LOCAL:
case OP_SET_FN_NAME_LOCAL: case OP_DOT_GET: case OP_DOT_SET:
@@ -63,7 +63,8 @@ static inline int op_verify_operands(uint8_t op8, VerifyRole roles[3]) {
case OP_LOCAL_IDX_DOT_GET: case OP_LOCAL_IDX_DOT_SET:
roles[0] = VR_RAW; roles[1] = VR_RAW; roles[2] = VR_NAME; return 3;
case OP_NULL: case OP_NUM_ZERO: case OP_NUM_ONE: case OP_TRUE: case OP_FALSE:
- case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: case OP_MOD:
+ case OP_INT_ZERO: case OP_INT_ONE:
+ case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: case OP_MOD: case OP_IDIV:
case OP_BAND: case OP_BOR: case OP_BXOR: case OP_SHL: case OP_SHR:
case OP_NEG: case OP_NOT: case OP_BNOT:
case OP_EQ: case OP_NE: case OP_LT: case OP_GT: case OP_LE: case OP_GE:
diff --git a/src/parser.c b/src/parser.c
index f51651507..5436ffc3d 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -558,7 +558,7 @@ void free_ast(ASTNode *node) {
}
static int is_compound_assign(TokType t) {
- return t >= TOK_PLUS_EQ && t <= TOK_SHR_EQ;
+ return (t >= TOK_PLUS_EQ && t <= TOK_SHR_EQ) || t == TOK_SLASH_SLASH_EQ;
}
static void compound_to_op(TokType t, char op[4]) {
@@ -568,6 +568,7 @@ static void compound_to_op(TokType t, char op[4]) {
case TOK_MINUS_EQ: op[0] = '-'; break;
case TOK_STAR_EQ: op[0] = '*'; break;
case TOK_SLASH_EQ: op[0] = '/'; break;
+ case TOK_SLASH_SLASH_EQ: op[0] = '/'; op[1] = '/'; break;
case TOK_PERCENT_EQ: op[0] = '%'; break;
case TOK_AMP_EQ: op[0] = '&'; break;
case TOK_BITOR_EQ: op[0] = '|'; break;
@@ -829,7 +830,7 @@ static ASTNode* parse_primary(Parser *p) {
if (t->type == TOK_NUM) {
p_advance(p);
ASTNode *n = make_node(AST_NUM, t->line);
- n->data.num = t->num_val;
+ n->data.number = t->number;
while (p_cur(p)->type == TOK_LBRACKET) {
if (chain_too_deep(p)) break;
n = parse_subscript_suffix(p, n);
@@ -856,7 +857,7 @@ static ASTNode* parse_primary(Parser *p) {
if (t->type == TOK_TRUE || t->type == TOK_FALSE) { /* #1637 */
p_advance(p);
ASTNode *b = make_node(AST_BOOL, t->line);
- b->data.num = (t->type == TOK_TRUE) ? 1.0 : 0.0;
+ b->data.boolean = (t->type == TOK_TRUE);
return b;
}
@@ -1202,11 +1203,13 @@ static ASTNode* parse_unary_body(Parser *p) {
static ASTNode* parse_multiply(Parser *p) {
ASTNode *left = parse_unary(p);
- while (p_cur(p)->type == TOK_STAR || p_cur(p)->type == TOK_SLASH || p_cur(p)->type == TOK_PERCENT) {
+ while (p_cur(p)->type == TOK_STAR || p_cur(p)->type == TOK_SLASH ||
+ p_cur(p)->type == TOK_SLASH_SLASH || p_cur(p)->type == TOK_PERCENT) {
if (chain_too_deep(p)) break;
char op[4] = {0};
if (p_cur(p)->type == TOK_STAR) op[0] = '*';
else if (p_cur(p)->type == TOK_SLASH) op[0] = '/';
+ else if (p_cur(p)->type == TOK_SLASH_SLASH) { op[0] = '/'; op[1] = '/'; }
else op[0] = '%';
Token *op_tok = p_advance(p);
ASTNode *right = parse_unary(p);
diff --git a/src/task.c b/src/task.c
index 5a1134bc1..ebfece58c 100644
--- a/src/task.c
+++ b/src/task.c
@@ -254,9 +254,9 @@ static uint64_t sched_rng_next(TaskScheduler *s) {
* the scheduler exists so the seed sticks even when set before the first
* task_spawn. The interleaving stays deterministic — a DST varies the seed to
* explore different interleavings, each fully reproducible. */
-void task_sched_set_seed(double seed) {
+void task_sched_set_seed(int64_t seed) {
TaskScheduler *s = sched_ensure();
- s->rng_state = (uint64_t)(int64_t)seed; /* integer seeds; fractions truncate */
+ s->rng_state = (uint64_t)seed; /* N5 validated by task_sched_seed */
s->seeded = 1;
}
@@ -311,9 +311,9 @@ Value *task_sched_trace_read(void) {
for (int i = 0; i < s->trace_count; i++) {
const SchedTraceEntry *e = &s->trace[i];
Value *d = make_dict(4);
- dict_set_owned(d, "seq", make_num((double)i));
- dict_set_owned(d, "tick", make_num(e->tick));
- dict_set_owned(d, "task", make_num(e->task));
+ dict_set_owned(d, "seq", make_int(i));
+ dict_set_owned(d, "tick", make_float(e->tick));
+ dict_set_owned(d, "task", make_int((int64_t)e->task));
dict_set_owned(d, "cause", make_str(e->cause < SCAUSE__COUNT
? sched_cause_name[e->cause] : "?"));
list_append_owned(out, d);
@@ -581,7 +581,7 @@ int task_do_kill(int tid) {
Value *ev = make_dict(3);
dict_set_owned(ev, "kind", make_str(err_kind_name(EK_INTERRUPT)));
dict_set_owned(ev, "message", make_str("task was killed"));
- dict_set_owned(ev, "line", make_num(0));
+ dict_set_owned(ev, "line", make_int(0));
t->error_value = ev;
}
for (int i = 1; i < HANDLE_TABLE_SIZE; i++) {
@@ -686,7 +686,7 @@ static void task_error_summary(Value *ev, char *out, size_t cap) {
if (msg && msg->type == VAL_STR) {
Value *line = dict_get_view(ev, "line", &lv);
/* The checking accessor (#1637): the type is proven, so it cannot raise. */
- double ln = (line && line->type == VAL_NUM) ? eigs_num_arg(line, __func__) : 0;
+ double ln = (line && val_is_number(line)) ? eigs_float_arg(line, __func__) : 0;
if (ln > 0)
snprintf(out, cap, "line %.0f: %s", ln, msg->data.str);
else
diff --git a/src/trace.c b/src/trace.c
index c34c3111d..d46b5adce 100644
--- a/src/trace.c
+++ b/src/trace.c
@@ -13,7 +13,7 @@
* O win per-binding observer window override (v3)
*
* Value encoding:
- * numeric (immediate, tracked, or heap VAL_NUM)
+ * numeric (immediate, tracked, or heap VAL_FLOAT)
* null VAL_NULL / immediate null slot
* true|false
* "" heap string, content truncated at TRACE_STR_MAX
@@ -737,7 +737,7 @@ int trace_query_at(int kind, const char *interned_name, int line, EigsSlot *out)
long long count = 0;
for (int i = 0; i < e->lc_count && e->lc[i].line <= line; i++)
count += e->lc[i].count;
- *out = slot_from_num((double)count);
+ *out = slot_from_int(count);
return 1;
}
@@ -752,7 +752,7 @@ int trace_query_at(int kind, const char *interned_name, int line, EigsSlot *out)
double r = (kind == 3) ? h->entropy
: (kind == 4) ? h->dH
: observer_settledness(h->dH); /* #412: how = f(dH) */
- *out = slot_from_num(r);
+ *out = slot_from_f64(r);
return 1;
}
@@ -801,7 +801,7 @@ int trace_query_when(int kind, const char *interned_name, long long ordinal,
* answerable without the entry, so it too only needs the range check. */
if (kind == 2) {
if (ordinal < 1 || ordinal > e->occ_total) return TRACE_WHEN_MISS;
- *out = slot_from_num((double)ordinal);
+ *out = slot_from_int(ordinal);
return TRACE_WHEN_HIT;
}
@@ -822,7 +822,7 @@ int trace_query_when(int kind, const char *interned_name, long long ordinal,
double r = (kind == 3) ? o->entropy
: (kind == 4) ? o->dH
: observer_settledness(o->dH); /* #412: how = f(dH) */
- *out = slot_from_num(r);
+ *out = slot_from_f64(r);
return TRACE_WHEN_HIT;
}
@@ -1584,11 +1584,16 @@ static void obs_cfg_sync(void) {
* %.17g fields in one call could silently truncate the record. */
emit_tag("O");
tp_puts("cfg ");
- tp_printf("%.17g ", binding->obs_dh_zero);
- tp_printf("%.17g ", binding->obs_dh_small);
- tp_printf("%.17g ", binding->obs_h_low);
+ char number[32];
+ eigs_num_text(number, sizeof number, binding->obs_dh_zero);
+ tp_printf("%s ", number);
+ eigs_num_text(number, sizeof number, binding->obs_dh_small);
+ tp_printf("%s ", number);
+ eigs_num_text(number, sizeof number, binding->obs_h_low);
+ tp_printf("%s ", number);
tp_printf("%d ", binding->obs_window);
- tp_printf("%.17g\n", binding->obs_scale);
+ eigs_num_text(number, sizeof number, binding->obs_scale);
+ tp_printf("%s\n", number);
}
/* `set_observer_window of ["x", n]` — the per-binding override, recorded at
@@ -1931,7 +1936,7 @@ static void replay_malformed_value(const char *value) {
* API declare their kinds with eigs_trace_declare_kind; a name in neither
* table is refused at replay. Bits are ValType (TK below). */
#define TK(t) (1u << (t))
-#define TK_NUM_ TK(VAL_NUM)
+#define TK_NUM_ (TK(VAL_FLOAT) | TK(VAL_INT))
#define TK_STR_ TK(VAL_STR)
#define TK_BOOL_ TK(VAL_BOOL)
#define TK_NULL_ TK(VAL_NULL)
@@ -2043,7 +2048,9 @@ static unsigned replay_expected_kinds(const char *fn) {
/* "a bool", "a list or null" */
static void kinds_text(unsigned kinds, char *buf, size_t n) {
- static const ValType order[] = {VAL_NUM, VAL_STR, VAL_BOOL, VAL_LIST, VAL_DICT,
+ /* Both numeric kinds have the one surface name, num. */
+ if (kinds & TK(VAL_INT)) kinds |= TK(VAL_FLOAT);
+ static const ValType order[] = {VAL_FLOAT, VAL_STR, VAL_BOOL, VAL_LIST, VAL_DICT,
VAL_BUFFER, VAL_NULL};
size_t used = 0; int first = 1;
buf[0] = '\0';
@@ -2294,7 +2301,8 @@ static int unescape_string(const char **p, char *out, int out_cap) {
*
* Recognized shapes:
* null | true | false immediates
- * strtod-parseable
+ * exact signed int64, no decimal point
+ * decimal point or exponent marks its kind
* "" escapes per unescape_string
* [v, v, …] list
* b[num, num, …] buffer (the leading 'b' disambiguates
@@ -2436,6 +2444,29 @@ static Value *parse_value_p(const char **p) {
* not replayable. Caller falls back to live source. */
if (c == '<' || (unsigned char)c == 0xE2) return NULL;
+ /* v8: a decimal digit token without a point/exponent is an int. Parse
+ * its magnitude exactly, including INT64_MIN, without routing through a
+ * double (9007199254740993 must retain its low bit on replay). */
+ const char *digits = *p;
+ int negative = *digits == '-';
+ if (negative) digits++;
+ const char *end_digits = digits;
+ while (*end_digits >= '0' && *end_digits <= '9') end_digits++;
+ if (end_digits != digits && at_token_boundary(*end_digits)) {
+ uint64_t limit = negative ? UINT64_C(9223372036854775808)
+ : UINT64_C(9223372036854775807);
+ uint64_t magnitude = 0;
+ for (const char *q = digits; q != end_digits; q++) {
+ unsigned digit = (unsigned)(*q - '0');
+ if (magnitude > (limit - digit) / 10) return NULL;
+ magnitude = magnitude * 10 + digit;
+ }
+ int64_t i = negative
+ ? -(int64_t)(magnitude - (magnitude != 0)) - (magnitude != 0)
+ : (int64_t)magnitude;
+ *p = end_digits;
+ return make_int(i);
+ }
char *end = NULL;
double d = strtod(*p, &end);
/* A genuine recording cannot contain NaN or infinity: num_guard rejects
@@ -2443,10 +2474,11 @@ static Value *parse_value_p(const char **p) {
* corrupt token at the common tape parser boundary, rather than exposing
* it to consumers (some of which legitimately convert numbers to C
* integer types). Returning NULL selects replay_malformed_value(), whose
- * diagnostic names the offending serialized value. */
- if (end == *p || !isfinite(d)) return NULL;
+ * diagnostic names the offending serialized value. Phase 2's non-finite
+ * tape tokens are scheduled in docs/design/phase2-numbers.md §8, R2.5. */
+ if (end == *p || !at_token_boundary(*end) || !isfinite(d)) return NULL;
*p = end;
- return make_num(d);
+ return make_float(d);
}
static Value *parse_value(const char *s) {
@@ -2792,7 +2824,7 @@ int trace_source_record(const char *kind, const char *source, size_t size,
Value *meta = make_list(3);
list_append_owned(meta, make_bool(0));
list_append_owned(meta, make_str(message));
- list_append_owned(meta, make_num(0));
+ list_append_owned(meta, make_int(0));
trace_nondet_value(kind, meta);
val_decref(meta);
rt_error(EK_IO, 0, "%s", message);
@@ -2801,7 +2833,7 @@ int trace_source_record(const char *kind, const char *source, size_t size,
Value *meta = make_list(3);
list_append_owned(meta, make_bool(error == NULL));
list_append_owned(meta, make_str(error ? error : (identity ? identity : "")));
- list_append_owned(meta, make_num(error ? 0.0 : (double)size));
+ list_append_owned(meta, make_int(error ? 0 : (int64_t)size));
trace_nondet_value(kind, meta);
val_decref(meta);
if (error) return 1;
@@ -2825,24 +2857,34 @@ int trace_source_replay(const char *kind, char **source, long *size,
if (!trace_replay_take(kind, &meta)) return -1;
if (!meta || meta->type != VAL_LIST || meta->data.list.count != 3 ||
list_elem_type(meta, 0) != VAL_BOOL || list_elem_type(meta, 1) != VAL_STR ||
- list_elem_type(meta, 2) != VAL_NUM) {
+ (list_elem_type(meta, 2) != VAL_INT && list_elem_type(meta, 2) != VAL_FLOAT)) {
val_decref(meta);
rt_error(EK_IO, 0, "%s: malformed source record", kind);
return -1;
}
int ok = list_get_ref(meta, 0)->data.boolean;
const char *detail = list_get_ref(meta, 1)->data.str;
- double taped_size = eigs_slot_elem_num(list_slot(meta, 2), __func__);
+ /* v8 metadata counts are exact ints. A float is refused with the
+ * existing numeric-size diagnostic, including an integral float. */
+ if (list_elem_type(meta, 2) != VAL_INT) {
+ double bad_size = 0.0;
+ slot_f64_value(list_slot(meta, 2), &bad_size);
+ val_decref(meta);
+ rt_error(EK_IO, 0, "%s: invalid source size %.17g (maximum %u)",
+ kind, bad_size, TRACE_SOURCE_MAX);
+ return -1;
+ }
+ int64_t taped_size = 0;
+ slot_int_value(list_slot(meta, 2), &taped_size);
if (!ok) {
*error = xstrdup(detail);
val_decref(meta);
return 1;
}
- if (!isfinite(taped_size) || taped_size < 0 ||
- taped_size > (double)TRACE_SOURCE_MAX || taped_size != floor(taped_size)) {
+ if (taped_size < 0 || taped_size > TRACE_SOURCE_MAX) {
val_decref(meta);
rt_error(EK_IO, 0, "%s: invalid source size %.17g (maximum %u)",
- kind, taped_size, TRACE_SOURCE_MAX);
+ kind, (double)taped_size, TRACE_SOURCE_MAX);
return -1;
}
long n = (long)taped_size;
@@ -2999,12 +3041,31 @@ static void write_string(const char *s) {
tp_putc('"');
}
+/* N6/T1: the point/exponent is the v8 float-kind marker. The shared
+ * shortest writer retains it for every integral float, including -0.0. */
+static void tape_float_text(char *buf, size_t nbuf, double n) {
+ eigs_num_text(buf, nbuf, n);
+}
+
+static void write_float(double n) {
+ char buf[32];
+ tape_float_text(buf, sizeof buf, n);
+ tp_puts(buf);
+}
+
+static void write_int(int64_t n) {
+ char buf[32];
+ eigs_int_text(buf, sizeof buf, n);
+ tp_puts(buf);
+}
+
/* Format a heap or tracked Value*. Numeric heap values unwrap to their
* number; collections show their size for at-a-glance scanning. */
static void write_value_ptr(Value *v) {
if (!v) { tp_puts("null"); return; }
switch (v->type) {
- case VAL_NUM: tp_printf("%.17g", VAL_NUM_RAW(v)); break;
+ case VAL_FLOAT: write_float(VAL_F64_RAW(v)); break;
+ case VAL_INT: write_int(VAL_I64_RAW(v)); break;
case VAL_NULL: tp_puts("null"); break;
case VAL_STR: write_string(v->data.str); break;
case VAL_LIST: tp_printf("", v->data.list.count); break;
@@ -3020,11 +3081,12 @@ static void write_value_ptr(Value *v) {
}
static void write_slot(EigsSlot s) {
- if (slot_is_num(s)) { tp_printf("%.17g", SLOT_NUM_RAW(s)); return; }
+ if (slot_is_f64(s)) { write_float(SLOT_F64_RAW(s)); return; }
if (slot_is_null(s)) { tp_puts("null"); return; }
if (slot_is_bool(s)) { tp_puts(slot_as_bool(s) ? "true" : "false"); return; }
if (slot_is_heap(s)) { write_value_ptr(slot_as_ptr(s)); return; }
- tp_puts("");
+ if (slot_is_smi(s)) { write_int(slot_smi_val(s)); return; }
+ eigs_slot_refuse("write_slot", s);
}
static void trace_assign_ex(const char *name, EigsSlot value, int filtered,
@@ -3146,7 +3208,18 @@ static int write_value_ptr_full(strbuf *buf, Value *v, int *budget,
}
WfAncestor here = {v, ancestors};
switch (v->type) {
- case VAL_NUM: wf_printf(buf, budget, "%.17g", VAL_NUM_RAW(v)); break;
+ case VAL_FLOAT: {
+ char number[32];
+ tape_float_text(number, sizeof number, VAL_F64_RAW(v));
+ wf_puts(buf, number, budget);
+ break;
+ }
+ case VAL_INT: {
+ char number[32];
+ eigs_int_text(number, sizeof number, VAL_I64_RAW(v));
+ wf_puts(buf, number, budget);
+ break;
+ }
case VAL_NULL: wf_puts(buf, "null", budget); break;
case VAL_BOOL: wf_puts(buf, v->data.boolean ? "true" : "false", budget); break;
case VAL_STR: write_string_full_len(buf, v->data.str, val_str_len(v), budget); break;
@@ -3184,7 +3257,9 @@ static int write_value_ptr_full(strbuf *buf, Value *v, int *budget,
int n = v->data.buffer.count;
for (int i = 0; i < n; i++) {
if (i) wf_puts(buf, ", ", budget);
- wf_printf(buf, budget, "%.17g", v->data.buffer.data[i]);
+ char number[32];
+ eigs_num_text(number, sizeof number, v->data.buffer.data[i]);
+ wf_puts(buf, number, budget);
}
wf_putc(buf, ']', budget);
break;
diff --git a/src/trace.h b/src/trace.h
index 1e63d6a7f..eb060b26d 100644
--- a/src/trace.h
+++ b/src/trace.h
@@ -20,7 +20,7 @@
* with the same sentinel value_slot.h uses to avoid a re-typedef. */
#ifndef EIGENSCRIPT_EIGSSLOT_UNION_DEFINED
#define EIGENSCRIPT_EIGSSLOT_UNION_DEFINED
-typedef union { double d_; uint64_t u; } EigsSlot; /* d_: read via SLOT_NUM_RAW (#1637) */
+typedef union { double d_; uint64_t u; } EigsSlot; /* d_: read via SLOT_F64_RAW (#1637) */
#endif
/* Tape format version (#411). Every tape's first line is a header record:
@@ -29,7 +29,7 @@ typedef union { double d_; uint64_t u; } EigsSlot; /* d_: read via SLOT_NUM_RA
* value serialization, escaping, truncation markers, the header itself.
* Replay refuses a tape whose format or runtime version differs from the
* running binary: version-and-reject, never migrate (docs/TRACE.md). */
-#define TRACE_FORMAT_VERSION 8 /* v8: length-framed, continued N records (#1816) */
+#define TRACE_FORMAT_VERSION 9 /* v9: combine phase2 v8 int/float kinds with main v8 continued N records; reject either older schema */
#define TRACE_STREAM_KEY_MAX 1024
/* B payload parser shared with the non-executing tape reader. key_hex points
@@ -351,7 +351,7 @@ int trace_query_prev(const char *interned_name, EigsSlot *out);
*
* `kind` mirrors the interrogative encoding:
* 0 (what), 6 (prev) → return historical slot
- * 2 (when) → return assignment count up to that line (as immediate num)
+ * 2 (when) → return exact int assignment count (SMI or owned box)
* 1 (who) → return binding name (string)
* 3/4/5 (where/why/how)→ observer snapshot captured at that assign
* (requires g_trace_obs_hist; miss returns 0)
diff --git a/src/value_slot.h b/src/value_slot.h
index b1ba7bbb0..f701ae840 100644
--- a/src/value_slot.h
+++ b/src/value_slot.h
@@ -1,15 +1,17 @@
/*
* EigenScript NaN-boxed slot encoding.
*
- * An EigsSlot is a 64-bit union aliasing a double. Numbers live as
+ * An EigsSlot is a 64-bit union aliasing a double. Floats live as
* immediate doubles. Pointers and other immediates encode into the
* unused QNaN-with-sign-set bit pattern, which num_guard() makes
* unreachable from user numbers (NaN -> 0, +/-Inf -> +/-1e308).
*
* Tag layout (high 16 bits):
+ * < 0xFFF8 : a double (slot_is_f64)
* 0xFFF8 tag 0 : NULL (payload ignored)
* 0xFFF9 tag 1 : BOOL (low bit = value)
- * 0xFFFA tag 2 : reserved (SMI) (reserved for future small-int)
+ * 0xFFFA tag 2 : SMI (an int in [-2^47, 2^47): 48-bit two's
+ * complement payload; #1758)
* 0xFFFB tag 3 : HEAP pointer (str/list/dict/fn/builtin/buffer/tb)
* 0xFFFC tag 4 : reserved (#262 Step E: was TRACKED — removed)
* 0xFFFD tag 5 : reserved
@@ -30,7 +32,7 @@
#ifndef EIGENSCRIPT_EIGSSLOT_UNION_DEFINED
#define EIGENSCRIPT_EIGSSLOT_UNION_DEFINED
-typedef union { double d_; uint64_t u; } EigsSlot; /* d_: read via SLOT_NUM_RAW (#1637) */
+typedef union { double d_; uint64_t u; } EigsSlot; /* d_: read via SLOT_F64_RAW (#1637) */
#endif
#define SLOT_QNAN_MASK 0xFFF8000000000000ULL
@@ -42,36 +44,89 @@ typedef union { double d_; uint64_t u; } EigsSlot; /* d_: read via SLOT_NUM_RA
#define SLOT_TRUE_BITS 0xFFF9000000000001ULL
#define TAG_BOOL 0xFFF9000000000000ULL
+#define TAG_SMI 0xFFFA000000000000ULL
#define TAG_HEAP 0xFFFB000000000000ULL
#define TAG_SENTINEL 0xFFFF000000000000ULL
+/* #1758: the SMI range, [-2^47, 2^47), and the payload's sign bit. */
+#define SLOT_SMI_MIN (-(INT64_C(1) << 47))
+#define SLOT_SMI_MAX ((INT64_C(1) << 47) - 1)
+#define SLOT_SMI_SIGN 0x0000800000000000ULL
+
+/* A slot reached a reader that does not handle its tag: a broken caller
+ * contract, never a program's fault. Prints the reader and the bits, then
+ * aborts (eigenscript.c). */
+#if defined(__GNUC__) || defined(__clang__)
+#define EIGS_SLOT_NORETURN __attribute__((noreturn, cold))
+#else
+#define EIGS_SLOT_NORETURN
+#endif
+EIGS_SLOT_NORETURN void eigs_slot_refuse(const char *who, EigsSlot s);
+/* slot_as_ptr refuses a slot that is not a heap pointer in sanitizer builds
+ * and under -DEIGS_SLOT_CHECKED (#1758 R1.1: an SMI must never be read as a
+ * pointer). Release keeps the bare mask: it is on every hot path. */
+#if defined(__SANITIZE_ADDRESS__) || defined(EIGS_SLOT_CHECKED)
+# define EIGS_SLOT_CHECKS 1
+#elif defined(__has_feature)
+# if __has_feature(address_sanitizer)
+# define EIGS_SLOT_CHECKS 1
+# endif
+#endif
+
+/* #1758 R1.3: the pre-flip source trap is retired now that SMI values are
+ * valid. Keep its build spelling for through-round gate invocations. */
+#define SLOT_SMI_TRAP(s) ((void)0)
+
/* Predicates */
-static inline int slot_is_num(EigsSlot s) {
+/* #1758: a double. Was slot_is_num; renamed so that every site that meant
+ * "a number" had to decide what an int (SMI) does there. */
+static inline int slot_is_f64(EigsSlot s) {
+ SLOT_SMI_TRAP(s);
return (s.u & SLOT_QNAN_MASK) != SLOT_QNAN_MASK;
}
-static inline int slot_is_null(EigsSlot s) { return s.u == SLOT_NULL_BITS; }
-static inline int slot_is_bool(EigsSlot s) { return (s.u & SLOT_TAG_MASK) == TAG_BOOL; }
-static inline int slot_is_heap(EigsSlot s) { return (s.u & SLOT_TAG_MASK) == TAG_HEAP; }
+static inline int slot_is_null(EigsSlot s) { SLOT_SMI_TRAP(s); return s.u == SLOT_NULL_BITS; }
+static inline int slot_is_bool(EigsSlot s) { SLOT_SMI_TRAP(s); return (s.u & SLOT_TAG_MASK) == TAG_BOOL; }
+static inline int slot_is_smi(EigsSlot s) { SLOT_SMI_TRAP(s); return (s.u & SLOT_TAG_MASK) == TAG_SMI; }
+static inline int slot_is_heap(EigsSlot s) { SLOT_SMI_TRAP(s); return (s.u & SLOT_TAG_MASK) == TAG_HEAP; }
/* #262 Step E: TAG_TRACKED is gone — a number is always an immediate, never a
* heap-boxed observer-tracked Value. Heap is the only pointer tag. */
-static inline int slot_is_ptr(EigsSlot s) { return (s.u & SLOT_TAG_MASK) == TAG_HEAP; }
-__attribute__((unused))
-static inline int slot_is_numeric(EigsSlot s) { return slot_is_num(s); }
+static inline int slot_is_ptr(EigsSlot s) { SLOT_SMI_TRAP(s); return (s.u & SLOT_TAG_MASK) == TAG_HEAP; }
/* Accessors (caller must check the predicate first) */
/* #1637 round 4: the ONE raw way into a slot's double (an lvalue), for a slot
- * proven a number (slot_is_num). Every use sits in a function listed in
+ * proven a double (slot_is_f64). Every use sits in a function listed in
* tools/num_read_allowlist.txt with its count (tools/num_read_check.sh). */
-#define SLOT_NUM_RAW(s) ((s).d_)
-static inline int slot_as_bool(EigsSlot s) { return (int)(s.u & 1ULL); }
-static inline Value *slot_as_ptr(EigsSlot s) { return (Value*)(uintptr_t)(s.u & SLOT_PAYLOAD_MASK); }
+#define SLOT_F64_RAW(s) ((s).d_)
+static inline int slot_as_bool(EigsSlot s) { SLOT_SMI_TRAP(s); return (int)(s.u & 1ULL); }
+static inline Value *slot_as_ptr(EigsSlot s) {
+ SLOT_SMI_TRAP(s);
+#ifdef EIGS_SLOT_CHECKS
+ if (__builtin_expect(!slot_is_heap(s), 0)) eigs_slot_refuse("slot_as_ptr", s);
+#endif
+ return (Value*)(uintptr_t)(s.u & SLOT_PAYLOAD_MASK);
+}
#define slot_as_heap slot_as_ptr
+/* The int an SMI holds, sign-extending payload bit 47 without an
+ * implementation-defined right shift (design 2.1). */
+static inline int64_t slot_smi_val(EigsSlot s) {
+ SLOT_SMI_TRAP(s);
+ return (int64_t)((s.u & SLOT_PAYLOAD_MASK) ^ SLOT_SMI_SIGN) - (int64_t)SLOT_SMI_SIGN;
+}
/* Constructors */
-static inline EigsSlot slot_from_num(double d) { EigsSlot s; SLOT_NUM_RAW(s) = d; return s; }
+/* #1758: the ONE double writer. Round 2 canonicalizes a NaN here. */
+static inline EigsSlot slot_from_f64(double d) { EigsSlot s; SLOT_F64_RAW(s) = d; return s; }
+static inline EigsSlot slot_from_heap(Value *v);
+/* #1758: the ONE int writer: an SMI inside [-2^47, 2^47), otherwise an
+ * owned VAL_INT heap box. */
+static inline EigsSlot slot_from_int(int64_t i) {
+ if (__builtin_expect(i < SLOT_SMI_MIN || i > SLOT_SMI_MAX, 0))
+ return slot_from_heap(make_int(i));
+ EigsSlot s; s.u = TAG_SMI | ((uint64_t)i & SLOT_PAYLOAD_MASK); return s;
+}
static inline EigsSlot slot_null(void) { EigsSlot s; s.u = SLOT_NULL_BITS; return s; }
static inline EigsSlot slot_true(void) { EigsSlot s; s.u = SLOT_TRUE_BITS; return s; }
static inline EigsSlot slot_false(void) { EigsSlot s; s.u = SLOT_FALSE_BITS; return s; }
@@ -102,6 +157,7 @@ static inline EigsSlot slot_from_heap(Value *v) {
* The outer `& SLOT_QNAN_MASK` branch peels off numbers in one
* predicted-not-taken test with zero memory access. */
static inline void slot_incref(EigsSlot s) {
+ SLOT_SMI_TRAP(s);
if ((s.u & SLOT_QNAN_MASK) == SLOT_QNAN_MASK) {
uint64_t tag = s.u & SLOT_TAG_MASK;
if (tag == TAG_HEAP) {
@@ -115,6 +171,7 @@ static inline void slot_incref(EigsSlot s) {
}
static inline void slot_decref(EigsSlot s) {
+ SLOT_SMI_TRAP(s);
if ((s.u & SLOT_QNAN_MASK) == SLOT_QNAN_MASK) {
uint64_t tag = s.u & SLOT_TAG_MASK;
if (tag == TAG_HEAP) {
@@ -137,22 +194,51 @@ static inline void slot_decref(EigsSlot s) {
}
/* Truthiness — used by JUMP_IF_FALSE / JUMP_IF_TRUE fast paths.
- * Immediate-number truthiness is a non-zero double; null and false
- * are falsy; everything else (heap, tracked) defers to caller-side
- * is_truthy() for now (Phase A keeps semantics identical). */
+ * Immediate-number truthiness is a non-zero double, or a non-zero int
+ * (#1758: an SMI is 0 exactly when its payload is 0); null and false
+ * are falsy; everything else (heap) defers to caller-side is_truthy(). */
__attribute__((unused))
static inline int slot_truthy_immediate(EigsSlot s, int *out_decided) {
- if (slot_is_num(s)) { *out_decided = 1; return SLOT_NUM_RAW(s) != 0.0; }
+ if (slot_is_f64(s)) { *out_decided = 1; return SLOT_F64_RAW(s) != 0.0; }
if (slot_is_null(s) || s.u == SLOT_FALSE_BITS) { *out_decided = 1; return 0; }
if (s.u == SLOT_TRUE_BITS) { *out_decided = 1; return 1; }
+ if (slot_is_smi(s)) { *out_decided = 1; return slot_smi_val(s) != 0; }
*out_decided = 0;
return 0;
}
+/* #1758: a float's double, from an immediate or a VAL_FLOAT box (the VM boxes
+ * numbers on purpose: the lifts, a for-in variable). 0 for anything else,
+ * an int (an SMI, later a VAL_INT box) included: the caller sends it to its
+ * slow path; *out is written only when the answer is 1. Replaces vm.c's
+ * slot_as_double.
+ * Leave it so: its callers sit in vm_run_ex's hot arms, and making it write
+ * *out on every path (0.0, or the slot's bits, on the failure path) changed
+ * vm_run_ex's code and cost +3.8% Ir on a JIT-off float loop, as much as
+ * initialising the callers' locals did (#1775, measured with callgrind). */
+static inline int slot_f64_value(EigsSlot s, double *out) {
+ if (slot_is_f64(s)) { *out = SLOT_F64_RAW(s); return 1; }
+ if (slot_is_heap(s)) {
+ Value *v = slot_as_ptr(s);
+ if (v->type == VAL_FLOAT) { *out = VAL_F64_RAW(v); return 1; } /* a VAL_INT box: 0 */
+ }
+ return 0;
+}
+/* #1758: an int's value. R1.1 knows the SMI; R1.2a adds the VAL_INT box
+ * arm with the type it names. */
+static inline int slot_int_value(EigsSlot s, int64_t *out) {
+ if (slot_is_smi(s)) { *out = slot_smi_val(s); return 1; }
+ if (slot_is_heap(s)) {
+ Value *v = slot_as_ptr(s);
+ if (v->type == VAL_INT) { *out = VAL_I64_RAW(v); return 1; }
+ }
+ return 0;
+}
+
/* Boundary shims declared here, defined in eigenscript.c.
*
* slot_from_value (takes ownership of the input ref):
- * - VAL_NUM -> immediate double, input decref'd (#262 Step E: numbers never
+ * - VAL_FLOAT -> immediate double, input decref'd (#262 Step E: numbers never
* carry observer state, so there is no tracked-pointer case).
* - VAL_NULL -> SLOT_NULL (incref skipped; null is borrowed).
* - any heap type -> TAG_HEAP pointer (incref).
diff --git a/src/vm.c b/src/vm.c
index 5e0aed9d4..bf91bf663 100644
--- a/src/vm.c
+++ b/src/vm.c
@@ -64,6 +64,16 @@ int vm_sandbox_work_charge(uint64_t amount) {
return 1;
}
+/* #1758 N4: floor modulo. Avoid a - floor(a/b)*b: a/b can overflow or
+ * round across an integer boundary. As in Lua's luai_nummod, correct fmod's
+ * truncating remainder to have the divisor's sign. The executor uses only
+ * this floor result, never the truncating remainder. */
+static double vm_floor_mod(double a, double b) {
+ double rem = fmod(a, b);
+ if (rem != 0.0 && ((rem < 0.0) != (b < 0.0))) rem += b;
+ return rem;
+}
+
/* #262 Phase-3 D: read the last-observed trajectory (dH, entropy) for the
* observer loop-stall check. Prefers the slot model (the default); falls back
* to the global last-observer Value when the slot isn't populated (e.g. a
@@ -115,13 +125,15 @@ static void obs_dump_num(double n, char *buf, size_t nbuf) {
* out-of-scope slot-value race class; a summary reads only the header
* words). No allocation. */
static void obs_dump_value(EigsSlot s, char *buf, size_t nbuf) {
- if (slot_is_num(s)) { obs_dump_num(SLOT_NUM_RAW(s), buf, nbuf); return; }
+ if (slot_is_f64(s)) { obs_dump_num(SLOT_F64_RAW(s), buf, nbuf); return; }
+ if (slot_is_smi(s)) { snprintf(buf, nbuf, "%lld", (long long)slot_smi_val(s)); return; } /* #1758: exact digits */
if (slot_is_null(s)) { snprintf(buf, nbuf, "null"); return; }
if (slot_is_bool(s)) { snprintf(buf, nbuf, "%s", slot_as_bool(s) ? "true" : "false"); return; }
if (slot_is_ptr(s)) {
Value *v = slot_as_ptr(s);
switch (v->type) {
- case VAL_NUM: obs_dump_num(VAL_NUM_RAW(v), buf, nbuf); break;
+ case VAL_FLOAT: obs_dump_num(VAL_F64_RAW(v), buf, nbuf); break;
+ case VAL_INT: eigs_int_text(buf, nbuf, VAL_I64_RAW(v)); break; /* #1758 R1.2a */
case VAL_STR: {
size_t o = 0, i;
buf[o++] = '"';
@@ -209,8 +221,10 @@ static void obs_dump_scope(const char *scope, Env *e, int shared,
* so refresh inline — observer_entropy_now would self-deadlock).
* The band classifies the same refreshed view. */
ObserverSlot qs = *os;
- if (slot_is_num(s)) {
- qs.entropy = observer_entropy_of_num(SLOT_NUM_RAW(s));
+ if (slot_is_f64(s)) {
+ qs.entropy = observer_entropy_of_num(SLOT_F64_RAW(s));
+ } else if (slot_is_smi(s)) { /* #1758: an int, as its double */
+ qs.entropy = observer_entropy_of_num((double)slot_smi_val(s));
} else if (slot_is_bool(s)) {
qs.entropy = compute_entropy(make_bool(slot_as_bool(s))); /* #1637 */
} else if (slot_is_ptr(s)) {
@@ -399,12 +413,23 @@ static int vm_slot_value_opaque(Env *e, int idx) {
* source line, and a binding assigned inside the block has no `used` slot at
* all, so a classifier-level check is never reached on the path that hangs. */
int observer_predicate_at(Env *e, int idx, int kind, int require_used) {
- if (vm_slot_value_opaque(e, idx)) return 0;
+ if (!e || idx < 0 || vm_slot_value_opaque(e, idx)) return 0;
+ /* The entropy walk can synchronize a live namespace and take #607;
+ * prepare it before the fold hold. The classification below may inspect
+ * eq_last's detached snapshot (S10-D1), whose ownership and window arrays
+ * must remain protected against a concurrent shared-binding update. */
+ double entropy;
+ int have_entropy = observer_entropy_now(e, idx, &entropy);
+ env_dump_lock(e);
const ObserverSlot *s = env_obs_slot(e, idx);
- if (!s) return 0;
- if (require_used && !s->used) return 0;
- ObserverSlot q = vm_slot_query_view(e, idx, s);
- return vm_slot_predicate(&q, (uint16_t)kind);
+ int result = 0;
+ if (s && (!require_used || s->used)) {
+ ObserverSlot q = *s;
+ if (have_entropy) q.entropy = entropy;
+ result = vm_slot_predicate(&q, (uint16_t)kind);
+ }
+ env_dump_unlock(e);
+ return result;
}
/* Phase 5: VM execution state (g_vm), loop-stall accounting
@@ -688,7 +713,7 @@ static inline void dict_set_cached(Value *dict, const char *key, uint32_t h, Val
}
/* Slot-aware dict set: for the DMG register-write hot path. If the dict
- * cache hits and the existing slot is an exclusive untracked VAL_NUM,
+ * cache hits and the existing slot is an exclusive untracked VAL_FLOAT,
* mutate data.num in place — no allocation, no refcount churn. Returns
* 1 if the in-place fast path fired; 0 means the caller must materialize
* and call dict_set_cached. */
@@ -703,7 +728,7 @@ dict_set_cached_immediate(Value *dict, const char *key, uint32_t h, double num)
/* Slot storage (#1665 arm D B10): a cache hit is a plain
* slot store of the already-guarded immediate, whatever the
* field held before (the old occupant is released). */
- return dict_value_rewrite_num(dict, ce->index, num);
+ return dict_value_rewrite_f64(dict, ce->index, num);
}
}
return 0;
@@ -783,9 +808,9 @@ static inline EigsSlot slot_bridge_wrap(Value *v) {
}
static inline Value *slot_bridge_unwrap(EigsSlot s) {
- if (slot_is_num(s)) {
+ if (slot_is_f64(s)) {
/* Materialize immediate -> fresh Value. Caller owns one ref. */
- return make_num(SLOT_NUM_RAW(s));
+ return make_float(SLOT_F64_RAW(s));
}
if (slot_is_null(s)) {
return make_null();
@@ -793,6 +818,8 @@ static inline Value *slot_bridge_unwrap(EigsSlot s) {
if (slot_is_bool(s)) {
return make_bool(slot_as_bool(s));
}
+ /* #1758: an int materializes through slot_to_value, the one SMI boxer. */
+ if (slot_is_smi(s)) return slot_to_value(s);
/* Heap or tracked: just unwrap the pointer (ref already in slot). */
return slot_as_ptr(s);
}
@@ -828,10 +855,13 @@ static inline Value *vm_slot_lift(int idx) {
EigsSlot s = g_vm.stack[idx];
if (slot_is_ptr(s)) return slot_as_ptr(s);
Value *v;
- if (slot_is_num(s)) v = make_num(SLOT_NUM_RAW(s));
+ if (slot_is_f64(s)) v = make_float(SLOT_F64_RAW(s));
else if (slot_is_null(s)) v = make_null();
else if (slot_is_bool(s)) v = make_bool(slot_as_bool(s));
- else v = make_null();
+ /* #1758: an int is boxed on purpose, like a double (design 2.1); a
+ * reserved tag used to become null here, silently. */
+ else if (slot_is_smi(s)) v = slot_to_value(s);
+ else eigs_slot_refuse("vm_slot_lift", s);
g_vm.stack[idx] = slot_from_heap(v);
return v;
}
@@ -859,26 +889,82 @@ static inline void vm_push_slot(EigsSlot s) {
}
/* Slot-aware truthiness — covers immediate num, null, bool, and falls
- * back to is_truthy() for heap/tracked. */
+ * back to is_truthy() for heap/tracked. #1758: an int is truthy when it is
+ * not 0, like a double; a reserved tag aborts instead of being dereferenced. */
static inline int slot_truthy(EigsSlot s) {
- if (slot_is_num(s)) return SLOT_NUM_RAW(s) != 0.0;
+ if (slot_is_f64(s)) return SLOT_F64_RAW(s) != 0.0;
if (slot_is_null(s)) return 0;
if (slot_is_bool(s)) return slot_as_bool(s);
- return is_truthy(slot_as_ptr(s));
+ if (slot_is_heap(s)) return is_truthy(slot_as_ptr(s));
+ if (slot_is_smi(s)) return slot_smi_val(s) != 0;
+ eigs_slot_refuse("slot_truthy", s);
}
-/* Pull a double out of a slot if it represents a number. Covers both
- * immediate doubles and heap/tracked VAL_NUMs (which still appear from
- * env loads and from the constant pool until B-3 flips those). */
-static inline int slot_as_double(EigsSlot s, double *out) {
- if (slot_is_num(s)) { *out = SLOT_NUM_RAW(s); return 1; }
- if (slot_is_ptr(s)) {
- Value *v = slot_as_ptr(s);
- if (v->type == VAL_NUM) { *out = VAL_NUM_RAW(v); return 1; }
+/* #1758 R1.3: floats keep the existing fast paths. Integer and mixed
+ * arithmetic is computed here, outside the JIT/leaf float executors. */
+static inline __attribute__((always_inline)) EigsSlot vm_int_arith(int64_t a, int64_t b, char op, int line) {
+ int64_t r;
+ int overflow;
+ if (op == '+') overflow = __builtin_add_overflow(a, b, &r);
+ else if (op == '-') overflow = __builtin_sub_overflow(a, b, &r);
+ else overflow = __builtin_mul_overflow(a, b, &r);
+ if (overflow) {
+ rt_error(EK_VALUE, line, "integer overflow in '%c'", op);
+ return slot_null();
+ }
+ return slot_from_int(r);
+}
+
+/* A number as an int64 for the bitwise operators (#1740, #1758 N8).
+ * An int never round-trips through double; float operands retain the exact
+ * integer-representation rule. Return -1 for a type error, 0 for value. */
+static int vm_bit_operand(EigsSlot s, int64_t *out) {
+ double f;
+ if (slot_int_value(s, out)) return 1;
+ if (slot_f64_value(s, &f)) return num_to_i64_exact(f, out);
+ return -1;
+}
+
+/* Exact N2 ordering; 2 means unordered, and 0 return means not numbers. */
+static int vm_number_compare(EigsSlot a, EigsSlot b, int *out) {
+ int64_t ai, bi;
+ double af, bf;
+ int ia = slot_int_value(a, &ai), ib = slot_int_value(b, &bi);
+ if (ia && ib) { *out = (ai > bi) - (ai < bi); return 1; }
+ if (ia && slot_f64_value(b, &bf)) { *out = eigs_cmp_int_float(ai, bf); return 1; }
+ if (ib && slot_f64_value(a, &af)) {
+ int c = eigs_cmp_int_float(bi, af);
+ *out = c == 2 ? 2 : -c;
+ return 1;
}
return 0;
}
+/* Iterator state is exact int metadata made by make_iter_state (S9-K1).
+ * Raw bytecode may supply floats too. Check the C range BEFORE conversion. */
+static int vm_iter_index(EigsSlot s, int *out) {
+ int64_t i;
+ if (slot_int_value(s, &i)) {
+ if (i < INT_MIN || i > INT_MAX) return 0;
+ *out = (int)i;
+ return 1;
+ }
+ double f;
+ if (slot_f64_value(s, &f)) {
+ if (!(f >= INT_MIN && f <= INT_MAX)) return 0;
+ *out = (int)f;
+ return 1;
+ }
+ return 0;
+}
+static int vm_iter_limit(EigsSlot s, int len) {
+ int i;
+ if (vm_iter_index(s, &i)) return i < len ? (i < 0 ? 0 : i) : len;
+ int64_t big;
+ if (slot_int_value(s, &big) && big < 0) return 0;
+ return len;
+}
+
static inline uint16_t read_u16(uint8_t *ip) {
return ip[0] | (ip[1] << 8);
}
@@ -923,7 +1009,7 @@ static int import_collision_first_report(const char *name) {
static Value *make_iter_state(Value *iterable) {
Value *state = make_list(3);
list_append(state, iterable);
- Value *idx = make_num(0);
+ Value *idx = make_int(0);
list_append(state, idx);
val_decref(idx);
/* #491: snapshot the length at loop entry (items[2]). ITER_NEXT bounds
@@ -936,7 +1022,7 @@ static Value *make_iter_state(Value *iterable) {
int snap = 0;
if (iterable && iterable->type == VAL_LIST) snap = iterable->data.list.count;
else if (iterable && iterable->type == VAL_BUFFER) snap = iterable->data.buffer.count;
- Value *slen = make_num((double)snap);
+ Value *slen = make_int(snap);
list_append(state, slen);
val_decref(slen);
return state;
@@ -1155,19 +1241,19 @@ static inline Env *env_resolve_store(Env *start, const char *name, uint32_t h,
/* #1063/#2016: the ONE implementation of "store TOS into fn-local slot", shared by
* CASE(SET_LOCAL) and the JIT's out-of-line path (jit_helper_set_local), so
* the two can never disagree. The in-place branch is load-bearing beyond
- * speed: g_last_observer may alias the exclusive VAL_NUM being overwritten,
+ * speed: g_last_observer may alias the exclusive VAL_FLOAT being overwritten,
* and a swap+decref would free it under the observer. A parameter arrives
* with assign_counts=1; every user SET_LOCAL advances that same counter, just
* as a name store does. Runtime bookkeeping stores pass count_assignment=0. */
-static inline void vm_store_local_slot(Env *e, int slot, EigsSlot tos,
+static inline __attribute__((always_inline)) void vm_store_local_slot(Env *e, int slot, EigsSlot tos,
int count_assignment) {
- if (slot_is_num(tos)) {
+ if (slot_is_f64(tos)) {
EigsSlot ex_s = e->values[slot];
if (slot_is_heap(ex_s)) {
Value *existing = slot_as_ptr(ex_s);
- if (existing && existing->type == VAL_NUM &&
+ if (existing && existing->type == VAL_FLOAT &&
existing->refcount == 1) {
- VAL_NUM_RAW(existing) = SLOT_NUM_RAW(tos);
+ VAL_F64_RAW(existing) = SLOT_F64_RAW(tos);
if (count_assignment && e->assign_counts)
e->assign_counts[slot]++;
return;
@@ -1250,8 +1336,9 @@ void jit_helper_set_name(EigsChunk *chunk, int idx) {
return;
}
env_set_local_pre_interned_slot(env_binding_home(start), name, chunk->intern_tbl, h, s);
+ /* Resolver outputs are initialized only on a successful lookup. */
Env *t2 = env_resolve_chain(start, name, h, &slot_idx, &depth);
- if (!g_vm_multithreaded && t2 == start) { /* #1171 */
+ if (!g_vm_multithreaded && t2 && t2 == start) { /* #1171 */
ic->starting_env = start;
ic->starting_ver = start->binding_version;
ic->target_ver = start->binding_version;
@@ -1281,7 +1368,7 @@ void jit_helper_set_name_local(EigsChunk *chunk, int idx) {
env_set_local_pre_interned_slot(start, name, chunk->intern_tbl, h, s);
int slot_idx, depth;
Env *target = env_resolve_chain(start, name, h, &slot_idx, &depth);
- if (!g_vm_multithreaded && target == start) { /* #1171 */
+ if (!g_vm_multithreaded && target && target == start) { /* #1171 */
ic->starting_env = start;
ic->starting_ver = start->binding_version;
ic->target_ver = start->binding_version;
@@ -1330,7 +1417,7 @@ void jit_helper_set_fn_name_local(EigsChunk *chunk, int idx) {
env_set_local_pre_interned_slot(target, name, chunk->intern_tbl, h, s);
int slot_idx, depth;
Env *resolved = env_resolve_chain(target, name, h, &slot_idx, &depth);
- if (!g_vm_multithreaded && resolved == target) { /* #1171 */
+ if (!g_vm_multithreaded && resolved && resolved == target) { /* #1171 */
ic->starting_env = target;
ic->starting_ver = target->binding_version;
ic->target_ver = target->binding_version;
@@ -1353,7 +1440,7 @@ int jit_helper_local_idx_get(int slot, int idx) {
int i = idx;
if (target->type == VAL_BUFFER) {
if (i < target->data.buffer.count) {
- vm_push_slot(slot_from_num(buffer_read_num(target, i)));
+ vm_push_slot(slot_from_f64(buffer_read_f64(target, i)));
} else {
rt_error(EK_INDEX, g_vm.current_line,
"buffer index %d out of range (length %d)",
@@ -1510,14 +1597,14 @@ void jit_helper_dot_set(EigsChunk *chunk, int name_idx) {
}
/* Slot fast path (mirrors CASE(LOCAL_DOT_SET)): an immediate-num
* write into an exclusive untracked num field mutates in place —
- * no make_num, no refcount churn. Was 2 make_num + 1.5 free_value
+ * no make_float, no refcount churn. Was 2 make_float + 1.5 free_value
* per DMG step via the vm_pop materialization below. */
EigsSlot val_s = g_vm.stack[g_vm.sp - 1];
EigsSlot tgt_s = g_vm.stack[g_vm.sp - 2];
- if (slot_is_num(val_s) && slot_is_ptr(tgt_s)) {
+ if (slot_is_f64(val_s) && slot_is_ptr(tgt_s)) {
Value *target = slot_as_ptr(tgt_s);
if (target->type == VAL_DICT &&
- dict_set_cached_immediate(target, key, h, SLOT_NUM_RAW(val_s))) {
+ dict_set_cached_immediate(target, key, h, SLOT_F64_RAW(val_s))) {
/* Same net stack effect as the slow path: pop val + target,
* push val (val is immediate — no refcount sides). */
g_vm.sp -= 1;
@@ -1584,8 +1671,8 @@ void jit_helper_local_dot_set(EigsChunk *chunk, int slot, int name_idx) {
if (chunk->const_hashes) chunk->const_hashes[name_idx] = h;
}
EigsSlot tos = g_vm.stack[g_vm.sp - 1];
- if (slot_is_num(tos) &&
- dict_set_cached_immediate(target, key, h, SLOT_NUM_RAW(tos))) {
+ if (slot_is_f64(tos) &&
+ dict_set_cached_immediate(target, key, h, SLOT_F64_RAW(tos))) {
return;
}
Value *val = vm_slot_lift(g_vm.sp - 1);
@@ -1629,8 +1716,26 @@ void jit_helper_observe_assign(EigsChunk *chunk, int name_idx) {
* (entropy: false 0.0, true 1.0 — compute_entropy's VAL_BOOL band). Callers
* test obs_slot_observable first: a heap pointer or a bool. Null is the only
* value still not observed. */
+/* #1758 R1.1: an SMI (or a reserved tag) is refused: every caller first
+ * routes a number to the numeric channel (obs_slot_num), so an int that got
+ * here would be silently unobserved. A BOXED int is a pointer and goes to
+ * observer_slot_update's Value arm (R1.2). */
static inline int obs_slot_observable(EigsSlot s) {
- return slot_is_ptr(s) || slot_is_bool(s);
+ if (slot_is_ptr(s) || slot_is_bool(s)) return 1;
+ if (slot_is_null(s) || slot_is_f64(s)) return 0;
+ eigs_slot_refuse("obs_slot_observable", s);
+}
+/* #1758: the observer's numeric channel takes a double -- a float's own, or
+ * an int's (exact: |SMI| < 2^47) until round 3 (design 7). 0 for anything
+ * that is not an immediate number. */
+static inline int obs_slot_num(EigsSlot s, double *out) {
+ if (slot_is_f64(s)) { *out = SLOT_F64_RAW(s); return 1; }
+ if (slot_is_smi(s)) { *out = (double)slot_smi_val(s); return 1; }
+ if (slot_is_heap(s) && slot_as_ptr(s)->type == VAL_INT) {
+ *out = (double)VAL_I64_RAW(slot_as_ptr(s));
+ return 1;
+ }
+ return 0;
}
static inline Value *obs_slot_value(EigsSlot s) {
return slot_is_bool(s) ? make_bool(slot_as_bool(s)) : slot_as_ptr(s);
@@ -1680,14 +1785,15 @@ void jit_helper_observe_assign_local(int slot) {
CallFrame *frame = &g_vm.frames[g_vm.frame_count - 1];
EigsSlot s = g_vm.stack[g_vm.sp - 1];
Env *e = frame->fn_env;
+ double d;
if (g_unobserved_depth != 0) {
/* #1049: elided — value-window sample only (mirrors the CASE body). */
- if (slot_is_num(s)) jit_sample_num_gated(e, slot, SLOT_NUM_RAW(s));
+ if (obs_slot_num(s, &d)) jit_sample_num_gated(e, slot, d);
else if (obs_slot_observable(s)) observer_slot_sample_gated(e, slot, obs_slot_value(s));
return;
}
- if (slot_is_num(s)) {
- observer_slot_update_num(e, slot, SLOT_NUM_RAW(s));
+ if (obs_slot_num(s, &d)) {
+ observer_slot_update_num(e, slot, d);
g_last_obs_slot_env = e; g_last_obs_slot_idx = slot;
} else if (obs_slot_observable(s)) {
observer_slot_update(e, slot, obs_slot_value(s));
@@ -1735,19 +1841,21 @@ void jit_helper_observe_name_post(EigsChunk *chunk, int name_idx) {
/* #262 Phase-3 D: TOS may be an immediate num (the default path no longer
* promotes observed names to tracked Values) or a heap value — observe the
* slot from whichever (#1637: a bool too). Skip null. */
- if (!slot_is_num(s) && !obs_slot_observable(s)) return;
+ double d;
+ int isnum = obs_slot_num(s, &d); /* #1758: a float or an int */
+ if (!isnum && !obs_slot_observable(s)) return;
const char *name = chunk->const_interns[name_idx];
uint32_t h = chunk->const_hashes ? chunk->const_hashes[name_idx] : 0;
if (h == 0) { h = env_hash_name(name); if (chunk->const_hashes) chunk->const_hashes[name_idx] = h; }
int oidx = -1, odepth = 0;
Env *oe = env_resolve_chain(frame->env, name, h, &oidx, &odepth);
if (oe && oidx >= 0 && g_unobserved_depth != 0) {
- if (slot_is_num(s)) observer_slot_sample_num_gated(oe, oidx, SLOT_NUM_RAW(s));
- else observer_slot_sample_gated(oe, oidx, obs_slot_value(s));
+ if (isnum) observer_slot_sample_num_gated(oe, oidx, d);
+ else observer_slot_sample_gated(oe, oidx, obs_slot_value(s));
return;
}
if (oe && oidx >= 0) {
- if (slot_is_num(s)) observer_slot_update_num(oe, oidx, SLOT_NUM_RAW(s));
+ if (isnum) observer_slot_update_num(oe, oidx, d);
else observer_slot_update(oe, oidx, obs_slot_value(s));
g_last_obs_slot_env = oe;
g_last_obs_slot_idx = oidx;
@@ -1776,11 +1884,11 @@ int jit_helper_iter_next(void) {
Value *state = slot_as_ptr(state_slot);
if (!state || state->type != VAL_LIST || state->data.list.count < 2)
return 1;
- EIGS_VIEW(iterable_view);
- Value *iterable = list_get_view(state, 0, &iterable_view);
+ /* Iterables are reference values; a numeric facade cannot be iterable. */
+ Value *iterable = list_get_ref(state, 0);
+ int idx;
+ if (!vm_iter_index(list_slot(state, 1), &idx)) return 1;
if (!iterable) return 1;
- if (list_elem_type(state, 1) != VAL_NUM) return 1;
- int idx = (int)eigs_slot_num(list_slot(state, 1), "for");
int len = 0;
if (iterable->type == VAL_LIST) len = iterable->data.list.count;
else if (iterable->type == VAL_BUFFER) len = iterable->data.buffer.count;
@@ -1788,23 +1896,23 @@ int jit_helper_iter_next(void) {
/* #491: cap by the entry-time snapshot (items[2]) — see make_iter_state.
* Mirrors CASE(ITER_NEXT). */
if (state->data.list.count >= 3) {
- EIGS_VIEW(snap_v_view);
- Value *snap_v = list_get_view(state, 2, &snap_v_view);
- if (snap_v && snap_v->type == VAL_NUM) {
- int snap = (int)VAL_NUM_RAW(snap_v);
- if (snap < len) len = snap;
- }
+ len = vm_iter_limit(list_slot(state, 2), len);
}
- if (idx >= len) return 1;
+ if ((unsigned)idx >= (unsigned)len) return 1;
EigsSlot elem;
if (iterable->type == VAL_BUFFER) {
- elem = slot_from_num(num_guard(buffer_read_num(iterable, idx)));
+ elem = slot_from_f64(num_guard(buffer_read_f64(iterable, idx)));
} else {
elem = slot_read_owned(list_slot(iterable, idx));
}
- /* In-place idx bump when the slot is an exclusive plain VAL_NUM
- * (matches the interpreter's NUM_REUSE fast path). */
- list_set_num(state, 1, (double)(idx + 1));
+ /* S9-K1-C: valid C-int positions and idx+1 fit in an SMI. Reuse the
+ * immediate slot in place. The raw-state fallback also stores a
+ * known canonical SMI and releases the replaced slot; it needs no
+ * generic storage normalization (which inhibits unrelated inlining). */
+ if (slot_is_smi(list_slot(state, 1)))
+ list_bump_smi(state, 1);
+ else
+ list_set_int(state, 1, (int64_t)idx + 1);
vm_push_slot(elem);
return g_has_error ? 2 : 0;
}
@@ -1858,15 +1966,15 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) {
break;
case OP_CONST: {
uint16_t idx = read_u16(ip); ip += 2;
- /* Scanner guarantees VAL_NUM. */
- mini[msp++] = slot_from_num(VAL_NUM_RAW(c->constants[idx]));
+ /* Scanner guarantees VAL_FLOAT. */
+ mini[msp++] = slot_from_f64(VAL_F64_RAW(c->constants[idx]));
break;
}
case OP_NUM_ZERO:
- mini[msp++] = slot_from_num(0.0);
+ mini[msp++] = slot_from_f64(0.0);
break;
case OP_NUM_ONE:
- mini[msp++] = slot_from_num(1.0);
+ mini[msp++] = slot_from_f64(1.0);
break;
case OP_GET_LOCAL: {
uint16_t slot = read_u16(ip); ip += 2;
@@ -1901,9 +2009,11 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) {
EigsSlot idx_s = mini[--msp];
EigsSlot tgt_s = mini[--msp];
double idx_d;
- /* slot_as_double: immediate OR boxed VAL_NUM index — a for-in
- * loop variable arrives as a heap num, same as ARITH_FAST. */
- if (!slot_as_double(idx_s, &idx_d) || !slot_is_ptr(tgt_s)) return 0;
+ /* slot_f64_value: immediate OR boxed VAL_FLOAT index — a for-in
+ * loop variable arrives as a heap num, same as ARITH_FAST.
+ * #1758: an int index is not a float: bail, and the generic call
+ * indexes with it (design 4.5; round 5 adds the SMI index). */
+ if (!slot_f64_value(idx_s, &idx_d) || !slot_is_ptr(tgt_s)) return 0;
Value *target = slot_as_ptr(tgt_s);
int i;
if (!vm_index_is_int(idx_d, &i)) return 0;
@@ -1912,12 +2022,13 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) {
mini[msp++] = slot_read_value(list_slot(target, i)); /* borrow, no incref */
} else if (target->type == VAL_BUFFER) {
if (!vm_index_resolve(&i, target->data.buffer.count)) return 0;
- /* #1417: this speculative evaluator has no callee frame and
- * skips OP_LINE. Let the generic call report a strict NaN at
- * the indexed expression, before the guard can raise here. */
+ /* R1.6d-D1: this speculative evaluator has no callee frame
+ * and skips OP_LINE. Every non-finite cell must fall back
+ * before a reader can raise, so the generic call diagnoses
+ * the indexed expression with the same kind/message/line. */
double raw = target->data.buffer.data[i];
- if (g_strict && raw != raw) return 0;
- mini[msp++] = slot_from_num(buffer_read_num(target, i));
+ if (!isfinite(raw)) return 0;
+ mini[msp++] = slot_from_f64(buffer_read_f64(target, i));
} else {
return 0;
}
@@ -1925,8 +2036,11 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) {
}
case OP_ADD: case OP_SUB: case OP_MUL: {
double a, b;
- if (!slot_as_double(mini[msp - 2], &a) ||
- !slot_as_double(mini[msp - 1], &b))
+ /* #1758: both operands must be floats. An int or mixed operation
+ * bails, so the generic path computes it with the caller's line
+ * (design 4.5; round 5 adds int arms whose overflow bails). */
+ if (!slot_f64_value(mini[msp - 2], &a) ||
+ !slot_f64_value(mini[msp - 1], &b))
return 0;
double r;
switch (ip[-1]) {
@@ -1944,7 +2058,7 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) {
break;
}
msp--;
- mini[msp - 1] = slot_from_num(num_guard(r));
+ mini[msp - 1] = slot_from_f64(num_guard(r));
break;
}
case OP_RETURN: {
@@ -1975,16 +2089,14 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) {
* mutates sp net -1). */
/* Stage 5c: cached writer for the per-iteration __loop_iterations__
* update. env_set_local resolves the 19-char name (hash + table walk)
- * and round-trips through make_num/val_decref on EVERY iteration of
- * EVERY `loop while` — measured as the dominant cost of observed loops
- * once the loop body itself compiles to native code. Cache the resolved
- * slot per (env, binding_version); on a hit with an immediate-num slot
- * (always, after the first write) the store is a bare double overwrite
- * plus the same assign_counts bump env_set_local_hashed performs.
+ * and round-trips through make_int/val_decref on every uncached iteration.
+ * Cache the resolved slot per (env, binding_version); an SMI count stays
+ * an immediate overwrite plus the same assign_counts bump
+ * env_set_local_hashed performs. S9-K1 (#1758) makes the published count
+ * an exact int; counts beyond the SMI range use the owned-box setter.
* binding_version moves on new-binding adds (which can realloc values[])
* and env recycles — exactly the events that could invalidate the index.
- * Semantics are unchanged: mid-loop reads of __loop_iterations__ see the
- * same value as before. */
+ * Mid-loop reads see the current cumulative count, now as an exact int. */
typedef struct {
Env *env;
uint64_t version; /* #1674 round 2: 64-bit, matches Env.binding_version */
@@ -2020,7 +2132,7 @@ void vm_thread_reset_caches(void) {
memset(&g_loop_iter_cache, 0, sizeof(g_loop_iter_cache));
}
-static void loop_iter_store(Env *env, double n) {
+static void loop_iter_store(Env *env, int64_t n) {
/* #1171: runtime-owned publication can target a shared root or module
* env even when user code shares no writable binding. Keep the slot
* replacement and assignment count under the existing env lock. The
@@ -2028,27 +2140,28 @@ static void loop_iter_store(Env *env, double n) {
* return before either path; a thread-local cache is not a slot lock. */
if (__builtin_expect(g_vm_multithreaded, 0) &&
__atomic_load_n(&env->mt_shared, __ATOMIC_RELAXED)) {
- Value *iter_val = make_num(n);
+ Value *iter_val = make_int(n);
env_set_local(env, "__loop_iterations__", iter_val);
val_decref(iter_val);
return;
}
LoopIterCache *c = &g_loop_iter_cache;
if (c->env == env && c->version == env->binding_version &&
- slot_is_num(env->values[c->slot])) {
- SLOT_NUM_RAW(env->values[c->slot]) = n;
+ slot_is_smi(env->values[c->slot]) &&
+ n >= SLOT_SMI_MIN && n <= SLOT_SMI_MAX) {
+ env->values[c->slot] = slot_from_int(n);
if (env->assign_counts)
env->assign_counts[c->slot]++;
return;
}
- Value *iter_val = make_num(n);
+ Value *iter_val = make_int(n);
env_set_local(env, "__loop_iterations__", iter_val);
val_decref(iter_val);
int slot_idx, depth;
Env *target = env_resolve_chain(env, "__loop_iterations__",
env_hash_name("__loop_iterations__"),
&slot_idx, &depth);
- if (target == env && depth == 0) {
+ if (target && target == env && depth == 0) {
c->env = env;
c->version = env->binding_version;
c->slot = slot_idx;
@@ -2067,13 +2180,22 @@ static EigsSlot *loop_counter_slot(CallFrame *frame, uint16_t depth) {
return NULL;
}
-static long long loop_counter_load(EigsSlot *slot) {
- return slot && slot_is_num(*slot) ? (long long)eigs_slot_num(*slot, __func__) : 0;
+static int64_t loop_counter_load(EigsSlot *slot) {
+ int64_t n;
+ if (slot && slot_int_value(*slot, &n)) return n;
+ rt_error(EK_INTERNAL, g_vm.current_line, "private loop counter is not an int");
+ return 0;
}
-static void loop_counter_store(EigsSlot *slot, long long n) {
+static void loop_counter_store(EigsSlot *slot, int64_t n) {
+ /* S9-K1/R1.8: counters remain exact ints; common SMI updates need no
+ * generic slot normalization or ownership work. */
+ if (slot_is_smi(*slot) && n >= SLOT_SMI_MIN && n <= SLOT_SMI_MAX) {
+ *slot = slot_from_int(n);
+ return;
+ }
slot_decref(*slot);
- *slot = slot_from_num((double)n);
+ *slot = slot_from_int(n);
}
static void eigs_loop_enter_named(Env *env, CallFrame *frame, uint16_t counter_depth) {
@@ -2083,14 +2205,14 @@ static void eigs_loop_enter_named(Env *env, CallFrame *frame, uint16_t counter_d
(unsigned)counter_depth, g_vm.sp - frame->bp);
return;
}
- vm_push_slot(slot_from_num(0.0));
+ vm_push_slot(slot_from_int(0));
g_loop_iterations = 0;
- loop_iter_store(env, 0.0);
+ loop_iter_store(env, 0);
}
void eigs_loop_enter(Env *env) {
g_loop_iterations = 0;
- loop_iter_store(env, 0.0);
+ loop_iter_store(env, 0);
}
/* Stage 4w: out-of-line helper for OP_LOOP_STALL_CHECK. Mirrors the
@@ -2153,7 +2275,7 @@ static int eigs_loop_stall_step_named(Env *e, CallFrame *frame, uint16_t counter
g_sandbox_cap_hit = 1; /* a capped run must not report ok (#960-adjacent) */
should_exit = 1;
}
- loop_iter_store(e, (double)g_loop_iterations);
+ loop_iter_store(e, g_loop_iterations);
if (should_exit) {
Value *exit_val = make_str(g_loop_exit_reason);
env_set_local(e, "__loop_exit__", exit_val);
@@ -2181,7 +2303,7 @@ static int eigs_loop_cap_step_named(Env *e, CallFrame *frame, uint16_t counter_d
g_sandbox_cap_hit = 1;
should_exit = 1;
}
- loop_iter_store(e, (double)g_loop_iterations);
+ loop_iter_store(e, g_loop_iterations);
if (should_exit) {
Value *exit_val = make_str(g_loop_exit_reason);
env_set_local(e, "__loop_exit__", exit_val);
@@ -2220,11 +2342,38 @@ void jit_helper_loop_exit(uint16_t counter_depth) {
CallFrame *f = &g_vm.frames[g_vm.frame_count - 1];
EigsSlot *counter = loop_counter_slot(f, counter_depth);
if (!counter) return;
- loop_iter_store(f->env, (double)loop_counter_load(counter));
+ loop_iter_store(f->env, loop_counter_load(counter));
while (g_vm.sp > f->bp + (int)counter_depth)
slot_decref(g_vm.stack[--g_vm.sp]);
}
+/* #1758 R1.2: an int subscript is its exact value in the VM's C-index
+ * domain. Floats keep the existing integrality test. No int exists yet. */
+static inline int vm_value_index_is_int(const Value *v, int *out) {
+ if (v->type == VAL_INT) {
+ int64_t i = VAL_I64_RAW(v);
+ if (i < INT_MIN || i > INT_MAX) return 0;
+ *out = (int)i;
+ return 1;
+ }
+ return vm_index_is_int(VAL_F64_RAW(v), out);
+}
+
+/* #1849 / Phase2: an exact int subscript outside the C index domain is a
+ * range error, just like an exact float. Clamp only the diagnostic-route
+ * endpoint; every container's bounds check refuses it before a read/write.
+ * Keep vm_value_index_is_int's separate AT/slice C-domain refusal unchanged. */
+static inline int vm_subscript_index_is_int(const Value *v, int *out) {
+ if (v->type == VAL_INT) {
+ int64_t i = VAL_I64_RAW(v);
+ if (i < INT_MIN) *out = INT_MIN;
+ else if (i > INT_MAX) *out = INT_MAX;
+ else *out = (int)i;
+ return 1;
+ }
+ return vm_index_is_int(VAL_F64_RAW(v), out);
+}
+
/* Stage 4v: out-of-line helper for OP_INDEX_SET. Pops 3 (val, idx,
* target), pushes val back (net sp -2). Full opcode semantics including
* the slot fast paths, so the emitter never bails; errors go through
@@ -2235,14 +2384,17 @@ void jit_helper_index_set(void) {
EigsSlot val_s = g_vm.stack[g_vm.sp - 1];
EigsSlot idx_s = g_vm.stack[g_vm.sp - 2];
EigsSlot tgt_s = g_vm.stack[g_vm.sp - 3];
- if (slot_is_num(idx_s) && slot_is_ptr(tgt_s)) {
+ if (slot_is_f64(idx_s) && slot_is_ptr(tgt_s)) {
Value *target = slot_as_ptr(tgt_s);
- int i = 0, _ok = vm_index_is_int(SLOT_NUM_RAW(idx_s), &i);
- if (target->type == VAL_BUFFER && slot_is_num(val_s)) {
+ /* #1775: `i` is read only when _ok, but GCC at -O1 cannot see that
+ * through vm_index_resolve; this is one of the three sites it flags,
+ * and the only initialisations kept (callgrind: none costs Ir). */
+ int i = 0, _ok = vm_index_is_int(SLOT_F64_RAW(idx_s), &i);
+ if (target->type == VAL_BUFFER && slot_is_f64(val_s)) {
if (_ok && vm_index_resolve(&i, target->data.buffer.count)) {
- target->data.buffer.data[i] = SLOT_NUM_RAW(val_s);
+ target->data.buffer.data[i] = SLOT_F64_RAW(val_s);
} else {
- if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s));
+ if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_F64_RAW(idx_s));
else rt_error(EK_INDEX, g_vm.current_line, "buffer index %d out of range (length %d)",
i, target->data.buffer.count);
}
@@ -2252,12 +2404,12 @@ void jit_helper_index_set(void) {
slot_decref(tgt_s);
return;
}
- if (target->type == VAL_LIST && slot_is_num(val_s)) {
+ if (target->type == VAL_LIST && slot_is_f64(val_s)) {
if (_ok && vm_index_resolve(&i, target->data.list.count)) {
- double n = SLOT_NUM_RAW(val_s); /* already guarded in a slot */
- list_rewrite_num(target, i, n);
+ double n = SLOT_F64_RAW(val_s); /* already guarded in a slot */
+ list_rewrite_f64(target, i, n);
} else {
- if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s));
+ if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_F64_RAW(idx_s));
else rt_error(EK_INDEX, g_vm.current_line, "index %d out of range (list length %d)",
i, target->data.list.count);
}
@@ -2269,24 +2421,24 @@ void jit_helper_index_set(void) {
}
}
Value *val = vm_pop(); Value *idx = vm_pop(); Value *target = vm_pop();
- if (target->type == VAL_LIST && idx->type == VAL_NUM) {
+ if (target->type == VAL_LIST && val_is_number(idx)) {
int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i)) {
- rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
+ if (!vm_subscript_index_is_int(idx, &i)) {
+ rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", eigs_float_arg(idx, "index"));
} else if (vm_index_resolve(&i, target->data.list.count)) {
val_incref(val);
list_set_slot_owned(target, i, slot_from_heap(val));
} else {
rt_error(EK_INDEX, g_vm.current_line, "index %d out of range (list length %d)", i, target->data.list.count);
}
- } else if (target->type == VAL_BUFFER && idx->type == VAL_NUM) {
+ } else if (target->type == VAL_BUFFER && val_is_number(idx)) {
int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i)) {
- rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
+ if (!vm_subscript_index_is_int(idx, &i)) {
+ rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", eigs_float_arg(idx, "index"));
} else if (!vm_index_resolve(&i, target->data.buffer.count)) {
rt_error(EK_INDEX, g_vm.current_line, "buffer index %d out of range (length %d)", i, target->data.buffer.count);
- } else if (val->type == VAL_NUM) {
- target->data.buffer.data[i] = VAL_NUM_RAW(val);
+ } else if (val_is_number(val)) {
+ target->data.buffer.data[i] = eigs_float_arg(val, "buffer store");
} else {
/* #1061 -- mirror of CASE(INDEX_SET)'s buffer arm. */
rt_error(EK_TYPE, g_vm.current_line, "cannot store %s in a buffer (buffers hold numbers)",
@@ -2306,16 +2458,16 @@ int jit_helper_index_get(void) {
EigsSlot idx_s = g_vm.stack[g_vm.sp - 1];
EigsSlot tgt_s = g_vm.stack[g_vm.sp - 2];
g_vm.sp -= 2;
- if (slot_is_num(idx_s) && slot_is_ptr(tgt_s)) {
+ if (slot_is_f64(idx_s) && slot_is_ptr(tgt_s)) {
Value *target = slot_as_ptr(tgt_s);
- int i, _ok = vm_index_is_int(SLOT_NUM_RAW(idx_s), &i);
+ int i, _ok = vm_index_is_int(SLOT_F64_RAW(idx_s), &i);
if (target->type == VAL_LIST) {
if (_ok && vm_index_resolve(&i, target->data.list.count)) {
EigsSlot r = slot_read_owned(list_slot(target, i));
slot_decref(tgt_s);
vm_push_slot(r);
} else {
- if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s));
+ if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_F64_RAW(idx_s));
else rt_error(EK_INDEX, g_vm.current_line,
"index %d out of range (list length %d)",
i, target->data.list.count);
@@ -2326,11 +2478,11 @@ int jit_helper_index_get(void) {
}
if (target->type == VAL_BUFFER) {
if (_ok && vm_index_resolve(&i, target->data.buffer.count)) {
- double v = buffer_read_num(target, i);
+ double v = buffer_read_f64(target, i);
slot_decref(tgt_s);
- vm_push_slot(slot_from_num(v));
+ vm_push_slot(slot_from_f64(v));
} else {
- if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s));
+ if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_F64_RAW(idx_s));
else rt_error(EK_INDEX, g_vm.current_line,
"buffer index %d out of range (length %d)",
i, target->data.buffer.count);
@@ -2346,10 +2498,10 @@ int jit_helper_index_get(void) {
Value *target = slot_to_value(tgt_s);
slot_decref(tgt_s);
Value *result = make_null();
- if (target->type == VAL_LIST && idx->type == VAL_NUM) {
+ if (target->type == VAL_LIST && val_is_number(idx)) {
int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i)) {
- rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
+ if (!vm_subscript_index_is_int(idx, &i)) {
+ rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", eigs_float_arg(idx, "index"));
} else if (vm_index_resolve(&i, target->data.list.count)) {
result = list_get_owned(target, i);
} else {
@@ -2361,10 +2513,10 @@ int jit_helper_index_get(void) {
EigsSlot v = vm_dict_slot_cached(target, idx->data.str,
env_hash_name(idx->data.str));
if (!slot_is_null(v)) result = eigs_slot_box(v);
- } else if (target->type == VAL_STR && idx->type == VAL_NUM) {
+ } else if (target->type == VAL_STR && val_is_number(idx)) {
int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i)) {
- rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
+ if (!vm_subscript_index_is_int(idx, &i)) {
+ rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", eigs_float_arg(idx, "index"));
} else if (vm_index_resolve(&i, (int)val_str_len(target))) {
char buf[2] = { target->data.str[i], 0 };
result = make_str_len(buf, 1);
@@ -2373,12 +2525,12 @@ int jit_helper_index_get(void) {
"string index %d out of range (length %d)",
i, (int)val_str_len(target));
}
- } else if (target->type == VAL_BUFFER && idx->type == VAL_NUM) {
+ } else if (target->type == VAL_BUFFER && val_is_number(idx)) {
int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i))
- rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
+ if (!vm_subscript_index_is_int(idx, &i))
+ rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", eigs_float_arg(idx, "index"));
else if (vm_index_resolve(&i, target->data.buffer.count))
- result = make_num(buffer_read_num(target, i));
+ result = make_float(buffer_read_f64(target, i));
else
rt_error(EK_INDEX, g_vm.current_line,
"buffer index %d out of range (length %d)",
@@ -2485,7 +2637,7 @@ static inline void vm_borrow_scan(Value *arg, Value *result,
/* #720: the one place the builtin-result borrow protocol is written down.
*
* A builtin's raw return may BORROW from its argument vector with no
- * compensating incref: `num` returns its argument unchanged for VAL_NUM,
+ * compensating incref: `num` returns its argument unchanged for VAL_FLOAT,
* `sort` returns its arg, and `append` / `dict_set` / `set_at` / `coalesce`
* return their target — a direct child of the arg vector. Whoever receives
* that result must compensate, or it over-releases into a use-after-free.
@@ -2944,7 +3096,7 @@ static Value *vm_take_error_value(void) {
Value *d = make_dict(4);
dict_set_owned(d, "kind", make_str(err_kind_name((ErrKind)g_error_kind)));
dict_set_owned(d, "message", make_str(g_error_raw));
- dict_set_owned(d, "line", make_num((double)g_error_line));
+ dict_set_owned(d, "line", make_int(g_error_line));
dict_set_owned(d, "file", make_str(g_error_file));
return d;
}
@@ -3070,7 +3222,9 @@ static const char *slot_type_name(EigsSlot s) {
if (slot_is_null(s)) return "none";
if (slot_is_ptr(s)) { Value *v = slot_as_ptr(s); return v ? val_type_name(v->type) : "none"; }
if (slot_is_bool(s)) return "bool"; /* #1637 */
- return "num"; /* immediate double */
+ if (slot_is_f64(s)) return "num"; /* immediate double */
+ if (slot_is_smi(s)) return "num"; /* #1758 (B): an int's type is "num" too */
+ eigs_slot_refuse("slot_type_name", s);
}
/* #408: forward decls — the copying-stack save/restore and the "who is
@@ -3160,9 +3314,12 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
static void *dispatch_table[OP_COUNT] = {
[OP_CONST] = &&lbl_CONST, [OP_NULL] = &&lbl_NULL,
[OP_NUM_ZERO] = &&lbl_NUM_ZERO, [OP_NUM_ONE] = &&lbl_NUM_ONE,
+ [OP_INT_ZERO] = &&lbl_INT_ZERO, [OP_INT_ONE] = &&lbl_INT_ONE,
+ [OP_INT_CONST] = &&lbl_INT_CONST,
[OP_TRUE] = &&lbl_TRUE, [OP_FALSE] = &&lbl_FALSE,
[OP_ADD] = &&lbl_ADD, [OP_SUB] = &&lbl_SUB,
[OP_MUL] = &&lbl_MUL, [OP_DIV] = &&lbl_DIV, [OP_MOD] = &&lbl_MOD,
+ [OP_IDIV] = &&lbl_IDIV,
[OP_BAND] = &&lbl_BAND, [OP_BOR] = &&lbl_BOR, [OP_BXOR] = &&lbl_BXOR,
[OP_SHL] = &&lbl_SHL, [OP_SHR] = &&lbl_SHR,
[OP_NEG] = &&lbl_NEG, [OP_NOT] = &&lbl_NOT, [OP_BNOT] = &&lbl_BNOT,
@@ -3328,8 +3485,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
Value *v = chunk->constants[idx];
/* Numeric constants without observer state ship as immediates —
* skips an incref/decref pair on every push/pop. */
- if (v->type == VAL_NUM) {
- vm_push_slot(slot_from_num(VAL_NUM_RAW(v)));
+ if (v->type == VAL_FLOAT) {
+ vm_push_slot(slot_from_f64(VAL_F64_RAW(v)));
} else {
val_incref(v);
vm_push(v);
@@ -3337,18 +3494,35 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
DISPATCH();
}
+ CASE(INT_ZERO): {
+ vm_push_slot(slot_from_int(0));
+ DISPATCH();
+ }
+ CASE(INT_ONE): {
+ vm_push_slot(slot_from_int(1));
+ DISPATCH();
+ }
+ CASE(INT_CONST): {
+ uint16_t idx = read_u16(ip); ip += 2;
+ Value *v = chunk->constants[idx];
+ int64_t i = VAL_I64_RAW(v); /* verifier: OP_INT_CONST requires VAL_INT */
+ if (i >= SLOT_SMI_MIN && i <= SLOT_SMI_MAX) vm_push_slot(slot_from_int(i));
+ else { val_incref(v); vm_push(v); }
+ DISPATCH();
+ }
+
CASE(NULL): {
vm_push_slot(slot_null());
DISPATCH();
}
CASE(NUM_ZERO): {
- vm_push_slot(slot_from_num(0.0));
+ vm_push_slot(slot_from_f64(0.0));
DISPATCH();
}
CASE(NUM_ONE): {
- vm_push_slot(slot_from_num(1.0));
+ vm_push_slot(slot_from_f64(1.0));
DISPATCH();
}
@@ -3365,28 +3539,28 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
/* ---- Arithmetic ---- */
/* NUM_REUSE — retained for slow-path fallbacks that allocate via
- * make_num then realize they could have mutated in place. The hot
+ * make_float then realize they could have mutated in place. The hot
* arith fast path no longer needs it: immediate doubles have no
* refcount and heap-num results are emitted as immediates. */
-#define NUM_REUSE(v) ((v)->type == VAL_NUM && (v)->refcount == 1)
+#define NUM_REUSE(v) ((v)->type == VAL_FLOAT && (v)->refcount == 1)
/* Stack-top fast path: read two slots, extract doubles from either
- * immediate or heap VAL_NUM form, compute. Result is wrapped as a
- * heap Value (via make_num or in-place NUM_REUSE). Immediates aren't
+ * immediate or heap VAL_FLOAT form, compute. Result is wrapped as a
+ * heap Value (via make_float or in-place NUM_REUSE). Immediates aren't
* emitted until B-3 audits every vm_peek caller for immediate-safe
* refcount handling. */
#define ARITH_FAST(OP, UF) do { \
EigsSlot _bs = g_vm.stack[g_vm.sp - 1]; \
EigsSlot _as = g_vm.stack[g_vm.sp - 2]; \
double _ad, _bd; \
- if (__builtin_expect(slot_as_double(_as, &_ad) & slot_as_double(_bs, &_bd), 1)) { \
+ if (__builtin_expect(slot_f64_value(_as, &_ad) & slot_f64_value(_bs, &_bd), 1)) { \
double _r = num_guard(_ad OP _bd); \
if ((UF) && _r == 0.0 && _ad != 0.0 && _bd != 0.0) \
g_math_flags |= EIGS_MATH_UNDERFLOW; \
if (slot_is_ptr(_as)) { \
Value *_a = slot_as_ptr(_as); \
if (NUM_REUSE(_a)) { \
- VAL_NUM_RAW(_a) = _r; \
+ VAL_F64_RAW(_a) = _r; \
slot_decref(_bs); \
g_vm.sp--; \
DISPATCH(); \
@@ -3395,7 +3569,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
if (slot_is_ptr(_bs)) { \
Value *_b = slot_as_ptr(_bs); \
if (NUM_REUSE(_b)) { \
- VAL_NUM_RAW(_b) = _r; \
+ VAL_F64_RAW(_b) = _r; \
g_vm.stack[g_vm.sp - 2] = _bs; \
slot_decref(_as); \
g_vm.sp--; \
@@ -3403,21 +3577,39 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
} \
} \
slot_decref(_as); slot_decref(_bs); \
- g_vm.stack[g_vm.sp - 2] = slot_from_num(_r); \
+ g_vm.stack[g_vm.sp - 2] = slot_from_f64(_r); \
g_vm.sp--; \
DISPATCH(); \
} \
} while(0)
+ /* Float first, then exact integer slots. A small integer result is an
+ * SMI; only a result outside the SMI range allocates. This is the basic
+ * interpreter producer (§2.1); native JIT/leaf int paths remain R5. */
+#define INT_ARITH(OP) do { \
+ EigsSlot _as = g_vm.stack[g_vm.sp - 2], _bs = g_vm.stack[g_vm.sp - 1]; \
+ int64_t _ai, _bi; \
+ if (slot_int_value(_as, &_ai) && slot_int_value(_bs, &_bi)) { \
+ EigsSlot _r = vm_int_arith(_ai, _bi, (OP)[0], current_line); \
+ slot_decref(_as); slot_decref(_bs); \
+ g_vm.stack[g_vm.sp - 2] = _r; \
+ g_vm.sp--; \
+ DISPATCH(); \
+ } \
+} while (0)
+
CASE(ADD): {
ARITH_FAST(+, 0);
+ INT_ARITH("+");
/* Slow path: handles string concat etc */
Value *b = vm_pop(); Value *a = vm_pop();
- if (a->type == VAL_NUM && b->type == VAL_NUM) {
- double r = num_guard(VAL_NUM_RAW(a) + VAL_NUM_RAW(b));
- if (NUM_REUSE(a)) { VAL_NUM_RAW(a) = r; vm_push(a); val_decref(b); DISPATCH(); }
- if (NUM_REUSE(b)) { VAL_NUM_RAW(b) = r; vm_push(b); val_decref(a); DISPATCH(); }
- vm_push(make_num(r));
+ if (a->type == VAL_FLOAT && b->type == VAL_FLOAT) {
+ double r = num_guard(VAL_F64_RAW(a) + VAL_F64_RAW(b));
+ if (NUM_REUSE(a)) { VAL_F64_RAW(a) = r; vm_push(a); val_decref(b); DISPATCH(); }
+ if (NUM_REUSE(b)) { VAL_F64_RAW(b) = r; vm_push(b); val_decref(a); DISPATCH(); }
+ vm_push(make_float(r));
+ } else if (val_is_number(a) && val_is_number(b)) {
+ vm_push(make_float(num_guard(eigs_float_arg(a, "addition") + eigs_float_arg(b, "addition"))));
} else if (a->type == VAL_STR && b->type == VAL_STR) {
int la = val_str_len(a), lb = val_str_len(b);
/* #292's byte budget charged only the size-controlled builtins;
@@ -3465,18 +3657,25 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
#define NUM_BINOP(NAME, OP, OPNAME, UF) \
CASE(NAME): { \
ARITH_FAST(OP, UF); \
+ INT_ARITH(OPNAME); \
Value *b = vm_pop(); Value *a = vm_pop(); \
- if (a->type == VAL_NUM && b->type == VAL_NUM) { \
- double r = num_guard(VAL_NUM_RAW(a) OP VAL_NUM_RAW(b)); \
- if ((UF) && r == 0.0 && VAL_NUM_RAW(a) != 0.0 && VAL_NUM_RAW(b) != 0.0) \
+ if (a->type == VAL_FLOAT && b->type == VAL_FLOAT) { \
+ double r = num_guard(VAL_F64_RAW(a) OP VAL_F64_RAW(b)); \
+ if ((UF) && r == 0.0 && VAL_F64_RAW(a) != 0.0 && VAL_F64_RAW(b) != 0.0) \
+ g_math_flags |= EIGS_MATH_UNDERFLOW; \
+ if (NUM_REUSE(a)) { VAL_F64_RAW(a) = r; vm_push(a); val_decref(b); DISPATCH(); } \
+ if (NUM_REUSE(b)) { VAL_F64_RAW(b) = r; vm_push(b); val_decref(a); DISPATCH(); } \
+ vm_push(make_float(r)); \
+ } else if (val_is_number(a) && val_is_number(b)) { \
+ double ad = eigs_float_arg(a, OPNAME), bd = eigs_float_arg(b, OPNAME); \
+ double r = num_guard(ad OP bd); \
+ if ((UF) && r == 0.0 && ad != 0.0 && bd != 0.0) \
g_math_flags |= EIGS_MATH_UNDERFLOW; \
- if (NUM_REUSE(a)) { VAL_NUM_RAW(a) = r; vm_push(a); val_decref(b); DISPATCH(); } \
- if (NUM_REUSE(b)) { VAL_NUM_RAW(b) = r; vm_push(b); val_decref(a); DISPATCH(); } \
- vm_push(make_num(r)); \
+ vm_push(make_float(r)); \
} else { \
rt_error(EK_TYPE, current_line, "cannot apply '%s' to %s and %s", \
OPNAME, val_type_name(a->type), val_type_name(b->type)); \
- vm_push(make_num(0)); \
+ vm_push(make_float(0)); \
} \
val_decref(a); val_decref(b); \
DISPATCH(); \
@@ -3494,134 +3693,174 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
CASE(DIV): {
Value *b = vm_pop(); Value *a = vm_pop();
- if (a->type == VAL_NUM && b->type == VAL_NUM) {
- if (VAL_NUM_RAW(b) == 0.0) {
+ if (val_is_number(a) && val_is_number(b)) {
+ if (eigs_float_arg(b, "division") == 0.0) {
rt_error(EK_VALUE, current_line, "division by zero");
- vm_push(make_num(0));
+ vm_push(make_float(0));
} else {
- double r = num_guard(VAL_NUM_RAW(a) / VAL_NUM_RAW(b));
+ double r = num_guard(eigs_float_arg(a, "division") / eigs_float_arg(b, "division"));
/* divisor already known nonzero (the raise above), so a zero
* quotient from a nonzero dividend is underflow. */
- if (r == 0.0 && VAL_NUM_RAW(a) != 0.0)
+ if (r == 0.0 && eigs_float_arg(a, "division") != 0.0)
g_math_flags |= EIGS_MATH_UNDERFLOW;
- if (NUM_REUSE(a)) { VAL_NUM_RAW(a) = r; vm_push(a); val_decref(b); DISPATCH(); }
- if (NUM_REUSE(b)) { VAL_NUM_RAW(b) = r; vm_push(b); val_decref(a); DISPATCH(); }
- vm_push(make_num(r));
+ if (NUM_REUSE(a)) { VAL_F64_RAW(a) = r; vm_push(a); val_decref(b); DISPATCH(); }
+ if (NUM_REUSE(b)) { VAL_F64_RAW(b) = r; vm_push(b); val_decref(a); DISPATCH(); }
+ vm_push(make_float(r));
}
} else {
rt_error(EK_TYPE, current_line, "cannot apply '/' to %s and %s",
val_type_name(a->type), val_type_name(b->type));
- vm_push(make_num(0));
+ vm_push(make_float(0));
+ }
+ val_decref(a); val_decref(b);
+ DISPATCH();
+ }
+
+ CASE(IDIV): {
+ Value *b = vm_pop(); Value *a = vm_pop();
+ if (val_is_number(a) && val_is_number(b)) {
+ if (a->type == VAL_INT && b->type == VAL_INT) {
+ int64_t ai = VAL_I64_RAW(a), bi = VAL_I64_RAW(b);
+ if (bi == 0) {
+ rt_error(EK_VALUE, current_line, "division by zero");
+ vm_push(make_int(0));
+ } else if (ai == INT64_MIN && bi == -1) {
+ rt_error(EK_VALUE, current_line, "integer overflow in '//'");
+ vm_push(make_int(0));
+ } else {
+ int64_t q = ai / bi, rem = ai % bi;
+ /* C truncates. A nonzero remainder with the opposite sign
+ * to the divisor moves the quotient down by one (N4). */
+ if (rem != 0 && ((rem < 0) != (bi < 0))) q--;
+ vm_push(make_int(q));
+ }
+ } else {
+ double divisor = eigs_float_arg(b, "floor division");
+ /* #1758 comment 6294926822: exactly DIV's error through R1.4.
+ * N11 changes this float arm with DIV/MOD to IEEE floor(x/0). */
+ if (divisor == 0.0) {
+ rt_error(EK_VALUE, current_line, "division by zero");
+ vm_push(make_float(0));
+ } else {
+ /* #1882: Python float_divmod's quotient, not floor(a/b).
+ * Derive it from fmod so decimal boundaries and an
+ * underflowed opposite-sign ratio agree with floor %. */
+ double dividend = eigs_float_arg(a, "floor division");
+ double rem = fmod(dividend, divisor);
+ double div = (dividend - rem) / divisor;
+ if (rem != 0.0 && ((rem < 0.0) != (divisor < 0.0)))
+ div -= 1.0;
+ double floordiv;
+ if (div != 0.0) {
+ floordiv = floor(div);
+ if (div - floordiv > 0.5) floordiv += 1.0;
+ } else {
+ floordiv = signbit(dividend / divisor) ? -0.0 : 0.0;
+ }
+ vm_push(make_float(num_guard(floordiv)));
+ }
+ }
+ } else {
+ rt_error(EK_TYPE, current_line, "cannot apply '//' to %s and %s",
+ val_type_name(a->type), val_type_name(b->type));
+ vm_push(make_int(0));
}
+ /* vm_pop owns both boxes, including INT64_MIN and a boxed divisor;
+ * every result and raise path releases both references. */
val_decref(a); val_decref(b);
DISPATCH();
}
CASE(MOD): {
Value *b = vm_pop(); Value *a = vm_pop();
- if (a->type == VAL_NUM && b->type == VAL_NUM && VAL_NUM_RAW(b) != 0.0) {
- double r = num_guard(fmod(VAL_NUM_RAW(a), VAL_NUM_RAW(b)));
- if (NUM_REUSE(a)) { VAL_NUM_RAW(a) = r; vm_push(a); val_decref(b); DISPATCH(); }
- if (NUM_REUSE(b)) { VAL_NUM_RAW(b) = r; vm_push(b); val_decref(a); DISPATCH(); }
- vm_push(make_num(r));
+ if (val_is_number(a) && val_is_number(b)) {
+ if (a->type == VAL_INT && b->type == VAL_INT) {
+ int64_t ai = VAL_I64_RAW(a), bi = VAL_I64_RAW(b);
+ if (bi == 0) {
+ rt_error(EK_VALUE, current_line, "modulo by zero");
+ vm_push(make_int(0));
+ } else {
+ /* N4b: x86 idiv faults for INT64_MIN / -1 even when only
+ * the remainder is needed. Handle it before C's %. */
+ int64_t rem = bi == -1 ? 0 : ai % bi;
+ if (rem != 0 && ((rem < 0) != (bi < 0))) rem += bi;
+ vm_push(make_int(rem));
+ }
+ } else {
+ double divisor = eigs_float_arg(b, "modulo");
+ if (divisor == 0.0) {
+ rt_error(EK_VALUE, current_line, "modulo by zero");
+ vm_push(make_float(0));
+ } else {
+ vm_push(make_float(num_guard(vm_floor_mod(eigs_float_arg(a, "modulo"), divisor))));
+ }
+ }
} else {
- if (!(a->type == VAL_NUM && b->type == VAL_NUM))
- rt_error(EK_TYPE, current_line, "cannot apply '%%' to %s and %s",
- val_type_name(a->type), val_type_name(b->type));
- else
- rt_error(EK_VALUE, current_line, "modulo by zero");
- vm_push(make_num(0));
+ rt_error(EK_TYPE, current_line, "cannot apply '%%' to %s and %s",
+ val_type_name(a->type), val_type_name(b->type));
+ vm_push(make_float(0));
}
val_decref(a); val_decref(b);
DISPATCH();
}
-/* #1740: both operands must be exact integers in int64 (Lua 5.4's rule);
- * a fractional or out-of-range one raises `value` (see num_to_i64_exact).
- * RHS is the (possibly transformed) right operand _bi. For `& | ^`
- * it is the plain int64 operand; for the shifts it is masked to [0,63] so a
- * large or negative shift count is defined (mod-64), never C UB — the same
- * `& 63` the bit_shl/bit_shr builtins apply, honoring the LANGUAGE_CONTRACT
- * "shifts are defined, not UB" promise. Without the mask, `1 << 64` on the
- * infix path is undefined behavior (UBSan trips; only untested counts hid it). */
-#define INT_BINOP_R(NAME, OP, OPNAME, RHS) \
+/* #1758 N8: exact int64 operands (#1740), integer results, Lua shifts.
+ * Both popped operands are owned, including boxed ints on raise paths. */
+#define INT_BINOP_R(NAME, OPNAME, EXPR) \
CASE(NAME): { \
EigsSlot _bs = g_vm.stack[g_vm.sp - 1]; \
EigsSlot _as = g_vm.stack[g_vm.sp - 2]; \
- double _ad = 0.0, _bd = 0.0; \
- if (__builtin_expect(slot_as_double(_as, &_ad) & slot_as_double(_bs, &_bd), 1)) { \
- int64_t _ai, _bi; \
- if (__builtin_expect(!num_to_i64_exact(_ad, &_ai) || \
- !num_to_i64_exact(_bd, &_bi), 0)) { \
- slot_decref(_as); slot_decref(_bs); \
- rt_error(EK_VALUE, current_line, EIGS_NO_INT_REP_FMT, OPNAME, \
- num_to_i64_exact(_ad, &_ai) ? _bd : _ad); \
- g_vm.stack[g_vm.sp - 2] = slot_from_num(0.0); \
- g_vm.sp--; \
- DISPATCH(); \
- } \
- double _r = (double)(INT_OP_##NAME(_ai, (RHS))); \
- if (slot_is_ptr(_as)) { \
- Value *_a = slot_as_ptr(_as); \
- if (NUM_REUSE(_a)) { VAL_NUM_RAW(_a) = _r; slot_decref(_bs); g_vm.sp--; DISPATCH(); } \
- } \
- if (slot_is_ptr(_bs)) { \
- Value *_b = slot_as_ptr(_bs); \
- if (NUM_REUSE(_b)) { VAL_NUM_RAW(_b) = _r; g_vm.stack[g_vm.sp - 2] = _bs; slot_decref(_as); g_vm.sp--; DISPATCH(); } \
- } \
- slot_decref(_as); slot_decref(_bs); \
- g_vm.stack[g_vm.sp - 2] = slot_from_num(_r); \
- g_vm.sp--; \
- DISPATCH(); \
- } \
- /* Mixed/non-numeric: error rather than silently returning 0, \
- * matching the arithmetic operators and the strict error model. \
- * Capture type names before decref (cf. NUM_CMP above). */ \
- { \
- const char *_tna = slot_type_name(_as), *_tnb = slot_type_name(_bs); \
- slot_decref(_as); slot_decref(_bs); \
+ int64_t _ai, _bi; \
+ int _ak = vm_bit_operand(_as, &_ai), _bk = vm_bit_operand(_bs, &_bi); \
+ EigsSlot _r = slot_null(); \
+ if (_ak == 1 && _bk == 1) { \
+ _r = slot_from_int(EXPR); \
+ } else if (_ak < 0 || _bk < 0) { \
rt_error(EK_TYPE, current_line, "cannot apply '%s' to %s and %s", \
- OPNAME, _tna, _tnb); \
+ OPNAME, slot_type_name(_as), slot_type_name(_bs)); \
+ } else { \
+ double _bad = 0.0; \
+ slot_f64_value(_ak == 0 ? _as : _bs, &_bad); \
+ rt_error(EK_VALUE, current_line, EIGS_NO_INT_REP_FMT, OPNAME, _bad); \
} \
- g_vm.stack[g_vm.sp - 2] = slot_from_num(0.0); \
+ slot_decref(_as); slot_decref(_bs); \
+ g_vm.stack[g_vm.sp - 2] = _r; \
g_vm.sp--; \
DISPATCH(); \
}
-/* SHL shifts unsigned: `<<` on a negative int64 is UB. Same bits as the
- * JIT's `shl`. */
-#define INT_OP_BAND(a, b) ((a) & (b))
-#define INT_OP_BOR(a, b) ((a) | (b))
-#define INT_OP_BXOR(a, b) ((a) ^ (b))
-#define INT_OP_SHL(a, b) ((int64_t)((uint64_t)(a) << (b)))
-#define INT_OP_SHR(a, b) ((a) >> (b))
-
-/* Non-shift bitwise ops: RHS is the plain int64 operand. */
-#define INT_BINOP(NAME, OP, OPNAME) INT_BINOP_R(NAME, OP, OPNAME, _bi)
-
- INT_BINOP(BAND, &, "&")
- INT_BINOP(BOR, |, "|")
- INT_BINOP(BXOR, ^, "^")
- INT_BINOP_R(SHL, <<, "<<", (_bi & 63))
- INT_BINOP_R(SHR, >>, ">>", (_bi & 63))
+ INT_BINOP_R(BAND, "&", _ai & _bi)
+ INT_BINOP_R(BOR, "|", _ai | _bi)
+ INT_BINOP_R(BXOR, "^", _ai ^ _bi)
+ INT_BINOP_R(SHL, "<<", eigs_bit_shift(_ai, _bi, 0))
+ INT_BINOP_R(SHR, ">>", eigs_bit_shift(_ai, _bi, 1))
/* ---- Unary ---- */
CASE(NEG): {
EigsSlot s = g_vm.stack[g_vm.sp - 1];
double d;
- if (__builtin_expect(slot_as_double(s, &d), 1)) {
+ if (__builtin_expect(slot_f64_value(s, &d), 1)) {
if (slot_is_ptr(s)) {
Value *v = slot_as_ptr(s);
- if (NUM_REUSE(v)) { VAL_NUM_RAW(v) = -d; DISPATCH(); }
+ if (NUM_REUSE(v)) { VAL_F64_RAW(v) = -d; DISPATCH(); }
}
slot_decref(s);
- g_vm.stack[g_vm.sp - 1] = slot_from_num(-d);
+ g_vm.stack[g_vm.sp - 1] = slot_from_f64(-d);
+ DISPATCH();
+ }
+ int64_t i;
+ if (slot_int_value(s, &i)) {
+ slot_decref(s);
+ if (i == INT64_MIN) {
+ rt_error(EK_VALUE, current_line, "integer overflow in unary '-'");
+ g_vm.stack[g_vm.sp - 1] = slot_null();
+ } else g_vm.stack[g_vm.sp - 1] = slot_from_int(-i);
DISPATCH();
}
rt_error(EK_TYPE, current_line, "cannot negate %s", slot_type_name(s)); /* #1637: names bool */
slot_decref(s);
- g_vm.stack[g_vm.sp - 1] = slot_from_num(0.0);
+ g_vm.stack[g_vm.sp - 1] = slot_from_f64(0.0);
DISPATCH();
}
@@ -3635,30 +3874,19 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
CASE(BNOT): {
EigsSlot s = g_vm.stack[g_vm.sp - 1];
- double d;
- if (__builtin_expect(slot_as_double(s, &d), 1)) {
- int64_t i;
- if (__builtin_expect(!num_to_i64_exact(d, &i), 0)) { /* #1740 */
- slot_decref(s);
- rt_error(EK_VALUE, current_line, EIGS_NO_INT_REP_FMT, "~", d);
- g_vm.stack[g_vm.sp - 1] = slot_from_num(0.0);
- DISPATCH();
- }
- double r = (double)(~i);
- if (slot_is_ptr(s)) {
- Value *v = slot_as_ptr(s);
- if (NUM_REUSE(v)) { VAL_NUM_RAW(v) = r; DISPATCH(); }
- }
- slot_decref(s);
- g_vm.stack[g_vm.sp - 1] = slot_from_num(r);
- DISPATCH();
- }
- {
- const char *_tn = slot_type_name(s);
- slot_decref(s);
- rt_error(EK_TYPE, current_line, "cannot apply '~' to %s", _tn);
+ int64_t i;
+ int k = vm_bit_operand(s, &i);
+ EigsSlot r = slot_null();
+ if (k == 1) r = slot_from_int(~i);
+ else if (k < 0)
+ rt_error(EK_TYPE, current_line, "cannot apply '~' to %s", slot_type_name(s));
+ else {
+ double bad = 0.0;
+ slot_f64_value(s, &bad);
+ rt_error(EK_VALUE, current_line, EIGS_NO_INT_REP_FMT, "~", bad);
}
- g_vm.stack[g_vm.sp - 1] = slot_from_num(0.0);
+ slot_decref(s);
+ g_vm.stack[g_vm.sp - 1] = r;
DISPATCH();
}
@@ -3667,8 +3895,13 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
CASE(EQ): {
EigsSlot _bs = g_vm.stack[g_vm.sp - 1];
EigsSlot _as = g_vm.stack[g_vm.sp - 2];
+ if (slot_is_smi(_as) && slot_is_smi(_bs)) {
+ g_vm.stack[g_vm.sp - 2] = slot_from_bool(_as.u == _bs.u);
+ g_vm.sp--;
+ DISPATCH();
+ }
double _ad, _bd;
- if (__builtin_expect(slot_as_double(_as, &_ad) & slot_as_double(_bs, &_bd), 1)) {
+ if (__builtin_expect(slot_f64_value(_as, &_ad) & slot_f64_value(_bs, &_bd), 1)) {
/* #1637: the result is a bool immediate. The old NUM_REUSE
* branches wrote a 0/1 result INTO a heap-number operand; a bool
* cannot live there, so they are gone, not retagged. */
@@ -3688,8 +3921,13 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
CASE(NE): {
EigsSlot _bs = g_vm.stack[g_vm.sp - 1];
EigsSlot _as = g_vm.stack[g_vm.sp - 2];
+ if (slot_is_smi(_as) && slot_is_smi(_bs)) {
+ g_vm.stack[g_vm.sp - 2] = slot_from_bool(_as.u != _bs.u);
+ g_vm.sp--;
+ DISPATCH();
+ }
double _ad, _bd;
- if (__builtin_expect(slot_as_double(_as, &_ad) & slot_as_double(_bs, &_bd), 1)) {
+ if (__builtin_expect(slot_f64_value(_as, &_ad) & slot_f64_value(_bs, &_bd), 1)) {
/* #1637: the result is a bool immediate. The old NUM_REUSE
* branches wrote a 0/1 result INTO a heap-number operand; a bool
* cannot live there, so they are gone, not retagged. */
@@ -3711,7 +3949,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
EigsSlot _bs = g_vm.stack[g_vm.sp - 1]; \
EigsSlot _as = g_vm.stack[g_vm.sp - 2]; \
double _ad, _bd; \
- if (__builtin_expect(slot_as_double(_as, &_ad) & slot_as_double(_bs, &_bd), 1)) { \
+ if (__builtin_expect(slot_f64_value(_as, &_ad) & slot_f64_value(_bs, &_bd), 1)) { \
/* #1637: a bool immediate; no NUM_REUSE (see CASE(EQ)). */ \
int _r = (_ad OP _bd); \
slot_decref(_as); slot_decref(_bs); \
@@ -3731,6 +3969,14 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
DISPATCH(); \
} \
} \
+ int _cmp; \
+ if (vm_number_compare(_as, _bs, &_cmp)) { \
+ int _r = _cmp != 2 && (_cmp OP 0); \
+ slot_decref(_as); slot_decref(_bs); \
+ g_vm.stack[g_vm.sp - 2] = slot_from_bool(_r); \
+ g_vm.sp--; \
+ DISPATCH(); \
+ } \
/* Mixed/uncomparable types: error rather than silently returning \
* false (e.g. "3" < 4 used to be 0). Capture type names before \
* decref. ==/!= keep cross-type "not equal"; only ordering errors. */ \
@@ -3901,7 +4147,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
* scope (#959), then populate IC. */
env_set_local_pre_interned_slot(env_binding_home(start), name, chunk->intern_tbl, h, s);
Env *t2 = env_resolve_chain(start, name, h, &slot_idx, &depth);
- if (!g_vm_multithreaded && t2 == start) { /* #297: see above */
+ if (!g_vm_multithreaded && t2 && t2 == start) { /* #297: see above */
ic->starting_env = start;
ic->starting_ver = start->binding_version;
ic->target_ver = start->binding_version;
@@ -3932,7 +4178,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
env_set_local_pre_interned_slot(start, name, chunk->intern_tbl, h, s);
int slot_idx, depth;
Env *target = env_resolve_chain(start, name, h, &slot_idx, &depth);
- if (!g_vm_multithreaded && target == start) { /* #297: see above */
+ if (!g_vm_multithreaded && target && target == start) { /* #297: see above */
ic->starting_env = start;
ic->starting_ver = start->binding_version;
ic->target_ver = start->binding_version;
@@ -3971,7 +4217,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
env_set_local_pre_interned_slot(target, name, chunk->intern_tbl, h, s);
int slot_idx, depth;
Env *resolved = env_resolve_chain(target, name, h, &slot_idx, &depth);
- if (!g_vm_multithreaded && resolved == target) { /* #297: see above */
+ if (!g_vm_multithreaded && resolved && resolved == target) { /* #297: see above */
ic->starting_env = target;
ic->starting_ver = target->binding_version;
ic->target_ver = target->binding_version;
@@ -4113,10 +4359,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
chunk_decref(chunk);
if (g_vm.frame_count <= base_frame) {
EigsSlot result_s = g_vm.stack[--g_vm.sp];
- if (slot_is_num(result_s)) return make_num(SLOT_NUM_RAW(result_s));
- if (slot_is_null(result_s)) return make_null();
- if (slot_is_bool(result_s)) return make_bool(slot_as_bool(result_s));
- return slot_as_ptr(result_s);
+ /* Transfer the slot's owned ref into a Value*: the one conversion,
+ * with its #1758 int arm (it was spelled out at four sites). */
+ return slot_bridge_unwrap(result_s);
}
frame = &g_vm.frames[g_vm.frame_count - 1];
ip = frame->ip;
@@ -4414,7 +4659,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
}
/* Pop args + fn from stack (slot-direct; immediates never
- * round-trip through make_num + free_value). #1171: the
+ * round-trip through make_float + free_value). #1171: the
* callee's reference is released only AFTER callframe_init has
* taken the frame's chunk reference. The stack can hold the
* function's LAST reference: a rebind of its name (another
@@ -4489,10 +4734,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
chunk_decref(fn_chunk);
if (g_vm.frame_count <= base_frame) {
EigsSlot result_s = g_vm.stack[--g_vm.sp];
- if (slot_is_num(result_s)) return make_num(SLOT_NUM_RAW(result_s));
- if (slot_is_null(result_s)) return make_null();
- if (slot_is_bool(result_s)) return make_bool(slot_as_bool(result_s));
- return slot_as_ptr(result_s);
+ /* Transfer the slot's owned ref into a Value*: the one conversion,
+ * with its #1758 int arm (it was spelled out at four sites). */
+ return slot_bridge_unwrap(result_s);
}
frame = &g_vm.frames[g_vm.frame_count - 1];
ip = frame->ip;
@@ -4536,7 +4780,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
}
CASE(RETURN): {
- /* Slot-native: avoid make_num/free_value round-trip when a callee
+ /* Slot-native: avoid make_float/free_value round-trip when a callee
* returns an immediate. Carry the result slot across the frame
* switch directly. */
EigsSlot result_s;
@@ -4571,11 +4815,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
g_vm.frame_count--;
if (g_vm.frame_count <= base_frame) {
/* Return to C: transfer slot's owned ref into a Value*.
- * Immediate -> materialize; pointer -> reuse slot's ref. */
- if (slot_is_num(result_s)) return make_num(SLOT_NUM_RAW(result_s));
- if (slot_is_null(result_s)) return make_null();
- if (slot_is_bool(result_s)) return make_bool(slot_as_bool(result_s));
- return slot_as_ptr(result_s);
+ * Immediate -> materialize; pointer -> reuse slot's ref. One
+ * conversion, with its #1758 int arm (it was spelled out at
+ * four sites). */
+ return slot_bridge_unwrap(result_s);
}
frame = &g_vm.frames[g_vm.frame_count - 1];
ip = frame->ip;
@@ -4657,16 +4900,16 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
EigsSlot idx_s = g_vm.stack[g_vm.sp - 1];
EigsSlot tgt_s = g_vm.stack[g_vm.sp - 2];
g_vm.sp -= 2;
- if (slot_is_num(idx_s) && slot_is_ptr(tgt_s)) {
+ if (slot_is_f64(idx_s) && slot_is_ptr(tgt_s)) {
Value *target = slot_as_ptr(tgt_s);
- int i, _ok = vm_index_is_int(SLOT_NUM_RAW(idx_s), &i);
+ int i, _ok = vm_index_is_int(SLOT_F64_RAW(idx_s), &i);
if (target->type == VAL_LIST) {
if (_ok && vm_index_resolve(&i, target->data.list.count)) {
EigsSlot r = slot_read_owned(list_slot(target, i));
slot_decref(tgt_s);
vm_push_slot(r);
} else {
- if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s));
+ if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_F64_RAW(idx_s));
else rt_error(EK_INDEX, current_line, "index %d out of range (list length %d)",
i, target->data.list.count);
slot_decref(tgt_s);
@@ -4676,11 +4919,11 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
}
if (target->type == VAL_BUFFER) {
if (_ok && vm_index_resolve(&i, target->data.buffer.count)) {
- double v = buffer_read_num(target, i);
+ double v = buffer_read_f64(target, i);
slot_decref(tgt_s);
- vm_push_slot(slot_from_num(v));
+ vm_push_slot(slot_from_f64(v));
} else {
- if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s));
+ if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_F64_RAW(idx_s));
else rt_error(EK_INDEX, current_line, "buffer index %d out of range (length %d)",
i, target->data.buffer.count);
slot_decref(tgt_s);
@@ -4689,48 +4932,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
DISPATCH();
}
}
- /* Slow path: materialize both via slot_to_value for unified handling. */
- Value *idx = slot_to_value(idx_s);
- slot_decref(idx_s);
- Value *target = slot_to_value(tgt_s);
- slot_decref(tgt_s);
- Value *result = make_null();
- if (target->type == VAL_LIST && idx->type == VAL_NUM) {
- int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i)) {
- rt_error(EK_VALUE, current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
- } else if (vm_index_resolve(&i, target->data.list.count)) {
- result = list_get_owned(target, i);
- } else {
- rt_error(EK_INDEX, current_line, "index %d out of range (list length %d)", i, target->data.list.count);
- }
- } else if (target->type == VAL_DICT && idx->type == VAL_STR) {
- EigsSlot v = vm_dict_slot_cached(target, idx->data.str,
- env_hash_name(idx->data.str));
- if (!slot_is_null(v)) result = eigs_slot_box(v);
- } else if (target->type == VAL_STR && idx->type == VAL_NUM) {
- int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i)) {
- rt_error(EK_VALUE, current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
- } else if (vm_index_resolve(&i, (int)val_str_len(target))) {
- char buf[2] = { target->data.str[i], 0 };
- result = make_str_len(buf, 1);
- } else {
- rt_error(EK_INDEX, current_line, "string index %d out of range (length %d)", i, (int)val_str_len(target));
- }
- } else if (target->type == VAL_BUFFER && idx->type == VAL_NUM) {
- int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i))
- rt_error(EK_VALUE, current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
- else if (vm_index_resolve(&i, target->data.buffer.count))
- result = make_num(buffer_read_num(target, i));
- else
- rt_error(EK_INDEX, current_line, "buffer index %d out of range (length %d)", i, target->data.buffer.count);
- } else {
- rt_error(EK_TYPE, current_line, "cannot index %s", val_type_name(target->type));
- }
- val_decref(target); val_decref(idx);
- vm_push(result);
+ /* The helper owns the same slow path and int decisions. Restore the
+ * two borrowed stack slots before handing their ownership to it. */
+ g_vm.sp += 2;
+ (void)jit_helper_index_get();
DISPATCH();
}
@@ -4740,14 +4945,14 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
EigsSlot val_s = g_vm.stack[g_vm.sp - 1];
EigsSlot idx_s = g_vm.stack[g_vm.sp - 2];
EigsSlot tgt_s = g_vm.stack[g_vm.sp - 3];
- if (slot_is_num(idx_s) && slot_is_ptr(tgt_s)) {
+ if (slot_is_f64(idx_s) && slot_is_ptr(tgt_s)) {
Value *target = slot_as_ptr(tgt_s);
- int i = 0, _ok = vm_index_is_int(SLOT_NUM_RAW(idx_s), &i);
- if (target->type == VAL_BUFFER && slot_is_num(val_s)) {
+ int i = 0, _ok = vm_index_is_int(SLOT_F64_RAW(idx_s), &i); /* #1775, as in jit_helper_index_set */
+ if (target->type == VAL_BUFFER && slot_is_f64(val_s)) {
if (_ok && vm_index_resolve(&i, target->data.buffer.count)) {
- target->data.buffer.data[i] = SLOT_NUM_RAW(val_s);
+ target->data.buffer.data[i] = SLOT_F64_RAW(val_s);
} else {
- if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s));
+ if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_F64_RAW(idx_s));
else rt_error(EK_INDEX, current_line, "buffer index %d out of range (length %d)",
i, target->data.buffer.count);
}
@@ -4757,11 +4962,11 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
slot_decref(tgt_s);
DISPATCH();
}
- if (target->type == VAL_LIST && slot_is_num(val_s)) {
+ if (target->type == VAL_LIST && slot_is_f64(val_s)) {
if (_ok && vm_index_resolve(&i, target->data.list.count)) {
- list_set_num(target, i, SLOT_NUM_RAW(val_s));
+ list_set_f64(target, i, SLOT_F64_RAW(val_s));
} else {
- if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s));
+ if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_F64_RAW(idx_s));
else rt_error(EK_INDEX, current_line, "index %d out of range (list length %d)",
i, target->data.list.count);
}
@@ -4773,24 +4978,24 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
}
}
Value *val = vm_pop(); Value *idx = vm_pop(); Value *target = vm_pop();
- if (target->type == VAL_LIST && idx->type == VAL_NUM) {
+ if (target->type == VAL_LIST && val_is_number(idx)) {
int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i)) {
- rt_error(EK_VALUE, current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
+ if (!vm_subscript_index_is_int(idx, &i)) {
+ rt_error(EK_VALUE, current_line, "index must be an integer, got %g", eigs_float_arg(idx, "index"));
} else if (vm_index_resolve(&i, target->data.list.count)) {
val_incref(val);
list_set_slot_owned(target, i, slot_from_heap(val));
} else {
rt_error(EK_INDEX, current_line, "index %d out of range (list length %d)", i, target->data.list.count);
}
- } else if (target->type == VAL_BUFFER && idx->type == VAL_NUM) {
+ } else if (target->type == VAL_BUFFER && val_is_number(idx)) {
int i;
- if (!vm_index_is_int(VAL_NUM_RAW(idx), &i)) {
- rt_error(EK_VALUE, current_line, "index must be an integer, got %g", VAL_NUM_RAW(idx));
+ if (!vm_subscript_index_is_int(idx, &i)) {
+ rt_error(EK_VALUE, current_line, "index must be an integer, got %g", eigs_float_arg(idx, "index"));
} else if (!vm_index_resolve(&i, target->data.buffer.count)) {
rt_error(EK_INDEX, current_line, "buffer index %d out of range (length %d)", i, target->data.buffer.count);
- } else if (val->type == VAL_NUM) {
- target->data.buffer.data[i] = VAL_NUM_RAW(val);
+ } else if (val_is_number(val)) {
+ target->data.buffer.data[i] = eigs_float_arg(val, "buffer store");
} else {
/* #1061: the last fail-soft numeric context. A non-number
* store was silently DROPPED (the old element stayed), so
@@ -4847,10 +5052,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
{
EigsSlot val_s = g_vm.stack[g_vm.sp - 1];
EigsSlot tgt_s = g_vm.stack[g_vm.sp - 2];
- if (slot_is_num(val_s) && slot_is_ptr(tgt_s)) {
+ if (slot_is_f64(val_s) && slot_is_ptr(tgt_s)) {
Value *t = slot_as_ptr(tgt_s);
if (t->type == VAL_DICT &&
- dict_set_cached_immediate(t, key, h, SLOT_NUM_RAW(val_s))) {
+ dict_set_cached_immediate(t, key, h, SLOT_F64_RAW(val_s))) {
g_vm.sp -= 1;
g_vm.stack[g_vm.sp - 1] = val_s;
slot_decref(tgt_s);
@@ -4911,8 +5116,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
uint32_t h = chunk->const_hashes ? chunk->const_hashes[name_idx] : 0;
if (h == 0) { h = env_hash_name(key); if (chunk->const_hashes) chunk->const_hashes[name_idx] = h; }
EigsSlot tos = g_vm.stack[g_vm.sp - 1];
- if (slot_is_num(tos) &&
- dict_set_cached_immediate(target, key, h, SLOT_NUM_RAW(tos))) {
+ if (slot_is_f64(tos) &&
+ dict_set_cached_immediate(target, key, h, SLOT_F64_RAW(tos))) {
DISPATCH();
}
Value *val = vm_slot_lift(g_vm.sp - 1);
@@ -4936,9 +5141,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
if (target) {
int i = (int)idx;
if (target->type == VAL_BUFFER) {
- /* DMG hot path: mem[addr] — emit immediate, skip make_num. */
+ /* DMG hot path: mem[addr] — emit immediate, skip make_float. */
if (i < target->data.buffer.count) {
- vm_push_slot(slot_from_num(buffer_read_num(target, i)));
+ vm_push_slot(slot_from_f64(buffer_read_f64(target, i)));
} else {
rt_error(EK_INDEX, current_line, "buffer index %d out of range (length %d)",
i, target->data.buffer.count);
@@ -5044,8 +5249,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
* mutate the existing dict slot's data.num in place
* and leave the stack slot as an immediate (no lift). */
EigsSlot tos = g_vm.stack[g_vm.sp - 1];
- if (slot_is_num(tos) &&
- dict_set_cached_immediate(dict, key, h, SLOT_NUM_RAW(tos))) {
+ if (slot_is_f64(tos) &&
+ dict_set_cached_immediate(dict, key, h, SLOT_F64_RAW(tos))) {
eigs_view_check(&dict_view);
DISPATCH();
}
@@ -5087,26 +5292,26 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
Value *state = vm_peek(0);
/* The index slot must be a number: make_iter_state always builds one,
* but an untrusted chunk (vm_run_bytecode / sandbox_run) can reach a
- * bare ITER_NEXT with any list on TOS, and reading VAL_NUM_RAW() off a
+ * bare ITER_NEXT with any list on TOS, and reading VAL_F64_RAW() off a
* VAL_STR is a type confusion whose result then indexes items[] (#721). */
- if (!state || state->type != VAL_LIST || state->data.list.count < 2 ||
- list_elem_type(state, 1) != VAL_NUM) {
+ if (!state || state->type != VAL_LIST || state->data.list.count < 2) {
ip += exit_offset;
DISPATCH();
}
- EIGS_VIEW_NOCHECK(iterable_view);
- Value *iterable = list_get_view(state, 0, &iterable_view);
- int idx = (int)eigs_slot_num(list_slot(state, 1), "for");
+ int idx;
+ if (!vm_iter_index(list_slot(state, 1), &idx)) {
+ ip += exit_offset;
+ DISPATCH();
+ }
+ Value *iterable = list_get_ref(state, 0);
int len = 0;
- if (iterable->type == VAL_LIST) len = iterable->data.list.count;
- else if (iterable->type == VAL_BUFFER) len = iterable->data.buffer.count;
+ if (iterable && iterable->type == VAL_LIST) len = iterable->data.list.count;
+ else if (iterable && iterable->type == VAL_BUFFER) len = iterable->data.buffer.count;
/* #491: cap by the entry-time snapshot (items[2]) so a body that
* appends can't extend the loop; min() with the live length keeps a
* remove from reading past the end. See make_iter_state. */
- if (state->data.list.count >= 3 &&
- list_elem_type(state, 2) == VAL_NUM) {
- int snap = (int)eigs_slot_num(list_slot(state, 2), "for");
- if (snap < len) len = snap;
+ if (state->data.list.count >= 3) {
+ len = vm_iter_limit(list_slot(state, 2), len);
}
/* Unsigned compare bounds BOTH ends in one branch: a negative index
@@ -5116,19 +5321,20 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
if ((unsigned)idx >= (unsigned)len) {
ip += exit_offset;
} else {
- /* Update index — mutate in place when the existing slot is
- * an exclusive plain VAL_NUM (avoids per-iter make_num/free). */
- list_set_num(state, 1, (double)(idx + 1));
+ /* S9-K1-C: mirror the helper's in-place SMI update. */
+ if (slot_is_smi(list_slot(state, 1)))
+ list_bump_smi(state, 1);
+ else
+ list_set_int(state, 1, (int64_t)idx + 1);
if (iterable->type == VAL_BUFFER) {
- /* Push number immediate directly — skip make_num + immediate-
+ /* Push number immediate directly — skip make_float + immediate-
* promote round-trip through vm_push. */
- vm_push_slot(slot_from_num(buffer_read_num(iterable, idx)));
+ vm_push_slot(slot_from_f64(buffer_read_f64(iterable, idx)));
} else {
EigsSlot elem = slot_read_owned(list_slot(iterable, idx));
vm_push_slot(elem);
}
}
- eigs_view_check(&iterable_view);
DISPATCH();
}
@@ -5281,8 +5487,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
* the slot persists across assignments, which is the #262 fix. */
EigsSlot s = g_vm.stack[g_vm.sp - 1];
Env *e = frame->fn_env;
- if (slot_is_num(s)) {
- observer_slot_update_num(e, (int)slot, SLOT_NUM_RAW(s));
+ double d;
+ if (obs_slot_num(s, &d)) { /* #1758: a float or an int */
+ observer_slot_update_num(e, (int)slot, d);
g_last_obs_slot_env = e; g_last_obs_slot_idx = (int)slot;
} else if (obs_slot_observable(s)) {
observer_slot_update(e, (int)slot, obs_slot_value(s));
@@ -5295,8 +5502,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
* observer_slot_sample_num (eigenscript.c). */
EigsSlot s = g_vm.stack[g_vm.sp - 1];
Env *e = frame->fn_env;
+ double d;
eigs_obs_count_call();
- if (slot_is_num(s)) observer_slot_sample_num_gated(e, (int)slot, SLOT_NUM_RAW(s));
+ if (obs_slot_num(s, &d)) observer_slot_sample_num_gated(e, (int)slot, d);
else if (obs_slot_observable(s)) observer_slot_sample_gated(e, (int)slot, obs_slot_value(s));
}
DISPATCH();
@@ -5461,9 +5669,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
list_append_owned(list, make_str(
vm_slot_value_opaque(e, (int)slot) ? "opaque"
: observer_slot_report(&q)));
- list_append_owned(list, make_num(q.entropy));
- list_append_owned(list, make_num(s->dH));
- list_append_owned(list, make_num(s->prev_dH));
+ list_append_owned(list, make_float(q.entropy));
+ list_append_owned(list, make_float(s->dH));
+ list_append_owned(list, make_float(s->prev_dH));
vm_push(list);
} else {
vm_push(builtin_observe(NULL));
@@ -5497,9 +5705,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
list_append_owned(list, make_str(
vm_slot_value_opaque(oe, oidx) ? "opaque" /* #708 */
: observer_slot_report(&q)));
- list_append_owned(list, make_num(q.entropy));
- list_append_owned(list, make_num(s->dH));
- list_append_owned(list, make_num(s->prev_dH));
+ list_append_owned(list, make_float(q.entropy));
+ list_append_owned(list, make_float(s->dH));
+ list_append_owned(list, make_float(s->prev_dH));
vm_push(list);
} else {
vm_push(builtin_observe(NULL));
@@ -5526,7 +5734,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
/* #262 Phase-3 D: TOS is now an immediate num for an observed name
* (default path no longer promotes), or a heap value. Observe the
* slot from whichever (#1637: a bool too); skip null. */
- if (slot_is_num(s) || obs_slot_observable(s)) {
+ double d;
+ int isnum = obs_slot_num(s, &d); /* #1758: a float or an int */
+ if (isnum || obs_slot_observable(s)) {
const char *name = chunk->const_interns[name_idx];
uint32_t h = chunk->const_hashes ? chunk->const_hashes[name_idx] : 0;
if (h == 0) { h = env_hash_name(name); if (chunk->const_hashes) chunk->const_hashes[name_idx] = h; }
@@ -5534,10 +5744,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
Env *oe = env_resolve_chain(frame->env, name, h, &oidx, &odepth);
if (oe && oidx >= 0 && g_unobserved_depth != 0) {
eigs_obs_count_call();
- if (slot_is_num(s)) observer_slot_sample_num_gated(oe, oidx, SLOT_NUM_RAW(s));
- else observer_slot_sample_gated(oe, oidx, obs_slot_value(s));
+ if (isnum) observer_slot_sample_num_gated(oe, oidx, d);
+ else observer_slot_sample_gated(oe, oidx, obs_slot_value(s));
} else if (oe && oidx >= 0) {
- if (slot_is_num(s)) observer_slot_update_num(oe, oidx, SLOT_NUM_RAW(s));
+ if (isnum) observer_slot_update_num(oe, oidx, d);
else observer_slot_update(oe, oidx, obs_slot_value(s));
g_last_obs_slot_env = oe;
g_last_obs_slot_idx = oidx;
@@ -5561,18 +5771,19 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
Value *result = make_null();
switch (kind) {
case 0: /* what */
- if (v && v->type == VAL_NUM) { result = make_num(VAL_NUM_RAW(v)); }
+ if (v && v->type == VAL_INT) { val_incref(v); result = v; }
+ else if (v && v->type == VAL_FLOAT) { result = make_float(VAL_F64_RAW(v)); }
/* #1637: a bool is its own value -- it fell to the else arm and
* answered 0, so `what is (5 > 3)` read as false. */
else if (v && v->type == VAL_BOOL) { result = make_bool(v->data.boolean); }
- else if (v && v->type == VAL_STR) { result = make_num(val_str_len(v)); }
- else if (v && v->type == VAL_LIST) { result = make_num(v->data.list.count); }
- else if (v && v->type == VAL_BUFFER) { result = make_num(v->data.buffer.count); }
- else { result = make_num(0); }
+ else if (v && v->type == VAL_STR) { result = make_int(val_str_len(v)); }
+ else if (v && v->type == VAL_LIST) { result = make_int(v->data.list.count); }
+ else if (v && v->type == VAL_BUFFER) { result = make_int(v->data.buffer.count); }
+ else { result = make_float(0); }
break;
case 1: /* who — returns descriptive type name */
if (v) result = make_str(
- v->type == VAL_NUM ? "number" :
+ val_is_number(v) ? "number" :
v->type == VAL_STR ? "string" :
v->type == VAL_LIST ? "list" :
v->type == VAL_DICT ? "dict" :
@@ -5586,10 +5797,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
* slot trajectory (observer state is binding-keyed). where/why/how on a
* bound ident go through INTERROGATE_NAMED; the bare value form reports
* the no-observation defaults. */
- case 2: /* when */ result = make_num(0); break;
- case 3: /* where */ result = make_num(0); break;
- case 4: /* why */ result = make_num(0); break;
- case 5: /* how */ result = make_num(1.0); break;
+ case 2: /* when */ result = make_int(0); break;
+ case 3: /* where */ result = make_float(0); break;
+ case 4: /* why */ result = make_float(0); break;
+ case 5: /* how */ result = make_float(1.0); break;
}
val_decref(v);
vm_push(result);
@@ -5609,7 +5820,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
result = make_str(name);
break;
case 2: /* when — return assignment count */
- result = make_num(env_get_assign_count(frame->env, name, h));
+ result = make_int(env_get_assign_count(frame->env, name, h));
break;
case 6: { /* prev — value just before the most recent assignment */
EigsSlot prev_slot;
@@ -5635,14 +5846,14 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
* in-place mutation is visible. dH (why/how) is the
* assignment-recorded trajectory and stays as stored. */
double h_now;
- result = make_num(observer_entropy_now(oe, oidx, &h_now)
+ result = make_float(observer_entropy_now(oe, oidx, &h_now)
? h_now : s->entropy);
}
- else if (kind == 4) result = make_num(s->dH);
- else result = make_num(observer_settledness(s->dH));
+ else if (kind == 4) result = make_float(s->dH);
+ else result = make_float(observer_settledness(s->dH));
} else {
/* #262 Step E: unobserved binding — no trajectory. */
- result = make_num(kind == 5 ? 1.0 : 0.0);
+ result = make_float(kind == 5 ? 1.0 : 0.0);
}
break;
}
@@ -5658,14 +5869,23 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
Value *line_v = vm_pop();
/* #1637: the line is a number, as `when`'s ordinal is. A bool (or any
* other type) used to read as line 0 and answer null silently. */
- if (!line_v || line_v->type != VAL_NUM) {
+ if (!line_v || !val_is_number(line_v)) {
rt_error(EK_TYPE, current_line, "'at' line must be a number, got %s",
line_v ? val_type_name(line_v->type) : "none");
val_decref(line_v);
vm_push(make_null());
DISPATCH();
}
- int line = (int)VAL_NUM_RAW(line_v);
+ int line;
+ if (line_v->type == VAL_INT) {
+ if (!vm_value_index_is_int(line_v, &line)) {
+ rt_error(EK_VALUE, current_line, "'at' line %lld is out of range",
+ (long long)VAL_I64_RAW(line_v));
+ val_decref(line_v);
+ vm_push(make_null());
+ DISPATCH();
+ }
+ } else line = (int)VAL_F64_RAW(line_v);
val_decref(line_v);
Value *result = make_null();
const char *name = chunk->const_interns[name_idx];
@@ -5685,8 +5905,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
uint16_t kind = read_u16(ip); ip += 2;
uint16_t name_idx = read_u16(ip); ip += 2;
Value *ord_v = vm_pop();
- int ord_is_num = (ord_v && ord_v->type == VAL_NUM);
- double ord_d = ord_is_num ? VAL_NUM_RAW(ord_v) : 0.0;
+ int ord_is_num = (ord_v && val_is_number(ord_v));
+ int ord_is_int = ord_v && ord_v->type == VAL_INT;
+ int64_t ord_i = ord_is_int ? VAL_I64_RAW(ord_v) : 0;
+ double ord_d = ord_is_num && !ord_is_int ? VAL_F64_RAW(ord_v) : 0.0;
val_decref(ord_v);
const char *name = chunk->const_interns[name_idx];
@@ -5705,7 +5927,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
DISPATCH();
}
- long long ordinal = (long long)ord_d;
+ long long ordinal = ord_is_int ? (long long)ord_i : (long long)ord_d;
EigsSlot w_slot;
long long total = 0, oldest = 0;
int r = trace_query_when(kind, name, ordinal, &w_slot, &total, &oldest);
@@ -5816,7 +6038,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
case VAL_BUFFER: len = target->data.buffer.count; break;
/* Not sliceable. Enumerated rather than covered by a `default:`
* so -Werror=switch forces a new ValType to choose here. */
- case VAL_NUM: case VAL_FN: case VAL_BUILTIN: case VAL_NULL:
+ case VAL_FLOAT: case VAL_INT: case VAL_FN: case VAL_BUILTIN: case VAL_NULL:
case VAL_DICT: case VAL_TEXT_BUILDER: case VAL_BOOL:
rt_error(EK_TYPE, g_vm.current_line, "cannot slice %s",
val_type_name(target->type));
@@ -5826,28 +6048,35 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
}
/* Extract a slice bound from a slot: null → use default; immediate
- * num or VAL_NUM → must be an exact integer. Returns 1 on success
+ * num or VAL_FLOAT → must be an exact integer. Returns 1 on success
* (with *out set), 0 on type error (after raising). */
int start = 0, end = len;
int bound_err = 0;
#define READ_SLICE_BOUND(slot, defval, outvar) \
do { \
- if (slot_is_num(slot)) { \
- if (!vm_index_is_int(SLOT_NUM_RAW(slot), &(outvar))) { \
+ if (slot_is_f64(slot)) { \
+ if (!vm_index_is_int(SLOT_F64_RAW(slot), &(outvar))) { \
rt_error(EK_VALUE, g_vm.current_line, \
- "slice bound must be an integer, got %g", SLOT_NUM_RAW(slot));\
+ "slice bound must be an integer, got %g", SLOT_F64_RAW(slot));\
bound_err = 1; \
} \
} else if (slot_is_ptr(slot)) { \
Value *bv = slot_as_ptr(slot); \
if (bv->type == VAL_NULL) { \
(outvar) = (defval); \
- } else if (bv->type == VAL_NUM) { \
- if (!vm_index_is_int(VAL_NUM_RAW(bv), &(outvar))) { \
+ } else if (bv->type == VAL_INT) { \
+ if (!vm_value_index_is_int(bv, &(outvar))) { \
+ rt_error(EK_VALUE, g_vm.current_line, \
+ "slice bound %lld is out of range", \
+ (long long)VAL_I64_RAW(bv)); \
+ bound_err = 1; \
+ } \
+ } else if (bv->type == VAL_FLOAT) { \
+ if (!vm_index_is_int(VAL_F64_RAW(bv), &(outvar))) { \
rt_error(EK_VALUE, g_vm.current_line, \
"slice bound must be an integer, got %g", \
- VAL_NUM_RAW(bv)); \
+ VAL_F64_RAW(bv)); \
bound_err = 1; \
} \
} else { \
@@ -5858,6 +6087,18 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
} \
} else if (slot_is_null(slot)) { \
(outvar) = (defval); /* an omitted bound */ \
+ } else if (slot_is_smi(slot)) { \
+ /* #1758: an int bound is its value, when an int \
+ * (the C type) holds it. */ \
+ int64_t _bv = slot_smi_val(slot); \
+ if (_bv < INT_MIN || _bv > INT_MAX) { \
+ rt_error(EK_VALUE, g_vm.current_line, \
+ "slice bound %lld is out of range", \
+ (long long)_bv); \
+ bound_err = 1; \
+ } else { \
+ (outvar) = (int)_bv; \
+ } \
} else { \
/* #1637: an immediate bool (or any other non-number \
* slot) is not a bound -- never the default one. */ \
@@ -6071,7 +6312,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
uint16_t counter_depth = read_u16(ip); ip += 2;
EigsSlot *counter = loop_counter_slot(frame, counter_depth);
if (counter) {
- loop_iter_store(frame->env, (double)loop_counter_load(counter));
+ loop_iter_store(frame->env, loop_counter_load(counter));
while (g_vm.sp > frame->bp + (int)counter_depth)
slot_decref(g_vm.stack[--g_vm.sp]);
}
@@ -6537,56 +6778,50 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
CASE(DISPATCH): {
/* Native dispatch: stack [table, key, arg] -> table[key](arg).
- * Key may be an immediate-num slot (literal key) or a boxed
- * VAL_NUM (key sourced from a fn return, arithmetic, variable
- * read — DMG's per-instruction `op` path). Table and arg are
- * heap pointers in practice (list + ctx). */
+ * Exact number extraction matches builtin_dispatch: ints retain all
+ * bits, floats satisfy N5 before the C index-range check (#1849).
+ * Table and arg are heap pointers in practice (list + ctx). */
EigsSlot arg_s = g_vm.stack[g_vm.sp - 1];
EigsSlot key_s = g_vm.stack[g_vm.sp - 2];
EigsSlot table_s = g_vm.stack[g_vm.sp - 3];
g_vm.sp -= 3;
- double key_d;
- if (slot_is_num(key_s)) {
- key_d = SLOT_NUM_RAW(key_s);
- } else if (slot_is_ptr(key_s) && slot_as_ptr(key_s)
- && slot_as_ptr(key_s)->type == VAL_NUM) {
- key_d = VAL_NUM_RAW(slot_as_ptr(key_s));
- } else {
+ ValType key_type = slot_type(key_s);
+ if (key_type != VAL_FLOAT && key_type != VAL_INT) {
slot_decref(arg_s); slot_decref(key_s); slot_decref(table_s);
rt_error(EK_TYPE, current_line, "dispatch: key must be a number");
vm_push_slot(slot_null());
DISPATCH();
}
+ EigsNum key_n = eigs_slot_number(key_s, "dispatch");
slot_decref(key_s);
-
- if (!slot_is_ptr(table_s)) {
+ int64_t key_i;
+ if (key_n.is_int) {
+ key_i = key_n.value.i64;
+ } else if (!num_to_i64_exact(key_n.value.f64, &key_i)) {
slot_decref(arg_s); slot_decref(table_s);
- rt_error(EK_TYPE, current_line, "dispatch: table must be a list");
+ rt_error(EK_VALUE, current_line,
+ "dispatch key must be an integer, got %g", key_n.value.f64);
vm_push_slot(slot_null());
DISPATCH();
}
-
- Value *table = slot_as_ptr(table_s);
- int key;
- /* Integrality first, then range: a fractional key is a value error
- * wherever it lies (2147483647.5 included), as ordinary indexing
- * reports it; only an integral key outside int range is index_range. */
- if (!vm_index_is_int(key_d, &key)) {
+ if (key_i < INT_MIN || key_i > INT_MAX) {
slot_decref(arg_s); slot_decref(table_s);
- rt_error(EK_VALUE, current_line,
- "dispatch key must be an integer, got %g", key_d);
+ rt_error(EK_INDEX, current_line,
+ "dispatch key %lld out of range", (long long)key_i);
vm_push_slot(slot_null());
DISPATCH();
}
- if (key_d < (double)INT_MIN || key_d > (double)INT_MAX) {
+ int key = (int)key_i;
+
+ if (!slot_is_ptr(table_s)) {
slot_decref(arg_s); slot_decref(table_s);
- rt_error(EK_INDEX, current_line,
- "dispatch key %g out of range", key_d);
+ rt_error(EK_TYPE, current_line, "dispatch: table must be a list");
vm_push_slot(slot_null());
DISPATCH();
}
+ Value *table = slot_as_ptr(table_s);
if (table->type != VAL_LIST) {
slot_decref(arg_s); slot_decref(table_s);
rt_error(EK_TYPE, current_line, "dispatch: table must be a list");
@@ -6708,10 +6943,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume,
if (g_vm.frame_count <= base_frame) {
EigsSlot result_s = g_vm.stack[--g_vm.sp];
eigs_view_check(&fn_view);
- if (slot_is_num(result_s)) return make_num(SLOT_NUM_RAW(result_s));
- if (slot_is_null(result_s)) return make_null();
- if (slot_is_bool(result_s)) return make_bool(slot_as_bool(result_s));
- return slot_as_ptr(result_s);
+ /* Transfer the slot's owned ref into a Value*: the one conversion,
+ * with its #1758 int arm (it was spelled out at four sites). */
+ return slot_bridge_unwrap(result_s);
}
frame = &g_vm.frames[g_vm.frame_count - 1];
ip = frame->ip;
diff --git a/src/vm.h b/src/vm.h
index bc59c4938..3593379a1 100644
--- a/src/vm.h
+++ b/src/vm.h
@@ -222,6 +222,12 @@ typedef enum {
* Appended to preserve the public bytecode ABI. */
OP_TRUE, /*obs:NONE*/ /* #1637: push the bool true immediate. Appended. */
OP_FALSE, /*obs:NONE*/ /* #1637: push the bool false immediate. Appended. */
+ /* #1758 R1.3: int literal opcodes append to preserve the bytecode ABI.
+ * The float literal opcodes above retain their original meanings. */
+ OP_INT_ZERO, /*obs:NONE*/ /* push int 0 */
+ OP_INT_ONE, /*obs:NONE*/ /* push int 1 */
+ OP_INT_CONST, /*obs:NONE*/ /* [idx:16] push int constant pool entry */
+ OP_IDIV, /*obs:NONE*/ /* #1758 N4: floor division, appended */
OP_LOOP_ENTER, /*obs:DIAG*/ /* legacy shared counter reset */
OP_LOOP_STALL_NAMED,/*obs:READS*/ /* [exit:16][stack_depth:16] per-loop stall/cap guard */
OP_LOOP_CAP_NAMED, /*obs:DIAG*/ /* [exit:16][stack_depth:16] per-loop cap guard */
@@ -695,7 +701,7 @@ int task_handle_unpack(double packed, int *id, uint32_t *gen);
Value *task_sched_trace_read(void); /* list of {seq, tick, task, cause} dicts; [] when off / no scheduler */
void task_sched_trace_clear(void); /* discard the recorded history (no-op without a scheduler) */
/* Inc 4 seeded scheduling strategy (builtins.c task_sched_seed). */
-void task_sched_set_seed(double seed); /* install a seed → seeded pick; ensures the scheduler */
+void task_sched_set_seed(int64_t seed); /* install a seed → seeded pick; ensures the scheduler */
/* ---- Public API ---- */
@@ -825,21 +831,17 @@ void vm_thread_reset_caches(void);
* integral double outside the C int range is clamped to the nearest endpoint.
* That preserves the predicate's compact
* interface while sending it through the caller's ordinary out-of-range path;
- * critically, the range test happens before the cast. Values outside the int
- * range but below 2^52 are tested with a (long long) round-trip (exact there, and
- * no libm: the freestanding profile has no trunc()); at and above 2^52 every finite
- * binary64 value is already integral. Negatives pass here and fail the usual
- * range check downstream, unchanged. */
+ * critically, Phase2's exact signed-int64 check happens before either cast.
+ * A fractional, non-finite or out-of-int64 float is a value refusal (N5);
+ * an exact int64 outside the C index domain takes main's range-error path.
+ * num_to_i64_exact is pure inline and needs no freestanding libm helper.
+ * Negatives pass here and fail the usual range check downstream, unchanged. */
static inline int vm_index_is_int(double d, int *out) {
- if (d < (double)INT_MIN || d > (double)INT_MAX) {
- if (!isfinite(d) || (fabs(d) < 4503599627370496.0 && (double)(long long)d != d))
- return 0;
- *out = d < 0.0 ? INT_MIN : INT_MAX;
- return 1;
- }
- int i = (int)d;
- if ((double)i != d) return 0;
- *out = i;
+ int64_t i;
+ if (!num_to_i64_exact(d, &i)) return 0;
+ if (i < INT_MIN) *out = INT_MIN;
+ else if (i > INT_MAX) *out = INT_MAX;
+ else *out = (int)i;
return 1;
}
diff --git a/tests/arming_mt_fixtures/arming_mt_st.expected b/tests/arming_mt_fixtures/arming_mt_st.expected
index ba570e5f4..6300fa49e 100644
--- a/tests/arming_mt_fixtures/arming_mt_st.expected
+++ b/tests/arming_mt_fixtures/arming_mt_st.expected
@@ -1,4 +1,4 @@
-PREV 2
+PREV 2.0
WHEN1 1
WHEN2 2
NOW 3
diff --git a/tests/bool_fuzz_anyvalue.txt b/tests/bool_fuzz_anyvalue.txt
index 84f5fc951..5f9df8467 100644
--- a/tests/bool_fuzz_anyvalue.txt
+++ b/tests/bool_fuzz_anyvalue.txt
@@ -20,6 +20,9 @@ str|e1|converts any value to text
type|u|names any value's type
type|e0|names any value's type
type|e1|names any value's type
+num_type|u|names a numeric kind or returns null for other values
+num_type|e0|a list is a non-number so num_type returns null
+num_type|e1|a list is a non-number so num_type returns null
json_encode|u|JSON has true and false
json_encode|e0|JSON has true and false
json_encode|e1|JSON has true and false
diff --git a/tests/bool_fuzz_gen.eigs b/tests/bool_fuzz_gen.eigs
index a19ec4346..9cfa2ac71 100644
--- a/tests/bool_fuzz_gen.eigs
+++ b/tests/bool_fuzz_gen.eigs
@@ -267,6 +267,8 @@ for v in ["true", "false"]:
append of [vm_calls, ["1 - " + v, "arith-sub", v]]
append of [vm_calls, [v + " * 2", "arith-mul", v]]
append of [vm_calls, ["2 / " + v, "arith-div", v]]
+ append of [vm_calls, [v + " // 2", "arith-idiv-left", v]]
+ append of [vm_calls, ["2 // " + v, "arith-idiv-right", v]]
append of [vm_calls, [v + " % 2", "arith-mod", v]]
append of [vm_calls, ["0 - " + v, "arith-neg", v]]
append of [vm_calls, [v + " < 1", "order", v]]
diff --git a/tests/enrolment_exemptions.txt b/tests/enrolment_exemptions.txt
index 70a599136..8accffc3c 100644
--- a/tests/enrolment_exemptions.txt
+++ b/tests/enrolment_exemptions.txt
@@ -1,8 +1,9 @@
-# Test scripts nothing in CI runs, on purpose (tools/enrolment_check.sh, #1264).
-# `path | reason`. A row for a missing or invoked file fails; prefer a section.
+# Test scripts and tools/ entry points that no workflow invokes, as tools/enrolment_check.sh
+# reads "invoked" (#1264; tools/ since #1758 R1.3). `path | reason`: the reason says how the
+# script does run. A row for a missing or invoked file fails; prefer a section or a workflow step.
tests/test_gc_runner_controls.py | manual deferred control for the GC traversal runner (tests/GC_TRAVERSAL.md); forks child sessions and must run with the heavy slot released, so it is run by hand
-tools/builtin_usage.sh | maintainer-only source audit that reports builtin use sites; it is not an executable CI check
-tools/doc_coupling_hook.sh | optional local Git pre-commit hook installed by a contributor, not a CI entry point
-tools/mutants.sh | manual mutation-train driver documented in concurrency and trace guides; individual product tests run in CI
+tools/builtin_usage.sh | a report over local consumer checkouts (counts builtin references), not a gate; run by hand
+tools/doc_coupling_hook.sh | a Claude Code hook (.claude/settings.json) for agent sessions, not a CI gate
+tools/mutants.sh | the manual mutation trains (its header); its --selftest is a tools/selftests.txt row
tests/test_gfx_utf8_title.eigs | alternate oracle: test_gfx_utf8_title.sh checks WM_NAME and _NET_WM_NAME on the live X11 window, covering the fixture's sole "X11 window opens" assertion
tests/test_variant_stdlib.eigs | alternate oracle: 18f-variant-stdlib-1679.sh runs a byte-for-byte temporary copy from outside the checkout and requires rc 0 plus its exact Tests: 1 | Pass: 1 | Fail: 0 summary, covering the fixture's stdlib-load assertion
diff --git a/tests/fixtures/jit_diff_agreement.eigs b/tests/fixtures/jit_diff_agreement.eigs
new file mode 100644
index 000000000..0da1a3f2d
--- /dev/null
+++ b/tests/fixtures/jit_diff_agreement.eigs
@@ -0,0 +1,2 @@
+# Clean control for ledger-comparison self-tests: REF and OBS agree.
+print of 2122
diff --git a/tests/fixtures/jit_diff_gate_nondeterministic.eigs b/tests/fixtures/jit_diff_gate_nondeterministic.eigs
new file mode 100644
index 000000000..907fb13b0
--- /dev/null
+++ b/tests/fixtures/jit_diff_gate_nondeterministic.eigs
@@ -0,0 +1,3 @@
+# Clean control: the harness's common seed makes this stable. The self-test
+# plant varies that process seed between runs; trace/replay then stabilizes it.
+print of (random_int of [1, 1000000])
diff --git a/tests/fixtures/jit_diff_taped_divergence.eigs b/tests/fixtures/jit_diff_taped_divergence.eigs
new file mode 100644
index 000000000..979544111
--- /dev/null
+++ b/tests/fixtures/jit_diff_taped_divergence.eigs
@@ -0,0 +1 @@
+print of (random_int of [1, 2])
diff --git a/tests/gfx_headless.sh b/tests/gfx_headless.sh
index 475346fd4..94d98d7b9 100644
--- a/tests/gfx_headless.sh
+++ b/tests/gfx_headless.sh
@@ -44,9 +44,10 @@ gfx_headless_run() {
platform=$(uname -s) || return 125
(
- unset DISPLAY WAYLAND_DISPLAY
export SDL_VIDEODRIVER=dummy
gfx_address_space_apply "$binary" "$instrumentation" "$platform" "$cap_kib" || exit 125
- exec "$timeout_cmd" "$seconds" "$@"
+ # Unsetting a function's temporary DISPLAY assignment can expose the
+ # caller's exported value. Remove it at the child process boundary.
+ exec env -u DISPLAY -u WAYLAND_DISPLAY "$timeout_cmd" "$seconds" "$@"
)
}
diff --git a/tests/jit_diff_expected.txt b/tests/jit_diff_expected.txt
index 3e9f13a59..a76b2f0e1 100644
--- a/tests/jit_diff_expected.txt
+++ b/tests/jit_diff_expected.txt
@@ -1,4 +1,21 @@
# OBS rows: these programs assert gate-CLOSED behaviour by design, so forcing the gate open changes their
# output. They are not defects to work down.
+bench_dmg_shape.eigs OBS COULD_NOT_ADJUDICATE
+bench_idxset.eigs OBS COULD_NOT_ADJUDICATE
+bench_perf.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_3dof_core.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_adaptive.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_arm.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_core.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_decomposition.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_edl.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_geodesic.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_lorentz.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_meta.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_multi_agent.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_noperthedron.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_qec.eigs OBS COULD_NOT_ADJUDICATE
+examples/invariant_similarity.eigs OBS COULD_NOT_ADJUDICATE
test_chunk_verify_stack.eigs OBS
test_desc_unrecorded_read.eigs OBS
+trace_mt_replay_workers.eigs OBS COULD_NOT_ADJUDICATE
diff --git a/tests/jit_env_flags_float.eigs b/tests/jit_env_flags_float.eigs
new file mode 100644
index 000000000..340e51635
--- /dev/null
+++ b/tests/jit_env_flags_float.eigs
@@ -0,0 +1,8 @@
+# R1.3: native env-flag witnesses use explicit float literals until the
+# JIT int fast-path round. Keep cost benchmarks unchanged.
+i is 0.0
+total is 0.0
+loop while i < 20000.0:
+ total is total + i
+ i is i + 1.0
+print of total
diff --git a/tests/line_after_callback_halt.eigs b/tests/line_after_callback_halt.eigs
index 077987bfa..5f57dcbfb 100644
--- a/tests/line_after_callback_halt.eigs
+++ b/tests/line_after_callback_halt.eigs
@@ -3,13 +3,13 @@
# error-halts (sandbox_run swallows the error), then sqrt raises in hot_sb's
# own expression on line 9. Pre-fix: "Error line 77: sqrt: ..." and
# "at hot_sb (line 77)". LINE-NUMBER SENSITIVE ([0g] greps these numbers).
-ok77 is [1, [68,77,0,0,0, 3, 40], []]
-halt77 is [1, [68,77,0,0,0, 3, 2, 7, 40], []]
+ok77 is [1.0, [68.0,77.0,0.0,0.0,0.0, 3.0, 40.0], []]
+halt77 is [1.0, [68.0,77.0,0.0,0.0,0.0, 3.0, 2.0, 7.0, 40.0], []]
define hot_sb(d, k) as:
- r is sqrt of (k - (len of (sandbox_run of [d, 100])))
+ r is sqrt of (k - (len of (sandbox_run of [d, 100.0])))
return r
-k is 60
-loop while k > 1:
+k is 60.0
+loop while k > 1.0:
w is hot_sb of [ok77, k]
- k is k - 1
-w is hot_sb of [halt77, 1]
+ k is k - 1.0
+w is hot_sb of [halt77, 1.0]
diff --git a/tests/line_after_callback_tape.eigs b/tests/line_after_callback_tape.eigs
index a599856e4..e4cdc5cbf 100644
--- a/tests/line_after_callback_tape.eigs
+++ b/tests/line_after_callback_tape.eigs
@@ -5,13 +5,13 @@
# the statement's line 15. [0g] steps the tape: after the last stop at line 9
# the next stop must be 15. LINE-NUMBER SENSITIVE.
define key(v) as:
- w is 0 - v
+ w is 0.0 - v
return w
define mk(n) as:
- q is [[n, 2], key]
+ q is [[n, 2.0], key]
return q
-k is 60
-loop while k > 0:
+k is 60.0
+loop while k > 0.0:
x is len of (sort_by of (mk of k))
- k is k - 1
-print of (what is x at 15)
+ k is k - 1.0
+print of (what is x at 15.0)
diff --git a/tests/line_after_return_fn.eigs b/tests/line_after_return_fn.eigs
index 8ee4903e5..4474d93ad 100644
--- a/tests/line_after_return_fn.eigs
+++ b/tests/line_after_return_fn.eigs
@@ -4,13 +4,13 @@
# Pre-fix: "Error line 8" and "at outer (line 8)".
# LINE-NUMBER SENSITIVE (the [0g] runner section greps these numbers).
define dec(x) as:
- y is x - 1
+ y is x - 1.0
return y
define outer(x) as:
- q is x + 1
- r is 1 / (dec of x)
+ q is x + 1.0
+ r is 1.0 / (dec of x)
return r
-k is 6001
-loop while k > 0:
+k is 6001.0
+loop while k > 0.0:
w is outer of k
- k is k - 1
+ k is k - 1.0
diff --git a/tests/line_after_return_module.eigs b/tests/line_after_return_module.eigs
index c2111239b..88e54a7c4 100644
--- a/tests/line_after_return_module.eigs
+++ b/tests/line_after_return_module.eigs
@@ -3,9 +3,9 @@
# last iteration. Pre-fix: "Error line 7" and "at (line 7)".
# LINE-NUMBER SENSITIVE (the [0g] runner section greps these numbers).
define dec(x) as:
- y is x - 1
+ y is x - 1.0
return y
-k is 6001
-loop while k > 0:
- r is 1 / (dec of k)
- k is k - 1
+k is 6001.0
+loop while k > 0.0:
+ r is 1.0 / (dec of k)
+ k is k - 1.0
diff --git a/tests/line_after_return_null.eigs b/tests/line_after_return_null.eigs
index 2e42a9186..891c7b5b6 100644
--- a/tests/line_after_return_null.eigs
+++ b/tests/line_after_return_null.eigs
@@ -7,14 +7,14 @@ define nothing(x) as:
y is x
define one(x) as:
y is x
- return 1
+ return 1.0
define hot_null(g, x) as:
- q is x + 1
+ q is x + 1.0
r is x / (g of x)
return r
-k is 0
-loop while k < 6000:
+k is 0.0
+loop while k < 6000.0:
w is nothing of k
w is hot_null of [one, k]
- k is k + 1
-w is hot_null of [nothing, 4]
+ k is k + 1.0
+w is hot_null of [nothing, 4.0]
diff --git a/tests/loader_mt_userrace.eigs b/tests/loader_mt_userrace.eigs
index b48708765..ff2c64dc8 100644
--- a/tests/loader_mt_userrace.eigs
+++ b/tests/loader_mt_userrace.eigs
@@ -14,14 +14,18 @@
# cannot lean on it (#1489).
#
# The deliberate race is the racer pair: two closures each write ONCE to the
-# the same element of a list they share, a race docs/CONCURRENCY.md still
+# same double in a buffer they share, a race docs/CONCURRENCY.md still
# leaves to the program. A ready channel plus
# separate release channels form a real rendezvous: main observes both workers
# at the barrier before releasing either, and neither release orders one
# worker's victim write before the other's. The
# racing word is not a spin/handshake word, so repeated polling cannot evict
# the first conflicting access from TSan's short per-word history (#1489).
-# It cannot corrupt memory because both stores are immediate numbers.
+# Capture each worker's local buffer reference BEFORE the rendezvous, and use
+# float index/value literals. Phase 2's integer literals take a boxing path;
+# its allocation/cleanup and shared closure lookups must not sit between the
+# release and the deliberately unsynchronized payload write. The float buffer
+# arm writes the shared double directly without changing any heap ownership.
define lm_ur_worker(k) as:
local i is 0
local n is 0
@@ -32,20 +36,22 @@ define lm_ur_worker(k) as:
return n
define lm_ur_race_pair() as:
- local victim is [0]
+ local victim is buffer of 1
local ready is channel of null
local release_a is channel of null
local release_b is channel of null
define racer_a as:
+ local cell is victim
send of [ready, 1]
recv of release_a
- victim[0] is 1
- return victim[0]
+ cell[0.0] is 1.0
+ return 0
define racer_b as:
+ local cell is victim
send of [ready, 1]
recv of release_b
- victim[0] is 2
- return victim[0]
+ cell[0.0] is 2.0
+ return 0
return [racer_a, racer_b, ready, release_a, release_b]
racers is lm_ur_race_pair of null
diff --git a/tests/observer_corpus/README.md b/tests/observer_corpus/README.md
index b0735c98c..a0eb20319 100644
--- a/tests/observer_corpus/README.md
+++ b/tests/observer_corpus/README.md
@@ -59,6 +59,21 @@ so the corpus watches their convergence *counts* but not their *results*. When
`dynamics` next refreshes `solve.eigs` it should print the elements (or
`buf_to_list`) so the numeric signal comes back.
+## Local edit note (#1758 R1.3, 2026-10-19)
+
+`iLambdaAi__test_halting_descent` differs from its source (iLambdaAi `246bd9a`,
+`eigenscript_native/tests/test_halting_descent.eigs`) in two lines: `x is 10`
+became `x is 10.0`. Since #1758 R1.3, `10` is an int literal and int `*` raises
+`value` on int64 overflow (decision (A)), so the source's runaway doubling stops
+with `integer overflow in '*'` after about 60 doublings instead of saturating at
+`1e308`. The source relied on its int silently becoming a float; `10.0` is the
+literal its author must write under (A). This row pins the observer's reading of
+a doubling sequence; overflow belongs to the N3/(A) rows of
+`tests/phase2/MAP.md`. **The golden is unchanged**: byte-identical with the edit
+in the interpreter, the default JIT and forced OSR, which is the proof that the
+observer's reading did not move. Re-vendoring from `246bd9a` would undo the edit;
+take the source's own fix when it lands, or keep this one.
+
## Sources (same-owner repos; vendored with provenance)
- **dynamics** — `solve`/`physics`/`life`: real solvers and trajectory demos
using `report`, the six predicates, `unobserved`, and `prev`.
diff --git a/tests/observer_corpus/golden/EigenScript__container_observer.out b/tests/observer_corpus/golden/EigenScript__container_observer.out
index c3d3cc9a5..cee618b7c 100644
--- a/tests/observer_corpus/golden/EigenScript__container_observer.out
+++ b/tests/observer_corpus/golden/EigenScript__container_observer.out
@@ -8,9 +8,9 @@
2.877443751081734
3.377443751081734
3.584962500721156
-2 2 stable
+2.0 2.0 stable
2.584962500721156 0.5849625007211561 stable
-3 0.4150374992788439 diverging
+3.0 0.4150374992788439 diverging
3.321928094887362 0.3219280948873622 diverging
3.584962500721156 0.2630344058337939 diverging
3.807354922057604 0.2223924213364481 diverging
diff --git a/tests/observer_corpus/golden/EigenScript__idioms.out b/tests/observer_corpus/golden/EigenScript__idioms.out
index b65985233..177d1b76e 100644
--- a/tests/observer_corpus/golden/EigenScript__idioms.out
+++ b/tests/observer_corpus/golden/EigenScript__idioms.out
@@ -8,7 +8,7 @@ Sum of positives: 26 (5 values)
10 / 3 = 3.3333333333333335
Warning: division by zero, returning 0
15 / 0 = 0
- 20 / 4 = 5
+ 20 / 4 = 5.0
Warning: division by zero, returning 0
0 / 0 = 0
100 / 7 = 14.285714285714286
@@ -34,7 +34,7 @@ GOOD (prefixed 'cs_total' is safe):
--- calling conventions ---
double of 7: 14
-lerp of [0, 100, 0.75]: 75
+lerp of [0, 100, 0.75]: 75.0
timestamp of null: 1712419200
--- early exit pattern ---
diff --git a/tests/observer_corpus/golden/EigenScript__numerical.out b/tests/observer_corpus/golden/EigenScript__numerical.out
index 9d46132e0..a7afafcfa 100644
--- a/tests/observer_corpus/golden/EigenScript__numerical.out
+++ b/tests/observer_corpus/golden/EigenScript__numerical.out
@@ -1,7 +1,7 @@
--- Newton's Method: Square Roots ---
sqrt(2) = 1.414213562373095
-sqrt(9) = 3
-sqrt(144) = 12
+sqrt(9) = 3.0
+sqrt(144) = 12.0
sqrt(2.5) = 1.5811388300841895
--- Bisection Method: x^3 - x - 2 = 0 ---
diff --git a/tests/observer_corpus/golden/EigenScript__structural_observer.out b/tests/observer_corpus/golden/EigenScript__structural_observer.out
index 0abb0d3cd..2e0d0a7b8 100644
--- a/tests/observer_corpus/golden/EigenScript__structural_observer.out
+++ b/tests/observer_corpus/golden/EigenScript__structural_observer.out
@@ -2,8 +2,8 @@ equilibrium
moving
["moving", 0.7219280948873623, -0.19636773916712724, -0.08170416594551044]
3.479597728084419e-06
-4
-1
+4.0
+1.0
1.191829090169325e-06
3.4366397314414046e-06
3.0014538431203897
diff --git a/tests/observer_corpus/golden/dynamics__physics.out b/tests/observer_corpus/golden/dynamics__physics.out
index 5521d2dee..c332878fe 100644
--- a/tests/observer_corpus/golden/dynamics__physics.out
+++ b/tests/observer_corpus/golden/dynamics__physics.out
@@ -3,11 +3,11 @@ observe ENERGY (Lyapunov) vs DISPLACEMENT (oscillation); flags = regime visited
zeta | E:osc conv div grew | x:osc conv div | reading
-0.05 | E:Y - - Y | x:Y - - | DIVERGES (energy grew)
- 0 | E:Y - - - | x:Y - - | x OSCILLATES, energy settles
+ 0.0 | E:Y - - - | x:Y - - | x OSCILLATES, energy settles
0.1 | E:- - - - | x:Y - - | x OSCILLATES, energy settles
0.4 | E:- Y - - | x:Y Y - | x OSCILLATES, energy settles
- 1 | E:- Y - - | x:- Y - | settles
- 2 | E:- Y - - | x:- - - | settles
+ 1.0 | E:- Y - - | x:- Y - | settles
+ 2.0 | E:- Y - - | x:- - - | settles
the gem is the zeta=0 row: energy conserved -> never converges, while x
oscillates. Same system, opposite verdict, set by what the observer watches.
diff --git a/tests/observer_corpus/golden/dynamics__solve.out b/tests/observer_corpus/golden/dynamics__solve.out
index 9a3f2aa3d..0cab89b3a 100644
--- a/tests/observer_corpus/golden/dynamics__solve.out
+++ b/tests/observer_corpus/golden/dynamics__solve.out
@@ -7,7 +7,7 @@ Jacobi Ax=b ->
Gauss-Seidel Ax=b ->
21 iters (fresh values -> fewer than Jacobi)
-Power iteration dominant eigenvalue -> 2 (14 iters, expect ~2)
+Power iteration dominant eigenvalue -> 2.0 (14 iters, expect ~2)
PageRank stationary distribution -> (48 iters)
diff --git a/tests/observer_corpus/golden/iLambdaAi__test_interrogative_spec.out b/tests/observer_corpus/golden/iLambdaAi__test_interrogative_spec.out
index 0af5af719..8c2b9a80d 100644
--- a/tests/observer_corpus/golden/iLambdaAi__test_interrogative_spec.out
+++ b/tests/observer_corpus/golden/iLambdaAi__test_interrogative_spec.out
@@ -39,15 +39,15 @@ WHERE returns entropy (number >= 0):
--- WHY: gradient (dH/dt) ---
WHY returns gradient (number):
-0
+0.0
--- HOW: stability (0-1) ---
HOW returns stability (0-1 range):
-1
+1.0
--- ARITHMETIC COMPOSITION ---
Arithmetic on interrogatives works:
-102
+102.0
--- ASSIGNMENT THEN INTERROGATIVE ---
256
diff --git a/tests/observer_corpus/programs/iLambdaAi__test_halting_descent.eigs b/tests/observer_corpus/programs/iLambdaAi__test_halting_descent.eigs
index 627e94b4c..174081f64 100644
--- a/tests/observer_corpus/programs/iLambdaAi__test_halting_descent.eigs
+++ b/tests/observer_corpus/programs/iLambdaAi__test_halting_descent.eigs
@@ -2,8 +2,8 @@ print of "=== HALTING: BOUNDED DESCENT TEST ==="
print of "--- HD1: Entropy strictly decreases each iteration ---"
counter is 0
-x is 10
-x is 10
+x is 10.0
+x is 10.0
loop while not converged:
counter is counter + 1
diff --git a/tests/phase2/MAP.md b/tests/phase2/MAP.md
new file mode 100644
index 000000000..1f63a4e57
--- /dev/null
+++ b/tests/phase2/MAP.md
@@ -0,0 +1,676 @@
+# tests/phase2/MAP.md: #1758 items to the test rows that pin them
+
+This is the skeleton, committed in round 0 with `docs/design/phase2-numbers.md`
+and revised in design round 2 (row shapes, tier witnesses, the leaf
+differential, the record-twice tape rows).
+Round 1 writes each row as `tests/phase2/rows/.eigs` (or `.sh` for a
+row that needs a shell: tape, env, files), with its exact expected stdout in
+`.out`.
+
+`tools/phase2_check.sh ` (design: §9.2 of the
+design doc) runs every row:
+- against the **v0.46.0 tag** (`4a3a5c19`, built from the tag commit, not
+ main) and the branch;
+- in four tiers: `EIGS_JIT_OFF=1`, the default JIT,
+ `EIGS_JIT_OSR_THRESHOLD=1`, and `trace` (`EIGS_TRACE=/t.tape`,
+ default JIT). The leaf evaluator is off under trace, so the `trace` tier is
+ the leaf differential: a row must print the same in it as in the others.
+
+Every change row must FAIL on the tag and PASS on the branch in every tier,
+and its witness must hold on the branch (design §9.1), or it is VACUOUS.
+`phase2_check.sh --through ` checks only the rows whose `[Rn]` has
+landed by that commit (a row tagged `[Ra/Rb]` lands with the later); R1.1,
+R1.2 and R2.1 have no rows of their own and are checked by their mechanical
+gates (design §9.2).
+
+## Format (machine-read by `phase2_check.sh`)
+
+- `### - ` opens an item. The items are fixed by #1758 Revision 1: `N1`
+ to `N14`, `N4b`, `A`, `B`, `T1`, `O1` to `O4`. Every item must have at
+ least one row.
+- ``- `` [Rn] `` is one row. Below it, two more
+ lines: `-> `, and `tag: `.
+ A row whose program pins several items lists them in its file header
+ (`# ITEMS: O3 T1`); it appears here once, under its first item.
+ - The row id is the file stem. Every id must have a file, and every file
+ must be listed here.
+ - `[Rn]` is the commit of the round plan that makes the row pass on the
+ branch.
+- In program sketches, ` / ` separates source lines and `{try X catch}` means:
+ ```
+ try:
+ X
+ catch e:
+ print of [e.kind, ...]
+ ```
+ The row file holds the exact program.
+- `tag:` says why the row fails on v0.46.0:
+ - `(probed main)` is output observed on the main-checkout binary (0.46.0,
+ built 2026-10-13). Main differs from the tag only by #1740/#1741
+ (bitwise and range conversion) and #1735.
+ - `(read)` is derived from the tag's source.
+- **Invariants** (`[Rn, invariant]`, R1.7-D3) already pass on the pre-round base. They run on every declared branch tier and are counted separately from change rows; the v0.46 anchor predates their landed contract. Pre-registration records base-green evidence.
+- **Controls** (`ctl.*`) must PASS on both binaries. They guard behaviour
+ the branch must preserve, and are counted separately: never in
+ `tag_fail`.
+- **Gates** (`gate.*`) name a check the Done-when runs on its own (the
+ observer oracle with `--floor`, the db parameter test). `phase2_check.sh`
+ lists them and verifies their command exists, but does not run or count
+ them as rows.
+- **Shape and witness.** A row without a `shape:` line is shape `S`
+ (straight-line module code, interpreted in every tier, witness `none`).
+ The other shapes carry a fourth line, `shape: witness:
+ ... [] ops: ...`:
+ - `L`: a one-line helper (the leaf evaluator); `F`: a multi-statement
+ function called at least 60 times (the function JIT), whose witnessed
+ call is its LAST call (`EIGS_JIT_HOT` records the last run); `O`: a
+ `loop while true:` loop, in a function or at module level, witnessed in
+ the `osr` tier, whose only exit is a `return` right after the claimed ops
+ (at module level, `print of ` first).
+ - witness kinds (design §9.1): `leaf ` (no `EIGS_JIT_HOT` entries for
+ `` in the normal run, K >= 1 in the `trace` run), `leaf-gen `
+ (the same K >= 1 entries in both: every call took the generic path),
+ `fn ` (`jit yes`, `adv RET`), `fn-stop ` (`jit yes`, the
+ op at `adv` is ``), `osr ` (`osr yes` and `oadv == len`: the
+ last OSR entry reached the return sentinel, so it ran the body natively),
+ `osr-stop ` (`oadv != len`, and the op at `oent + oadv` is
+ ``). Each kind was calibrated on main against a plant where the
+ claimed op did not run as claimed (design §9.1).
+ - `leaf` and `leaf-gen` may name several helpers; each must hold.
+ - `ops:` lists the opcodes whose JIT or leaf arm the row exercises: run
+ natively, or stopped at exactly (a `-stop` witness's ``, which proves
+ the guard or the scanner's refusal of that op). Ops after a stop are not
+ listed. `phase2_check.sh` requires the union over all `L`/`F`/`O` rows to
+ cover every JIT-emitter and leaf-evaluator arm that holds a numeric site (the
+ inventory file's `## Numeric arms` lists them: 15 emitter arms and 5 leaf
+ arms today).
+ - The witnesses state round 1's expectation. The JIT and the leaf bail on
+ an int that meets an arithmetic, comparison, bitwise or index op, or on
+ an int literal, until round 5, so such a row's witness is a `-stop` or
+ `leaf-gen`: the guard fired and the interpreter produced the output. An
+ int that only moves (into a local, a field, a list element or a module
+ binding) passes through the JIT's C helpers and the leaf's borrows, so
+ those rows' witnesses are native, as are the float rows'.
+ `(witness probed main)` marks a witness observed on the main binary
+ (with a double where the row passes an int); the others are read from
+ design §2.7, §4.1 and §4.5.
+
+---
+
+### N1
+- `N1.negative-past-int64-is-float` [R1.7, invariant] R1.7-D1: `-9223372036854775809` is a float equal to INT64_MIN; N6 prints Python repr
+ -> `[true, "float"]`, then `-9.223372036854776e+18`
+ tag: num_type undefined; R1.7-D3 prints the literal separately; observed base already prints the N6 text (15..17 round-trip writer), so the entire row is an invariant under D3.
+- `N1.leaf-float-kind` [R1.3] leaf helpers return 1.0, 0.0 and 0.5; `num_type` checks their kinds before N6 changes their display
+ -> `["float", "float", "float"]`
+ tag: `num_type` undefined.
+ shape: L witness: leaf onef zerof halff ops: NUM_ONE NUM_ZERO CONST
+- `N1.fn-float-kind` [R1.3] a hot function computes `a * 1.0 + 0.0`, then checks the result's kind
+ -> `["float", true]`
+ tag: `num_type` undefined.
+ shape: F witness: fn f ops: NUM_ONE NUM_ZERO MUL ADD
+- `N1.literal-kind` [R1.3] `print of [num_type of 3, num_type of 3.0, num_type of 1e3, num_type of 0x10]`
+ -> `["int", "float", "float", "int"]`
+ tag: `num_type` is undefined, an error (read)
+- `N1.exact-past-2p53` [R1.3] `print of [9007199254740993, 9223372036854775807]`
+ -> `[9007199254740993, 9223372036854775807]`
+ tag: `[9007199254740992, 9.223372036854776e+18]` (probed main)
+- `N1.decimal-past-int64-is-float` [R1.3] `x is 9223372036854775808 / print of [num_type of x, x]`
+ -> `["float", 9.223372036854776e+18]`
+ tag: `num_type` undefined (read)
+- `N1.hex-wraps` [R1.3] `print of [0x1FFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0x7FFFFFFFFFFFFFFF]`
+ -> `[-1, -1, 9223372036854775807]`
+ tag: `[3.6893488147419103e+19, 1.8446744073709552e+19, 9.223372036854776e+18]` (probed main)
+- `N1.canon-eq` [R1.3] `a is abs of -5 / b is 0 - 5 / print of [a == 5, -b == 5, num_type of a]`
+ -> `[true, true, "int"]`
+ tag: `num_type` undefined. (Round 0 named this row as plant 1's check; it cannot see that plant, because everything compares by value (design §2.1). Plant 1's check is `tests/phase2/slot_layer.c`.)
+- `N1.leaf-int-literal` [R1.3] `define big(x) as: return 9007199254740993 / define one(x) as: return 1 / print of [big of 0, one of 0]`
+ -> `[9007199254740993, 1]`
+ tag: `[9007199254740992, 1]` (probed main, all three tiers)
+ shape: L witness: leaf-gen big one (the leaf scanner refuses `OP_INT_CONST` and `OP_INT_ONE`) ops: INT_CONST INT_ONE
+- `N1.leaf-float-literal` [R1.7] `define onef(x) as: return 1.0 / define zerof(x) as: return 0.0 / define halff(x) as: return 0.5 / print of [onef of 7, zerof of 7, halff of 7]`
+ -> `[1.0, 0.0, 0.5]` (the leaf's `NUM_ONE`, `NUM_ZERO` and `CONST` keep their float meaning)
+ tag: `[1, 0, 0.5]` (probed main)
+ shape: L witness: leaf onef zerof halff (witness probed main: no entries normally, 1 each under trace) ops: NUM_ONE NUM_ZERO CONST
+- `N1.leaf-dot-get` [R1.3] `define px(p) as: return p.x / d is {"x": 9007199254740993} / print of (px of d)`
+ -> `9007199254740993` (an int field is borrowed through the leaf with no arithmetic, so the leaf computes it even in round 1)
+ tag: `9007199254740992` (probed main)
+ shape: L witness: leaf px (witness probed main: no entries normally, 1 under trace) ops: LOCAL_DOT_GET
+- `N1.fn-float-literals` [R1.7] `define f(a) as: t is a * 1.0 / u is t + 0.0 / return u`, called 60 times with `2.0`; print the last result
+ -> `2.0` (the JIT's `NUM_ONE`/`NUM_ZERO` emitters keep their float meaning)
+ tag: `2` (probed main)
+ shape: F witness: fn f (witness probed main: `f 60 yes ... RET`) ops: NUM_ONE NUM_ZERO MUL ADD
+- `N1.fn-int-literal` [R1.3] `define g(a) as: warm is 0.5 / t is a + 1 / return t * 2`, called 60 times with `9007199254740992`; print the last result
+ -> `18014398509481986`
+ tag: `18014398509481984` (probed main)
+ shape: F witness: fn-stop g INT_ONE (the JIT scanner refuses the new opcode) ops: INT_ONE
+- `N1.fn-dot-fields` [R1.3] `define setx(p, q) as: p.x is q / return p.x`, called 60 times with `[{"x": 1}, 9007199254740993]` (a fresh dict each time); print the last result
+ -> `9007199254740993` (the field store and load take their C helpers for a non-double and the thunk runs on)
+ tag: `9007199254740992` (probed main)
+ shape: F witness: fn setx (witness probed main, with a double: `setx 60 yes ... RET`) ops: LOCAL_DOT_SET LOCAL_DOT_GET
+- `ctl.N1.fn-int-truthiness` (control) `define truth(a) as: if a: return 1 / return 2`, called 60 times with `0`; `print of [truth of 0.0, truth of 5, truth of 0]` (the int call is LAST, so `EIGS_JIT_HOT` records it)
+ -> `[2, 1, 2]` on both (0 and 0.0 are falsy)
+ shape: F witness: fn-stop truth JUMP_IF_FALSE (an SMI condition fails the bool and number checks) ops: JUMP_IF_FALSE
+- `ctl.N1.fn-int-not` (control) `define neg(a) as: r is not a / return r`, called 60 times with `0`; `print of [neg of 0.0, neg of 5, neg of 0]` (the int call is LAST)
+ -> `[true, false, true]` on both
+ shape: F witness: fn-stop neg NOT (an SMI fails NOT's number check) ops: NOT
+- `N1.fn-int-local` [R1.3] `define keep(q) as: t is q / return t`, called 60 times with `9007199254740993`; print the last result
+ -> `9007199254740993` (an int stored into a local takes `SET_LOCAL`'s generic swap and the thunk runs on)
+ tag: `9007199254740992` (probed main)
+ shape: F witness: fn keep (witness probed main, with a double: `keep 60 yes ... RET`) ops: SET_LOCAL POP
+- `N1.osr-global-int-store` [R1.3] `x is 0 / define put(big, n) as: i is 0.0 / loop while true: (x is big / i is i + 1.0 / if i >= n: return x)`; `print of (put of [9007199254740993, 4.0])` (`x is big` inside `put` writes the module binding: `SET_NAME`)
+ -> `9007199254740993`
+ tag: `9007199254740992` (probed main)
+ shape: O witness: osr put (the boxed int moves through `SET_NAME` without a guard; witness probed main with a double: `oadv 106` = `len 106`) ops: SET_NAME NUM_ONE ADD GE JUMP_IF_FALSE
+- `N1.osr-int-counter` [R1.3] `define count(n) as: i is 9007199254740990 / k is 0.0 / loop while true: (i is i + 1 / k is k + 1.0 / if k >= n: return i)`; `print of (count of 10.0)`
+ -> `9007199254741000`
+ tag: `9007199254740992`: the double counter stalls at 2^53 (probed main)
+ shape: O witness: osr-stop count INT_ONE (the OSR scanner stops at the literal `1`, the body's first int op) ops: INT_ONE
+
+### N2
+- `N2.sort-by-exact` [R1.3] numeric key callbacks return both int and float keys around 2^53, with a stable equal-key pair
+ -> `["low", "tie-a", "tie-b", "high"]`
+ tag: the high int rounds to the low float, so the input order survives.
+- `N2.int-eq-float-kinds` [R1.3] `print of [1 == 1.0, 1 != 1.0, num_type of 1, num_type of 1.0]`
+ -> `[true, false, "int", "float"]`
+ tag: `num_type` undefined (read)
+- `N2.exact-eq-past-2p53` [R1.3] `print of [9007199254740993 == 9007199254740992.0, 9007199254740993 != 9007199254740992.0]`
+ -> `[false, true]`
+ tag: `[true, false]` (probed main: the lexer rounds)
+- `N2.exact-order` [R1.3] `print of [9007199254740993 > 9007199254740992.0, 9223372036854775807 < 9223372036854775808.0]`
+ -> `[true, true]`
+ tag: `[false, false]` (probed main for the second)
+- `N2.membership-exact` [R1.3] `print of [list_contains of [[9007199254740993], 9007199254740992.0], list_index_of of [[1, 9007199254740993], 9007199254740992.0]]`
+ -> `[false, -1]`
+ tag: `[true, 1]` (read: everything is a double)
+- `N2.sort-exact` [R1.3] `print of (sort of [9007199254740993, 9007199254740992, -1])`
+ -> `[-1, 9007199254740992, 9007199254740993]`
+ tag: the literal rounds, so `[-1, 9007199254740992, 9007199254740992]` (probed main, same shape)
+ round: R1.2 wrote the exact int comparator; the first int literals make it live in R1.3.
+- `N2.fn-exact-compare` [R1.3] `define lt(a, b) as: t is a < b / return t`, called 60 times with `[9007199254740992, 9007199254740993]`; print the last result
+ -> `true`
+ tag: `false` (probed main)
+ shape: F witness: fn-stop lt LT (a boxed int fails the operand guard) ops: LT
+- `N2.fn-compare-all` [R1.3] `define cmp(a, b) as: e is a == b / n is a != b / g is a > b / l is a <= b / q is a >= b / return [e, n, g, l, q]`, called 60 times with `[9007199254740993, 9007199254740992.0]`; print the last result
+ -> `[false, true, true, false, true]` (N2's exact cross-kind compare)
+ tag: `[true, false, false, true, true]` (probed main)
+ shape: F witness: fn-stop cmp EQ (a boxed int fails the first comparison's guard) ops: EQ
+- `ctl.N2.fn-float-compare` (control) `define ltf(a, b) as: t is a < ( / b) / return t`, called 60 times with `[0.5, 0.75]`; the written comparison spans physical lines (#1425)
+ -> `true` on both
+ shape: F witness: fn ltf (the native full return must cross both operator-line markers) ops: BINARY_LINE LT BINARY_LINE_END
+- `ctl.N2.float-eq` (control) `print of [0.5 + 0.25 == 0.75, 1 == 1.0, 3 < 3.5]`
+ -> `[true, true, true]`, both binaries
+
+### N3
+- `N3.int-ops-kind` [R1.3] `print of [num_type of (2 + 3), num_type of (7 - 9), num_type of (6 * 7), num_type of (2 + 0.5)]`
+ -> `["int", "int", "int", "float"]`
+ tag: `num_type` undefined
+- `N3.exact-product` [R1.3] `print of [3037000499 * 3037000499, 4503599627370496 * 2 + 1]`
+ -> `[9223372030926249001, 9007199254740993]`
+ tag: `[9.223372030926249e+18, 9007199254740992]` (probed main)
+- `ctl.N3.small-ints` (control) `print of [1 + 2, 10 - 3, 6 * 7, -4]`
+ -> `[3, 7, 42, -4]`, both binaries
+- `N3.leaf-mul-exact` [R1.3] `define sq(x) as: return x * x / print of (sq of 94906267)`
+ -> `9007199515875289`
+ tag: `9007199515875288` (probed main; the critic's gdb probe shows the leaf computed it there)
+ shape: L witness: leaf-gen sq (an SMI operand makes the leaf's MUL bail) ops: MUL
+- `N3.leaf-add-exact` [R1.3] `define add2(a, b) as: return a + b / print of (add2 of [9007199254740992, 1])`
+ -> `9007199254740993`
+ tag: `9007199254740992` (probed main)
+ shape: L witness: leaf-gen add2 ops: ADD
+- `ctl.N3.leaf-float` (control) `define sqf(x) as: return x * x / print of (sqf of 1.5)`
+ -> `2.25` on both
+ shape: L witness: leaf sqf (witness probed main: no entries normally, 1 under trace) ops: MUL
+- `N3.fn-arith-exact` [R1.3] `define f(a, b) as: t is a * ( / b) / u is t + a / return u - b`, called 60 times with `[94906267, 94906267]`; print the last result; the operator-line stamp precedes the int guard fallback
+ -> `9007199515875289`
+ tag: `9007199515875288` (probed main)
+ shape: F witness: fn-stop f MUL ops: BINARY_LINE MUL
+- `N3.osr-sum-exact` [R1.3] `define total(n, step) as: acc is 9007199254740000 / i is 0.0 / loop while true: (acc is acc + step / i is i + 1.0 / if i >= n: return acc)`; `print of (total of [1000.0, 1])`
+ -> `9007199254741000`
+ tag: `9007199254740992`: the double sum stalls at 2^53 (probed main)
+ shape: O witness: osr-stop total ADD (a boxed int and an SMI fail ADD's operand guard) ops: ADD
+- `N3.osr-module-sum` [R1.3] module-level `acc is 9007199254740000 / step is 1 / i is 0.0 / loop while true: (acc is acc + step / i is i + 1.0 / if i >= 1000.0: (print of acc / return acc))`
+ -> `9007199254741000`
+ tag: `9007199254740992` (probed main)
+ shape: O witness: osr-stop ADD (module-level OSR; a boxed int fails ADD's operand guard) ops: ADD
+- `ctl.N3.osr-float` (control) `define totalf(n) as: acc is 0.5 / i is 0.0 / loop while true: (acc is acc + ( / 1.5) / i is i + 1.0 / if i >= n: return acc)`; `print of (totalf of 100.0)`; the addition spans physical lines (#1425)
+ -> `150.5` on both
+ shape: O witness: osr totalf (the sole in-body return must follow both operator-line markers) ops: BINARY_LINE ADD BINARY_LINE_END CONST NUM_ONE GE JUMP_IF_FALSE SET_LOCAL POP
+
+### A
+- `A.add-overflow-raises` [R1.3] `{try print of (9223372036854775807 + 1) catch}`, printing `[e.kind, (index_of of [e.message, "+"]) >= 0]`
+ -> `["value", true]`
+ tag: prints a rounded double, no raise (read). **Plant 4's named check.**
+- `A.sub-overflow-raises` [R1.3] `{try print of (-9223372036854775807 - 2) catch}`, the same print with `"-"`
+ -> `["value", true]`
+ tag: a double, no raise
+- `A.mul-overflow-raises` [R1.3] `{try print of (4611686018427387904 * 2) catch}`, the same print with `"*"`
+ -> `["value", true]`
+ tag: `9.223372036854776e+18`, no raise
+- `A.sum-overflow-raises` [R1.6b] `{try print of (sum of [9223372036854775807, 1]) catch}`
+ -> `["value", true]`, the message naming `sum`
+ tag: a double sum, no raise
+- `A.tensor-scalar-overflow-raises` [R1.6b] `{try print of (multiply of [4611686018427387904, 2]) catch}`
+ -> `["value", true]`, the message naming `multiply`
+ tag: a double
+- `A.leaf-mul-overflow` [R1.3] `define sq(x) as: return x * x / {try print of (sq of 3037000500) catch}`, printing `e.kind`
+ -> `value` (the leaf bails; the generic path raises with the caller's line)
+ tag: `9.22337203700025e+18`, no raise (probed main)
+ shape: L witness: leaf-gen sq ops: MUL
+- `A.fn-add-overflow` [R1.3] `define inc(a) as: warm is 0.5 / t is a + 1 / return t`, called 60 times with small ints, then `{try print of (inc of 9223372036854775807) catch}`, printing `e.kind`
+ -> `value`
+ tag: `9.223372036854776e+18`, no raise (probed main)
+ shape: F witness: fn-stop inc INT_ONE ops: INT_ONE
+- `A.osr-double-overflow` [R1.3] `define dbl(n, two) as: x is two / i is 1.0 / loop while true: (x is x * two / i is i + 1.0 / if i >= n: return x)`; `{try print of (dbl of [63.0, 2]) catch}`, printing `e.kind`
+ -> `value` (2^63 overflows on the 63rd doubling)
+ tag: `9.223372036854776e+18`, no raise (probed main)
+ shape: O witness: osr-stop dbl MUL (two SMIs fail MUL's operand guard) ops: MUL
+
+### B
+- `B.type-of-both-num` [R1.3] `print of [type of 3, type of 3.0, (type of 3) == (type of 3.0), num_type of 3]`
+ -> `["num", "num", true, "int"]`
+ tag: `num_type` undefined
+- `ctl.B.type-of` (control) `print of [type of 3, type of 3.0, type of -2]`
+ -> `["num", "num", "num"]`, both binaries
+
+### N4
+- `N4.div-always-float` [R1.7] `print of [7 / 2, 6 / 2, num_type of (6 / 2)]`
+ -> `[3.5, 3.0, "float"]`
+ tag: `[3.5, 3, ...]` then `num_type` undefined (probed main for `[3.5, 3]`)
+- `N4.floordiv` [R1.4] `print of [7 // 2, -7 // 2, 7.5 // 2, num_type of (7 // 2), num_type of (7.5 // 2)]`
+ -> `[3, -4, 3, "int", "float"]` (R1.4 text; N6 adds `.0` to the float at R1.7)
+ tag: parse error, `unexpected '/'` (probed main)
+- `N4.floor-mod` [R1.4] `print of [-7 % 3, 7 % -3, -7.5 % 2, 7 % 3]`
+ -> `[2, -2, 0.5, 1]`
+ tag: `[-1, 1, -1.5, 1]`, i.e. fmod (probed main)
+- `N4.int-floordiv-zero-raises` [R1.4] `{try print of (7 // 0) catch}`, printing `e.kind`
+ -> `value`
+ tag: parse error
+- `N4.float-div-zero-inf` [R2.2] `print of [1.0 / 0, -1.0 / 0, 1 / 0]`
+ -> `[inf, -inf, inf]`
+ tag: raises `division by zero` (read)
+- `N4.float-zero-div-zero-nan` [R2.2] `x is 0.0 / 0 / print of [x == x, num_type of x]`
+ -> `[false, "float"]`
+ tag: raises
+- `N4.float-mod-zero-nan` [R2.2] `x is 5.5 % 0 / print of (x == x)`
+ -> `false`
+ tag: raises `modulo by zero`
+- `ctl.N4.int-mod-zero-raises` (control) `{try x is 7 % 0 catch}`, printing `e.kind`
+ -> `value`, both binaries (probed main)
+- `N4.fn-floor-mod` [R1.4] `define m(a, b) as: t is a % b / return t`, called 60 times with `[-7, 3]`; `print of [r, m of [-7.5, 2]]`
+ -> `[2, 0.5]` (floor modulo for an int and a float)
+ tag: `[-1, -1.5]` (probed main)
+ shape: F witness: fn-stop m MOD (the JIT scanner refuses MOD in round 1) ops: MOD
+- `N4.fn-div-float` [R1.7] `define d(a, b) as: t is a / b / return t`, called 60 times with `[6, 2]`; print the last result
+ -> `3.0`
+ tag: `3` (probed main)
+ shape: F witness: fn-stop d DIV (SMI operands fail DIV's operand guard) ops: DIV
+
+- `N4.fn-floordiv` [R1.4] a multi-statement `d(a, b)` returns `a // b`, called 60 times with `[-7.5, 2]`; print the value and kind
+ -> `[-4, "float"]` (R1.4 float text)
+ tag: parse error
+ shape: F witness: fn-stop d IDIV (the scanner's default refuses IDIV) ops: IDIV
+- `N4.compound-floordiv` [R1.4] apply `//=` to a name, a dict field and list indices, with int and float operands of both signs
+ -> `[-4, -4, [-4, -4], "int", "float"]`
+ tag: parse error
+- `N4.float-floordiv-zero-raises` [R1.4] float/mixed `// 0`, including `0.0 // 0.0` and negative zero
+ -> `["value", "division by zero"]` four times (same as `/`, until N11; #1758 comment 6294926822)
+ tag: parse error
+
+- `N4.float-floordiv-rounding` [R1.4] Python float-divmod quotient at decimal boundaries: `1.0 // 0.1`, mixed `1 // 0.1`, negative pairs, `7.5 // 0.1`, opposite signs, `3.0 // -0.1` quotient snapping and result kind
+ -> `[9, 9, 9, 74, -10, -30, "float"]`
+ tag: parse error
+- `N4.float-floordiv-underflow` [R1.4] `1e-300 // -1e300`, the sign-reversed pair, a positive underflow and result kind
+ -> `[-1, -1, 0, "float"]`
+ tag: parse error
+- `N4.fn-float-floordiv-rounding` [R1.4] multi-statement `rd(a, b)` called 60 times with `[1.0, 0.1]`, then mixed/negative/7.5 and quotient-snapping boundaries
+ -> `[9, 9, 9, 74, -30, "float"]`
+ tag: parse error
+ shape: F witness: fn-stop rd IDIV (the scanner's default refuses IDIV) ops: IDIV
+- `N4.fn-float-floordiv-underflow` [R1.4] multi-statement `ud(a, b)` called 60 times with `[1e-300, -1e300]`, then the sign-reversed pair
+ -> `[-1, -1, "float"]`
+ tag: parse error
+ shape: F witness: fn-stop ud IDIV (the scanner's default refuses IDIV) ops: IDIV
+- `N4.compound-float-floordiv-boundaries` [R1.4] `//=` at the decimal boundary on a name and dict field, and both underflow signs on list indices
+ -> `[9, 9, [-1, -1], "float", "float"]`
+ tag: parse error
+
+### N4b
+- `N4b.neg-min-raises` [R1.3] `m is -9223372036854775807 - 1 / {try print of (-m) catch}`
+ -> `["value", true]`, the message naming the unary minus
+ tag: a double, no raise. **Plant 5's named check.**
+- `N4b.abs-min-raises` [R1.3] `m is -9223372036854775807 - 1 / {try print of (abs of m) catch}`
+ -> `["value", true]`
+ tag: `9.223372036854776e+18`, no raise
+- `N4b.floordiv-min-by-neg1-raises` [R1.4] `m is ... / {try print of (m // -1) catch}`
+ -> `["value", true]`
+ tag: parse error
+- `N4b.min-mod-neg1-zero` [R1.4] `m is ... / print of [m % -1, num_type of (m % -1)]`
+ -> `[0, "int"]`, with no hardware fault (x86 `idiv` faults on `INT64_MIN % -1`)
+ tag: `num_type` undefined
+- `N4b.fn-neg-min` [R1.3] `define n(a) as: t is -a / return t`, called 60 times with small ints, then `m is -9223372036854775807 - 1 / {try print of (n of m) catch}`, printing `e.kind`
+ -> `value`
+ tag: `9.223372036854776e+18`, no raise (probed main)
+ shape: F witness: fn-stop n NEG ops: NEG
+
+### N5
+- `N5.http-port-before-config` [R1.6f] C6: an effective PORT=65536 raises value in early_bind and serve before binding or freezing route configuration. Each caught refusal is followed by successful route registration.
+- `N5.net-send-byte-raises` [R1.6f] R1.6d-D1 / C7: matched server builds reject buffer/list bytes 256 and 1.5 before any send; the peer receives only the subsequent valid integral-float payload `ok`. Counts are ints. The raw stored-byte half is `gate.N5.net-byte-boundary` (R1.6f-D2).
+- `gate.N5.net-byte-boundary` [R1.6f] `tests/phase2/net_byte_boundary.c` through `tools/phase2_readers.sh`: R1.6d-D1 net_send byte site. Raw stored NaN/infinities, the first-NaN INVALID/diagnostic and valid 255 are checked in both strict modes through the actual client and a real socketpair; a refusal sends no valid prefix (`phase2-net-byte: PASS checks=72 failures=0`). R1.6f-D2 moved it out of the row: compiled and executed once per `--through` at or after R1.6f, outside the rows' per-tier 30 s timeout, in which its amalgamation build timed out.
+- `N5.store-handle-fraction-raises` [R1.6f] R1.6f-D1 (#1924 comment 6367017285): all nine shared store handle-reader callers reject a fractional `_store_id` and a fractional `_store_gen` with `value` before store access; no frozen expectation changes.
+- `gate.N5.iterator-metadata` [R1.3] `tests/phase2/slot_layer.sh` Part E builds raw iterator states and runs the interpreter and `jit_helper_iter_next`
+ -> float controls and both SMI/boxed index/snapshot metadata yield 42; range and malformed states preserve the pinned exhaustion/ignore rules.
+ tag: n/a (a C fixture of the four R1.2 refusal sites); final R1.3 arms accept exact C-int indices and clamp snapshots without narrowing.
+- `N5.substr-fraction-raises` [R1.6c] `{try print of (substr of ["hello", 1.5, 2]) catch}`, printing `e.kind`
+ -> `value`
+ tag: `el` (probed main: truncation)
+- `N5.store-key-fraction-raises` [R1.6b] `s is store_open of "/db" / k is store_put of [s, "c", {"v": 1}] / {try print of (store_get of [s, "c", 1.9]) catch}`
+ -> `value`
+ tag: returns record "1" (probed main)
+- `N5.bitwise-fraction-raises` [R1.5] `{try print of (1.5 | 0) catch}`, printing `e.kind`
+ -> `value`
+ tag: `1` (the bar critic's probe of the tag; main already raises: #1740)
+- `N5.integral-float-converts` [R1.5] `print of [6.0 | 1, num_type of (6.0 | 1), (list_slice of [[1, 2, 3], 1.0, 2.0])]`
+ -> `[7, "int", [2]]`
+ tag: `num_type` undefined
+- `N5.range-fraction-raises` [R1.6b] `{try print of (range of 2.5) catch}`, printing `e.kind`
+ -> `value`
+ tag: no raise (read: `range_bound` accepts any double inside the int range)
+- `N5.gather-index-fraction-raises` [R1.6d] `t is buffer of [2, 3]` filled 10, 11, 12, 20, 21, 22 / `ib is buffer of 2 / ib[0] is 1.5 / ib[1] is 2.0 / {try g is gather of [t, ib] / print of [g[0], g[1]] catch}`, printing `e.kind`
+ -> `value` (an index is integer-valued: N5)
+ tag: `[11, 22]`: `flat_index_at` truncates 1.5 to 1 (probed main; the critic's `gather.eigs`)
+- `N5.pcm16-byte-raises` [R1.6d] `b is buffer of 2 / b[0] is 300 / b[1] is 0 / {try buf_from_pcm16le of [b, 0, 1] catch}` printing `e.kind`, then the same with `b[0] is 1.5`
+ -> `value` twice (byte values: N5, then 0..255; #1758 comment 6174964306)
+ tag: no raise, samples `0.009155552842799158` and `4.577776421399579e-05` (probed main)
+- `N5.audio-channel-domain` [R1.6e] R1.6e-D1 (#1922 comment 6355101247): audio_open, audio_capture_open and audio_stream_open reject channels 0, 256, 257, -1 and 1.5 with `value` at the N5/domain reader; 255 and 1 pass that reader (unsupported counts remain SDL's to refuse). Validation precedes SDL/device/tape side effects.
+- `gate.N5.gfx-byte-boundary` [R1.6e] `tests/phase2/gfx_byte_boundary.c` through `tools/phase2_readers.sh`: R1.6d-D1 pre-registration for buffer_read_byte's gfx_fb and ppu_render_frame callers. Both reject stored lenient NaN and every infinity with `value`; strict first NaN preserves INVALID/diagnostic before a later infinity. Q1 rejects 1.5 and 256, while integral float 255 succeeds. Actual client calls and rendered/framebuffer sentinels prove that rejection at the first palette byte or first LCDC register stops before the sink changes; the PPU retains its existing progressive writes after valid reads.
+- `gate.N5.q7-readers` [R1.6e] `tests/phase2/q7_readers.c` (through the amalgamation, no SDL): `eigs_geom_shape` clips `RECT [-3e9, 0, 6e9, 10]` on a 640x480 drawable area to `[0, 0, 640, 10]` (saturating each argument would give half of that), clips a `LINE` with Liang-Barsky, bounds a `CIRCLE` of radius 3e9 to the visible rows; `eigs_bounded_arg` clamps 300 to 255; `eigs_magnitude_arg` raises `value` on 3e12 for a C `int`
+ -> every case as stated
+ tag: n/a (a C test of new readers), so it is a gate
+- `N5.leaf-buffer-index` [R1.7] `define bat(b, i) as: return b[i] / b is buffer of 3 / print of (bat of [b, 1])`
+ -> `0.0` (a buffer element is an f64, Q2)
+ tag: `0` (probed main)
+ shape: L witness: leaf-gen bat (an SMI index makes the leaf's INDEX_GET bail in round 1) ops: INDEX_GET
+- `ctl.N5.leaf-float-index` (control) `define nth(xs, i) as: return xs[i] / print of (nth of [[10, 20, 30], 1.0])`
+ -> `20` on both (an integral float index converts, N5)
+ shape: L witness: leaf nth (witness probed main: no entries normally, 1 under trace) ops: INDEX_GET
+- `N5.osr-index-set` [R1.3] `define fill(big, n) as: xs is [0, 0, 0, 0] / i is 0.0 / loop while true: (xs[i] is big / i is i + 1.0 / if i >= n: return xs)`; `print of (fill of [9007199254740993, 4.0])` (the critic's `osr_ret`)
+ -> `[9007199254740993, 9007199254740993, 9007199254740993, 9007199254740993]` (an integral float index converts, N5; the int is stored exactly)
+ tag: four times `9007199254740992` (probed main)
+ shape: O witness: osr fill (the float counter keeps the loop native; the boxed int takes `INDEX_SET`'s C helper; witness probed main: `oadv 123` = `len 123`, 3 native `jit_helper_index_set` calls under gdb) ops: INDEX_SET NUM_ONE ADD GE JUMP_IF_FALSE SET_LOCAL POP
+
+### N6
+- `gate.N6.const-dedup-bits` [R1.7] `tools/phase2_readers.sh` release/asan `const_dedup_bits`: direct `chunk_add_constant` of +0.0 and -0.0 has distinct indices and memcmp-exact stored doubles (R1.7-D4); repeated identical bits deduplicate.
+- `N6.float-repr` [R1.7] `print of [3.0, 1e3, -0.0, 0.1 + 0.2, 1e15, 1e16, 1e-05, 0.0001]`
+ -> `[3.0, 1000.0, -0.0, 0.30000000000000004, 1000000000000000.0, 1e+16, 1e-05, 0.0001]`
+ tag: `[3, 1000, 0, 0.30000000000000004, 1000000000000000, 1e+16, 1e-05, 0.0001]` (probed main for the first seven)
+- `N6.int-exact-digits` [R1.7, invariant] `print of [9007199254740993, -9223372036854775807 - 1]`
+ -> `[9007199254740993, -9223372036854775808]`
+ tag: rounded doubles
+- `N6.str-fstring` [R1.7] `x is 2.0 / print of [str of x, f"{x}", str of 2]`
+ -> `["2.0", "2.0", "2"]`
+ tag: `["2", "2", ...]` (probed main)
+- `N6.neg-zero-literal-dedup` [R1.7] `a is 0.0 / b is -0.0 / print of [a, b]`
+ -> `[0.0, -0.0]`
+ tag: `[0, 0]`. (The constant pool dedup folds `-0.0` into `+0.0`, chunk.c:196; the row pins that a literal `-0.0` survives a chunk that also holds `0.0`.)
+- `N6.nonfinite` [R2.5] `print of [1.0 / 0, -1.0 / 0, 0.0 / 0, -(0.0 / 0)]`
+ -> `[inf, -inf, nan, nan]` (never `-nan`)
+ tag: raises
+- `ctl.N6.small-ints` (control) `print of [1, 2, 3, -4, 100000]`
+ -> `[1, 2, 3, -4, 100000]`, both binaries
+
+### N7
+- `N7.num-type-float` [R1.3] `print of [num_type of 3.0, num_type of -0.0, num_type of 1e300, num_type of 2.5]`
+ -> `["float", "float", "float", "float"]`
+ tag: `num_type` undefined. **Plant 6's named check.**
+- `N7.num-type-int` [R1.3] `print of [num_type of 0, num_type of -1, num_type of 9223372036854775807, num_type of (2 * 3)]`
+ -> `["int", "int", "int", "int"]`
+ tag: undefined
+- `N7.num-type-non-number` [R1.3] `print of [num_type of "3", num_type of null, num_type of true, num_type of ([1])]`
+ -> `[null, null, null, null]` (Lua `math.type` returns fail)
+ tag: undefined
+- `gate.N7.embed-int` [R1.8] `make embed-smoke` with new rows: `eigs_value_new_int` round-trips 9007199254740993 exactly; `eigs_value_as_int` succeeds on an int and on an integral float, and fails with its flag on 1.5; `eigs_value_is_int` tells the kinds apart; `eigs_value_as_num` converts an int (Q3); `EIGS_TYPE_NUM` answers for both
+ -> every case as stated
+ tag: n/a (a C test of new embed functions), so it is a gate
+
+### N8
+- `ctl.N8.refuse-band` (control) a hot function applies `a & b` to float operands 6.0 and 3.0
+ -> `2` on both; the JIT scanner refuses this opcode at R1.3.
+ shape: F witness: fn-stop f BAND ops: BAND
+- `ctl.N8.refuse-bor` (control) a hot function applies `a | b` to float operands 6.0 and 3.0
+ -> `7` on both; the JIT scanner refuses this opcode at R1.3.
+ shape: F witness: fn-stop f BOR ops: BOR
+- `ctl.N8.refuse-bxor` (control) a hot function applies `a ^ b` to float operands 6.0 and 3.0
+ -> `5` on both; the JIT scanner refuses this opcode at R1.3.
+ shape: F witness: fn-stop f BXOR ops: BXOR
+- `ctl.N8.refuse-shl` (control) a hot function applies `a << b` to float operands 6.0 and 3.0
+ -> `48` on both; the JIT scanner refuses this opcode at R1.3.
+ shape: F witness: fn-stop f SHL ops: SHL
+- `ctl.N8.refuse-shr` (control) a hot function applies `a >> b` to float operands 6.0 and 3.0
+ -> `0` on both; the JIT scanner refuses this opcode at R1.3.
+ shape: F witness: fn-stop f SHR ops: SHR
+- `ctl.N8.refuse-bnot` (control) a hot function applies `~a` to float operands 6.0 and 3.0
+ -> `-7` on both; the JIT scanner refuses this opcode at R1.3.
+ shape: F witness: fn-stop f BNOT ops: BNOT
+- `ctl.N8.refuse-mod` (control) a hot function applies `a % b` to float operands 6.0 and 3.0;
+ print `(r == 0)` -> `true` on both. R1.7-D7 (#1758, N6) makes the
+ observable text-stable while retaining the hot float-MOD witness program.
+ The JIT scanner refuses this opcode at R1.3.
+ shape: F witness: fn-stop f MOD ops: MOD
+- `N8.bitwise-results-int` [R1.5] `print of [6 & 3, 6 | 3, 6 ^ 3, ~0, num_type of (6 & 3)]`
+ -> `[2, 7, 5, -1, "int"]`
+ tag: `num_type` undefined
+- `N8.lua-shifts` [R1.5] `print of [1 << 64, 1 << 63, 1 << -1, -1 >> 1, 8 >> -1]`
+ -> `[0, -9223372036854775808, 0, 9223372036854775807, 16]`
+ tag: not this (probed main: `1 << 64` is `1`, `1 << 63` a negative double, `-1 >> 1` is `-1`)
+- `N8.bit-builtins-int64` [R1.5] `print of [bit_or of [4611686018427387904, 1], bit_shl of [1, 64]]`
+ -> `[4611686018427387905, 0]`
+ tag: `4.611686018427388e+18`, ...
+- `N8.fn-bitwise` [R1.5] `define b(a, c) as: t is a & c / return t`, called 60 times with `[9007199254740993, 9007199254740993]`; print the last result
+ -> `9007199254740993`
+ tag: `9007199254740992` (probed main)
+ shape: F witness: fn-stop b BAND (the JIT scanner refuses the bitwise ops in round 1) ops: BAND
+- `N8.fn-bitwise-all` [R1.5] `define bits(a, c) as: o is a | c / x is a ^ c / l is a << 1 / r is a >> 1 / n is ~a / return [o, x, l, r, n]`, called 60 times with `[9007199254740993, 2]`; print the last result
+ -> `[9007199254740995, 9007199254740995, 18014398509481986, 4503599627370496, -9007199254740994]`
+ tag: `[9007199254740994, 9007199254740994, 18014398509481984, 4503599627370496, -9007199254740992]` (probed main)
+ shape: F witness: fn-stop bits BOR (the scanner refuses the bitwise ops) ops: BOR
+
+### N9
+Names, operands and operations follow the decided [#1758 comment 6304880619](https://github.com/InauguralSystems/EigenScript/issues/1758#issuecomment-6304880619); Rust std is the oracle.
+- `N9.wrapping` [R1.6g] `print of [i64_wrapping_add of [9223372036854775807, 1], u8_wrapping_add of [255, 1]]`
+ -> `[-9223372036854775808, 0]`
+ tag: undefined
+- `N9.checked-raises` [R1.6g] `{try print of (u8_checked_add of [200, 100]) catch}`
+ -> `["value", ...]`
+ tag: undefined
+- `N9.saturating` [R1.6g] `print of [i8_saturating_sub of [-100, 100], u64_saturating_add of [-1, 1]]`
+ -> `[-128, -1]`. A u64 is carried in an int64 bit pattern, so u64 max is `-1`.
+ tag: undefined
+- `N9.unsigned-ops` [R1.6g] `print of [u64_compare of [-1, 1], u64_to_string of [-1], u64_div of [-1, 2]]`
+ -> `[1, "18446744073709551615", 9223372036854775807]`
+ tag: undefined
+- `N9.fround` [R1.6g] `print of (fround of 0.1)`
+ -> `0.10000000149011612`
+ tag: undefined
+
+### N10
+- `N10.partial-refused` [R1.6b] `{try print of (num of "12abc") catch}`, printing `e.kind`
+ -> `value`
+ tag: `12` (probed main)
+- `N10.radix` [R1.6b] `print of [num of "0x1F", num of "0X1f"]`, then catch `num of "0b101"` and `num of "0o17"` separately and print each `e.kind`
+ -> `[31, 31]`, `value`, `value` (the lexer's own numeric grammar; #1758 comment 6304880619)
+ tag: `[31, 31]`, `0`, `0`; Lua agrees on hex and rejects both unsupported prefixes. Hex floats are the declared divergence: Lua accepts them, EigenScript's lexer and `num` refuse them.
+- `N10.int-or-float` [R1.6b] `print of [num_type of (num of "3"), num_type of (num of "3.0"), num_type of (num of "9223372036854775808"), num of "9007199254740993"]`
+ -> `["int", "float", "float", 9007199254740993]`
+ tag: `num_type` undefined
+
+### N11
+- `N11.sqrt-neg-nan` [R2.2] `x is sqrt of -1 / print of [x == x, num_type of x]`
+ -> `[false, "float"]`
+ tag: raises `sqrt: argument out of domain` (probed main)
+- `N11.log-zero` [R2.2] `print of (log of 0)`
+ -> `-inf`
+ tag: raises `log: argument out of domain` (probed main)
+- `N11.no-clamp` [R2.2] `print of [1e308 * 10, 1e308 * 10 == 1e308]`
+ -> `[inf, false]`
+ tag: `1e+308` then `true` (probed main for the first)
+- `N11.strict-opt-in` [R2.2] (sh row, `EIGS_STRICT=1`) `{try print of (1e308 * 10) catch}`
+ -> `value`. Default strict off: the same program prints `inf`.
+ tag: `1e+308` under either setting (read: the clamp, not a raise)
+- `N11.constant-1e308-converges` [R2.2] the #1746 row: a binding assigned the literal `1e308` 12 times; `print of (converged of x)`
+ -> `true`
+ tag: `false`, because saturation reads as diverging (#861; probed on the #1746 baseline)
+- `N11.fn-overflow-inf` [R2.5] `define big(a) as: t is a * 10.0 / return t`, called 60 times with `1e308`; print the last result
+ -> `inf` (an inf result is not a NaN, so the JIT keeps it native)
+ tag: `1e+308`, the clamp (probed main)
+ shape: F witness: fn big ops: MUL CONST
+- `N11.min-max-nan` [R2.2] `x is (1e308 * 10) - (1e308 * 10) / a is min of [1, x] / b is min of [x, 1] / c is max of [x, 1] / print of [a == a, b == b, c == c]`
+ -> `[false, false, false]` (NaN propagates in either argument order: Q8, IEEE 754-2019 `minimum`/`maximum`)
+ tag: `[true, true, true]`: the clamp turns `x` into 0 (probed main)
+- `N11.sort-total-order` [R2.5] `x is (1e308 * 10) - (1e308 * 10) / inf is 1e308 * 10 / print of (sort of [3, x, 1, inf, 2])`
+ -> `[1, 2, 3, inf, nan]` (Q8: IEEE `totalOrder`, NaN after +inf)
+ tag: `[0, 1, 2, 3, 1e+308]` (probed main)
+
+### N12
+- `N12.f64-bits` [R2.4] `print of [f64_bits of 1.0, f64_bits of -0.0, f64_from_bits of 4607182418800017408]`
+ -> `[4607182418800017408, -9223372036854775808, 1.0]`
+ tag: undefined
+- `N12.nan-canonical-bits` [R2.4] `print of (f64_bits of (0.0 / 0))`
+ -> `9221120237041090560` (`0x7FF8000000000000`)
+ tag: raises
+- `N12.nan-payload-tags` [R2.1/R2.4] for every tag-spelling payload `p` in `0xFFF8000000000000, 0xFFF9000000000001, 0xFFFA000000000005, 0xFFFB000000001000, 0xFFFF000000000000`, `x is f64_from_bits of p`, stored in a variable, a list, a dict and a buffer; print `[type of x, x == x, f64_bits of x]` for each
+ -> `["num", false, 9221120237041090560]` five times
+ tag: undefined. **Plant 2's named check**, run under the plant build with ASan.
+- `N12.payload-through-ints` [R2.4] `i is 9218868437227405313 / print of [f64_bits of (f64_from_bits of i), of ( of [buffer of 1, 0, i])]`
+ -> `[9221120237041090560, 9218868437227405313]`. Ints and buffer bits keep the payload; a float does not.
+ tag: undefined
+- `N12.fn-nan-canonical` [R2.4] `define nz(a) as: t is a - a / return t`, called 60 times with `x is 1e308 * 10`; `print of (f64_bits of (nz of x))`
+ -> `9221120237041090560` (the JIT bails on the NaN result; the interpreter's single writer canonicalizes)
+ tag: `f64_bits` undefined
+ shape: F witness: fn-stop nz SUB ops: SUB
+- `N12.leaf-nan-canonical` [R2.4] `define dif(a, b) as: return a - b / x is 1e308 * 10 / print of (f64_bits of (dif of [x, x]))`
+ -> `9221120237041090560` (computed in the leaf, written through `slot_from_f64`)
+ tag: `f64_bits` undefined
+ shape: L witness: leaf dif ops: SUB
+
+### N13
+- `N13.binary-line-int-errors` [R1.8] warm a two-int addition helper 60 times, then exact int64 overflow and floor-division overflow/zero through written multiline operators; catch the physical operator line and inspect its numeric kind; a negative integer quotient stays exact
+ -> `["value", true, "int"]` three times, then `[-4, "int"]`
+ tag: floor-division syntax and num_type are absent; int overflow is a double operation (read). Existing Phase2 contracts plus main #1425.
+ shape: F witness: fn-stop binary_add ADD ops: BINARY_LINE ADD
+- `N13.binary-line-parent-restore` [R1.8, invariant] warm a float sum used as a parent index; a null target raises at the right operand line restored after the successful sum; inspect exact int e.line; the final finite call crosses both markers and returns
+ -> `["type_mismatch", true, "int"]` then `1.5`
+ tag: num_type is absent; the v0.46 anchor predates the landed exact-line contract. Bank ed09f908 and merged probes both produce the exact output with fn RET; classify as an invariant under R1.7-D3.
+ shape: F witness: fn restored_index ops: BINARY_LINE ADD BINARY_LINE_END INDEX_GET
+- `gate.N13.audio-buffer-boundary` [R1.6e] `tests/phase2/gfx_audio_boundary.c` through `tools/phase2_readers.sh`: R1.6d-D1 pre-registration for audio_convert_samples (audio_play, audio_play_loop, audio_stream_push). Stored lenient NaN and every infinity raise `value`; strict NaN retains INVALID and the first diagnostic before a later infinity. Continuous finite samples clamp to [-1,1], then round PCM half away from zero. The fixture drives the actual helper with raw payloads because strict scalar constructors refuse first; no SDL dependency is exercised.
+- `N13.json-refuses-inf` [R2.3] `x is 1e308 * 10 / {try print of (json_encode of [x]) catch}`, printing `e.kind`
+ -> `value`
+ tag: `1e+308`, the clamp (read; a one-element literal list passes its element, #405). **Plant 3's named check.**
+- `N13.store-refuses-nan` [R2.3] `x is (1e308 * 10) - (1e308 * 10) / s is store_open of "