diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 4ef036d1..6cdd8355 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -54,7 +54,7 @@ jobs: # Shallow fetch of one commit; `git clone` of a branch is not a pin. - name: Fetch consumer DMG at its pin env: - DMG_REF: cf6ef5e151deff98cc29250a7c8fc394ee4b64d6 # DMG master, 2026-09-21 + DMG_REF: afd9bd52778cb0a8d1ab0473ec726aaae556fb90 # DMG master, 2026-10-06 (DMG#79: ready for the boolean type, #1637) run: | set -euo pipefail mkdir -p .codspeed-consumers/DMG && cd .codspeed-consumers/DMG diff --git a/Makefile b/Makefile index 98ed6723..634d8a5e 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ define AUX_REFRESH done endef -.PHONY: 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 +.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 # ---- Per-variant objdir engine (#740) ------------------------------------- # The engine's rules are defined before `all`, so pin the default goal. @@ -298,6 +298,16 @@ build/$(TRACE_CORRESPONDENCE_VARIANT)/test_trace_correspondence: tests/test_trac trace-correspondence-test: build/$(TRACE_CORRESPONDENCE_VARIANT)/test_trace_correspondence @echo "Trace correspondence test built: $<" +# #1637: db parameter binding without a PostgreSQL server -- the test +# includes src/ext_db.c (static db_build_query) and links the server-db +# variant's other objects. +DB_PARAMS_OBJ := $(filter-out build/server-db/main.o build/server-db/ext_db.o,$(OBJ_server-db)) +build/server-db/test_db_params: tests/test_db_params.c $(SRC_DIR)/ext_db.c $(DB_PARAMS_OBJ) $(wildcard $(SRC_DIR)/*.h) Makefile tools/werror_flags.txt + $(CC) $(FLAGS_server-db) -I$(SRC_DIR) -o $@ $< $(DB_PARAMS_OBJ) $(LIBS_server-db) +.PHONY: db-params-test +db-params-test: build/server-db/test_db_params + @echo "DB params test built: $<" + TRACE_CONTEXT_VARIANT ?= release TRACE_CONTEXT_OBJ := $(filter-out build/$(TRACE_CONTEXT_VARIANT)/main.o,$(OBJ_$(TRACE_CONTEXT_VARIANT))) build/$(TRACE_CONTEXT_VARIANT)/test_trace_context: tests/test_trace_context.c $(TRACE_CONTEXT_OBJ) $(wildcard $(SRC_DIR)/*.h) Makefile tools/werror_flags.txt diff --git a/README.md b/README.md index 7f43e9b2..d21788cc 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ blocked concurrency calls wake, and the process exits with status `N` after worker teardown. Native I/O must return before teardown completes. Embedded 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: 0, 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. +`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. 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. @@ -155,6 +155,10 @@ Alice Error: {"kind": "undefined_name", "message": "undefined variable 'risky_operation'", "line": 41} ``` +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). + ### Ask Your Code Every value quietly remembers how it has been changing — and you don't pay diff --git a/changes/breaking/1637-boolean-type.md b/changes/breaking/1637-boolean-type.md new file mode 100644 index 00000000..f25d9db8 --- /dev/null +++ b/changes/breaking/1637-boolean-type.md @@ -0,0 +1,42 @@ +- **A distinct `bool` type (#1637).** Comparisons and `not` return `true`/`false` of type `bool` + instead of the numbers `1`/`0`. Truthiness is unchanged (`false` is falsy, `true` truthy). + Arithmetic on a bool raises, and so does `==`/`!=` between a bool and a number: migrate + `(pred of x) == 1` to `if pred of x:` or `== true`. + `list_contains`/`list_index_of` raise on bool vs number the same way. Any + builtin given a bool where it does not take one raises, even under + `EIGS_STRICT=0`: as its argument, at a position of its argument list, or + as an element of a list it reads as numbers. The slots that take a bool are + listed in `tests/bool_fuzz_anyvalue.txt`. A bool slice bound, index, `range` + bound or `at` line raises. Deep `==` stops at the first unequal pair, and a + bool/number pair raises when the walk reaches it. Trace tapes are format v6; + a v5 tape is refused, and so is a record of a kind its builtin cannot + return. Embedding hosts declare a recorded name's kinds with + `eigs_trace_declare_kind`. `db_execute`/`db_query_value`/`db_query_json` take a bool + parameter and bind it as an SQL boolean, so a bool read from a boolean + column can be written back. `json_build` writes a bool value as JSON + `true`/`false`, and `what is` a bool answers the bool. The embedding API's + `eigs_value_as_num` answers NaN for a bool (0.0 for any other non-number). +- **Wrong-typed numbers that used to be read as garbage now raise (#1637).** The + bool work routed every C number read through a checking accessor, which + also changes some non-bool cases: + - a string `at` line (`what is x at "s"`) raises; it answered null; + - a non-number net timeout (`net_accept`/`net_dial`/`net_recv`), a + non-number `http_serve` port, and a non-number code byte in a + `vm_run_bytecode`/`sandbox_run` descriptor raise; they were read as + garbage or as 0 (a non-number `param_count` placeholder such as `[]` + still means 0 parameters; only a bool there raises); + - the tensor mutators `numerical_grad*`/`sgd_update*` raise on a non-number + cell or row in every strict mode, `EIGS_STRICT=0` included: v0.44.0 read + a non-number cell as 0 (and `numerical_grad_rows`/`_cols` wrote into it + in place) and skipped a null row; + - a non-number `screen_put` color and a non-number, non-bool + `write_bytes` append flag raise in every strict mode (neither had a + documented `EIGS_STRICT=0` answer); + - a non-number cell `gather` selects raises under `EIGS_STRICT` (it was + 0); `EIGS_STRICT=0` keeps the 0. + `EIGS_STRICT=0` keeps every answer it documents for non-bool values: + `str_from_bytes`, `inflate`/`deflate` and the other byte-list builtins + still read a non-number element as 0 there (a bool raises in every mode). + Embedders that include `eigenscript.h`: a Value's number is `VAL_NUM_RAW(v)` + and a slot's is `SLOT_NUM_RAW(s)` (the members were renamed so an unchecked + read does not compile). diff --git a/docs/BUILTINS.md b/docs/BUILTINS.md index 4fb2a5ad..4fca1c04 100644 --- a/docs/BUILTINS.md +++ b/docs/BUILTINS.md @@ -63,7 +63,7 @@ audio (`audio_open`, `audio_close`, `audio_pause`, `audio_play`, | `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). | | `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) | -| `math_flags` | `math_flags of null` | Sticky numeric status: `{overflow, invalid}` — 1 when a clamp has fired since the last `clear_math_flags` (#865) | +| `math_flags` | `math_flags of null` | Sticky numeric status: `{overflow, invalid}` — `true` when a clamp has fired since the last `clear_math_flags` (#865) | | `clear_math_flags` | `clear_math_flags of null` | Reset both status bits | | `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). | @@ -94,7 +94,7 @@ numeric fast paths used by reassignment and `unobserved` blocks. | `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_contains` | `list_contains of [list, value]` | 1 if any element structurally equals `value`, else 0 — the list counterpart of the string-only `contains` | +| `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]` | Sort list by numeric keys from key_fn (qsort, O(n log n), stable). 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 | @@ -105,9 +105,9 @@ 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 `[]` | -| `contains` | `contains of [haystack, needle]` | 1 if haystack contains needle, else 0 (non-string operands are 0, never a spurious match) | -| `starts_with` | `starts_with of [s, prefix]` | 1 if s starts with prefix, else 0 | -| `ends_with` | `ends_with of [s, suffix]` | 1 if s ends with suffix, else 0 | +| `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 | | `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). | @@ -125,7 +125,7 @@ numeric fast paths used by reassignment and `unobserved` blocks. | `text_builder_part_count` | `text_builder_part_count of builder` | Count appended parts | | `text_builder_clear` | `text_builder_clear of builder` | Empty a builder for reuse | | `text_builder_to_string` | `text_builder_to_string of builder` | Render buffered text | -| `secure_equals` | `secure_equals of [a, b]` | Constant-time string equality (`1`/`0`). Compares every byte regardless of where a mismatch occurs, so comparison time doesn't leak how much of a secret matched. Non-strings → `0` | +| `secure_equals` | `secure_equals of [a, b]` | Constant-time string equality (`true`/`false`). Compares every byte regardless of where a mismatch occurs, so comparison time doesn't leak how much of a secret matched. Non-strings → `false` | ### Regex @@ -185,16 +185,16 @@ Compact typed arrays of doubles with O(1) indexed access. Iterable with | `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 buffer. 1-arg form caps at 10MB; a file over the cap **raises** a catchable `io` error naming size + cap (#601 — was a silent null indistinguishable from "file missing"). `max_bytes` is the bounded opt-in, hard ceiling 512MB (a buffer stores one double per byte — 8x expansion — so the ceiling bounds worst-case memory at the amplification point). Missing/unopenable file returns null | | `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, 0 on failure. | -| `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 1/0. | -| `remove_file` | `remove_file of path` | Delete a file. Returns 1/0. | +| `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`/`false`. | +| `remove_file` | `remove_file of path` | Delete a file. Returns `true`/`false`. | ### 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: 0, 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). | +| `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). | | `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. | -| `sandbox_run` | `sandbox_run of [descriptor, max_iterations?, max_bytes?, max_work?]` | Run a chunk (same ABI-stamped descriptor as `vm_run_bytecode`) under safety bounds. Surplus outer call operands raise a catchable error by default; `EIGS_STRICT=0` ignores them. Descriptor failures are structured results. An ABI-revision mismatch comes back as `{ok: 0, error: {kind, message, line}}` with a message naming the revision, distinct from a malformed chunk's `invalid chunk descriptor`, so a grading ladder can tell "your producer is stale" from "your bytecode is wrong" without re-running. **Fail-closed**: only a pure-compute *allowlist* (math, bit, list/dict/string ops, buffers, json, regex, observer reads, parse/tokenize, `print`/`assert`) is visible — every other builtin (file/process/network/db, code-exec, threads, channels, terminal, `exit`, global-state mutators like `set_observer_thresholds`, and the whole extension surface) is shadowed by a blocked stub, so a new builtin is denied by default. The sandbox env is a **sealed root**, not a child of the host global env: the allowlist is copied in, so an outward assignment (`x is v`, `OP_SET_NAME`) has no outer binding to write through to and a name the host defines later is not reachable. `import` is gated at the opcode — it is not a name, so the allowlist never covered it. A **callable cannot cross the boundary**: a `fn` in the result closes over the sandbox env and would run in the host after the caps are restored, so it comes back as `{ok: 0, error: {kind: "sandbox", ...}}` instead. That scan is node/depth budgeted and fails closed, so a result too large to scan is also refused — with a message saying so, distinct from the one naming an actual callable. `max_iterations` bounds loops through **two** counters, whichever trips first, and they are scoped differently. The compiler-emitted cap check (`OP_LOOP_CAP_CHECK`) is **per call frame** — each invocation starts fresh, and tripping it exits the loop gracefully and reports the run as a capped *partial run*. The back-edge counter (`OP_JUMP_BACK`) is a **cumulative total for the whole `sandbox_run`**, deliberately not restored per frame so an assembled chunk cannot reset its own DoS budget by calling a function; tripping it raises. So the same loop called twice within one run can trip the cumulative budget even though each call is individually well inside `max_iterations` — the bound is on the run, not on any one loop. In addition, `max_work` (default 10,000,000) charges each interpreted VM instruction cumulatively across function frames and bytecode callback re-entry; exhaustion is an uncatchable-within-the-sandbox structured `sandbox` refusal. It does not meter time spent inside a native builtin, so a blocking native callback must return before the VM can enforce this budget. **A chunk that could underflow the operand stack is refused before it runs** (see `vm_run_bytecode` for the balance rule): the interpreter's arithmetic fast paths index the value stack directly rather than through the guarded pop, so an opcode reached with too few operands read and wrote below the stack's base — `[OP_ADD, OP_RETURN]` was enough. The equivalent env-chain fault is caught at the instruction instead, since it cannot be settled statically: an `OP_LOOP_ENV_END` with no matching `OP_LOOP_ENV_FRESH` raises `loop-env underflow` rather than walking the frame off the end of its scope chain. **Memory is capped at `max_bytes` (default 256 MiB)**: the size-controlled allocators (`zeros`/`fill`/`buffer`/`range`/`concat`) and the zlib codecs (`inflate`/`deflate` and their `zlib_*` duals — both the codec buffer and the list of values built from it) charge a per-run budget, so a single huge allocation *or* an aggregate across a loop raises a caught error (→ `{ok:0}`) instead of an uncatchable out-of-memory `abort()`. The codecs matter here because every other allocator makes the caller *name* a size, which is what the charge reads, while a compressed blob names nothing and amplifies ~1000x. **The descriptor itself is verified as untrusted data, under one bounded context for the whole graph**: a single work allowance covering the root chunk, every nested function chunk, code, constants, function lists and local names — nested chunks share it rather than each declaring their own — plus an explicit recursion-depth bound and back-edge (cycle) refusal. A graph that cannot be verified within that allowance comes back as `{ok: 0, "invalid chunk descriptor"}`. **Descriptor constants are data-only and ISOLATED**: a callable (or text builder) anywhere in the pool is refused at any depth, and each mutable constant (list/dict/buffer) is deep-copied, so the sandbox mutates its own copy — an allowlisted `append`/`set_at`/`dict_set`/`buf_set` on a constant cannot be seen by the host, and the host cannot change a constant mid-run. For the same reason the allowlist copies only the pure C builtin each allowed name actually holds: if the host has rebound one of those names, the sandbox gets the blocked stub rather than the host's value. Runtime errors are caught. Returns `{ok: 1/0, result: value}`. For validating untrusted/generated code. | +| `sandbox_run` | `sandbox_run of [descriptor, max_iterations?, max_bytes?, max_work?]` | Run a chunk (same ABI-stamped descriptor as `vm_run_bytecode`) under safety bounds. Surplus outer call operands raise a catchable error by default; `EIGS_STRICT=0` ignores them. Descriptor failures are structured results. An ABI-revision mismatch comes back as `{ok: false, error: {kind, message, line}}` with a message naming the revision, distinct from a malformed chunk's `invalid chunk descriptor`, so a grading ladder can tell "your producer is stale" from "your bytecode is wrong" without re-running. **Fail-closed**: only a pure-compute *allowlist* (math, bit, list/dict/string ops, buffers, json, regex, observer reads, parse/tokenize, `print`/`assert`) is visible — every other builtin (file/process/network/db, code-exec, threads, channels, terminal, `exit`, global-state mutators like `set_observer_thresholds`, and the whole extension surface) is shadowed by a blocked stub, so a new builtin is denied by default. The sandbox env is a **sealed root**, not a child of the host global env: the allowlist is copied in, so an outward assignment (`x is v`, `OP_SET_NAME`) has no outer binding to write through to and a name the host defines later is not reachable. `import` is gated at the opcode — it is not a name, so the allowlist never covered it. A **callable cannot cross the boundary**: a `fn` in the result closes over the sandbox env and would run in the host after the caps are restored, so it comes back as `{ok: false, error: {kind: "sandbox", ...}}` instead. That scan is node/depth budgeted and fails closed, so a result too large to scan is also refused — with a message saying so, distinct from the one naming an actual callable. `max_iterations` bounds loops through **two** counters, whichever trips first, and they are scoped differently. The compiler-emitted cap check (`OP_LOOP_CAP_CHECK`) is **per call frame** — each invocation starts fresh, and tripping it exits the loop gracefully and reports the run as a capped *partial run*. The back-edge counter (`OP_JUMP_BACK`) is a **cumulative total for the whole `sandbox_run`**, deliberately not restored per frame so an assembled chunk cannot reset its own DoS budget by calling a function; tripping it raises. So the same loop called twice within one run can trip the cumulative budget even though each call is individually well inside `max_iterations` — the bound is on the run, not on any one loop. In addition, `max_work` (default 10,000,000) charges each interpreted VM instruction cumulatively across function frames and bytecode callback re-entry; exhaustion is an uncatchable-within-the-sandbox structured `sandbox` refusal. It does not meter time spent inside a native builtin, so a blocking native callback must return before the VM can enforce this budget. **A chunk that could underflow the operand stack is refused before it runs** (see `vm_run_bytecode` for the balance rule): the interpreter's arithmetic fast paths index the value stack directly rather than through the guarded pop, so an opcode reached with too few operands read and wrote below the stack's base — `[OP_ADD, OP_RETURN]` was enough. The equivalent env-chain fault is caught at the instruction instead, since it cannot be settled statically: an `OP_LOOP_ENV_END` with no matching `OP_LOOP_ENV_FRESH` raises `loop-env underflow` rather than walking the frame off the end of its scope chain. **Memory is capped at `max_bytes` (default 256 MiB)**: the size-controlled allocators (`zeros`/`fill`/`buffer`/`range`/`concat`) and the zlib codecs (`inflate`/`deflate` and their `zlib_*` duals — both the codec buffer and the list of values built from it) charge a per-run budget, so a single huge allocation *or* an aggregate across a loop raises a caught error (→ `{ok: false}`) instead of an uncatchable out-of-memory `abort()`. The codecs matter here because every other allocator makes the caller *name* a size, which is what the charge reads, while a compressed blob names nothing and amplifies ~1000x. **The descriptor itself is verified as untrusted data, under one bounded context for the whole graph**: a single work allowance covering the root chunk, every nested function chunk, code, constants, function lists and local names — nested chunks share it rather than each declaring their own — plus an explicit recursion-depth bound and back-edge (cycle) refusal. A graph that cannot be verified within that allowance comes back as `{ok: false, "invalid chunk descriptor"}`. **Descriptor constants are data-only and ISOLATED**: a callable (or text builder) anywhere in the pool is refused at any depth, and each mutable constant (list/dict/buffer) is deep-copied, so the sandbox mutates its own copy — an allowlisted `append`/`set_at`/`dict_set`/`buf_set` on a constant cannot be seen by the host, and the host cannot change a constant mid-run. For the same reason the allowlist copies only the pure C builtin each allowed name actually holds: if the host has rebound one of those names, the sandbox gets the blocked stub rather than the host's value. Runtime errors are caught. Returns `{ok: true/false, result: value}`. For validating untrusted/generated code. | ### Bytes ↔ values @@ -248,7 +248,7 @@ Requires the `zlib` build (`make zlib`, `-DEIGENSCRIPT_EXT_ZLIB=1 |------|-----------|-------------| | `keys` | `keys of dict` | List of keys | | `values` | `values of dict` | List of values | -| `has_key` | `has_key of [dict, "key"]` | 1 or 0 | +| `has_key` | `has_key of [dict, "key"]` | `true` or `false` | | `dict_set` | `dict_set of [dict, "key", value]` | Set key in dict (mutates), return dict | | `dict_remove` | `dict_remove of [dict, "key"]` | Remove key from dict (mutates), return dict | @@ -338,14 +338,14 @@ Boolean keywords that check the most recently observed value: | Name | Signature | Description | |------|-----------|-------------| | `load_file` | `load_file of "path.eigs"` | Execute a file in the current scope, yielding its top-level return value. Uses the same file-based resolution chain as `import` (below). A missing/unreadable path raises a catchable `io` error naming the roots tried; a parse/compile failure raises `parse`. | -| `file_exists` | `file_exists of "path"` | 1 if the path exists (any kind: file, directory, device, fifo), 0 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"` | 1 if the path names a directory, 0 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"` | 1 iff the path names a REGULAR file (`S_ISREG`); 0 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 | +| `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 file contents as string ("" on failure, 10 MB cap) | | `read_line` | `read_line of null` | Blocking line read from **stdin**: next line without its trailing newline (`\r\n` stripped as one unit), `null` at EOF; an empty line is `""`. Works on pipes — the stream-safe primitive `read_text of "/dev/stdin"` can't be (fseek fails on unseekable fds, #558). Trace-recorded, so replay is deterministic | | `read_bytes` | `read_bytes of "path"` | Read a file's raw bytes as a list of integers 0–255 (`null` on failure, 10 MB cap). Trace-recorded, so replay is deterministic | | `proc_read_buf` | `proc_read_buf of [out_fd, max]` | Single `read(2)` of up to `max` bytes from a child fd, returned as a list of integers 0–255 — the byte-list twin of `proc_read`. `null` on EOF / error, 10 MB cap. Replay-gated | -| `write_text` | `write_text of ["path", text]` | Write string to file (1 on success, 0 on failure) | +| `write_text` | `write_text of ["path", text]` | Write string to file (`true` on success, `false` on failure) | | `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]` | | `proc_spawn` | `proc_spawn of ["cmd", "arg1", ...]` | Fork+execvp a child with stdin/stdout connected to anonymous pipes. Returns `[pid, in_fd, out_fd]` (or `[-1,-1,-1]` on failure). Caller is responsible for `proc_close` on both fds and `proc_wait` on the pid. SIGPIPE is set to `SIG_IGN` process-wide on first spawn so the parent gets `EPIPE` from `proc_write` instead of dying; child resets to `SIG_DFL` post-fork. | | `proc_write` | `proc_write of [in_fd, text]` | Write bytes to child's stdin pipe (raw `write(2)`, no parent-side buffering). Returns bytes written, or `-1` on error (including `EPIPE` when the child has closed its stdin). | @@ -355,14 +355,14 @@ Boolean keywords that check the most recently observed value: | `proc_wait` | `proc_wait of pid` | Block on `waitpid(pid, ...)` and return the exit code (or `128 + signum` if killed by a signal). Answers `-1` when the pid is not a positive number or `waitpid` reports no such child — there is no exit status to give. | | `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). | -| `try_parse` | `try_parse of code_string` | 1 if string is valid EigenScript syntax, 0 otherwise | -| `mkdir` | `mkdir of "path"` | Create directory (and parents). 1 on success, 0 on failure. Trace-recorded: replay serves the recorded bit and does not re-create the directory (#585) | +| `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, `false` on failure. Trace-recorded: replay serves the recorded bool 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`). Trace-recorded, so replay is deterministic (#585) | | `getcwd` | `getcwd of null` | Current working directory as string. Trace-recorded, so replay is deterministic (#585) | | `exe_path` | `exe_path of null` | Absolute path of the running interpreter binary. Lets a script re-invoke the same interpreter (e.g. `exec_capture of [exe_path of null, file]`) without assuming `eigenscript` is on PATH. Trace-recorded, so replay is deterministic (#585) | -| `chdir` | `chdir of "path"` | Change working directory. 1 on success, 0 on failure | +| `chdir` | `chdir of "path"` | Change working directory. `true` on success, `false` on failure | | `mktemp` | `mktemp of null` | Create a temporary file and return its path. Under `EIGS_REPLAY`, raises a catchable filesystem-boundary error before creating a file | -| `rm` | `rm of "path"` | Remove a file. 1 on success, 0 on failure | +| `rm` | `rm of "path"` | Remove a file. `true` on success, `false` on failure | | `write` | `write of value` | Write to stdout without newline | | `flush` | `flush of null` | Flush stdout | @@ -374,9 +374,9 @@ 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 1 on success, 0 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. 1 on success, 0 on failure | -| `stream_close` | `stream_close of null` | Close the stream. 1 on success, 0 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. 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_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 containing file's directory, the `eigs_modules` walk (stopping at `eigs.json`), @@ -700,7 +700,7 @@ 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 `1` if key present, else `0`. | +| `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_keys` | `shared_keys of null` | Return list of keys. | | `shared_size` | `shared_size of null` | Return current key count. | @@ -734,7 +734,7 @@ 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 `1` on success or `0` when libSDL2 is unavailable. A non-numeric size raises by default; under `EIGS_STRICT=0` it returns `0` (#1007 — it used to be read without a type check, so a string opened a `0x0` window and still answered `1`). | +| `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 `1`). | | `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 | @@ -748,7 +748,7 @@ first use, so no SDL2 headers or link-time SDL dependency are needed. | `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_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` (0/1); wheel `x`/`y` are scroll deltas | +| `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_title` | `gfx_title of "text"` | Update window title | @@ -885,7 +885,7 @@ Values carry their column's SQL type rather than arriving as strings: | SQL type | Arrives as | Note | |---|---|---| | NULL (any column type) | `null` | Distinct from `""` — checked before the type | -| `boolean` | `true` / `false` → `1` / `0` | `if row.is_admin:` means what it reads as | +| `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 | | `real`, `double precision` | number | `NaN`/`Infinity` arrive as strings; JSON has no literal for them | | `numeric` | **string** | Deliberate — see below | @@ -923,7 +923,7 @@ Requires full build. Transformer model inference and training. | Name | Signature | Description | |------|-----------|-------------| | `eigen_model_load` | `eigen_model_load of "path.json"` | Load model weights from JSON | -| `eigen_model_loaded` | `eigen_model_loaded of null` | 1 if model loaded, 0 otherwise | +| `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. 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. Raises when the prompt exceeds the model's `max_seq_len`. | @@ -943,17 +943,17 @@ Requires full build. Transformer model inference and training. | `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 1 if closed, 0 otherwise. An unknown or reclaimed channel is closed (1). A value that is not a channel handle raises by default; under `EIGS_STRICT=0` it answers 1 (#971). | +| `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). | | `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 1 while the task is runnable or suspended, 0 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). | | `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 an `arena_mark`…`arena_reset` scope or a nested evaluation (raises `value`). | | `must_not_yield` | `must_not_yield of fn` | Run `fn of null` as an **atomic** critical section, asserting it issues no scheduler yield (#488). Any *suspending* task builtin inside — `task_yield`, a blocking `task_recv`/`task_join`, `task_sleep` — raises `value` instead of suspending, so a yield introduced into a region that relies on cooperative atomicity fails loudly rather than corrupting under a rare interleaving. Non-suspending ops (`task_try_recv`, `task_send`, joining an already-finished task) are allowed. Returns `fn`'s result (or propagates its error); the region depth is balanced even if the body raises. Nestable. | | `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 1 if delivered, 0 if `id` is finished/unknown (a silent drop — send-to-dead is never an error). Never blocks. | +| `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, 0 if `id` is finished/unknown (a silent drop — send-to-dead is never an error). Never blocks. | | `task_recv` | `task_recv of null` | Return the next message from this task's mailbox, or block cooperatively until one arrives. Forbidden inside an `arena_mark`…`arena_reset` scope or a nested evaluation (raises `value`). | | `task_try_recv` | `task_try_recv of null` | Non-blocking receive: the next mailbox message, or `null` if empty. Never suspends. | -| `task_kill` | `task_kill of id` | Tear down task `id`: drop its mailbox, mark it dead, wake any joiner with an `interrupt` error. Returns 1 if killed, 0 for a finished/unknown/self target. | +| `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, 0 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. A detached task's uncaught death still prints its trace and still fails the process at exit (#493). A reaped id reads as unknown afterwards (`task_join` null, `task_alive` 0). A task may detach itself: `task_detach of (task_self of null)`. Returns 1, or 0 for main/unknown. | | `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. Forbidden inside an `arena_mark`…`arena_reset` scope. | | `task_now` | `task_now of null` | The current virtual-clock value (a number; 0 before any `task_sleep`). Deterministic — reads a logical counter, records no nondeterminism. | @@ -990,7 +990,7 @@ receiver. | `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 `1` on success, `0` on a closed device or bad shape; a `samples` that is not a list or buffer raises by default and answers `0` under `EIGS_STRICT=0` (#1007). | +| `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 `0` 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_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. | diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index 302a8295..9b90facf 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -24,7 +24,7 @@ asking the running program about its own state and history. | 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 | -| booleans | `True/False` | `true/false` | `bool` | `#t/#f` | `1`/`0` | +| booleans | `True/False` | `true/false` | `bool` | `#t/#f` | `true`/`false` (`bool`) | | null | `None` | `null/undefined` | `Option` | `nil` | `null` | | errors | exceptions | exceptions | `Result` | conditions | `try`/`catch` | | self-inspection | none | none | none | macros | interrogatives + observer | @@ -35,7 +35,7 @@ an integral count from 1 through that cap, and `build_corpus` includes file separators in its capped token count. These limits also raise under `EIGS_STRICT=0`; see [BUILTINS.md](BUILTINS.md) for the I/O contracts. -`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: 0, 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. +`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. 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. @@ -505,7 +505,7 @@ print of (e < 0.001) x 3 20 -1 +true ``` The past is addressable per **assignment**, not just as a single step back — @@ -589,7 +589,7 @@ print of (converged of r) ``` ```output diverging -0 +false ``` ## Before / after: porting checklist @@ -602,7 +602,7 @@ Transformations you will apply constantly when porting Python code: | `f(a)` | `f of a` | application keyword | | `f(a, b)` | `f of [a, b]` | bare literal list = argument list | | `f([a])` (pass a 1-element list) | `f of ([a])` | parentheses = one argument | -| `True` / `False` / `None` | `1` / `0` / `null` | no boolean type | +| `True` / `False` / `None` | `true` / `false` / `null` | `bool` is not a number: `true + 1` and `true == 1` raise | | `x ** y` | `pow of [x, y]` | `^` is XOR | | `len(x)` | `len of x` | builtin, same name | | `xs.append(v)` | `append of [xs, v]` | builtins, not methods | diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index 9c71508a..ddbea2cb 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -342,6 +342,7 @@ embed API refuses it too: `eigs_set_global`, `eigs_get_global` and EigsValue *eigs_value_new_num(double 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) */ EigsValue *eigs_value_new_list(int capacity); EigsValue *eigs_value_new_dict(int capacity); EigsValue *eigs_value_new_buffer(int count); /* zeroed, 1-D; the binary carrier */ @@ -358,8 +359,9 @@ void eigs_value_release(EigsValue *v); Inspection: ```c -EigsValueType eigs_value_type(EigsValue *v); /* EIGS_TYPE_NUM, _STR, _LIST, _DICT, _NULL, _FN, _BUFFER, _OTHER */ -double eigs_value_as_num(EigsValue *v); /* 0.0 if wrong type */ +EigsValueType eigs_value_type(EigsValue *v); /* EIGS_TYPE_NUM, _STR, _LIST, _DICT, _NULL, _FN, _BUFFER, _OTHER, _BOOL */ +double eigs_value_as_num(EigsValue *v); /* NaN for a bool (#1637); 0.0 for any other non-num */ +int eigs_value_as_bool(EigsValue *v); /* 1 for true; 0 for false or wrong type */ const char *eigs_value_as_string(EigsValue *v); /* NULL if wrong type; borrowed pointer */ int eigs_value_list_len(EigsValue *v); EigsValue *eigs_value_list_get(EigsValue *v, int i); /* counted ref */ @@ -399,6 +401,8 @@ int eigs_set_replay_tape(const char *bytes, size_t len, int strict); /* copied; int eigs_replay_advance_session(void); /* explicit quiescent host boundary */ int eigs_replay_take(const char *name, EigsValue **out); /* 1 = served from tape */ void eigs_trace_record_nondet(const char *name, EigsValue *v); +#define EIGS_KIND(t) (1u << (unsigned)(t)) +int eigs_trace_declare_kind(const char *name, unsigned kinds); /* 0 = refused */ ``` The sink receives ONE complete newline-terminated record per call, @@ -539,7 +543,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 5 grammar +memory replacement leaves the active context untouched. The version 6 grammar and older-format refusal rule are documented in `docs/TRACE.md`. Host builtins participate with the take/record pair, the same contract @@ -553,8 +557,19 @@ static EigsValue *my_sensor(EigsValue *arg) { eigs_trace_record_nondet("my_sensor", v); /* onto the tape */ return v; } + +/* at registration: the kinds a recorded my_sensor value can have */ +eigs_register_function("my_sensor", my_sensor); +eigs_trace_declare_kind("my_sensor", EIGS_KIND(EIGS_TYPE_NUM)); ``` +Replay checks every recorded value against its name's declared kinds and +refuses (exit 3) a record of another kind -- a hand-edited +`my_sensor=true` never reaches code that reads a number -- and a host name +that was never declared. The runtime's own taped builtins are declared by +the runtime; `eigs_trace_declare_kind` refuses their names, an empty name, +`EIGS_TYPE_FN`/`EIGS_TYPE_OTHER` and an empty kind set (returns 0). + ## FFI: calling host functions from script ```c @@ -578,17 +593,25 @@ static EigsValue *host_add(EigsValue *arg) { if (eigs_value_list_len(arg) != 2) return eigs_value_new_null(); EigsValue *a = eigs_value_list_get(arg, 0); EigsValue *b = eigs_value_list_get(arg, 1); - double sum = eigs_value_as_num(a) + eigs_value_as_num(b); + /* Check the types: eigs_value_as_num answers NaN for a bool and 0.0 for + * other non-numbers, so an unchecked read turns `host_add of [true, 1]` + * into a wrong number. */ + EigsValue *r = (eigs_value_type(a) == EIGS_TYPE_NUM && eigs_value_type(b) == EIGS_TYPE_NUM) + ? eigs_value_new_num(eigs_value_as_num(a) + eigs_value_as_num(b)) + : eigs_value_new_null(); eigs_value_release(a); eigs_value_release(b); - return eigs_value_new_num(sum); + return r; } eigs_register_function("host_add", host_add); ``` After registration, the script side calls it the same way as any -builtin: `host_add of [3, 4]`. +builtin: `host_add of [3, 4]`. Check argument types with `eigs_value_type` +before reading them: `eigs_value_as_num` answers NaN for a bool -- so a bool +read as a number poisons the host's arithmetic visibly instead of passing for +0 -- and 0.0 for any other non-number. Read a bool with `eigs_value_as_bool`. A registered function joins the state's builtin layer as well as its global scope (#1388): imported module code sees it, and a later host rebinding of diff --git a/docs/OBSERVER.md b/docs/OBSERVER.md index db543790..8140f3fe 100644 --- a/docs/OBSERVER.md +++ b/docs/OBSERVER.md @@ -62,6 +62,10 @@ Two of these are the load-bearing pair: is a trajectory of *assignments*: mutation does not move it, and asking never writes anything back.) For a number it is, in spirit, *how many bits it takes to pin the value down*. + A bool (#1637) is one bit: `false` sits at 0 and `true` at 1, the two + ends of the binary-entropy range (the values the old `1`/`0` comparison + results had), and since a bool is not a number the predicates read a bool + binding on the entropy channel, not the value channel. For a string it is the Shannon entropy of its characters; for a list or dict it is the average of its elements plus a size term, and the walk **stops at a reference** — an element that is itself a container diff --git a/docs/SPEC.md b/docs/SPEC.md index cbd3430a..ac0be319 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -350,7 +350,7 @@ print of (bit_shl of [1, 40]) # exact past 2^32 ``` ```output 9007199254740992 -1 +true 1e+308 255 1099511627776 @@ -448,14 +448,14 @@ print of (len of "hello") # 5 — all ASCII, 1 byte each print of (len of "héllo") # 6 — é is 2 UTF-8 bytes euro is "€" print of (len of euro) # 3 — one character, three bytes -print of (euro == "€") # 1 — bytes round-trip exactly +print of (euro == "€") # true — bytes round-trip exactly print of ("price: " + euro) # f-strings / concat are byte-wise, multibyte-safe ``` ```output 5 6 3 -1 +true price: € ``` @@ -469,10 +469,16 @@ the VM, JIT, AOT, and every tool, for a bill this scale doesn't need. ## Booleans, comparison, and logic -There is no separate boolean type: comparisons produce `1` (true) or -`0` (false), and any value can be tested for truthiness (0, `null`, -empty string/list/dict are falsy). Logical operators are the words -`and`, `or`, `not`. +`bool` is a type of its own with two values, the keywords `true` and `false` +(#1637). Comparisons (`== != < <= > >=`), `not`, the observer predicates, and +every predicate builtin and `lib/` predicate return a `bool`. `print` and +`str` give `true`/`false`; `json_encode` writes JSON's `true`/`false` and +`json_decode` reads them back as bools. Logical operators are the words `and`, +`or`, `not`; `and`/`or` return one of their operands (not necessarily a bool), +and `not` always returns a bool. + +Truthiness is unchanged: `0`, `0.0`, `null`, `""`, `[]`, `{}` and `false` are +falsy; every other value, including `true`, is truthy. ```eigenscript print of (3 > 2) @@ -483,15 +489,76 @@ print of (3 >= 3) print of (1 and 0) print of (1 or 0) print of (not 0) +print of (type of true) ``` ```output -1 -0 -1 -0 -1 +true +false +true +false +true 0 1 +true +bool +``` + +A bool is not a number. The exact rule, in every strict mode +(`EIGS_STRICT=0` keeps its soft stand-ins for other wrong types, never for a +bool): + +- Arithmetic, ordering (`<`, `sort`), indexing, slice bounds, `range`, an + `at` line and a `when` ordinal raise on a bool, and so do `==` and `!=` + between a bool and a number. +- A builtin raises when it is given a bool anywhere it does not take one: as + its argument, at a position of its argument list, or as an element of a + list it reads as numbers (`sum of [1, true]`, `sgd_update`'s gradient). The + positions that DO take a bool are few and reviewed: printing and + conversion (`print`, `write`, `str`, `type of`, `json_encode`), the + observer, `assert`'s condition, `write_bytes`'s append flag, and the + element slots of containers and channels (`append`, `set_at`, `dict_set`, + `fill`, `list_insert_at`, `send`, a `json_build` value). The full list is + `tests/bool_fuzz_anyvalue.txt`; `tests/test_bool_fuzz.sh` puts `true` and + `false` into every argument slot of every builtin and checks it. + +So a check written against the old `1`/`0` answers fails loudly instead of +quietly flipping: write `if pred of x:` or `(pred of x) == true`. The membership builtins `list_contains` and +`list_index_of` search with the same comparison and raise the same way, so an +old 1/0 membership test cannot quietly flip from found to not-found. Every +other mixed-type pair is simply unequal +(`null == false` is `false`, `"true" == true` is `false`). A bool converts to +a number only explicitly, by branching on it; `num of b` raises. + +```eigenscript +try: + print of (true + 1) +catch e: + print of e.message +try: + print of ((1 < 2) == 1) +catch e: + print of e.message +xs is [10, 20, 30] +try: + print of xs[(1 < 2):] +catch e: + print of e.message +try: + print of (sum of [1, 2 > 1]) +catch e: + print of e.message +print of (null == false) +n is 0 +if 2 > 1: + n is 1 +print of n +``` +```output +cannot apply '+' to bool and num +cannot compare bool and num with '==' +slice bound must be an integer or null, got bool +sum: argument 2 is a bool, which sum does not take there +false 1 ``` @@ -503,9 +570,27 @@ print of ({"a": 1} == {"a": 1}) print of ({"a": 1} == {"a": 2}) ``` ```output -1 -1 -0 +true +true +false +``` + +A deep comparison walks the pairs in order (lists by index, dicts in the +left operand's key order) and stops at the first unequal pair. A bool/number +pair raises when the walk reaches it, so whether `[x, true] == [y, 1]` raises +depends on the pairs before it: an earlier unequal pair answers `false` +first. + +```eigenscript +print of ([1, true] == [2, 1]) +try: + print of ([2, true] == [2, 1]) +catch e: + print of e.message +``` +```output +false +cannot compare bool and num with '==' ``` ## Bitwise operators @@ -1041,7 +1126,7 @@ expressions match too ## Error handling -`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: 0, 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. +`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. 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. @@ -1501,8 +1586,8 @@ print of (e < 0.001) print of converged ``` ```output -1 -1 +true +true ``` For a **numeric** binding the predicates classify the value's own @@ -1587,7 +1672,7 @@ print of ((where is d) == (where is e)) print of why is d ``` ```output -1 +true 0 ``` @@ -1614,7 +1699,7 @@ print of (equilibrium of f) ``` ```output opaque -0 +false ``` **The saturation ceiling is not a rest state** (#861). Overflow saturates @@ -1640,7 +1725,7 @@ print of (converged of z) ``` ```output diverging -0 +false ``` **The value channel** (`report_value of x`) is, since #861, the same diff --git a/docs/STDLIB.md b/docs/STDLIB.md index 186edeed..53925c61 100644 --- a/docs/STDLIB.md +++ b/docs/STDLIB.md @@ -175,8 +175,8 @@ signature comment above each function (e.g., `# clamp of [value, lo, hi]`). | `flatten` | `flatten of list` | Flatten one level of nesting | | `take` | `take of [list, n]` | First n elements (clamps to length) | | `drop` | `drop of [list, n]` | All but the first n elements | -| `any` | `any of [list, fn]` | 1 if fn is truthy for some element, else 0 (empty: 0) | -| `all` | `all of [list, fn]` | 1 if fn is truthy for every element, else 0 (empty: 1) | +| `any` | `any of [list, fn]` | `true` if fn is truthy for some element, else `false` (empty: `false`) | +| `all` | `all of [list, fn]` | `true` if fn is truthy for every element, else `false` (empty: `true`) | | `find_index` | `find_index of [list, fn]` | Index of first element where fn is truthy, or -1 | | `partition` | `partition of [list, fn]` | Split into `[passing, failing]` in one pass | | `group_by` | `group_by of [list, key_fn]` | Bucket elements into a dict by stringified key | @@ -254,9 +254,9 @@ iteration. | Function | Signature | Description | |----------|-----------|-------------| | `sanitize_text` | `sanitize_text of string` | Trim whitespace | -| `is_garble` | `is_garble of string` | 1 if text looks like nonsense | +| `is_garble` | `is_garble of string` | `true` if text looks like nonsense | | `clean_response` | `clean_response of string` | Trim at "User:" marker | -| `check_openai` | `check_openai of null` | 1 if OpenAI API key available | +| `check_openai` | `check_openai of null` | `true` if OpenAI API key available | ### lib/auth.eigs — Token Authentication @@ -277,11 +277,11 @@ Requires: `env_get`, `random_hex`, `http_request_headers` builtins. | `entropy_of` | `entropy_of of value` | Current entropy | | `delta_of` | `delta_of of value` | Current dH (rate of change) | | `prev_delta_of` | `prev_delta_of of value` | Previous dH | -| `is_converged` | `is_converged of value` | 1 if converged | -| `is_stable` | `is_stable of value` | 1 if stable or converged | -| `is_improving` | `is_improving of value` | 1 if entropy decreasing | -| `is_diverging` | `is_diverging of value` | 1 if entropy increasing | -| `is_oscillating` | `is_oscillating of value` | 1 if dH sign-flipping | +| `is_converged` | `is_converged of value` | `true` if converged | +| `is_stable` | `is_stable of value` | `true` if stable or converged | +| `is_improving` | `is_improving of value` | `true` if entropy decreasing | +| `is_diverging` | `is_diverging of value` | `true` if entropy increasing | +| `is_oscillating` | `is_oscillating of value` | `true` if dH sign-flipping | | `wait_until_converged` | `wait_until_converged of [val, fn, max]` | Run fn until convergence | | `track_regimes` | `track_regimes of [val, fn, max]` | Log regime transitions | | `threshold_alert` | `threshold_alert of [val, lo, hi]` | "below", "above", or "ok" | @@ -489,7 +489,7 @@ registers into epoch seconds with these and no host clock. |----------|-----------|-------------| | `json_get` | `json_get of [json, "key"]` | Extract top-level value | | `json_get_path` | `json_get_path of [json, "a.b"]` | Extract nested value | -| `json_has` | `json_has of [json, "key"]` | 1 if key exists | +| `json_has` | `json_has of [json, "key"]` | `true` if key exists | | `json_from_pairs` | `json_from_pairs of pairs` | [[k,v],...] to JSON | | `json_merge` | `json_merge of [json_a, json_b]` | Flat-merge two objects (keys of `json_b` win; nested values replace whole); raises if either is not an object | | `json_pretty` | `json_pretty of json_str` | Indented output; layout is added only outside string tokens, so string values come through byte-for-byte | @@ -547,7 +547,7 @@ success on the first attempt costs none. | `sort_desc` | `sort_desc of list` | Sort descending | | `sort_by` | `sort_by of [list, key_fn]` | Sort by key function | | `sorted_indices` | `sorted_indices of list` | Indices that would sort the list | -| `is_sorted` | `is_sorted of list` | 1 if ascending order | +| `is_sorted` | `is_sorted of list` | `true` if ascending order | | `unique` | `unique of list` | Sorted, deduplicated list | ### lib/map.eigs — Key-Value Data Structure @@ -559,7 +559,7 @@ Maps are lists of `[key, value]` pairs. Keys are compared with `==`. | `map_new` | `map_new of null` | Create empty map | | `map_get` | `map_get of [map, key]` | Get value or null | | `map_get_default` | `map_get_default of [map, key, default]` | Get value or default | -| `map_has` | `map_has of [map, key]` | 1 if key exists | +| `map_has` | `map_has of [map, key]` | `true` if key exists | | `map_set` | `map_set of [map, key, value]` | Set key (returns new map) | | `map_remove` | `map_remove of [map, key]` | Remove key (returns new map) | | `map_keys` | `map_keys of map` | List all keys | @@ -942,7 +942,7 @@ semantics when you need them, decoding UTF-8 over the byte primitives. | `utf8_codepoints` | `utf8_codepoints of s` | List of codepoint numbers | | `utf8_at` | `utf8_at of [s, i]` | i-th codepoint (0-indexed), or -1 | | `utf8_char_at` | `utf8_char_at of [s, i]` | i-th character as a (multi-byte) string, or "" | -| `utf8_validate` | `utf8_validate of s` | 1 if structurally valid UTF-8, else 0 | +| `utf8_validate` | `utf8_validate of s` | `true` if structurally valid UTF-8, else `false` | | `utf8_encode` | `utf8_encode of cp` | Codepoint → UTF-8 byte string ("" if unencodable) | | `utf8_from_codepoints` | `utf8_from_codepoints of cps` | List of codepoints → UTF-8 string (inverse of `utf8_codepoints`) | @@ -961,7 +961,7 @@ per-file and total tallies. | Function | Signature | Description | |----------|-----------|-------------| | `parse_args` | `parse_args of null` | Parse CLI args into map | -| `get_flag` | `get_flag of [parsed, "--flag"]` | 1 if flag present | +| `get_flag` | `get_flag of [parsed, "--flag"]` | `true` if flag present | | `get_opt` | `get_opt of [parsed, "--key", default]` | Get option or default | | `get_positional` | `get_positional of parsed` | List of positional args | | `has_flag` | `has_flag of [parsed, "--flag"]` | Alias for get_flag | @@ -971,7 +971,7 @@ per-file and total tallies. load_file of "lib/args.eigs" # eigenscript myscript.eigs --verbose --output=result.txt input.csv parsed is parse_args of null -if (get_flag of [parsed, "--verbose"]) == 1: +if get_flag of [parsed, "--verbose"]: print of "Verbose mode on" outfile is get_opt of [parsed, "--output", "out.txt"] files is get_positional of parsed # ["input.csv"] @@ -996,7 +996,7 @@ Sets are sorted lists with no duplicates. | Function | Signature | Description | |----------|-----------|-------------| | `set_from` | `set_from of list` | Create set from list | -| `set_has` | `set_has of [set, value]` | 1 if member | +| `set_has` | `set_has of [set, value]` | `true` if member | | `set_add` | `set_add of [set, value]` | Add element | | `set_remove` | `set_remove of [set, value]` | Remove element | | `set_size` | `set_size of set` | Element count | @@ -1004,9 +1004,9 @@ Sets are sorted lists with no duplicates. | `intersect` | `intersect of [a, b]` | Common elements | | `difference` | `difference of [a, b]` | In a, not in b | | `symmetric_diff` | `symmetric_diff of [a, b]` | In one, not both | -| `is_subset` | `is_subset of [a, b]` | 1 if a ⊆ b | -| `is_superset` | `is_superset of [a, b]` | 1 if a ⊇ b | -| `set_equal` | `set_equal of [a, b]` | 1 if same elements | +| `is_subset` | `is_subset of [a, b]` | `true` if a ⊆ b | +| `is_superset` | `is_superset of [a, b]` | `true` if a ⊇ b | +| `set_equal` | `set_equal of [a, b]` | `true` if same elements | ### lib/log.eigs — Structured Logging @@ -1082,7 +1082,7 @@ All structures are immutable — operations return new structures. | `sm_add_transition` | `sm_add_transition of [sm, from, event, to]` | Add rule | | `sm_send` | `sm_send of [sm, event]` | Trigger transition | | `sm_try_send` | `sm_try_send of [sm, event]` | Trigger or no-op | -| `sm_can_send` | `sm_can_send of [sm, event]` | 1 if valid | +| `sm_can_send` | `sm_can_send of [sm, event]` | `true` if valid | | `sm_state` | `sm_state of sm` | Current state | | `sm_history` | `sm_history of sm` | State history | | `sm_is` | `sm_is of [sm, "state"]` | Check current state | @@ -1102,7 +1102,7 @@ print of (sm_can_send of [sm, "stop"]) # 1 ``` ```output running -1 +true ``` ### lib/template.eigs — String Templating @@ -1503,7 +1503,7 @@ Computational geometry: 2D/3D points and vectors, line/segment intersection, tri | `polygon_area` | `polygon_area of vertices` | Positive = counter-clockwise, negative = clockwise | | `polygon_centroid` | `polygon_centroid of vertices` | Centroid of simple polygon | | `polygon_perimeter` | `polygon_perimeter of vertices` | Sum of edge lengths | -| `point_in_polygon` | `point_in_polygon of [point, vertices]` | Ray casting algorithm -- odd number of crossings = inside | +| `point_in_polygon` | `point_in_polygon of [point, vertices]` | `true` when the point is inside (ray casting: an odd number of crossings) | | `polygon_is_convex` | `polygon_is_convex of vertices` | Check if all cross products have same sign | | `polygon_is_clockwise` | `polygon_is_clockwise of vertices` | Check winding order via signed area | | `convex_hull` | `convex_hull of points` | Convex hull (Andrew's monotone chain) | @@ -1771,7 +1771,7 @@ Experiment management composing EigenStore, observer semantics, stats, and the e | `record_batch` | `record_batch of [exp, variable, vals]` | Record a list of measurements | | `record_with_tag` | `record_with_tag of [exp, variable, value, tag]` | Record a measurement with a tag | | `status` | `status of [exp, variable]` | Current observer status for a variable | -| `is_stable` | `is_stable of [exp, variable]` | 1 if the variable's measurements are stable | +| `is_stable` | `is_stable of [exp, variable]` | `true` if the variable's measurements are stable | | `wait_for_stable` | `wait_for_stable of [exp, variable, collect_fn, interval, timeout]` | Poll collect_fn until the variable stabilizes or times out | | `measurements` | `measurements of [exp, variable]` | All measurement records for a variable | | `values` | `values of [exp, variable]` | Flat list of a variable's values | @@ -1880,6 +1880,8 @@ Internal names below are implementation contracts for maintainers, not a stable | `text_width(text, scale)` | Measures active-backend text; bitmap fallback advances six pixels per character per scale. | | `text_height(scale)` | Measures active-backend font height; bitmap fallback is seven pixels per scale. | | `_combo_matches(combo, ctrl, shift, alt, key)` | Internal hotkey match requires exact modifiers and key. | +| `_ev_flag(v)` | Internal: an event modifier as a bool; accepts a bool or the older 1/0 number, anything else is false. | +| `_ev_stop(r)` | Internal: whether an on_key answer stops the loop; false or the older 0 stops, anything else continues. | | `label(id, x, y, text)` | Creates a visible automatically measured text widget with theme color and scale. | | `separator(id, x, y, length, vertical)` | Creates a one-pixel line whose requested length is its height when vertical. | | `section(id, x, y, w, text)` | Creates a 20-pixel-high section caption with the requested width. | diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index 14b17a59..417d0f22 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -312,7 +312,11 @@ This works for factory patterns, callbacks, and higher-order programming. compare by value; lists and dicts compare *structurally* (so `[1,2] == [1,2]` is true and `match` works on list/dict patterns); functions compare by identity. Operands of different types are simply - not equal — `"3" == 3` is `false`, never an error. + not equal — `"3" == 3` is `false`, never an error — with one exception: + a `bool` against a number raises (`true == 1`, #1637), because a bool is + not a number. +- **Bools** (`true`/`false`) are what comparisons, `not` and predicates + return. Arithmetic on a bool raises; test a bool directly (`if ok:`). ### `of` binds tighter than arithmetic diff --git a/docs/TRACE.md b/docs/TRACE.md index dd32c5a9..1affe65b 100644 --- a/docs/TRACE.md +++ b/docs/TRACE.md @@ -27,7 +27,7 @@ The tape is plain text, one record per line: | Record | Meaning | |--------|---------| -| `V ` | Version header — always the first record (e.g. `V 5 0.43.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 6 0.44.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`. | @@ -181,7 +181,8 @@ 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, `null`, and booleans are written verbatim. +- Numbers and `null` are written verbatim; a bool is written `true` or + `false` and read back as a bool (format v6, #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]`, @@ -707,9 +708,24 @@ everywhere else in the runtime (version-and-reject, never migrate). tape encoding; the runtime string is the recording binary's version. History: **v2** (#539) added the scope-transition `S` records; **v3** (#1044/#1045 follow-up) added the observer-configuration `O` records. - **v4** added flat stream IDs; **v5** adds the mandatory `B` associations. + **v4** added flat stream IDs; **v5** added the mandatory `B` associations. A v4 tape cannot establish host/causal correspondence and is refused by replay, stepping and DAP under the same version-and-reject rule. + **v6** (#1637) gives `true`/`false` their own type: the value encoding + writes and reads them as bools. A v5 tape recorded a predicate builtin's + 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 (`tests/test_tape_observer_config.sh`, section 7). Within a + v6 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 + `random_normal` returns a list or null, so `random_normal=true` is refused), + and `tools/tape_kinds_check.sh` fails when a taped builtin has no row. A + name a host records through the embedding API declares its kinds with + `eigs_trace_declare_kind` when it registers the function; replay refuses + a record of an undeclared kind and a name declared nowhere + (docs/EMBEDDING.md; suite sections [0fb] and [0f5]). A v2 tape cannot say what its knobs were — the calls simply are not on it — so the compat decision for the bump is the standing one, and it is the loud half: a v2 tape is **refused** by `--step`, by the DAP server and by diff --git a/docs/llms.txt b/docs/llms.txt index ff87b6e1..4e111117 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -56,7 +56,7 @@ two of [1] ``` ```output [] -1 +true ``` - **Parentheses always mean exactly one argument**: `f of ([a, b])` passes the @@ -197,6 +197,18 @@ sqrt(2) = 1.414213562373095 ## Values and strings +- `bool` is its own type: `true`/`false`. Comparisons, `not`, the observer + predicates and every predicate builtin/lib function return a bool; `and`/`or` + return an operand. A bool is NOT a number: `true + 1`, `abs of true`, + `xs[true]`, `xs[(n > 1):]` and `range of true` raise (even under + `EIGS_STRICT=0`), and so does `(pred of x) == 1` (bool vs number). Any + builtin raises on a bool where it does not take one -- the few slots that + do (print/str/type/json_encode, assert's condition, container element + slots like `append of [xs, true]`) are listed in + `tests/bool_fuzz_anyvalue.txt`. Test a bool directly (`if pred of x:`) or + compare with `== true`. Deep `==` on lists/dicts stops at the first + unequal pair, so a bool/number pair raises only if the walk reaches it. + `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. @@ -315,7 +327,7 @@ match x: print of "other" ``` -`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: 0, 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. +`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. 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. diff --git a/editors/vim/syntax/eigenscript.vim b/editors/vim/syntax/eigenscript.vim index 811d81e4..a8b8cc65 100644 --- a/editors/vim/syntax/eigenscript.vim +++ b/editors/vim/syntax/eigenscript.vim @@ -12,6 +12,7 @@ syn keyword eigsOperatorWord is of and or not at syn keyword eigsInterrogative what who when where why how prev state_at report report_value syn keyword eigsPredicate converged stable improving oscillating diverging equilibrium syn keyword eigsNull null +syn keyword eigsBoolean true false syn match eigsComment "#.*$" " Hex integers, leading/trailing-dot decimals, exponents (docs/SPEC.md, Numbers) @@ -31,6 +32,7 @@ hi def link eigsOperatorWord Operator hi def link eigsInterrogative Special hi def link eigsPredicate Constant hi def link eigsNull Constant +hi def link eigsBoolean Boolean hi def link eigsComment Comment hi def link eigsNumber Number hi def link eigsString String diff --git a/editors/vscode/syntaxes/eigenscript.tmLanguage.json b/editors/vscode/syntaxes/eigenscript.tmLanguage.json index 548ade89..9ad83c42 100644 --- a/editors/vscode/syntaxes/eigenscript.tmLanguage.json +++ b/editors/vscode/syntaxes/eigenscript.tmLanguage.json @@ -13,6 +13,7 @@ { "include": "#interrogatives" }, { "include": "#predicates" }, { "include": "#constants" }, + { "include": "#booleans" }, { "include": "#call" }, { "include": "#operators" } ], @@ -88,6 +89,10 @@ "match": "\\bnull\\b", "name": "constant.language.null.eigenscript" }, + "booleans": { + "match": "\\b(true|false)\\b", + "name": "constant.language.boolean.eigenscript" + }, "call": { "match": "\\b([A-Za-z_][A-Za-z0-9_]*)\\s+(?=of\\b)", "captures": { diff --git a/examples/debugger.eigs b/examples/debugger.eigs index 628bdd79..6d11ff35 100644 --- a/examples/debugger.eigs +++ b/examples/debugger.eigs @@ -22,7 +22,7 @@ _target_path is "examples/fibonacci.eigs" # Fallback if file doesn't exist _target_exists is file_exists of _target_path -if _target_exists == 0: +if (not _target_exists): _target_path is "examples/hello.eigs" _dbg_source is read_text of _target_path @@ -65,7 +65,7 @@ define _capture_env_snapshot(env) as: for i in range of (len of bindings): b is bindings[i] name is b[0] - if (has_key of [snap, name]) == 0: + if (not (has_key of [snap, name])): val is b[1] skip is 0 if (type of val) == "list": @@ -101,7 +101,7 @@ define _debug_hook(info) as: if _dbg_step_mode == 1: should_pause is 1 key is str of line - if (has_key of [_dbg_breakpoints, key]) == 1: + if (has_key of [_dbg_breakpoints, key]): if _dbg_breakpoints[key] == 1: should_pause is 1 @@ -142,7 +142,7 @@ define _snapshot_entropy(env) as: for i in range of (len of bindings): b is bindings[i] name is b[0] - if (has_key of [_eigen_obs, name]) == 1: + if (has_key of [_eigen_obs, name]): obs is _eigen_obs[name] total is total + obs[1] count is count + 1 @@ -281,7 +281,7 @@ define _paint_source(widget, ax, ay) as: # Breakpoint marker (red circle in gutter) bp_key is str of line_num - if (has_key of [_dbg_breakpoints, bp_key]) == 1: + if (has_key of [_dbg_breakpoints, bp_key]): if _dbg_breakpoints[bp_key] == 1: gfx_circle of [ax + 12, ly + 9, 5, 200, 50, 50] @@ -317,7 +317,7 @@ define _on_source_mouse(widget, ev) as: # Only toggle if clicked in gutter region if ev.x < (gutter_w + 20): bp_key is str of line_num - if (has_key of [_dbg_breakpoints, bp_key]) == 1: + if (has_key of [_dbg_breakpoints, bp_key]): if _dbg_breakpoints[bp_key] == 1: dict_set of [_dbg_breakpoints, bp_key, 0] else: @@ -436,7 +436,7 @@ define _update_var_table() as: entropy_str is "" dh_str is "" status is "" - if (has_key of [_eigen_obs, name]) == 1: + if (has_key of [_eigen_obs, name]): obs is _eigen_obs[name] obs_count is str of obs[0] ent is obs[1] diff --git a/examples/self_referential.eigs b/examples/self_referential.eigs index 58ce3221..00de5dd2 100644 --- a/examples/self_referential.eigs +++ b/examples/self_referential.eigs @@ -71,7 +71,7 @@ for i in range of (len of candidates): code is make_function of candidates[i] valid is try_parse of code label is candidates[i][0] - if valid == 1: + if valid: print of (" " + label + ": VALID") else: print of (" " + label + ": INVALID") diff --git a/examples/stem/greenhouse_controller.eigs b/examples/stem/greenhouse_controller.eigs index 3058ddad..1bfc0b26 100644 --- a/examples/stem/greenhouse_controller.eigs +++ b/examples/stem/greenhouse_controller.eigs @@ -151,13 +151,13 @@ define choose_decision(final_state, temp_window, temp_spikes, config) as: if temp_ok and ph_ok and nutrient_ok and biomass_ok and stable_ok and clean_signal: return "APPROVE RUN" - if nutrient_ok == 0: + if (not nutrient_ok): return "REFEED NUTRIENT" - if stable_ok == 0: + if (not stable_ok): return "HOLD FOR THERMAL STABILITY" - if ph_ok == 0: + if (not ph_ok): return "ADJUST PH" - if biomass_ok == 0: + if (not biomass_ok): return "CONTINUE GROW-OUT" return "REVIEW SENSOR TRACE" diff --git a/examples/structural_self_ref.eigs b/examples/structural_self_ref.eigs index a71b9119..3ec22a7f 100644 --- a/examples/structural_self_ref.eigs +++ b/examples/structural_self_ref.eigs @@ -63,10 +63,10 @@ print of count # ---- Check if generated code is valid before use ---- candidate is "x is 5" -if (try_parse of candidate) == 1: +if (try_parse of candidate): print of "valid" # ---- Guard pattern for self-modification ---- new_code is "result is 100" -if (try_parse of new_code) == 1: +if (try_parse of new_code): write_text of ["/tmp/gen.eigs", new_code] diff --git a/lib/args.eigs b/lib/args.eigs index 2707ece8..6c393675 100644 --- a/lib/args.eigs +++ b/lib/args.eigs @@ -11,7 +11,7 @@ # # "--flag" -> "true" # # "--key" -> "value" # -# get_flag of [parsed, "--verbose"] # 1 if present, else 0 +# get_flag of [parsed, "--verbose"] # true if present, else false # get_opt of [parsed, "--output", ""] # value or default # get_positional of parsed # list of positional args # has_flag of [parsed, "--help"] # 1 or 0 @@ -27,7 +27,7 @@ define parse_args as: i is 0 loop while i < (len of raw): arg is raw[i] - if (starts_with of [arg, "--"]) == 1: + if (starts_with of [arg, "--"]): # Check for --key=value eq_pos is index_of of [arg, "="] if eq_pos >= 0: @@ -44,7 +44,7 @@ define parse_args as: # Check if next arg is a value or another flag if (i + 1) < (len of raw): next is raw[i + 1] - if (starts_with of [next, "--"]) == 1: + if (starts_with of [next, "--"]): # It's a boolean flag append of [result, [arg, "true"]] else: @@ -54,7 +54,7 @@ define parse_args as: else: # Last arg, must be a flag append of [result, [arg, "true"]] - elif (starts_with of [arg, "-"]) == 1: + elif (starts_with of [arg, "-"]): # Short flag: -v, -n etc. Treat as boolean append of [result, [arg, "true"]] else: @@ -70,8 +70,8 @@ define parse_args as: define get_flag(parsed, flag) as: for i in range of (len of parsed): if parsed[i][0] == flag: - return 1 - return 0 + return true + return false # ---- get_opt: get option value with default ---- # get_opt of [parsed, "--output", "default"] -> value or default diff --git a/lib/auth.eigs b/lib/auth.eigs index fe638d4f..fb8cd2f9 100644 --- a/lib/auth.eigs +++ b/lib/auth.eigs @@ -28,7 +28,7 @@ define auth_login(password) as: if admin_pw == "": return json_build of ["error", "ADMIN_PASSWORD not set"] # constant-time compare so timing can't leak how much of the password matched - if (secure_equals of [password, admin_pw]) == 0: + if (not (secure_equals of [password, admin_pw])): return json_build of ["error", "invalid password"] _auth_token is "eigen_" + (random_hex of 32) return json_build of ["status", "ok", "token", _auth_token] diff --git a/lib/autograd.eigs b/lib/autograd.eigs index fd10d9af..df6c6ea3 100644 --- a/lib/autograd.eigs +++ b/lib/autograd.eigs @@ -154,17 +154,17 @@ define _ag_reduce_to(who, pv, g) as: # same four cases, asked BEFORE the node is pushed. define _ag_reducible(pv, g) as: if (type of pv) == "num": - return 1 + return true if (type of g) == "num": - return 1 + return true if (len of pv) == (len of g): - return 1 + return true local dp is _ag_dims of pv local dg is _ag_dims of g if dp[0] == 1: if dp[1] == dg[1]: - return 1 - return 0 + return true + return false # _ag_check_bin of [who, av, bv, v]: the forward guard for the elementwise # rules. The four arithmetic BUILTINS share the list path's shape algebra, so @@ -178,7 +178,7 @@ define _ag_reducible(pv, g) as: define _ag_check_bin(who, av, bv, v) as: local oka is _ag_reducible of [av, v] local okb is _ag_reducible of [bv, v] - if oka == 0 or okb == 0: + if (not oka) or (not okb): local sa is str of (shape of av) local sb is str of (shape of bv) local sv is str of (shape of v) @@ -194,8 +194,8 @@ define _ag_push(t, value, op, parents, saved, track) as: define _ag_tracked(a, b) as: if a.track or b.track: - return 1 - return 0 + return true + return false # Accumulate `delta` into node `id`'s gradient. Copies on first arrival so a # gradient passed through unchanged (ag_add's vjp) is never shared between two diff --git a/lib/biology.eigs b/lib/biology.eigs index 79854b90..96f1dad7 100644 --- a/lib/biology.eigs +++ b/lib/biology.eigs @@ -66,7 +66,7 @@ define punnett_square(parent1, parent2) as: g is x + y if x > y: g is y + x - if (has_key of [result, g]) == 1: + if (has_key of [result, g]): cur is result[g] dict_set of [result, g, cur + 0.25] else: diff --git a/lib/chemistry.eigs b/lib/chemistry.eigs index 19e74e25..6b975aa4 100644 --- a/lib/chemistry.eigs +++ b/lib/chemistry.eigs @@ -86,45 +86,45 @@ define atomic_mass(symbol) as: # Stoichiometry — formula parsing & conversions # ============================================================ -# _is_upper of char -> 1 if A-Z, else 0 +# _is_upper of char -> true if A-Z, else false define _is_upper(ch) as: if ch == "A" or ch == "B" or ch == "C" or ch == "D" or ch == "E": - return 1 + return true if ch == "F" or ch == "G" or ch == "H" or ch == "I" or ch == "J": - return 1 + return true if ch == "K" or ch == "L" or ch == "M" or ch == "N" or ch == "O": - return 1 + return true if ch == "P" or ch == "Q" or ch == "R" or ch == "S" or ch == "T": - return 1 + return true if ch == "U" or ch == "V" or ch == "W" or ch == "X" or ch == "Y": - return 1 + return true if ch == "Z": - return 1 - return 0 + return true + return false -# _is_lower of char -> 1 if a-z, else 0 +# _is_lower of char -> true if a-z, else false define _is_lower(ch) as: if ch == "a" or ch == "b" or ch == "c" or ch == "d" or ch == "e": - return 1 + return true if ch == "f" or ch == "g" or ch == "h" or ch == "i" or ch == "j": - return 1 + return true if ch == "k" or ch == "l" or ch == "m" or ch == "n" or ch == "o": - return 1 + return true if ch == "p" or ch == "q" or ch == "r" or ch == "s" or ch == "t": - return 1 + return true if ch == "u" or ch == "v" or ch == "w" or ch == "x" or ch == "y": - return 1 + return true if ch == "z": - return 1 - return 0 + return true + return false -# _is_digit of char -> 1 if 0-9, else 0 +# _is_digit of char -> true if 0-9, else false define _is_digit(ch) as: if ch == "0" or ch == "1" or ch == "2" or ch == "3" or ch == "4": - return 1 + return true if ch == "5" or ch == "6" or ch == "7" or ch == "8" or ch == "9": - return 1 - return 0 + return true + return false # molecular_weight of formula -> g/mol # Parses simple formulas: "H2O", "NaCl", "H2SO4", "C6H12O6" @@ -135,13 +135,13 @@ define molecular_weight(formula) as: n is len of formula loop while i < n: ch is char_at of [formula, i] - if (_is_upper of ch) == 1: + if (_is_upper of ch): sym is ch i is i + 1 # Collect lowercase letters loop while i < n: nch is char_at of [formula, i] - if (_is_lower of nch) == 1: + if (_is_lower of nch): sym is sym + nch i is i + 1 else: @@ -152,7 +152,7 @@ define molecular_weight(formula) as: count_str is "" loop while i < n: dch is char_at of [formula, i] - if (_is_digit of dch) == 1: + if (_is_digit of dch): count_str is count_str + dch i is i + 1 else: diff --git a/lib/complex.eigs b/lib/complex.eigs index 6dec3f7d..8a50b9ec 100644 --- a/lib/complex.eigs +++ b/lib/complex.eigs @@ -118,10 +118,10 @@ define eq_near(a, b, tol) as: if di < 0: di is 0 - di if dr > tol: - return 0 + return false if di > tol: - return 0 - return 1 + return false + return true # ============================================================ # POLYNOMIALS diff --git a/lib/concurrent.eigs b/lib/concurrent.eigs index ea4fce88..cd8dbe60 100644 --- a/lib/concurrent.eigs +++ b/lib/concurrent.eigs @@ -42,7 +42,7 @@ define worker_pool(n, work_fn, ch) as: handles is [] for i in range of n: define worker() as: - loop while (channel_closed of ch) == 0: + loop while (not (channel_closed of ch)): item is recv of ch if item != null: work_fn of item diff --git a/lib/config.eigs b/lib/config.eigs index f99e8e69..7eac63fa 100644 --- a/lib/config.eigs +++ b/lib/config.eigs @@ -24,7 +24,7 @@ define load_env_file(path) as: line is trim of lines[i] if (len of line) == 0: i is i - elif (starts_with of [line, "#"]) == 1: + elif (starts_with of [line, "#"]): i is i else: eq_pos is index_of of [line, "="] @@ -57,11 +57,11 @@ define load_ini(path) as: line is trim of lines[i] if (len of line) == 0: i is i - elif (starts_with of [line, "#"]) == 1: + elif (starts_with of [line, "#"]): i is i - elif (starts_with of [line, ";"]) == 1: + elif (starts_with of [line, ";"]): i is i - elif (starts_with of [line, "["]) == 1: + elif (starts_with of [line, "["]): # Section header end is index_of of [line, "]"] if end > 1: @@ -114,7 +114,7 @@ define config_section(cfg, prefix_name) as: result is [] for i in range of (len of cfg): key is cfg[i][0] - if (starts_with of [key, prefix]) == 1: + if (starts_with of [key, prefix]): short_key is substr of [key, len of prefix, (len of key) - (len of prefix)] append of [result, [short_key, cfg[i][1]]] return result diff --git a/lib/contract.eigs b/lib/contract.eigs index 582fa1e3..ee32e17e 100644 --- a/lib/contract.eigs +++ b/lib/contract.eigs @@ -58,7 +58,7 @@ define _num_guard(a, who) as: t is type of a if t == "num": - return 1 + return true throw of (who + ": trajectory must be a scalar (num), got " + t + " — return a residual/estimate from step_fn, not the raw state") # ---- _budget_guard: reject a budget too small to classify ---- @@ -68,14 +68,14 @@ define _num_guard(a, who) as: define _budget_guard(max_obs, who) as: if max_obs < 10: throw of (who + ": max_obs must be >= 10 (the observer window); got " + (str of max_obs)) - return 1 + return true # ---- require of [cond, msg] — precondition; throw unless cond is truthy ---- define require(n) as: cond is n[0] msg is n[1] if cond: - return 1 + return true throw of ("require failed: " + msg) # ---- ensure of [cond, msg] — postcondition; throw unless cond is truthy ---- @@ -85,7 +85,7 @@ define ensure(n) as: cond is n[0] msg is n[1] if cond: - return 1 + return true throw of ("ensure failed: " + msg) # ---- expect_converging of [x0, step_fn, max_obs, msg] ---- diff --git a/lib/data.eigs b/lib/data.eigs index 2e5b2fa0..604fdaa1 100644 --- a/lib/data.eigs +++ b/lib/data.eigs @@ -127,7 +127,7 @@ define df_slice(df, start, cnt) as: # ---- Sorting ---- -# df_sort_by: sort by column value. asc=1 ascending, asc=0 descending. +# df_sort_by: sort by column value. asc true (or 1) ascending, false (or 0) descending. define df_sort_by(df, column, asc) as: n is len of df if n == 0: @@ -141,7 +141,7 @@ define df_sort_by(df, column, asc) as: j is i loop while j > 0: do_swap is 0 - if asc == 1: + if asc: # #1637: true/false, or the older 1/0 if pairs[j][0] < pairs[j - 1][0]: do_swap is 1 else: diff --git a/lib/eigen.eigs b/lib/eigen.eigs index e384e6b8..bb721a65 100644 --- a/lib/eigen.eigs +++ b/lib/eigen.eigs @@ -87,8 +87,8 @@ define _is_alpha(c) as: return _eigen_contains of ["abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ", c] define _is_alnum(c) as: - if (_is_alpha of c) == 1: - return 1 + if (_is_alpha of c): + return true return _is_digit of c # f-string interpolation extent (#1252/#1253 parity with src/lexer.c's @@ -104,7 +104,7 @@ define _fstr_interp_end(source, pos) as: local c is "" loop while pos < n: c is _eigen_char_at of [source, pos] - if (c == "f") and ((pos + 1) < n) and ((_eigen_char_at of [source, pos + 1]) == "\"") and ((pos == start) or ((_is_alnum of (_eigen_char_at of [source, pos - 1])) == 0)): + if (c == "f") and ((pos + 1) < n) and ((_eigen_char_at of [source, pos + 1]) == "\"") and ((pos == start) or ((not (_is_alnum of (_eigen_char_at of [source, pos - 1]))))): pos is _fstr_skip_fstring of [source, pos] elif c == "\"": pos is pos + 1 @@ -150,10 +150,10 @@ define _fstr_skip_fstring(source, pos) as: define _is_space(c) as: if c == " ": - return 1 + return true if c == "\t": - return 1 - return 0 + return true + return false # Full keyword list matching C lexer _keywords is ["is", "of", "define", "as", "if", "else", "elif", "loop", "while", "return", "and", "or", "not", "for", "in", "null", "try", "catch", "break", "continue", "import", "match", "case", "unobserved", "what", "who", "when", "where", "why", "how", "converged", "stable", "improving", "oscillating", "diverging", "equilibrium", "true", "false"] @@ -171,26 +171,26 @@ _reserved_forms is ["report", "report_value"] define _is_keyword(word) as: for i in _eigen_range of (_eigen_len of _keywords): if word == _keywords[i]: - return 1 - return 0 + return true + return false define _is_interrogative(word) as: for i in _eigen_range of (_eigen_len of _interrogatives): if word == _interrogatives[i]: - return 1 - return 0 + return true + return false define _is_predicate(word) as: for i in _eigen_range of (_eigen_len of _predicates): if word == _predicates[i]: - return 1 - return 0 + return true + return false define _is_reserved_form(word) as: for i in _eigen_range of (_eigen_len of _reserved_forms): if word == _reserved_forms[i]: - return 1 - return 0 + return true + return false define _predicate_index(word) as: for i in _eigen_range of (_eigen_len of _predicates): @@ -278,7 +278,7 @@ define _eigen_tokenize_at(source, line, check_layout) as: c is _eigen_char_at of [source, pos] handled is 0 # Skip inline spaces - if (_is_space of c) == 1: + if (_is_space of c): pos is pos + 1 handled is 1 # Comments @@ -433,13 +433,13 @@ define _eigen_tokenize_at(source, line, check_layout) as: _eigen_append of [tokens, ["str", buf, str_line]] handled is 1 # Numbers - if (handled == 0) and ((_is_digit of c) == 1): + if (handled == 0) and ((_is_digit of c)): buf is "" has_dot is 0 reading_num is 1 loop while (pos < src_len) and (reading_num == 1): ch is _eigen_char_at of [source, pos] - if (_is_digit of ch) == 1: + if (_is_digit of ch): buf is buf + ch pos is pos + 1 elif (ch == ".") and (has_dot == 0): @@ -451,12 +451,12 @@ define _eigen_tokenize_at(source, line, check_layout) as: _eigen_append of [tokens, ["num", _eigen_num of buf, line]] handled is 1 # Identifiers and keywords - if (handled == 0) and ((_is_alpha of c) == 1): + if (handled == 0) and ((_is_alpha of c)): buf is "" reading_id is 1 loop while (pos < src_len) and (reading_id == 1): ch is _eigen_char_at of [source, pos] - if (_is_alnum of ch) == 1: + if (_is_alnum of ch): buf is buf + ch pos is pos + 1 else: @@ -464,16 +464,16 @@ define _eigen_tokenize_at(source, line, check_layout) as: if buf == "null": _eigen_append of [tokens, ["null", null, line]] elif buf == "true": - _eigen_append of [tokens, ["true", 1, line]] + _eigen_append of [tokens, ["true", true, line]] elif buf == "false": - _eigen_append of [tokens, ["false", 0, line]] - elif (_is_interrogative of buf) == 1: + _eigen_append of [tokens, ["false", false, line]] + elif (_is_interrogative of buf): _eigen_append of [tokens, ["interrogative", buf, line]] - elif (_is_predicate of buf) == 1: + elif (_is_predicate of buf): _eigen_append of [tokens, ["predicate", buf, line]] - elif (_is_reserved_form of buf) == 1: + elif (_is_reserved_form of buf): _eigen_append of [tokens, ["report", buf, line]] - elif (_is_keyword of buf) == 1: + elif (_is_keyword of buf): _eigen_append of [tokens, ["kw", buf, line]] else: _eigen_append of [tokens, ["ident", buf, line]] @@ -508,7 +508,7 @@ define _eigen_tokenize_at(source, line, check_layout) as: handled is 1 # Single-character operators and punctuation if handled == 0: - if (_eigen_contains of ["+-*/%<>", c]) == 1: + if (_eigen_contains of ["+-*/%<>", c]): _eigen_append of [tokens, ["op", c, line]] pos is pos + 1 handled is 1 @@ -1092,13 +1092,14 @@ define _p_parse_primary(p) as: _p_advance of p return ["null"] + # #1637: true/false are the host's bool values, not 1/0. if tt == "true": _p_advance of p - return ["num", 1] + return ["bool", true] if tt == "false": _p_advance of p - return ["num", 0] + return ["bool", false] # Interrogatives: what of x, who of x, etc. if tt == "interrogative": @@ -1119,6 +1120,11 @@ define _p_parse_primary(p) as: tok is _p_cur of p _p_advance of p kind is _predicate_index of tok[1] + # #1637: the named form `converged of x`, as the C parser accepts. + if (_p_peek_type of p) == "kw": + if (_p_peek_val of p) == "of": + _p_advance of p + return ["predicate_of", kind, _p_parse_primary of p] return ["predicate", kind] if tt == "ident": @@ -1278,7 +1284,7 @@ define _ns_env_for(target_node, key, target) as: if (_eigen_char_at of [key, 0]) == "_": return null mod_name is target_node[1] - if (_eigen_has_key of [_eigen_module_envs, mod_name]) == 0: + if (not (_eigen_has_key of [_eigen_module_envs, mod_name])): return null if target != _eigen_module_ns[mod_name]: return null @@ -1338,8 +1344,8 @@ define _env_has_local(env, name) as: bindings is env[0] for i in _eigen_range of (_eigen_len of bindings): if bindings[i][0] == name: - return 1 - return 0 + return true + return false define _env_unset_local(env, name) as: bindings is env[0] @@ -1369,7 +1375,7 @@ define _eigen_module_path(name) as: exedir + "/../lib/" + name + ".eigs", exedir + "/../lib/eigenscript/" + name + ".eigs"] for ci in _eigen_range of (_eigen_len of cands): - if (_eigen_file_exists of cands[ci]) == 1: + if (_eigen_file_exists of cands[ci]): return cands[ci] return null @@ -1414,9 +1420,17 @@ define eigen_set_hook(hook) as: # Observer state per binding: [name, value, obs_count, entropy, prev_entropy, dH] # For simplicity, we store observer metadata in a separate dict keyed by name. _eigen_obs is {} +# #1637: the value of the most recently observed assignment, for the bare +# predicates' bridge (a dict so module functions update it by mutation). +_eigen_last is {"v": null} define _compute_entropy(val) as: t is _eigen_type of val + # #1637: a bool is one bit — false 0, true 1, as the C observer. + if t == "bool": + if val: + return 1 + return 0 if t == "num": x is _eigen_abs of val if (x == 0) or (x == 1): @@ -1444,8 +1458,9 @@ define _compute_entropy(val) as: return 0 define _update_observer(name, val) as: + _eigen_last.v is val new_entropy is _compute_entropy of val - if (_eigen_has_key of [_eigen_obs, name]) == 1: + if (_eigen_has_key of [_eigen_obs, name]): old is _eigen_obs[name] obs_count is old[0] + 1 prev_ent is old[1] @@ -1456,7 +1471,7 @@ define _update_observer(name, val) as: return null define _get_observer(name) as: - if (_eigen_has_key of [_eigen_obs, name]) == 1: + if (_eigen_has_key of [_eigen_obs, name]): return _eigen_obs[name] return [0, 0, 0, 0] @@ -1476,6 +1491,8 @@ define eigen_eval(node, env) as: return node[1] if ntype == "null": return null + if ntype == "bool": + return node[1] if ntype == "ident": # #306: the C VM RAISES on an unbound name (it does not silently return @@ -1548,39 +1565,27 @@ define eigen_eval(node, env) as: if right == 0: _eigen_throw of "modulo by zero" return left % right + # #1637: the host's own comparison — a bool result, and the same + # raise on bool-vs-number equality the C VM performs. if op == "==": - if left == right: - return 1 - return 0 + return left == right if op == "!=": - if left != right: - return 1 - return 0 + return left != right if op == "<": - if left < right: - return 1 - return 0 + return left < right if op == ">": - if left > right: - return 1 - return 0 + return left > right if op == "<=": - if left <= right: - return 1 - return 0 + return left <= right if op == ">=": - if left >= right: - return 1 - return 0 + return left >= right if ntype == "unary": operand is eigen_eval of [node[2], env] if node[1] == "-": return 0 - operand if node[1] == "not": - if operand: - return 0 - return 1 + return not operand if ntype == "call": fn is eigen_eval of [node[1], env] @@ -1673,7 +1678,7 @@ define eigen_eval(node, env) as: for mi in _eigen_range of (_eigen_len of mbind): if mbind[mi][0] == key: return mbind[mi][1] - if (_eigen_has_key of [target, key]) == 1: + if (_eigen_has_key of [target, key]): return target[key] return null _eigen_throw of f"cannot access .{key} on {_eigen_type of target}" @@ -1724,9 +1729,9 @@ define eigen_eval(node, env) as: running is 1 loop while (max_iter > 0) and (running == 1): cond is eigen_eval of [node[1], env] - if cond == 0: - running is 0 - elif cond == null: + # #1637: the host's own truthiness (false, 0, null, "", [], {}) + # -- `cond == 0` raised on a bool condition. + if not cond: running is 0 else: result is _eval_block of [node[2], env] @@ -1752,7 +1757,7 @@ define eigen_eval(node, env) as: # own binding (if any) and restoring or removing it on every exit. had_prior is _env_has_local of [env, var_name] prior_val is null - if had_prior == 1: + if had_prior: prior_val is _env_get of [env, var_name] try: for i in _eigen_range of (_eigen_len of iter): @@ -1766,12 +1771,12 @@ define eigen_eval(node, env) as: if _eigen_continuing == 1: _eigen_continuing is 0 catch _for_error: - if had_prior == 1: + if had_prior: _env_set_local of [env, var_name, prior_val] else: _env_unset_local of [env, var_name] _eigen_throw of _for_error - if had_prior == 1: + if had_prior: _env_set_local of [env, var_name, prior_val] else: _env_unset_local of [env, var_name] @@ -1874,9 +1879,7 @@ define eigen_eval(node, env) as: _env_set_local of [comp_env, var_name, iter[i]] if filter_node != null: cond is eigen_eval of [filter_node, comp_env] - if cond == 0: - continue - if cond == null: + if not cond: # #1637: host truthiness; `cond == 0` raised on a bool continue val is eigen_eval of [expr_node, comp_env] _eigen_append of [result, val] @@ -1894,6 +1897,8 @@ define eigen_eval(node, env) as: # what: value / magnitude if (_eigen_type of target) == "num": return target + if (_eigen_type of target) == "bool": # #1637: its own value, as in the VM + return target if (_eigen_type of target) == "str": return _eigen_len of target if (_eigen_type of target) == "list": @@ -1937,51 +1942,16 @@ define eigen_eval(node, env) as: return 1.0 return 0 + # #1637: observer predicates answer a bool, through the same value-only + # bridge `report` uses (#1102/#1111): this interpreter keeps no host + # trajectory, so the host predicate is asked about a fresh parameter + # binding holding the value -- the bare form the last observed value, the + # named form its operand. That is the VM's answer for a binding without a + # full window (false), and always a bool, never the old 1/0 heuristic. if ntype == "predicate": - kind is node[1] - # Look at last observer state - # kind: 0=converged, 1=stable, 2=improving, 3=oscillating, 4=diverging, 5=equilibrium - h is 0 - dh is 0 - prev_dh is 0 - # Try to get the latest observer state from __observer__ binding - last_obs is _env_get of [env, "__observer__"] - if last_obs != null: - h is _compute_entropy of last_obs - if kind == 0: - # converged: low dH and low entropy - if (_eigen_abs of dh) < 0.001: - if h < 0.1: - return 1 - return 0 - if kind == 1: - # stable: low dH, moderate entropy - if (_eigen_abs of dh) < 0.01: - if h >= 0.1: - return 1 - return 0 - if kind == 2: - # improving: negative dH - if dh < (0 - 0.001): - return 1 - return 0 - if kind == 3: - # oscillating: sign change in dH - if (dh * prev_dh) < 0: - if (_eigen_abs of dh) > 0.001: - return 1 - return 0 - if kind == 4: - # diverging: positive dH - if dh > 0.001: - return 1 - return 0 - if kind == 5: - # equilibrium: very low dH - if (_eigen_abs of dh) < 0.001: - return 1 - return 0 - return 0 + return (_pred_bridges[node[1]]) of _eigen_last.v + if ntype == "predicate_of": + return (_pred_bridges[node[1]]) of (eigen_eval of [node[2], env]) return null @@ -2018,6 +1988,9 @@ define _eval_block(stmts, env) as: # opaque) — without exposing the host's named binding history to the meta env. _report_bridge is (v) => report of v _report_value_bridge is (v) => report_value of v +# #1637: the predicate bridges, in _predicates order (converged, stable, +# improving, oscillating, diverging, equilibrium). +_pred_bridges is [(v) => converged of v, (v) => stable of v, (v) => improving of v, (v) => oscillating of v, (v) => diverging of v, (v) => equilibrium of v] # #1322: the builtin string conversion, unhijackable. An f-string HERE lowers to the host's # reserved `_#fstr` binding, which no source, module, embedder or bytecode can diff --git a/lib/functional.eigs b/lib/functional.eigs index 66f18150..0fc00330 100644 --- a/lib/functional.eigs +++ b/lib/functional.eigs @@ -48,11 +48,11 @@ define apply_all(fns, val) as: # ---- complement: negate a predicate function ---- # Since EigenScript lacks closures, this takes [pred, value] at call time. -# complement of [pred, value] -> 1 if pred returns falsy, else 0 +# complement of [pred, value] -> true if pred returns falsy, else false define complement(pred, val) as: if pred of val: - return 0 - return 1 + return false + return true # ---- times: call function n times with index ---- # times of [count, fn] -> list of results @@ -109,8 +109,8 @@ define wait_until(pred, tries, sleep_fn) as: local i is 0 loop while i < tries: if pred of []: - return 1 + return true i is i + 1 if sleep_fn != null and i < tries: sleep_fn of [] - return 0 + return false diff --git a/lib/geometry.eigs b/lib/geometry.eigs index a126c6d0..5f0d7e97 100644 --- a/lib/geometry.eigs +++ b/lib/geometry.eigs @@ -174,8 +174,8 @@ define orientation(p, q, r) as: # Are three points collinear? define collinear(p, q, r) as: if (orientation of [p, q, r]) == 0: - return 1 - return 0 + return true + return false # Is q on segment pr? (assuming collinear) define on_segment(p, q, r) as: @@ -192,8 +192,8 @@ define on_segment(p, q, r) as: if r[1] > max_y: max_y is r[1] if q[0] >= min_x and q[0] <= max_x and q[1] >= min_y and q[1] <= max_y: - return 1 - return 0 + return true + return false # Do two line segments intersect? define segments_intersect(a1, a2, b1, b2) as: @@ -203,17 +203,17 @@ define segments_intersect(a1, a2, b1, b2) as: o4 is orientation of [b1, b2, a2] # General case if o1 != o2 and o3 != o4: - return 1 + return true # Collinear special cases - if o1 == 0 and (on_segment of [a1, b1, a2]) == 1: - return 1 - if o2 == 0 and (on_segment of [a1, b2, a2]) == 1: - return 1 - if o3 == 0 and (on_segment of [b1, a1, b2]) == 1: - return 1 - if o4 == 0 and (on_segment of [b1, a2, b2]) == 1: - return 1 - return 0 + if o1 == 0 and (on_segment of [a1, b1, a2]): + return true + if o2 == 0 and (on_segment of [a1, b2, a2]): + return true + if o3 == 0 and (on_segment of [b1, a1, b2]): + return true + if o4 == 0 and (on_segment of [b1, a2, b2]): + return true + return false # Find intersection point of two lines (defined by point pairs) # Returns point or null if parallel @@ -287,8 +287,8 @@ define point_in_triangle(p, a, b, c) as: v is bc[1] w is bc[2] if u >= 0 and v >= 0 and w >= 0: - return 1 - return 0 + return true + return false # Returns [u, v, w] barycentric coordinates define barycentric_coords(p, a, b, c) as: @@ -382,7 +382,7 @@ define polygon_perimeter(vertices) as: # Ray casting algorithm -- odd number of crossings = inside define point_in_polygon(point, vertices) as: n is len of vertices - inside is 0 + inside is false px is point[0] py is point[1] j is n - 1 @@ -395,7 +395,7 @@ define point_in_polygon(point, vertices) as: if (yi > py) != (yj > py): slope is (xj - xi) * (py - yi) / (yj - yi) + xi if px < slope: - inside is 1 - inside + inside is not inside j is i return inside @@ -403,7 +403,7 @@ define point_in_polygon(point, vertices) as: define polygon_is_convex(vertices) as: n is len of vertices if n < 3: - return 0 + return false sign is 0 for i in range of n: i1 is i + 1 @@ -417,15 +417,15 @@ define polygon_is_convex(vertices) as: if sign == 0: sign is o elif o != sign: - return 0 - return 1 + return false + return true # Check winding order via signed area define polygon_is_clockwise(vertices) as: a is polygon_area of vertices if a < 0: - return 1 - return 0 + return true + return false # ============================================================ # Convex Hull — Andrew's Monotone Chain Algorithm @@ -527,13 +527,13 @@ define circles_intersect(c1x, c1y, c1r, c2x, c2y, c2r) as: dy is c2y - c1y d is sqrt of (dx * dx + dy * dy) if d > c1r + c2r: - return 0 + return false diff is c1r - c2r if diff < 0: diff is 0 - diff if d < diff: - return 0 - return 1 + return false + return true # Returns list of intersection points (0, 1, or 2 points) define circle_intersection_points(c1x, c1y, c1r, c2x, c2y, c2r) as: @@ -567,8 +567,8 @@ define point_in_circle(px, py, cx, cy, r) as: dx is px - cx dy is py - cy if dx * dx + dy * dy <= r * r: - return 1 - return 0 + return true + return false define circle_area(r) as: return _PI * r * r diff --git a/lib/http.eigs b/lib/http.eigs index 6e03e86c..a2fa97b2 100644 --- a/lib/http.eigs +++ b/lib/http.eigs @@ -28,26 +28,26 @@ # http_get of "url" -> [exit_code, body] define _http_is_safe_url(url) as: if (type of url) != "str": - return 0 + return false if (len of url) == 0: - return 0 - if (starts_with of [url, "-"]) == 1: - return 0 - if (starts_with of [url, "http://"]) == 1: - return 1 - if (starts_with of [url, "https://"]) == 1: - return 1 - return 0 + return false + if (starts_with of [url, "-"]): + return false + if (starts_with of [url, "http://"]): + return true + if (starts_with of [url, "https://"]): + return true + return false define http_get(url) as: - if (_http_is_safe_url of url) == 0: + if (not (_http_is_safe_url of url)): return [-1, ""] return exec_capture of ["curl", "-s", "-L", "--proto", "=http,https", "--proto-redir", "=http,https", "--", url] # ---- http_post_json: POST JSON data ---- # http_post_json of ["url", json_string] -> [exit_code, body] define http_post_json(url, data) as: - if (_http_is_safe_url of url) == 0: + if (not (_http_is_safe_url of url)): return [-1, ""] if (type of data) != "str": data is json_encode of data diff --git a/lib/io.eigs b/lib/io.eigs index 95f8116c..d7060461 100644 --- a/lib/io.eigs +++ b/lib/io.eigs @@ -76,6 +76,6 @@ define file_copy(src, dst) as: # ---- slurp: read file with existence check ---- # slurp of "path" -> string (exits with error if file not found) define slurp(path) as: - if (file_exists of path) == 0: + if (not (file_exists of path)): assert of [0, "slurp: file not found: " + path] return read_text of path diff --git a/lib/json.eigs b/lib/json.eigs index b47d12ea..881c4eed 100644 --- a/lib/json.eigs +++ b/lib/json.eigs @@ -7,7 +7,7 @@ # # json_get of [json_str, "key"] # extract top-level key # json_get_path of [json_str, "a.b.c"] # extract nested value -# json_has of [json_str, "key"] # 1 if key exists +# json_has of [json_str, "key"] # true if key exists # json_keys of json_str # list of top-level keys # json_from_pairs of pairs # [[k,v], ...] -> JSON # json_merge of [json_a, json_b] # merge two JSON objects @@ -22,12 +22,12 @@ define json_get_path(json_str, path) as: return json_path of [json_str, path] # ---- json_has: check if top-level key exists ---- -# Returns 1 if key exists and value is not null, 0 otherwise +# Returns true if key exists and value is not null, false otherwise define json_has(json_str, key) as: val is json_path of [json_str, key] if (type of val) == "null": - return 0 - return 1 + return false + return true # ---- json_from_pairs: convert [[k,v], ...] to JSON string ---- define json_from_pairs(pairs) as: diff --git a/lib/lab.eigs b/lib/lab.eigs index 47b78501..ec42967e 100644 --- a/lib/lab.eigs +++ b/lib/lab.eigs @@ -63,7 +63,7 @@ define _cache_get(key) as: return null define _cache_ensure(key) as: - if has_key of [_variable_data, key] == 0: + if (not has_key of [_variable_data, key]): dict_set of [_variable_data, key, []] # ---- Experiment Lifecycle ---- @@ -245,10 +245,10 @@ define is_stable(exp, variable) as: key is _tracker_key of [exp, variable] vals is _cache_get of key if vals == null: - return 0 + return false n is len of vals if n < 3: - return 0 + return false return is_measurement_stable of [vals, 3] define wait_for_stable(exp, variable, collect_fn, interval, timeout) as: @@ -340,7 +340,7 @@ define tagged_groups(exp, variable) as: for i in range of (len of recs): tag is recs[i].tag if tag != "": - if has_key of [groups, tag] == 0: + if (not has_key of [groups, tag]): dict_set of [groups, tag, []] append of [groups[tag], recs[i].value] return groups @@ -362,7 +362,7 @@ define save(exp) as: meta.variables is exp.variables meta.notes is exp.notes store_update of [exp.db, "experiments", exp.id, meta] - return 1 + return true define load(name) as: # `local` on every internal binding: without it, `name is expr` updates an @@ -426,7 +426,7 @@ define delete_experiment(name) as: # Delete experiment metadata store_delete of [db, "experiments", id] store_close of db - return 1 + return true # ---- Analysis Helpers ---- diff --git a/lib/list.eigs b/lib/list.eigs index a12010e9..c344449a 100644 --- a/lib/list.eigs +++ b/lib/list.eigs @@ -100,22 +100,22 @@ define drop(items, n) as: return result # ---- any: does any element satisfy the predicate? (short-circuit) ---- -# any of [list, fn] -> 1 if fn is truthy for some element, else 0 +# any of [list, fn] -> true if fn is truthy for some element, else false # Empty list -> 0. define any(items, fn) as: for i in range of (len of items): if fn of items[i]: - return 1 - return 0 + return true + return false # ---- all: do all elements satisfy the predicate? (short-circuit) ---- -# all of [list, fn] -> 1 if fn is truthy for every element, else 0 +# all of [list, fn] -> true if fn is truthy for every element, else false # Empty list -> 1 (vacuous truth). define all(items, fn) as: for i in range of (len of items): if not (fn of items[i]): - return 0 - return 1 + return false + return true # ---- find_index: index of first element where fn is truthy ---- # find_index of [list, fn] -> number (index, or -1 if none match) @@ -148,7 +148,7 @@ define group_by(items, key_fn) as: for i in range of (len of items): item is items[i] k is str of (key_fn of item) - if (has_key of [result, k]) == 1: + if (has_key of [result, k]): bucket is result[k] append of [bucket, item] else: @@ -189,7 +189,7 @@ define frequencies(items) as: result is {} for i in range of (len of items): k is str of items[i] - if (has_key of [result, k]) == 1: + if (has_key of [result, k]): dict_set of [result, k, result[k] + 1] else: dict_set of [result, k, 1] diff --git a/lib/map.eigs b/lib/map.eigs index c3c51e8e..9097157d 100644 --- a/lib/map.eigs +++ b/lib/map.eigs @@ -47,8 +47,8 @@ define map_get_default(m, key, default) as: define map_has(m, key) as: for i in range of (len of m): if m[i][0] == key: - return 1 - return 0 + return true + return false # ---- map_set: set a key-value pair (returns new map) ---- # map_set of [map, key, value] -> map diff --git a/lib/observer.eigs b/lib/observer.eigs index 420d9f88..e7c82243 100644 --- a/lib/observer.eigs +++ b/lib/observer.eigs @@ -7,10 +7,10 @@ # # entropy_of of x # current entropy # delta_of of x # current dH (rate of change) -# is_converged of x # 1 if converged, else 0 -# is_stable of x # 1 if stable or converged -# is_improving of x # 1 if improving -# is_oscillating of x # 1 if oscillating +# is_converged of x # true if converged, else false +# is_stable of x # true if stable or converged +# is_improving of x # true if improving +# is_oscillating of x # true if oscillating # wait_until_converged of [x, update_fn, max_steps] # track_regimes of [x, update_fn, max_steps] # threshold_alert of [x, lo, hi] @@ -38,10 +38,10 @@ define is_converged(x) as: define is_stable(x) as: status is report of x if status == "stable": - return 1 + return true if status == "converged": - return 1 - return 0 + return true + return false # ---- is_improving: check if entropy is decreasing ---- define is_improving(x) as: diff --git a/lib/observer_slots.eigs b/lib/observer_slots.eigs index d331e277..903c1ab1 100644 --- a/lib/observer_slots.eigs +++ b/lib/observer_slots.eigs @@ -6,7 +6,7 @@ # import observer_slots # # observer_slots.feed of [w.slot, w.progress] # each frame/step -# if (observer_slots.is_stable of (w.slot)) == 1: +# if (observer_slots.is_stable of (w.slot)): # mark_stalled of w # wedged: value stopped moving # # Feed BOUNDED signatures (e.g. 1.5 + (raw % 8)), not raw counters — @@ -59,7 +59,7 @@ define feed(i, v) as: _s7 is v + 0.0 else: throw of "observer_slots: slot out of range (8 slots)" - return 1 + return true define is_stable(i) as: if i == 0: diff --git a/lib/pkg.eigs b/lib/pkg.eigs index 086f4361..fbd07081 100644 --- a/lib/pkg.eigs +++ b/lib/pkg.eigs @@ -49,7 +49,7 @@ MODULES_DIR is "eigs_modules" # of sprinkling `--` separators and hoping each new one remembers. GIT_ALLOWED_FLAGS is ["-C", "--depth", "--branch", "--porcelain"] -# Returns 1 on success, 0 on failure. Streams git's stderr+stdout to +# Returns true on success, false on failure. Streams git's stderr+stdout to # stdout on failure so the user sees what went wrong. define run_git(arg_list) as: argn is len of arg_list @@ -58,7 +58,7 @@ define run_git(arg_list) as: if (type of a) != "str": throw of "git argument must be a string" if (len of a) > 0: - if a[0] == "-" and (list_contains of [GIT_ALLOWED_FLAGS, a]) == 0: + if a[0] == "-" and (not (list_contains of [GIT_ALLOWED_FLAGS, a])): throw of f"refusing option-shaped git argument '{a}' — a value from eigs.json/eigs.lock.json in an option position can execute code (--upload-pack)" cmd is concat of [["git"], arg_list] result is exec_capture of cmd @@ -71,8 +71,8 @@ define run_git(arg_list) as: joined is join of [cmd, " "] print of f" {joined}" print of result[1] - return 0 - return 1 + return false + return true # Resolved commit SHA at HEAD of the given working tree, or "" on # failure. We strip the trailing newline git rev-parse always prints. @@ -108,16 +108,16 @@ define git_tree_hash(work_dir) as: break return s[0:n] -# Returns 1 if the working tree at matches HEAD exactly -# (no unstaged edits, no untracked files), 0 if it's been tampered -# with, and 0 if git itself failed. +# Returns true if the working tree at matches HEAD exactly +# (no unstaged edits, no untracked files), false if it's been tampered +# with, and false if git itself failed. define git_worktree_clean(work_dir) as: result is exec_capture of ["git", "-C", work_dir, "status", "--porcelain"] if result[0] != 0: - return 0 + return false if result[1] == "": - return 1 - return 0 + return true + return false # Remove a directory tree. exec_capture's exit code is ignored — if the # tree wasn't there, that's still the desired post-state. @@ -131,13 +131,13 @@ define rmtree(path) as: define is_valid_name_part(part) as: n is len of part if n == 0: - return 0 + return false if part == "." or part == "..": - return 0 + return false first is part[0] if first == "-" or first == ".": - return 0 - ok is 1 + return false + ok is true for i in range of n: c is part[i] c_ok is 0 @@ -150,7 +150,7 @@ define is_valid_name_part(part) as: if c == "-" or c == "_" or c == ".": c_ok is 1 if c_ok == 0: - ok is 0 + ok is false break return ok @@ -173,9 +173,9 @@ define validate_pkg_name(name) as: return "package name must be / — bare names like '" + name + "' are reserved. Use a namespace, e.g. 'alice/" + name + "'." owner is name[0:slash_idx] leaf is name[slash_idx + 1:n] - if (is_valid_name_part of owner) == 0: + if not (is_valid_name_part of owner): return "invalid '" + owner + "' in '" + name + "' (alphanumerics + - _ ., not starting with - or .)." - if (is_valid_name_part of leaf) == 0: + if not (is_valid_name_part of leaf): return "invalid '" + leaf + "' in '" + name + "' (alphanumerics + - _ ., not starting with - or .)." return "" @@ -208,11 +208,11 @@ define read_manifest() as: parsed is json_decode of text if (type of parsed) != "dict": throw of "eigs.json is malformed (not a JSON object)" - if (has_key of [parsed, "name"]) == 0: + if (not (has_key of [parsed, "name"])): parsed.name is "" - if (has_key of [parsed, "version"]) == 0: + if (not (has_key of [parsed, "version"])): parsed.version is "0.0.0" - if (has_key of [parsed, "deps"]) == 0: + if (not (has_key of [parsed, "deps"])): parsed.deps is {} return parsed @@ -252,13 +252,13 @@ define cmd_list() as: name is names[i] spec is deps[name] git_url is "(missing url)" - if (has_key of [spec, "git"]) == 1: + if (has_key of [spec, "git"]): git_url is spec.git tag is "(no tag)" - if (has_key of [spec, "tag"]) == 1: + if (has_key of [spec, "tag"]): tag is spec.tag marker is "" - if (has_key of [lock, name]) == 1: + if (has_key of [lock, name]): short_commit is lock[name].commit[0:8] marker is " [locked @ " + short_commit + "]" print of f" {name} {git_url} {tag}{marker}" @@ -298,7 +298,7 @@ define fetch_dep(name, git_url, tag) as: target is MODULES_DIR + "/" + (pkg_leaf of name) rmtree of target ok is run_git of (clone_args of [git_url, target, tag]) - if ok == 0: + if not ok: return {} commit is git_head_commit of target if commit == "": @@ -330,7 +330,7 @@ define cmd_add(arg_list) as: # Write manifest first — the source-of-truth update is what makes # `add` recoverable if the network step fails (re-run `install`). manifest is read_manifest of null - if (has_key of [manifest, "deps"]) == 0: + if (not (has_key of [manifest, "deps"])): manifest.deps is {} # An omitted tag is recorded as omitted — the manifest carries the # REQUESTED ref, the lockfile carries the resolved commit. Writing a @@ -398,23 +398,23 @@ define cmd_install() as: # Clone with depth 1 at the tag, then if we have a locked # commit, fetch that specific commit and check it out. ok is run_git of (clone_args of [git_url, target, tag]) # #879 - if ok == 0: + if not ok: throw of f"install failed: clone for {name} failed" locked_commit is "" - if (has_key of [lock, name]) == 1: + if (has_key of [lock, name]): locked_entry is lock[name] - if (has_key of [locked_entry, "commit"]) == 1: + if (has_key of [locked_entry, "commit"]): locked_commit is locked_entry.commit if locked_commit != "": # Re-fetch the exact pinned commit. A tag move would # otherwise be invisible. ok2 is run_git of ["-C", target, "fetch", "--depth", "1", "origin", locked_commit] - if ok2 == 0: + if not ok2: throw of f"install failed: fetch of locked commit for {name} failed" ok3 is run_git of ["-C", target, "checkout", locked_commit] - if ok3 == 0: + if not ok3: throw of f"install failed: checkout of locked commit for {name} failed" current is git_head_commit of target @@ -457,7 +457,7 @@ define cmd_verify() as: for i in range of n: name is names[i] target is MODULES_DIR + "/" + (pkg_leaf of name) - if (has_key of [lock, name]) == 0: + if (not (has_key of [lock, name])): print of f" MISSING LOCK: {name} (run --pkg install)" failed is failed + 1 continue @@ -477,13 +477,13 @@ define cmd_verify() as: continue # Working tree clean? - if (git_worktree_clean of target) == 0: + if (not (git_worktree_clean of target)): print of f" TREE DRIFT: {name} working tree was edited" failed is failed + 1 continue # Tree hash matches (catches case where lockfile pre-dates 1c)? - if (has_key of [entry, "tree"]) == 1: + if (has_key of [entry, "tree"]): current_tree is git_tree_hash of target if current_tree != entry.tree: print of f" TREE HASH DRIFT: {name} tree={current_tree[0:8]} locked={entry.tree[0:8]}" @@ -505,7 +505,7 @@ define cmd_update(arg_list) as: names is keys of deps if (len of arg_list) >= 1: only is arg_list[0] - if (has_key of [deps, only]) == 0: + if (not (has_key of [deps, only])): throw of f"update: '{only}' is not in eigs.json" names is [only] n is len of names @@ -523,9 +523,9 @@ define cmd_update(arg_list) as: git_url is spec.git tag is spec.tag prev_commit is "" - if (has_key of [lock, name]) == 1: + if (has_key of [lock, name]): prev_entry is lock[name] - if (has_key of [prev_entry, "commit"]) == 1: + if (has_key of [prev_entry, "commit"]): prev_commit is prev_entry.commit fetched is fetch_dep of [name, git_url, tag] diff --git a/lib/sanitize.eigs b/lib/sanitize.eigs index 854fe26b..fd0b8a37 100644 --- a/lib/sanitize.eigs +++ b/lib/sanitize.eigs @@ -17,19 +17,19 @@ define sanitize_text(text) as: # Heuristics: too short, no spaces in long text, too few words define is_garble(text) as: if (len of text) < 2: - return 1 + return true if (len of text) > 20: - if (contains of [text, " "]) == 0: - return 1 + if (not (contains of [text, " "])): + return true words is split of [text, " "] wcount is len of words if wcount < 1: - return 1 - return 0 + return true + return false # ---- Clean response: trim at "User:" marker, find sentence boundary ---- define clean_response(text) as: - if (contains of [text, "User:"]) == 1: + if (contains of [text, "User:"]): parts is split of [text, "User:"] text is parts[0] text is trim of text @@ -43,5 +43,5 @@ define check_openai(_x) as: if key == "": key is env_get of "OPENAI_API_KEY" if key == "": - return 0 - return 1 + return false + return true diff --git a/lib/set.eigs b/lib/set.eigs index 8961f59f..b77dd52f 100644 --- a/lib/set.eigs +++ b/lib/set.eigs @@ -10,13 +10,13 @@ # intersect of [set_a, set_b] # elements in both # difference of [set_a, set_b] # in a but not b # symmetric_diff of [set_a, set_b] # in one but not both -# is_subset of [set_a, set_b] # 1 if a ⊆ b -# is_superset of [set_a, set_b] # 1 if a ⊇ b -# set_has of [set, value] # 1 if value in set +# is_subset of [set_a, set_b] # true if a ⊆ b +# is_superset of [set_a, set_b] # true if a ⊇ b +# set_has of [set, value] # true if value in set # set_add of [set, value] # add element # set_remove of [set, value] # remove element # set_size of set # number of elements -# set_equal of [set_a, set_b] # 1 if same elements +# set_equal of [set_a, set_b] # true if same elements # # Sets are sorted lists with no duplicates. @@ -50,14 +50,14 @@ define set_from(items) as: define set_has(s, val) as: for i in range of (len of s): if s[i] == val: - return 1 + return true if s[i] > val: - return 0 - return 0 + return false + return false # ---- set_add: add element to set ---- define set_add(s, val) as: - if (set_has of [s, val]) == 1: + if (set_has of [s, val]): return s result is [] inserted is 0 @@ -143,22 +143,22 @@ define symmetric_diff(a, b) as: right is difference of [b, a] return union of [left, right] -# ---- is_subset: 1 if a ⊆ b ---- +# ---- is_subset: true if a ⊆ b ---- define is_subset(a, b) as: for i in range of (len of a): - if (set_has of [b, a[i]]) == 0: - return 0 - return 1 + if (not (set_has of [b, a[i]])): + return false + return true -# ---- is_superset: 1 if a ⊇ b ---- +# ---- is_superset: true if a ⊇ b ---- define is_superset(a, b) as: return is_subset of [b, a] -# ---- set_equal: 1 if same elements ---- +# ---- set_equal: true if same elements ---- define set_equal(a, b) as: if (len of a) != (len of b): - return 0 + return false for i in range of (len of a): if a[i] != b[i]: - return 0 - return 1 + return false + return true diff --git a/lib/sort.eigs b/lib/sort.eigs index a8b95970..8b2bee5b 100644 --- a/lib/sort.eigs +++ b/lib/sort.eigs @@ -9,7 +9,7 @@ # sort_desc of [5, 3, 1, 4, 2] # [5, 4, 3, 2, 1] # sort_by of [items, key_fn] # sort by key function (C builtin, O(n log n)) # sorted_indices of list # indices that would sort the list -# is_sorted of list # 1 if ascending order +# is_sorted of list # true if ascending order # unique of [3, 1, 2, 1, 3] # [1, 2, 3] # ---- sort_asc: sort list in ascending order ---- @@ -75,11 +75,11 @@ define sorted_indices(items) as: # ---- is_sorted: check if list is in ascending order ---- define is_sorted(items) as: if (len of items) < 2: - return 1 + return true for i in range of ((len of items) - 1): if items[i + 1] < items[i]: - return 0 - return 1 + return false + return true # ---- unique: return sorted list with duplicates removed ---- define unique(items) as: diff --git a/lib/state.eigs b/lib/state.eigs index 7272fedd..c482affd 100644 --- a/lib/state.eigs +++ b/lib/state.eigs @@ -82,8 +82,8 @@ define sm_can_send(sm, event) as: t is transitions[i] if t[0] == current: if t[1] == event: - return 1 - return 0 + return true + return false # ---- sm_available_events: list valid events from current state ---- define sm_available_events(sm) as: diff --git a/lib/supervise.eigs b/lib/supervise.eigs index 2856bc7a..2acc3960 100644 --- a/lib/supervise.eigs +++ b/lib/supervise.eigs @@ -85,9 +85,9 @@ define _sup_feed(pair) as: _sup_o7 is sig + 0.0 else: throw of "supervise: slot out of range (8 slots)" - return 1 + return true -# _sup_stable(slot) — 1 if slot's signature trajectory has frozen. +# _sup_stable(slot) — true if slot's signature trajectory has frozen. define _sup_stable(slot) as: if slot == 0: return stable of _sup_o0 @@ -121,7 +121,7 @@ define heartbeat(pair) as: slot is pair[0] value is pair[1] _sup_progress is set_at of [_sup_progress, slot, value] - return 1 + return true # _run_child([fn, slot]) — the wrapper task. Runs the worker, records normal # completion; catches a raise as `crashed` so a supervised crash never turns @@ -143,7 +143,7 @@ define _reset_slot(slot) as: _sup_done is set_at of [_sup_done, slot, 0] _sup_crashed is set_at of [_sup_crashed, slot, 0] _sup_warm is set_at of [_sup_warm, slot, 0] - return 1 + return true # supervise([sup, fn, slot]) — spawn `fn` as a supervised worker in `slot`. # `fn` must accept its slot index and publish progress via `heartbeat`. @@ -157,20 +157,20 @@ define supervise(triple) as: sup.children is append of [sup.children, child] return child -# _restart([sup, child]) — restart a worker if under its cap. Returns 1 if it -# restarted, else 0. +# _restart([sup, child]) — restart a worker if under its cap. Returns true if it +# restarted, else false. define _restart(pair) as: sup is pair[0] child is pair[1] if child.restarts >= sup.max_restarts: - return 0 + return false slot is child.slot - if (task_alive of child.id) == 1: + if (task_alive of child.id): task_kill of child.id _reset_slot of slot child.id is task_spawn of [_run_child, [child.fn, slot]] child.restarts is child.restarts + 1 - return 1 + return true # supervise_step(sup) — one supervision tick. Feeds each alive worker's # progress into its trajectory and restarts wedged (alive + frozen after the @@ -180,16 +180,18 @@ define supervise_step(sup) as: restarted is 0 for child in sup.children: slot is child.slot - if (task_alive of child.id) == 1: + if (task_alive of child.id): sig is 1.5 + (_sup_progress[slot] % 8) _sup_feed of [slot, sig] _sup_warm is set_at of [_sup_warm, slot, _sup_warm[slot] + 1] warmed is _sup_warm[slot] >= 10 - if warmed == 1 and (_sup_stable of slot) == 1: - restarted is restarted + (_restart of [sup, child]) + if warmed and (_sup_stable of slot): + if _restart of [sup, child]: + restarted is restarted + 1 else: if _sup_crashed[slot] == 1: - restarted is restarted + (_restart of [sup, child]) + if _restart of [sup, child]: + restarted is restarted + 1 return restarted # _all_settled(sup) — every worker has finished normally, or exhausted its @@ -197,18 +199,18 @@ define supervise_step(sup) as: define _all_settled(sup) as: for child in sup.children: slot is child.slot - finished is (task_alive of child.id) == 0 + finished is (not (task_alive of child.id)) done is _sup_done[slot] == 1 capped is child.restarts >= sup.max_restarts - if done == 1: + if done: settled is 1 - elif finished == 1 and capped == 1: + elif finished and capped: settled is 1 else: settled is 0 if settled == 0: - return 0 - return 1 + return false + return true # supervise_run([sup, max_ticks]) — drive supervision from the main task until # every worker is settled or `max_ticks` elapse. Deterministic: the same @@ -219,7 +221,7 @@ define supervise_run(pair) as: t is 0 total is 0 loop while t < max_ticks: - if (_all_settled of sup) == 1: + if (_all_settled of sup): return total total is total + (supervise_step of sup) task_yield of null diff --git a/lib/test_runner.eigs b/lib/test_runner.eigs index b616fae9..67b5ecdb 100644 --- a/lib/test_runner.eigs +++ b/lib/test_runner.eigs @@ -124,7 +124,7 @@ if json_mode: parts is [] for rec in results: status is "fail" - if rec.ok == 1: + if rec.ok: status is "pass" item is "{" + (json_str of "file") + ":" + (json_str of rec.file) item is item + "," + (json_str of "status") + ":" + (json_str of status) @@ -151,15 +151,15 @@ else: print of "" for rec in results: tag is "FAIL" - if rec.ok == 1: + if rec.ok: tag is "PASS" detail is "" if rec.ap >= 0: detail is " (" + (str of rec.ap) + " pass, " + (str of rec.af) + " fail)" - elif rec.ok != 1: + elif (not rec.ok): detail is " (exit " + (str of rec.code) + ")" print of (" " + tag + " " + rec.file + detail) - if rec.ok != 1: + if (not rec.ok): # show the failing file's captured output, indented for ln in (split of [rec.output, "\n"]): if (len of (trim of ln)) > 0: diff --git a/lib/ui.eigs b/lib/ui.eigs index bed616b4..959e9e8e 100644 --- a/lib/ui.eigs +++ b/lib/ui.eigs @@ -29,6 +29,22 @@ # as key events; headless synthesized event dicts may omit them # (absent keys read null). +# #1637: an event modifier (shift/ctrl/alt) as a bool. gfx_poll delivers +# bools; a hand-built event may still carry the older 1/0. Absent is false. +define _ev_flag(v) as: + if (type of v) == "bool": + return v + if (type of v) == "num": + return v != 0 + return false + +# #1637: an on_key answer that stops the loop: false, or the older 0. +# `result == 0` raised when a handler returned a bool. +define _ev_stop(r) as: + if (type of r) == "bool": + return not r + return ((type of r) == "num") and (r == 0) + load_file of "lib/ui_theme.eigs" load_file of "lib/ui_draw.eigs" load_file of "lib/ui_layout.eigs" @@ -456,45 +472,39 @@ define unregister_hotkey(combo) as: define _combo_matches(combo, ctrl, shift, alt, key) as: # Parse combo string like "ctrl+s", "ctrl+shift+z", "alt+f" - need_ctrl is 0 - need_shift is 0 - need_alt is 0 + need_ctrl is false + need_shift is false + need_alt is false target_key is combo - if (contains of [combo, "ctrl+"]) == 1: - need_ctrl is 1 + if (contains of [combo, "ctrl+"]): + need_ctrl is true target_key is str_replace of [target_key, "ctrl+", ""] - if (contains of [combo, "shift+"]) == 1: - need_shift is 1 + if (contains of [combo, "shift+"]): + need_shift is true target_key is str_replace of [target_key, "shift+", ""] - if (contains of [combo, "alt+"]) == 1: - need_alt is 1 + if (contains of [combo, "alt+"]): + need_alt is true target_key is str_replace of [target_key, "alt+", ""] - if ctrl != need_ctrl: - return 0 - if shift != need_shift: - return 0 - if alt != need_alt: - return 0 + if (_ev_flag of ctrl) != need_ctrl: + return false + if (_ev_flag of shift) != need_shift: + return false + if (_ev_flag of alt) != need_alt: + return false if key == target_key: - return 1 - return 0 + return true + return false define _match_hotkey(ev) as: - ctrl is 0 - shift is 0 - alt is 0 - if ev.ctrl != null: - ctrl is ev.ctrl - if ev.shift != null: - shift is ev.shift - if ev.alt != null: - alt is ev.alt + ctrl is _ev_flag of ev.ctrl + shift is _ev_flag of ev.shift + alt is _ev_flag of ev.alt for i in range of (len of _ui.hotkeys): hk is _ui.hotkeys[i] - if (_combo_matches of [hk.combo, ctrl, shift, alt, ev.key]) == 1: + if (_combo_matches of [hk.combo, ctrl, shift, alt, ev.key]): hk.callback of null - return 1 - return 0 + return true + return false # Modal stack @@ -565,11 +575,11 @@ define show_dialog(dlg, parent_w, parent_h) as: define _has_open_popup(widget) as: if widget.type == "combobox" or widget.type == "dropdown": if widget.open == 1: - return 1 + return true if widget.type == "menu_bar": if widget.open_index >= 0: - return 1 - return 0 + return true + return false define claim_drag(widget) as: _ui.drag_widget is widget @@ -593,18 +603,18 @@ define request_quit() as: define handle_key(root, ev) as: if ev.type != "keydown": - return 0 - if (_match_hotkey of ev) == 1: - return 1 + return false + if (_match_hotkey of ev): + return true if ev.key == "escape" and (len of _ui.modal_stack) > 0: pop_modal of null - return 1 + return true if _ui.focused_widget != null and _ui.focused_widget.type == "combobox" and _ui.focused_widget.open == 1: _handle_combobox_key of [_ui.focused_widget, ev] - return 1 + return true if _ui.focused_input != null: _handle_text_key of [_ui.focused_input, ev] - return 1 + return true # Focus navigation reports its own consumption, so a key it ignores # (an unbound letter with a widget merely focused) stays free for the # app rather than being silently swallowed. @@ -612,7 +622,7 @@ define handle_key(root, ev) as: return _handle_focus_key of [root, ev] if _ui.focused_widget != null and _ui.focused_input == null: return _handle_focus_key of [root, ev] - return 0 + return false # Event dispatch — registry-driven @@ -806,7 +816,7 @@ define dispatch(root, ev) as: # through to the registry on_mousedown below. if dbl_ok == 1 and hit.type != "editable_label" and hit.on_dblclick == null: dbl_ok is 0 - if dbl_ok == 1 and (_has_open_popup of hit) == 0 and hit.id == _ui.last_click_id and (now - _ui.last_click_time) < 400: + if dbl_ok == 1 and (not (_has_open_popup of hit)) and hit.id == _ui.last_click_id and (now - _ui.last_click_time) < 400: if hit.type == "editable_label": _start_editing of hit else: @@ -908,12 +918,12 @@ define app_loop(root, on_key, on_tick) as: handle_key of [root, ev] if on_key != null: result is on_key of ev - if result == 0: + if _ev_stop of result: _running is 0 elif ev.type == "keyup": if on_key != null: result is on_key of ev - if result == 0: + if _ev_stop of result: _running is 0 else: dispatch of [root, ev] diff --git a/lib/ui_draw.eigs b/lib/ui_draw.eigs index 30df0996..e04a3dbb 100644 --- a/lib/ui_draw.eigs +++ b/lib/ui_draw.eigs @@ -9,8 +9,8 @@ define _point_in_rect(px, py, x, y, w, h) as: if px >= x and px < x + w and py >= y and py < y + h: - return 1 - return 0 + return true + return false # ---- Theme lookup ---- # Apps build custom themes by copying a built-in dict and overwriting diff --git a/lib/ui_focus.eigs b/lib/ui_focus.eigs index 2f8e1a85..b15c2f53 100644 --- a/lib/ui_focus.eigs +++ b/lib/ui_focus.eigs @@ -6,7 +6,7 @@ define _build_focus_list(widget) as: if widget.visible == 0: return null - if (_is_focusable of widget) == 1: + if (_is_focusable of widget): append of [_ui.focus_list, widget] if _is_container of widget.type: for i in range of (len of widget.children): @@ -22,7 +22,7 @@ define _build_focus_list(widget) as: _build_focus_list of widget.panel_b define _sync_focus_to(widget) as: - if (_is_focusable of widget) == 1: + if (_is_focusable of widget): _ui.focused_widget is widget # Try to find index in current focus list for i in range of (len of _ui.focus_list): @@ -57,20 +57,20 @@ define _focus_next(root, reverse) as: define _handle_focus_key(root, ev) as: if ev.key == "tab": - if ev.shift == 1: + if ev.shift: _focus_next of [root, 1] else: _focus_next of [root, 0] - return 1 + return true # Activation keys if _ui.focused_widget == null: - return 0 + return false w is _ui.focused_widget if ev.key == "return" or ev.key == "space": if w.type == "button" and w.enabled == 1: if w.on_click != null: w.on_click of w - return 1 + return true if w.type == "toggle" or w.type == "checkbox": if w.value == 1: w.value is 0 @@ -78,7 +78,7 @@ define _handle_focus_key(root, ev) as: w.value is 1 if w.on_change != null: w.on_change of w - return 1 + return true if w.type == "toggle_button": if w.value == 1: w.value is 0 @@ -86,7 +86,7 @@ define _handle_focus_key(root, ev) as: w.value is 1 if w.on_click != null: w.on_click of w - return 1 + return true # Arrow keys for sliders/knobs if ev.key == "left" or ev.key == "right" or ev.key == "up" or ev.key == "down": if w.type == "slider" or w.type == "vslider" or w.type == "knob": @@ -101,7 +101,7 @@ define _handle_focus_key(root, ev) as: w.value is w.max_val if w.on_change != null: w.on_change of w - return 1 + return true if w.type == "radio_group": if ev.key == "left" or ev.key == "up": w.selected is w.selected - 1 @@ -113,7 +113,7 @@ define _handle_focus_key(root, ev) as: w.selected is 0 if w.on_change != null: w.on_change of w - return 1 + return true if w.type == "item_list": if ev.key == "up": w.selected is w.selected - 1 @@ -125,7 +125,7 @@ define _handle_focus_key(root, ev) as: w.selected is (len of w.items) - 1 if w.on_select != null: w.on_select of w - return 1 + return true # Tree navigation with arrow keys if ev.key == "up" or ev.key == "down" or ev.key == "left" or ev.key == "right": if w != null and w.type == "tree": @@ -150,7 +150,7 @@ define _handle_focus_key(root, ev) as: elif ev.key == "right" and cur_fi >= 0: if flat[cur_fi].has_children == 1: flat[cur_fi].node.expanded is 1 - return 1 + return true # Grid: arrow keys move hover, space toggles cell if w.type == "grid": if ev.key == "left" or ev.key == "right" or ev.key == "up" or ev.key == "down": @@ -166,7 +166,7 @@ define _handle_focus_key(root, ev) as: w.hover_row is w.hover_row - 1 elif ev.key == "down" and w.hover_row < w.rows - 1: w.hover_row is w.hover_row + 1 - return 1 + return true if ev.key == "space" or ev.key == "return": if w.hover_row >= 0 and w.hover_col >= 0: # Mirror the mouse path's ownership rule (#572), or @@ -178,7 +178,7 @@ define _handle_focus_key(root, ev) as: w.cells[w.hover_row][w.hover_col] is 1 if w.on_cell != null: w.on_cell of [w, w.hover_row, w.hover_col] - return 1 + return true # Chart: left/right move hover_index if w.type == "chart": if ev.key == "left" or ev.key == "right": @@ -192,19 +192,19 @@ define _handle_focus_key(root, ev) as: max_n is len of w.series[si].data if w.hover_index >= max_n: w.hover_index is max_n - 1 - return 1 + return true # Bar chart: left/right move hover, enter triggers on_click if w.type == "bar_chart": if ev.key == "left" and w.hover_index > 0: w.hover_index is w.hover_index - 1 - return 1 + return true elif ev.key == "right" and w.hover_index < (len of w.values) - 1: w.hover_index is w.hover_index + 1 - return 1 + return true elif (ev.key == "return" or ev.key == "space") and w.hover_index >= 0: if w.on_click != null: w.on_click of [w, w.hover_index] - return 1 + return true # Color picker: arrows adjust hue/saturation/value if w.type == "color_picker": if ev.key == "left": @@ -224,59 +224,59 @@ define _handle_focus_key(root, ev) as: if w.val < 0: w.val is 0 else: - return 0 + return false w.color is _hsv_to_rgb of [w.hue, w.sat, w.val] if w.on_change != null: w.on_change of w - return 1 + return true # Editable label: enter starts editing if w.type == "editable_label": if ev.key == "return": _start_editing of w - return 1 + return true # Code view: up/down scroll if w.type == "code_view": if ev.key == "up": w.scroll_y is w.scroll_y - 18 if w.scroll_y < 0: w.scroll_y is 0 - return 1 + return true elif ev.key == "down": w.scroll_y is w.scroll_y + 18 - return 1 + return true # Waveform view: left/right adjust offset if w.type == "waveform_view": if ev.key == "left": w.offset is w.offset - floor of (w.w * w.zoom) if w.offset < 0: w.offset is 0 - return 1 + return true elif ev.key == "right": w.offset is w.offset + floor of (w.w * w.zoom) - return 1 + return true # Piano keyboard: arrows select note, enter triggers if w.type == "piano_kb": if ev.key == "left" and w.hover_note > 0: w.hover_note is w.hover_note - 1 - return 1 + return true elif ev.key == "right" and w.hover_note < w.octaves * 12: w.hover_note is w.hover_note + 1 - return 1 + return true elif ev.key == "return" or ev.key == "space": if w.hover_note >= 0 and w.on_note != null: w.on_note of [w, w.hover_note, 1] - return 1 + return true # Canvas: enter fires synthetic click at center if w.type == "canvas": if ev.key == "return" or ev.key == "space": if w.on_mouse != null: w.on_mouse of [w, {"type": "mousedown", "x": floor of (w.w / 2), "y": floor of (w.h / 2), "button": 1}] - return 1 + return true # Escape closes dropdowns/menus if ev.key == "escape": _close_dropdowns of root _close_menus of root if (len of _ui.modal_stack) > 0: pop_modal of null - return 1 - return 0 + return true + return false diff --git a/lib/ui_registry.eigs b/lib/ui_registry.eigs index ee5cc75e..13c6c61c 100644 --- a/lib/ui_registry.eigs +++ b/lib/ui_registry.eigs @@ -11,15 +11,15 @@ define _register_widget(type_name, opts) as: define _is_container(t) as: entry is _widget_registry[t] if entry != null and entry.container == 1: - return 1 - return 0 + return true + return false define _is_focusable(widget) as: entry is _widget_registry[widget.type] if entry != null and entry.focusable == 1: if widget.enabled != null and widget.enabled == 0: - return 0 + return false if widget.visible == 0: - return 0 - return 1 - return 0 + return false + return true + return false diff --git a/lib/ui_w_dialog.eigs b/lib/ui_w_dialog.eigs index b8496fc8..cfd5bf13 100644 --- a/lib/ui_w_dialog.eigs +++ b/lib/ui_w_dialog.eigs @@ -96,7 +96,7 @@ define property_editor(id, x, y, w, h, properties, on_change) as: add_child of [sp, inp] elif prop.type == "bool": bval is 0 - if prop.value == 1 or prop.value == "true": + if (_ev_flag of prop.value) or prop.value == "true": # #1637: true, 1 or "true" bval is 1 cb is _make_prop_cb of [on_change, prop.key] inp is toggle of [f"{id}_inp_{pi}", input_x, row_y, bval, cb] @@ -368,7 +368,7 @@ define _fd_join(dir, name) as: if name == "..": return _fd_parent of dir base is dir - if (ends_with of [base, "/"]) == 1: + if (ends_with of [base, "/"]): base is substr of [base, 0, (len of base) - 1] if base == "": return "/" + name @@ -376,7 +376,7 @@ define _fd_join(dir, name) as: define _fd_parent(dir) as: base is dir - if (len of base) > 1 and (ends_with of [base, "/"]) == 1: + if (len of base) > 1 and (ends_with of [base, "/"]): base is substr of [base, 0, (len of base) - 1] cut is 0 - 1 for i in range of (len of base): @@ -400,7 +400,7 @@ define file_dialog_refresh(fd) as: if entries != null: for i in range of (len of entries): name is entries[i] - if (_fd_is_dir of [fd.dir, name]) == 1: + if (_fd_is_dir of [fd.dir, name]): append of [dirs, name + "/"] else: append of [files, name] @@ -438,9 +438,9 @@ define file_dialog(id, w, h, title, start_dir, on_ok) as: if lst.selected < 0 or lst.selected >= (len of lst.items): return null name is lst.items[lst.selected] - if name == ".." or (ends_with of [name, "/"]) == 1: + if name == ".." or (ends_with of [name, "/"]): entry is name - if (ends_with of [entry, "/"]) == 1: + if (ends_with of [entry, "/"]): entry is substr of [entry, 0, (len of entry) - 1] fd.dir is _fd_join of [fd.dir, entry] file_dialog_refresh of fd diff --git a/lib/ui_w_dock.eigs b/lib/ui_w_dock.eigs index e61516e9..770489dd 100644 --- a/lib/ui_w_dock.eigs +++ b/lib/ui_w_dock.eigs @@ -283,10 +283,10 @@ define _render_dock(widget, ax, ay) as: define _dock_in(r, mx, my) as: if r == null: - return 0 + return false if mx >= r[0] and mx < r[0] + r[2] and my >= r[1] and my < r[1] + r[3]: - return 1 - return 0 + return true + return false # Handle hit-test with slop: the visual bar is handle_w (~5px) thin, a # hard target for a real pointer (found by real-input acceptance — the @@ -294,37 +294,37 @@ define _dock_in(r, mx, my) as: # stay thin. define _dock_in_handle(r, mx, my, horiz) as: if r == null: - return 0 + return false local sx is 3 local sy is 0 if horiz == 1: sx is 0 sy is 3 if mx >= r[0] - sx and mx < r[0] + r[2] + sx and my >= r[1] - sy and my < r[1] + r[3] + sy: - return 1 - return 0 + return true + return false # Hit-testing: handles and title bars belong to the dock itself (so the # press routes to _mousedown_dock); a point in a visible body or the # center recurses into the hosted widget. define _hit_test_dock(widget, ax, ay, mx, my) as: - if (_point_in_rect of [mx, my, ax, ay, widget.w, widget.h]) == 0: + if (not (_point_in_rect of [mx, my, ax, ay, widget.w, widget.h])): return null local v is dock_view of widget - if (_dock_in_handle of [v.hw, mx, my, 0]) == 1 or (_dock_in_handle of [v.he, mx, my, 0]) == 1 or (_dock_in_handle of [v.hs, mx, my, 1]) == 1: + if (_dock_in_handle of [v.hw, mx, my, 0]) or (_dock_in_handle of [v.he, mx, my, 0]) or (_dock_in_handle of [v.hs, mx, my, 1]): return widget for reg in ["west", "east", "south"]: local pes is v.panels[reg] for i in range of (len of pes): local pe is pes[i] - if (_dock_in of [pe.title, mx, my]) == 1: + if (_dock_in of [pe.title, mx, my]): return widget - if pe.body != null and (_dock_in of [pe.body, mx, my]) == 1: + if pe.body != null and (_dock_in of [pe.body, mx, my]): local r is _hit_test of [pe.p.widget, ax, ay, mx, my] if r != null: return r return widget - if widget.center != null and (_dock_in of [v.center, mx, my]) == 1: + if widget.center != null and (_dock_in of [v.center, mx, my]): local rc is _hit_test of [widget.center, ax, ay, mx, my] if rc != null: return rc @@ -334,15 +334,15 @@ define _mousedown_dock(hit, root, mx, my, ev) as: if hit.interactive == 0: return null local v is dock_view of hit - if (_dock_in_handle of [v.hw, mx, my, 0]) == 1: + if (_dock_in_handle of [v.hw, mx, my, 0]): hit._drag_region is "west" _ui.drag_widget is hit return null - if (_dock_in_handle of [v.he, mx, my, 0]) == 1: + if (_dock_in_handle of [v.he, mx, my, 0]): hit._drag_region is "east" _ui.drag_widget is hit return null - if (_dock_in_handle of [v.hs, mx, my, 1]) == 1: + if (_dock_in_handle of [v.hs, mx, my, 1]): hit._drag_region is "south" _ui.drag_widget is hit return null @@ -350,7 +350,7 @@ define _mousedown_dock(hit, root, mx, my, ev) as: local pes is v.panels[reg] for i in range of (len of pes): local pe is pes[i] - if (_dock_in of [pe.title, mx, my]) == 1: + if (_dock_in of [pe.title, mx, my]): pe.p.collapsed is 1 - pe.p.collapsed if hit.on_layout != null: hit.on_layout of hit diff --git a/lib/ui_w_input.eigs b/lib/ui_w_input.eigs index 89c6bdee..ac5f741e 100644 --- a/lib/ui_w_input.eigs +++ b/lib/ui_w_input.eigs @@ -294,14 +294,10 @@ define _clear_hover_combobox(widget) as: define _handle_text_key(widget, ev) as: k is ev.key - shift is 0 - if ev.shift != null: - shift is ev.shift - ctrl is 0 - if ev.ctrl != null: - ctrl is ev.ctrl + shift is _ev_flag of ev.shift # #1637: gfx_poll delivers bools + ctrl is _ev_flag of ev.ctrl # Ctrl+key shortcuts - if ctrl == 1: + if ctrl: if k == "a": widget.sel_start is 0 widget.sel_end is len of widget.text @@ -329,7 +325,7 @@ define _handle_text_key(widget, ev) as: widget.on_change of widget return null # Shift+arrow extends selection - if shift == 1 and (k == "left" or k == "right" or k == "home" or k == "end"): + if shift and (k == "left" or k == "right" or k == "home" or k == "end"): if widget.sel_start == null or widget.sel_start < 0: widget.sel_start is widget.cursor widget.sel_end is widget.cursor @@ -397,7 +393,7 @@ define _handle_text_key(widget, ev) as: _insert_char of [widget, " "] elif (len of k) == 1: ch is k - if shift == 1: + if shift: ch is str_upper of k # Shift+number -> symbol if k == "1": @@ -546,18 +542,16 @@ define _handle_combobox_key(widget, ev) as: widget.filter_text is widget.filter_text + " " widget.cursor is len of widget.filter_text elif (len of k) == 1: - shift is 0 - if ev.shift != null: - shift is ev.shift + shift is _ev_flag of ev.shift ch is k - if shift == 1: + if shift: ch is str_upper of k widget.filter_text is widget.filter_text + ch widget.cursor is len of widget.filter_text # Re-filter widget.filtered is [] for i in range of (len of widget.items): - if (len of widget.filter_text) == 0 or (contains of [str_lower of widget.items[i], str_lower of widget.filter_text]) == 1: + if (len of widget.filter_text) == 0 or (contains of [str_lower of widget.items[i], str_lower of widget.filter_text]): append of [widget.filtered, widget.items[i]] widget.hover_index is 0 diff --git a/lib/utf8.eigs b/lib/utf8.eigs index ad9335e0..12fb65a7 100644 --- a/lib/utf8.eigs +++ b/lib/utf8.eigs @@ -14,7 +14,7 @@ # utf8_codepoints of s # list of codepoint numbers # utf8_at of [s, i] # i-th codepoint (0-indexed), or -1 if out of range # utf8_char_at of [s, i] # i-th character as a (multi-byte) string, or "" -# utf8_validate of s # 1 if structurally valid UTF-8, else 0 +# utf8_validate of s # true if structurally valid UTF-8, else false # utf8_encode of cp # codepoint -> UTF-8 byte string ("" if unencodable) # utf8_from_codepoints of cps # list of codepoints -> UTF-8 string # @@ -116,7 +116,7 @@ define utf8_char_at(n) as: j is j + 1 return "" -# ---- utf8_validate: 1 if structurally valid UTF-8, else 0 ---- +# ---- utf8_validate: true if structurally valid UTF-8, else false ---- # Checks lead bytes and that each sequence has its full run of 0x80..0xBF # continuation bytes. (Structural: does not reject overlong encodings or # surrogate-range codepoints.) @@ -127,19 +127,19 @@ define utf8_validate(s) as: local b0 is ord of (char_at of [s, i]) local nb is _u8_lead_len of b0 if nb == 0: - return 0 + return false if (i + nb) > slen: - return 0 + return false local k is 1 loop while k < nb: local cb is ord of (char_at of [s, i + k]) if cb < 128: - return 0 + return false if cb > 191: - return 0 + return false k is k + 1 i is i + nb - return 1 + return true # ---- utf8_encode: codepoint -> UTF-8 bytes as a string ---- # Structural like the rest of the module: any codepoint in 1..0x10FFFF encodes diff --git a/lib/validate.eigs b/lib/validate.eigs index cff2992e..aa445125 100644 --- a/lib/validate.eigs +++ b/lib/validate.eigs @@ -5,14 +5,14 @@ # How to use: # load_file of "lib/validate.eigs" # -# is_nonempty of value # 1 if non-empty string/list -# is_number of value # 1 if numeric -# is_integer of value # 1 if whole number -# in_range of [value, lo, hi] # 1 if lo <= value <= hi -# is_one_of of [value, allowed_list] # 1 if value in list +# is_nonempty of value # true if non-empty string/list +# is_number of value # true if numeric +# is_integer of value # true if whole number +# in_range of [value, lo, hi] # true if lo <= value <= hi +# is_one_of of [value, allowed_list] # true if value in list # is_email of "user@example.com" # basic email check -# is_alpha of "hello" # 1 if only letters -# is_alphanumeric of "abc123" # 1 if letters and digits +# is_alpha of "hello" # true if only letters +# is_alphanumeric of "abc123" # true if letters and digits # validate_all of [[check1, check2, ...]] # run multiple checks # ---- is_nonempty: check if value is non-empty ---- @@ -22,16 +22,16 @@ define is_nonempty(value) as: if (type of value) == "list": return (len of value) > 0 if (type of value) == "null": - return 0 - return 1 + return false + return true # ---- is_number: check if value is numeric or parseable as number ---- define is_number(value) as: if (type of value) == "num": - return 1 + return true if (type of value) == "str": if (len of value) == 0: - return 0 + return false # Optional leading '-', at most one '.', and at least one digit. # A dot alone ('.' / '-.') used to pass this scan; is_integer then # treated num-of-that as zero (#1235). @@ -40,13 +40,13 @@ define is_number(value) as: local start is 0 if (char_at of [value, 0]) == "-": if (len of value) == 1: - return 0 + return false start is 1 for i in range of ((len of value) - start): c is char_at of [value, i + start] if c == ".": if has_dot == 1: - return 0 + return false has_dot is 1 elif c == "0": has_digit is 1 @@ -69,96 +69,96 @@ define is_number(value) as: elif c == "9": has_digit is 1 else: - return 0 + return false if has_digit == 0: - return 0 - return 1 - return 0 + return false + return true + return false # ---- is_integer: check if value is a whole number ---- define is_integer(value) as: if (type of value) == "num": return value == (floor of value) if (type of value) == "str": - if (is_number of value) == 0: - return 0 + if (not (is_number of value)): + return false val is num of value return val == (floor of val) - return 0 + return false # ---- in_range: check if value is within [lo, hi] ---- # in_range of [value, lo, hi] define in_range(val, lo, hi) as: if val < lo: - return 0 + return false if val > hi: - return 0 - return 1 + return false + return true # ---- is_one_of: check if value is in an allowed list ---- # is_one_of of [value, [allowed1, allowed2, ...]] define is_one_of(val, allowed) as: for i in range of (len of allowed): if val == allowed[i]: - return 1 - return 0 + return true + return false # ---- is_email: basic email format check ---- # Checks for: non-empty local part, @, non-empty domain with dot define is_email(value) as: if (type of value) != "str": - return 0 + return false at_pos is index_of of [value, "@"] if at_pos < 1: - return 0 + return false domain is substr of [value, at_pos + 1, (len of value) - at_pos - 1] if (len of domain) < 3: - return 0 + return false dot_pos is index_of of [domain, "."] if dot_pos < 1: - return 0 + return false if dot_pos >= ((len of domain) - 1): - return 0 - return 1 + return false + return true # ---- is_alpha: check if string contains only letters ---- define is_alpha(value) as: if (type of value) != "str": - return 0 + return false if (len of value) == 0: - return 0 + return false 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 (contains of ["abcdefghijklmnopqrstuvwxyz", c]) == 0: - return 0 - return 1 + if (not (contains of ["abcdefghijklmnopqrstuvwxyz", c])): + return false + return true # ---- is_alphanumeric: check if string is letters and digits only ---- define is_alphanumeric(value) as: if (type of value) != "str": - return 0 + return false if (len of value) == 0: - return 0 + return false lc is str_lower of value allowed is "abcdefghijklmnopqrstuvwxyz0123456789" for i in range of (len of lc): c is char_at of [lc, i] - if (contains of [allowed, c]) == 0: - return 0 - return 1 + if (not (contains of [allowed, c])): + return false + return true # ---- is_url: basic URL format check ---- define is_url(value) as: if (type of value) != "str": - return 0 - if (starts_with of [value, "http://"]) == 1: + return false + if (starts_with of [value, "http://"]): return (len of value) > 10 - if (starts_with of [value, "https://"]) == 1: + if (starts_with of [value, "https://"]): return (len of value) > 11 - return 0 + return false # ---- validate_all: run a list of [value, check_fn, description] triples ---- # Returns list of failed descriptions. Empty list = all passed. @@ -168,6 +168,6 @@ define validate_all(checks) as: val is checks[i][0] check_fn is checks[i][1] desc is checks[i][2] - if (check_fn of val) == 0: + if not (check_fn of val): # #1637: predicates answer bools append of [errors, desc] return errors diff --git a/src/builtins.c b/src/builtins.c index e70efecd..7700e5b7 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -149,8 +149,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)arg->data.num; + int us = (int)eigs_num_arg(arg, __func__); if (us > 0) { struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); @@ -176,11 +177,16 @@ Value* builtin_usleep(Value *arg) { Value* builtin_screen_put(Value *arg) { STRICT_LIST_MAX(arg, 4, "screen_put"); if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) return make_null(); - int row = (int)arg->data.list.items[0]->data.num; - int col = (int)arg->data.list.items[1]->data.num; + int row = (int)eigs_list_num(arg, 0, __func__); + int col = (int)eigs_list_num(arg, 1, __func__); + if (g_has_error) return make_null(); /* #1637: a non-number slot raised */ const char *ch = arg->data.list.items[2]->type == VAL_STR ? arg->data.list.items[2]->data.str : " "; - int color = (arg->data.list.count >= 4 && arg->data.list.items[3]->type == VAL_NUM) - ? (int)arg->data.list.items[3]->data.num : 0; + /* #1637: an optional color; given, it must be a number (a bool raises + * instead of meaning "no color"). */ + double color_d = 0; + if (arg->data.list.count >= 4) eigs_opt_num(arg->data.list.items[3], &color_d, "screen_put"); + if (g_has_error) return make_null(); + int color = (int)color_d; if (color > 0) printf("\033[%d;%dH\033[%dm%s", row, col, color, ch); else @@ -211,12 +217,13 @@ Value* builtin_screen_render(Value *arg) { STRICT_LIST_MAX(arg, 7, "screen_render"); if (!arg || arg->type != VAL_LIST || arg->data.list.count < 7) return make_null(); Value *entities = arg->data.list.items[0]; - int sw = (int)arg->data.list.items[1]->data.num; - int sh = (int)arg->data.list.items[2]->data.num; - double px = arg->data.list.items[3]->data.num; - double py = arg->data.list.items[4]->data.num; - double ww = arg->data.list.items[5]->data.num; - double wh = arg->data.list.items[6]->data.num; + int sw = (int)eigs_list_num(arg, 1, __func__); + int sh = (int)eigs_list_num(arg, 2, __func__); + double px = eigs_list_num(arg, 3, __func__); + double py = eigs_list_num(arg, 4, __func__); + double ww = eigs_list_num(arg, 5, __func__); + double wh = eigs_list_num(arg, 6, __func__); + if (g_has_error) return make_null(); /* #1637: a non-number slot raised */ if (!entities || entities->type != VAL_LIST) return make_null(); if (sw <= 0 || sh <= 0 || sw > 10000 || sh > 10000) return make_null(); @@ -236,10 +243,11 @@ Value* builtin_screen_render(Value *arg) { for (int i = 0; i < entities->data.list.count; i++) { Value *ent = entities->data.list.items[i]; if (!ent || ent->type != VAL_LIST || ent->data.list.count < 4) continue; - double ex = ent->data.list.items[0]->data.num; - double ey = ent->data.list.items[1]->data.num; + double ex = eigs_list_num(ent, 0, __func__); + double ey = eigs_list_num(ent, 1, __func__); const char *ch = ent->data.list.items[2]->type == VAL_STR ? ent->data.list.items[2]->data.str : " "; - int color = (int)ent->data.list.items[3]->data.num; + int color = (int)eigs_list_num(ent, 3, __func__); + if (g_has_error) { free(chars); free(cols); return make_null(); } /* Torus delta */ double dx = ex - px; @@ -473,8 +481,8 @@ static int bit_pair(Value *arg, int64_t *a_out, int64_t *b_out) { Value *va = arg->data.list.items[0]; Value *vb = arg->data.list.items[1]; if (!va || va->type != VAL_NUM || !vb || vb->type != VAL_NUM) return 0; - *a_out = (int64_t)va->data.num; - *b_out = (int64_t)vb->data.num; + *a_out = (int64_t)eigs_num_arg(va, __func__); + *b_out = (int64_t)eigs_num_arg(vb, __func__); return 1; } @@ -501,7 +509,7 @@ Value* builtin_bit_xor(Value *arg) { 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(); } - int64_t a = (int64_t)arg->data.num; + int64_t a = (int64_t)eigs_num_arg(arg, __func__); return make_num((double)(~a)); } @@ -602,6 +610,14 @@ Value* builtin_str(Value *arg) { 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->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 @@ -689,9 +705,10 @@ Value* builtin_set_observer_thresholds(Value *arg) { rt_error(EK_TYPE, 0, "set_observer_thresholds requires [dh_zero, dh_small, h_low]"); return make_null(); } - double dh_zero = arg->data.list.items[0]->data.num; - double dh_small = arg->data.list.items[1]->data.num; - double h_low = arg->data.list.items[2]->data.num; + double dh_zero = eigs_list_num(arg, 0, __func__); + double dh_small = eigs_list_num(arg, 1, __func__); + double h_low = eigs_list_num(arg, 2, __func__); + if (g_has_error) return make_null(); /* #1637: a non-number slot raised */ if (dh_zero <= 0 || dh_small <= 0 || h_low <= 0) { rt_error(EK_VALUE, 0, "observer thresholds must be positive"); return make_null(); @@ -743,7 +760,7 @@ static int obs_window_arg(Value *v, const char *who) { rt_error(EK_TYPE, 0, "%s: window depth must be a number", who); return -1; } - double d = v->data.num; + double d = eigs_num_arg(v, __func__); if (d != (int)d || d < OBSERVER_WINDOW_MIN || d > OBSERVER_WINDOW_MAX) { rt_error(EK_VALUE, 0, "%s: window depth must be an integer in [%d, %d], got %g", who, OBSERVER_WINDOW_MIN, OBSERVER_WINDOW_MAX, d); @@ -772,7 +789,7 @@ Value* builtin_set_observer_window(Value *arg) { const char *name = arg->data.list.items[0]->data.str; Value *nv = arg->data.list.items[1]; int n; - if (nv && nv->type == VAL_NUM && nv->data.num == 0.0) { + if (nv && nv->type == VAL_NUM && eigs_num_arg(nv, __func__) == 0.0) { n = 0; /* clear the override */ } else { n = obs_window_arg(nv, "set_observer_window"); @@ -840,8 +857,8 @@ Value* builtin_get_observer_window(Value *arg) { snprintf(key, sizeof(key), "_#win:%s", name); Env *mod = builtin_window_module_env(start); Value *wv = mod ? env_get(mod, key) : NULL; - if (wv && wv->type == VAL_NUM && wv->data.num > 0) - return make_num(wv->data.num); + if (wv && wv->type == VAL_NUM && eigs_num_arg(wv, __func__) > 0) + return make_num(eigs_num_arg(wv, __func__)); return make_num((double)observer_slot_window(NULL)); } const ObserverSlot *s = (slot < target->obs_cap) ? env_obs_slot(target, slot) : NULL; @@ -864,7 +881,7 @@ Value* builtin_set_observer_scale(Value *arg) { rt_error(EK_TYPE, 0, "set_observer_scale requires a number"); return make_null(); } - double sc = arg->data.num; + double sc = eigs_num_arg(arg, __func__); if (!(sc > 0.0) || sc > 1e300) { rt_error(EK_VALUE, 0, "observer scale must be positive and finite, got %g", sc); return make_null(); @@ -892,11 +909,11 @@ Value* builtin_exit(Value *arg) { STRICT_LIST_MAX(arg, 1, "exit"); int code = 0; if (arg && arg->type == VAL_NUM) { - code = (int)arg->data.num; + code = (int)eigs_num_arg(arg, __func__); } else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 1 && arg->data.list.items[0] && arg->data.list.items[0]->type == VAL_NUM) { - code = (int)arg->data.list.items[0]->data.num; + code = (int)eigs_list_num(arg, 0, __func__); } g_exit_code = code; g_exit_requested = 1; /* this thread's unwind: uncatchable */ @@ -993,11 +1010,11 @@ Value* builtin_values(Value *arg) { Value* builtin_has_key(Value *arg) { STRICT_LIST_MAX(arg, 2, "has_key"); - ARG_GUARD(arg->type != VAL_LIST || arg->data.list.count < 2, "has_key", "[dict, key]", make_num(0)); + ARG_GUARD(arg->type != VAL_LIST || arg->data.list.count < 2, "has_key", "[dict, key]", make_bool(0)); Value *d = arg->data.list.items[0]; Value *key = arg->data.list.items[1]; - ARG_GUARD(d->type != VAL_DICT || key->type != VAL_STR, "has_key", "[dict, string]", make_num(0)); - return make_num(dict_has(d, key->data.str) ? 1 : 0); + ARG_GUARD(d->type != VAL_DICT || key->type != VAL_STR, "has_key", "[dict, string]", make_bool(0)); + return make_bool(dict_has(d, key->data.str)); } Value* builtin_dict_set(Value *arg) { @@ -1105,9 +1122,10 @@ Value* builtin_classify(Value *arg) { Value* builtin_math_flags(Value *arg) { (void)arg; Value *d = make_dict(3); - Value *ov = make_num((g_math_flags & EIGS_MATH_OVERFLOW) ? 1 : 0); - Value *iv = make_num((g_math_flags & EIGS_MATH_INVALID) ? 1 : 0); - Value *uv = make_num((g_math_flags & EIGS_MATH_UNDERFLOW) ? 1 : 0); + /* #1637: each flag is a bool. */ + Value *ov = make_bool((g_math_flags & EIGS_MATH_OVERFLOW) != 0); + Value *iv = make_bool((g_math_flags & EIGS_MATH_INVALID) != 0); + Value *uv = make_bool((g_math_flags & EIGS_MATH_UNDERFLOW) != 0); dict_set_owned(d, "overflow", ov); dict_set_owned(d, "invalid", iv); dict_set_owned(d, "underflow", uv); @@ -1134,6 +1152,7 @@ Value* builtin_type(Value *arg) { case VAL_DICT: return make_str("dict"); case VAL_BUFFER: return make_str("buffer"); case VAL_TEXT_BUILDER: return make_str("text_builder"); + case VAL_BOOL: return make_str("bool"); } return make_str("none"); } @@ -1177,7 +1196,7 @@ static int eigs_json_encode_value(Value *v, strbuf *out, int depth) { * 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), v->data.num); + eigs_num_text(nb, sizeof(nb), eigs_num_arg(v, __func__)); strbuf_append(out, nb); break; } @@ -1205,6 +1224,9 @@ static int eigs_json_encode_value(Value *v, strbuf *out, int depth) { eigs_json_escape_string(out, v->data.text_builder.data ? v->data.text_builder.data : ""); break; } + case VAL_BOOL: /* #1637: JSON's own literals; json_decode gives the bool back */ + strbuf_append(out, v->data.boolean ? "true" : "false"); + break; case VAL_LIST: { strbuf_append_char(out, '['); for (int i = 0; i < v->data.list.count; i++) { @@ -1649,11 +1671,11 @@ Value* eigs_json_parse_value(const char *s, int *pos) { } if (s[*pos] == '-' || isdigit(s[*pos])) return eigs_json_parse_number(s, pos); if (strncmp(s + *pos, "null", 4) == 0) { *pos += 4; return make_null(); } - if (strncmp(s + *pos, "true", 4) == 0) { *pos += 4; return make_num(1); } - /* fs:LITERAL this IS how JSON `false` is returned — 0 is the parsed VALUE, - * not a stand-in for a rejected argument. A sweep over `return - * make_num(0)` would have made every `false` in parsed JSON raise. */ - if (strncmp(s + *pos, "false", 5) == 0) { *pos += 5; return make_num(0); } + /* #1637: JSON's true/false decode to the bool values. */ + if (strncmp(s + *pos, "true", 4) == 0) { *pos += 4; return make_bool(1); } + /* fs:LITERAL this IS how JSON `false` is returned — the parsed VALUE, + * not a stand-in for a rejected argument. */ + if (strncmp(s + *pos, "false", 5) == 0) { *pos += 5; return make_bool(0); } g_json_parse_err = 1; /* #495: unrecognized token */ return make_null(); } @@ -1738,10 +1760,12 @@ Value* builtin_json_build(Value *arg) { Value *val = arg->data.list.items[i + 1]; if (val->type == VAL_NUM) { char nb[32]; /* #875: the shared rule */ - eigs_num_text(nb, sizeof(nb), val->data.num); + eigs_num_text(nb, sizeof(nb), eigs_num_arg(val, __func__)); strbuf_append(&out, nb); } else if (val->type == VAL_NULL) { strbuf_append(&out, "null"); + } else if (val->type == VAL_BOOL) { /* #1637: a JSON boolean, not the string "true" */ + strbuf_append(&out, val->data.boolean ? "true" : "false"); } else if (val->type == VAL_JSON_RAW) { strbuf_append(&out, val->data.str); } else if (val->type == VAL_STR) { @@ -1786,18 +1810,18 @@ Value* builtin_str_lower(Value *arg) { * everything, so `contains of [[1,2,3], 2]` reported a spurious hit. */ Value* builtin_contains(Value *arg) { STRICT_LIST_MAX(arg, 2, "contains"); - ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "contains", "[haystack, needle]", make_num(0)); + ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "contains", "[haystack, needle]", make_bool(0)); Value *h = arg->data.list.items[0], *n = arg->data.list.items[1]; - ARG_GUARD(!h || h->type != VAL_STR || !n || n->type != VAL_STR, "contains", "two strings", make_num(0)); - return make_num(strstr(h->data.str, n->data.str) != NULL ? 1 : 0); + ARG_GUARD(!h || h->type != VAL_STR || !n || n->type != VAL_STR, "contains", "two strings", make_bool(0)); + return make_bool(strstr(h->data.str, n->data.str) != NULL); } Value* builtin_starts_with(Value *arg) { STRICT_LIST_MAX(arg, 2, "starts_with"); - ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "starts_with", "[string, prefix]", make_num(0)); + ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "starts_with", "[string, prefix]", make_bool(0)); Value *s = arg->data.list.items[0], *p = arg->data.list.items[1]; - ARG_GUARD(!s || s->type != VAL_STR || !p || p->type != VAL_STR, "starts_with", "two strings", make_num(0)); - return make_num(strncmp(s->data.str, p->data.str, val_str_len(p)) == 0 ? 1 : 0); + ARG_GUARD(!s || s->type != VAL_STR || !p || p->type != VAL_STR, "starts_with", "two strings", make_bool(0)); + return make_bool(strncmp(s->data.str, p->data.str, val_str_len(p)) == 0); } Value* builtin_split(Value *arg) { @@ -2245,7 +2269,7 @@ Value* builtin_char_at(Value *arg) { Value *idx_val = arg->data.list.items[1]; ARG_GUARD(!str_val || str_val->type != VAL_STR || !idx_val || idx_val->type != VAL_NUM, "char_at", "[string, number]", make_str("")); - int idx = (int)idx_val->data.num; + int idx = (int)eigs_num_arg(idx_val, __func__); int len = val_str_len(str_val); if (idx < 0) idx += len; /* fs:ANSWER an index outside the string has no character; "" is the @@ -2259,15 +2283,15 @@ Value* builtin_char_at(Value *arg) { /* ==== BUILTIN: ends_with ==== */ Value* builtin_ends_with(Value *arg) { STRICT_LIST_MAX(arg, 2, "ends_with"); - ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "ends_with", "[string, suffix]", make_num(0)); + ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "ends_with", "[string, suffix]", make_bool(0)); Value *sv = arg->data.list.items[0], *xv = arg->data.list.items[1]; - ARG_GUARD(!sv || sv->type != VAL_STR || !xv || xv->type != VAL_STR, "ends_with", "two strings", make_num(0)); + ARG_GUARD(!sv || sv->type != VAL_STR || !xv || xv->type != VAL_STR, "ends_with", "two strings", make_bool(0)); const char *str = sv->data.str, *suffix = xv->data.str; int slen = strlen(str), xlen = strlen(suffix); /* fs:ANSWER a suffix longer than the string is not a suffix of it; 0 is * the predicate's answer, not a stand-in for a rejected argument. */ - if (xlen > slen) return make_num(0); - return make_num(strcmp(str + slen - xlen, suffix) == 0 ? 1 : 0); + if (xlen > slen) return make_bool(0); + return make_bool(strcmp(str + slen - xlen, suffix) == 0); } /* ==== BUILTIN: substr ==== */ @@ -2282,8 +2306,8 @@ Value* builtin_substr(Value *arg) { 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("")); int slen = val_str_len(str_val); - int start = (int)start_val->data.num; - int rlen = (int)len_val->data.num; + int start = (int)eigs_num_arg(start_val, __func__); + int rlen = (int)eigs_num_arg(len_val, __func__); /* #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. */ @@ -2335,22 +2359,22 @@ 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(arg->data.num)); + return make_num(sin(eigs_num_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(arg->data.num)); + return make_num(cos(eigs_num_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(arg->data.num)); + return make_num(tan(eigs_num_arg(arg, __func__))); } Value* builtin_asin(Value *arg) { ARG_GUARD(!arg || arg->type != VAL_NUM, "asin", "a number", make_num(0)); - double x = arg->data.num; + double x = eigs_num_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) { @@ -2364,7 +2388,7 @@ Value* builtin_asin(Value *arg) { Value* builtin_acos(Value *arg) { ARG_GUARD(!arg || arg->type != VAL_NUM, "acos", "a number", make_num(0)); - double x = arg->data.num; + double x = eigs_num_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) { @@ -2378,7 +2402,7 @@ Value* builtin_acos(Value *arg) { Value* builtin_atan(Value *arg) { ARG_GUARD(!arg || arg->type != VAL_NUM, "atan", "a number", make_num(0)); - return make_num(atan(arg->data.num)); + return make_num(atan(eigs_num_arg(arg, __func__))); } Value* builtin_atan2(Value *arg) { @@ -2387,27 +2411,27 @@ Value* builtin_atan2(Value *arg) { Value *y = arg->data.list.items[0]; Value *x = arg->data.list.items[1]; ARG_GUARD(!y || y->type != VAL_NUM || !x || x->type != VAL_NUM, "atan2", "two numbers", make_num(0)); - return make_num(atan2(y->data.num, x->data.num)); + return make_num(atan2(eigs_num_arg(y, __func__), eigs_num_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(arg->data.num)); + return make_num(floor(eigs_num_arg(arg, __func__))); } Value* builtin_ceil(Value *arg) { ARG_GUARD(!arg || arg->type != VAL_NUM, "ceil", "a number", make_num(0)); - return make_num(ceil(arg->data.num)); + return make_num(ceil(eigs_num_arg(arg, __func__))); } Value* builtin_round(Value *arg) { ARG_GUARD(!arg || arg->type != VAL_NUM, "round", "a number", make_num(0)); - return make_num(round(arg->data.num)); + return make_num(round(eigs_num_arg(arg, __func__))); } Value* builtin_abs(Value *arg) { ARG_GUARD(!arg || arg->type != VAL_NUM, "abs", "a number", make_num(0)); - return make_num(fabs(arg->data.num)); + return make_num(fabs(eigs_num_arg(arg, __func__))); } /* #317: min/max are N-ary reductions over a flat list of numbers — the old @@ -2416,7 +2440,7 @@ Value* builtin_abs(Value *arg) { * an empty list or any non-number element keeps the old 0 fallback rather * than inventing a partial answer. */ static Value* minmax_reduce(Value *arg, int want_max) { - if (arg && arg->type == VAL_NUM) return make_num(arg->data.num); + 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 @@ -2429,8 +2453,8 @@ static Value* minmax_reduce(Value *arg, int want_max) { for (int i = 0; i < arg->data.list.count; i++) { Value *v = arg->data.list.items[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 ? v->data.num > best : v->data.num < best)) - best = v->data.num; + if (i == 0 || (want_max ? eigs_num_arg(v, __func__) > best : eigs_num_arg(v, __func__) < best)) + best = eigs_num_arg(v, __func__); } return make_num(best); } @@ -2514,8 +2538,8 @@ Value* builtin_random_int(Value *arg) { /* 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 = lo->data.num; - double hi_d = hi->data.num; + 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) { @@ -2542,12 +2566,12 @@ Value* builtin_random_int(Value *arg) { /* seed_random of n → seeds the RNG, returns 1 */ Value* builtin_seed_random(Value *arg) { - ARG_GUARD(!arg || arg->type != VAL_NUM, "seed_random", "a number", make_num(0)); + ARG_GUARD(!arg || arg->type != VAL_NUM, "seed_random", "a number", make_bool(0)); pthread_mutex_lock(&g_random_lock); - srand48((long)arg->data.num); + srand48((long)eigs_num_arg(arg, __func__)); __atomic_store_n(&g_random_seeded, 1, __ATOMIC_RELEASE); pthread_mutex_unlock(&g_random_lock); - return make_num(1); + return make_bool(1); } @@ -2784,7 +2808,7 @@ Value* builtin_json_path(Value *arg) { } if (current->type == VAL_NUM) { char buf[64]; - eigs_num_text(buf, sizeof(buf), current->data.num); /* #875 */ + eigs_num_text(buf, sizeof(buf), eigs_num_arg(current, __func__)); /* #875 */ val_decref(root); return make_str(buf); } @@ -3000,7 +3024,7 @@ 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)arg->data.num; + int id = (int)eigs_num_arg(arg, __func__); static const char *names[] = { "NUM", "STR", "IDENT", "IS", "OF", "DEFINE", "AS", @@ -3035,8 +3059,9 @@ Value* builtin_token_name(Value *arg) { * null if the line argument isn't a number. Phase 4 backward-state query; * 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)arg->data.num); + Value *d = trace_state_at((int)eigs_num_arg(arg, __func__)); return d ? d : make_null(); } @@ -3050,10 +3075,10 @@ Value* builtin_state_at(Value *arg) { * loop runs over the longer operand.) */ Value* builtin_secure_equals(Value *arg) { STRICT_LIST_MAX(arg, 2, "secure_equals"); - ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "secure_equals", "[string, string]", make_num(0)); + ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "secure_equals", "[string, string]", make_bool(0)); Value *a = arg->data.list.items[0]; Value *b = arg->data.list.items[1]; - ARG_GUARD(!a || !b || a->type != VAL_STR || b->type != VAL_STR, "secure_equals", "two strings", make_num(0)); + ARG_GUARD(!a || !b || a->type != VAL_STR || b->type != VAL_STR, "secure_equals", "two strings", make_bool(0)); const char *sa = a->data.str ? a->data.str : ""; const char *sb = b->data.str ? b->data.str : ""; size_t la = strlen(sa), lb = strlen(sb); @@ -3065,7 +3090,7 @@ Value* builtin_secure_equals(Value *arg) { unsigned char cb = i < lb ? (unsigned char)sb[i] : 0; diff |= (unsigned char)(ca ^ cb); } - return make_num(diff == 0 ? 1.0 : 0.0); + return make_bool(diff == 0); } /* ==== BUILTIN: http_request_headers ==== */ @@ -3086,7 +3111,7 @@ Value* builtin_chr(Value *arg) { rt_error(EK_TYPE, 0, "chr requires a number"); return make_null(); } - double num = arg->data.num; + double num = eigs_num_arg(arg, __func__); if (num != (double)(long long)num || 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)"); @@ -3111,12 +3136,13 @@ Value* builtin_hex(Value *arg) { double num; long long width = 0; if (arg && arg->type == VAL_NUM) { - num = arg->data.num; + num = eigs_num_arg(arg, __func__); } else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2 && arg->data.list.items[0] && arg->data.list.items[0]->type == VAL_NUM && arg->data.list.items[1] && arg->data.list.items[1]->type == VAL_NUM) { - num = arg->data.list.items[0]->data.num; - width = (long long)arg->data.list.items[1]->data.num; + num = eigs_list_num(arg, 0, __func__); + width = (long long)eigs_list_num(arg, 1, __func__); + if (g_has_error) return make_null(); } else { rt_error(EK_TYPE, 0, "hex requires a number or [number, nibbles]"); return make_null(); @@ -3157,12 +3183,12 @@ Value* builtin_try_parse(Value *arg) { * documented answer. Phase A's writeup used `try_parse of "!!!"` as its * example of a 0 that is an answer — that is the string path, which is * untouched here. Handing `try_parse` a number is not that. */ - ARG_GUARD(!arg || arg->type != VAL_STR, "try_parse", "a string", make_num(0)); + ARG_GUARD(!arg || arg->type != VAL_STR, "try_parse", "a string", make_bool(0)); const char *src = arg->data.str; /* fs:ANSWER empty source is not valid EigenScript syntax; 0 is the * documented result ("1 if valid, 0 if not"), the same channel a * syntactically bad program uses. */ - if (!src || !src[0]) return make_num(0); + if (!src || !src[0]) return make_bool(0); /* Suppress stderr during parse attempt (needs fd plumbing — parse * diagnostics print unsuppressed in the freestanding profile) */ @@ -3194,7 +3220,7 @@ Value* builtin_try_parse(Value *arg) { /* make_node zero-initializes children, so free_ast walks partial * parses safely (NULL children are no-ops). */ free_ast(ast); - return make_num(valid); + return make_bool(valid); } /* ==== BUILTIN: eval ==== */ @@ -3271,12 +3297,12 @@ 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)rev->data.num != EIGS_BYTECODE_ABI) { + if ((int)eigs_num_arg(rev, __func__) != 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)rev->data.num, EIGS_BYTECODE_ABI); + (int)eigs_num_arg(rev, __func__), EIGS_BYTECODE_ABI); return buf; } return NULL; @@ -3376,6 +3402,7 @@ static Value *desc_isolate_const(DescVerify *ctx, Value *v) { case VAL_STR: case VAL_NULL: case VAL_JSON_RAW: + case VAL_BOOL: val_incref(v); return v; case VAL_BUFFER: { @@ -3480,8 +3507,14 @@ static EigsChunk *vm_build_chunk_desc_body(Value *desc, int off, int sandbox_mod for (int i = 0; i < code->data.list.count; i++) { Value *b = code->data.list.items[i]; - int byte = (b && b->type == VAL_NUM) ? ((int)b->data.num & 0xFF) : 0; - chunk_emit(chunk, (uint8_t)byte, 1); + /* #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) { + 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); } /* 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 @@ -3527,8 +3560,16 @@ static EigsChunk *vm_build_chunk_desc_body(Value *desc, int off, int sandbox_mod } } + /* #1637: a bool param_count is refused rather than read as 0. Any other + * non-number keeps its documented placeholder meaning, 0 parameters + * (producers pass [] there; test_vm_run_bytecode's #1030 rows). */ + if (n >= 4 && d[3] && d[3]->type == VAL_BOOL) { + chunk_free(chunk); + 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)d[3]->data.num : 0; + ? (int)eigs_num_arg(d[3], __func__) : 0; chunk->param_count = param_count; chunk->first_default = param_count; /* no defaults */ @@ -3834,7 +3875,7 @@ static Value *sandbox_finish_run(Value *out) { * builtins are shadowed by a blocked stub, and loops are capped at * max_iterations (default 1,000,000). A separate cumulative instruction * limit bounds VM work, but does not bound time inside a native builtin. Runtime errors - * are caught (not propagated). Returns {"ok": 1/0, "result": value} — the graded + * are caught (not propagated). Returns {"ok": true/false, "result": value} — the graded * "does it run?" rung for a self-hosted compiler validating generated code. */ Value* builtin_sandbox_run(Value *arg) { STRICT_LIST_MAX(arg, 4, "sandbox_run"); @@ -3848,15 +3889,15 @@ Value* builtin_sandbox_run(Value *arg) { int max_iter = 1000000; if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2 && arg->data.list.items[1] && arg->data.list.items[1]->type == VAL_NUM) - max_iter = (int)arg->data.list.items[1]->data.num; + max_iter = (int)eigs_list_num(arg, 1, __func__); /* #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 && arg->data.list.items[2] && arg->data.list.items[2]->type == VAL_NUM && - arg->data.list.items[2]->data.num > 0) - max_bytes = (size_t)arg->data.list.items[2]->data.num; + eigs_list_num(arg, 2, __func__) > 0) + max_bytes = (size_t)eigs_list_num(arg, 2, __func__); /* 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 @@ -3864,7 +3905,7 @@ Value* builtin_sandbox_run(Value *arg) { uint64_t max_work = UINT64_C(10000000); if (arg && arg->type == VAL_LIST && arg->data.list.count >= 4 && arg->data.list.items[3] && arg->data.list.items[3]->type == VAL_NUM) { - double requested = arg->data.list.items[3]->data.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 && @@ -3898,9 +3939,7 @@ Value* builtin_sandbox_run(Value *arg) { * rejecting the graph. Restore the caller's scope before constructing * the host-owned diagnostic dictionary, then release this run's names. */ env_intern_scope_end(intern_scope, saved_intern_scope); - Value *zero = make_num(0); - dict_set(out, "ok", zero); /* dict_set increfs; drop our ref */ - val_decref(zero); + dict_set(out, "ok", make_bool(0)); /* #1637: ok is a bool */ /* #406: surface the build failure structurally — a descriptor * that doesn't verify is a bad argument value, same shape as a * runtime failure's error field. */ @@ -4153,9 +4192,7 @@ Value* builtin_sandbox_run(Value *arg) { dict_set_owned(out, "error", ev); } - Value *okv = make_num((double)ok); - dict_set(out, "ok", okv); /* dict_set increfs; drop our ref */ - val_decref(okv); + dict_set(out, "ok", make_bool(ok != 0)); /* #1637: ok is a bool (immortal) */ if (result) { dict_set(out, "result", result); val_decref(result); } chunk_free(chunk); env_decref(sbox); @@ -4183,7 +4220,7 @@ Value* builtin_record_history(Value *arg) { return make_null(); } int prev = g_trace_hist; - int on = (arg->data.num != 0.0) ? 1 : 0; + int on = (eigs_num_arg(arg, __func__) != 0.0) ? 1 : 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); } @@ -4221,7 +4258,7 @@ Value* builtin_copy_into(Value *arg) { offv ? val_type_name(offv->type) : "null"); return make_null(); } - int offset = (int)offv->data.num; + int offset = (int)eigs_num_arg(offv, __func__); 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 */ @@ -4238,7 +4275,7 @@ Value* builtin_copy_into(Value *arg) { it ? val_type_name(it->type) : "null", i); return make_null(); } - dest->data.buffer.data[offset + i] = it->data.num; + dest->data.buffer.data[offset + i] = eigs_num_arg(it, __func__); } return dest; /* borrowed, like the list path below */ } @@ -4281,11 +4318,13 @@ Value* builtin_list_slice(Value *arg) { if (!list || list->type != VAL_LIST) return make_null(); Value *start_v = arg->data.list.items[1]; Value *end_v = arg->data.list.items[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) return make_null(); int n = list->data.list.count; - int start = (int)start_v->data.num; - int end = (int)end_v->data.num; + int start = (int)eigs_num_arg(start_v, __func__); + int end = (int)eigs_num_arg(end_v, __func__); if (start < 0) start += n; if (end < 0) end += n; if (start < 0) start = 0; @@ -4306,8 +4345,9 @@ Value* builtin_list_slice(Value *arg) { /* num_copy of val → fresh heap-allocated copy of a numeric Value. * Use to extract a scalar from arena before arena_reset. */ Value* builtin_num_copy(Value *arg) { + BOOL_REFUSE(arg, "num_copy"); if (!arg || arg->type != VAL_NUM) return make_null(); - return make_num_permanent(arg->data.num); + return make_num_permanent(eigs_num_arg(arg, __func__)); } /* ==== BUILTIN: concat ==== */ @@ -4342,7 +4382,7 @@ Value* builtin_range(Value *arg) { if (arg->type == VAL_NUM) { /* range of n */ - end = (int)arg->data.num; + end = (int)eigs_num_arg(arg, __func__); } else if (arg->type == VAL_LIST) { int argc = arg->data.list.count; /* #497: every provided bound must be numeric. A non-number used to @@ -4357,13 +4397,13 @@ Value* builtin_range(Value *arg) { } } if (argc >= 1) - start = (int)arg->data.list.items[0]->data.num; + start = (int)eigs_list_num(arg, 0, __func__); if (argc >= 2) - end = (int)arg->data.list.items[1]->data.num; + end = (int)eigs_list_num(arg, 1, __func__); else { end = start; start = 0; } /* single-element list: treat as range of n */ if (argc >= 3) { - step = (int)arg->data.list.items[2]->data.num; + step = (int)eigs_list_num(arg, 2, __func__); /* 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 @@ -4426,7 +4466,9 @@ Value* builtin_fill(Value *arg) { rt_error(EK_TYPE, 0, "fill requires [count, value]"); return make_list(0); } - int count = (int)arg->data.list.items[0]->data.num; + BOOL_REFUSE(arg->data.list.items[0], "fill"); /* the count; the value may be anything */ + int count = (int)eigs_list_num(arg, 0, __func__); + if (g_has_error) return make_null(); /* #1637: a non-number slot raised */ Value *val = arg->data.list.items[1]; if (count < 0) count = 0; if (count > 10000000) count = 10000000; /* 10M cap */ @@ -4451,11 +4493,11 @@ static int at_index(Value *idx_val, int count, const char *what, rt_error(EK_VALUE, 0, "%s index must be an integer", what); return 0; } - int idx = (int)idx_val->data.num; + int idx = (int)eigs_num_arg(idx_val, __func__); 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)idx_val->data.num, count); + (int)eigs_num_arg(idx_val, __func__), count); return 0; } *out = idx; @@ -4471,11 +4513,11 @@ static int buf_at_index(Value *idx_val, int count, int *out) { val_type_name(idx_val ? idx_val->type : VAL_NULL)); return 0; } - int idx = (int)idx_val->data.num; + int idx = (int)eigs_num_arg(idx_val, __func__); if (idx < 0) idx += count; if (idx < 0 || idx >= count) { rt_error(EK_INDEX, 0, "buffer index %d out of range (length %d)", - (int)idx_val->data.num, count); + (int)eigs_num_arg(idx_val, __func__), count); return 0; } *out = idx; @@ -4521,7 +4563,7 @@ Value* builtin_set_at(Value *arg) { val_type_name(val ? val->type : VAL_NULL)); return make_null(); } - buf->data.buffer.data[off] = val->data.num; + buf->data.buffer.data[off] = eigs_num_arg(val, __func__); /* 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`. */ @@ -4765,7 +4807,7 @@ static void *thread_entry(void *arg) { bin_arg = l; } int consumes_arg = (fn->data.builtin == builtin_free_val); - Value *result = fn->data.builtin(bin_arg); + Value *result = eigs_call_builtin(fn->data.builtin, bin_arg); /* #720: this site owns bin_arg (increfed above, or freshly built) * and drops it below, so it runs the VM's own contract — * caller_owns_arg=1, and `result == bin_arg` transfers rather than @@ -5021,8 +5063,8 @@ Value* builtin_thread_join(Value *arg) { rt_error(EK_TYPE, 0, "thread_join requires a thread handle"); return make_null(); } - int hid = (int)hv->data.num; - uint32_t hgen = (gv && gv->type == VAL_NUM) ? (uint32_t)gv->data.num : 0; + 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 why = HANDLE_CLAIM_GONE; ThreadHandle *h = (ThreadHandle*)handle_claim(hid, hgen, HANDLE_THREAD, &why); if (!h) { @@ -5103,9 +5145,9 @@ static Channel* get_channel_why(Value *v, int *why, int *out_id) { if (!v || v->type != VAL_DICT) return NULL; Value *cv = dict_get(v, "_channel_id"); if (!cv || cv->type != VAL_NUM) return NULL; - *out_id = (int)cv->data.num; + *out_id = (int)eigs_num_arg(cv, __func__); Value *gv = dict_get(v, "_channel_gen"); - uint32_t gen = (gv && gv->type == VAL_NUM) ? (uint32_t)gv->data.num : 0; + 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); } @@ -5277,7 +5319,7 @@ Value* builtin_recv_timeout(Value *arg) { rt_error(EK_TYPE, 0, "recv_timeout: ms must be a number"); return make_null(); } - double ms = ms_v->data.num; + 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 @@ -5332,9 +5374,9 @@ Value* builtin_channel_closed(Value *arg) { * channel". The second is the documented answer (a reclaimed channel is * closed); the first is a type mistake reading as closed. Split. */ int not_a_handle = !arg || arg->type != VAL_DICT || !dict_get(arg, "_channel_id"); - ARG_GUARD(not_a_handle, "channel_closed", "a channel", make_num(1)); + ARG_GUARD(not_a_handle, "channel_closed", "a channel", make_bool(1)); Channel *ch = get_channel(arg); - if (!ch) return make_num(1); /* fs:ANSWER an unknown/reclaimed channel is closed */ + if (!ch) return make_bool(1); /* fs:ANSWER an unknown/reclaimed channel is closed */ /* Read ch->closed under the mutex: close_channel writes it while holding * the lock, so a bare read here is a data race (caught by the #401 TSan * gate — it fired in CI where two workers polled channel_closed against a @@ -5342,7 +5384,7 @@ Value* builtin_channel_closed(Value *arg) { pthread_mutex_lock(&ch->mutex); int closed = ch->closed; pthread_mutex_unlock(&ch->mutex); - return make_num(closed ? 1 : 0); + return make_bool(closed); } /* ==== #408 cooperative task layer (increment 1a: spawn + alive) ==== @@ -5469,7 +5511,7 @@ 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(arg->data.num, &id, &gen)) return NULL; + !task_handle_unpack(eigs_num_arg(arg, __func__), &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)) @@ -5571,7 +5613,7 @@ Value* builtin_task_join(Value *arg) { * dropped. Never blocks (the mailbox is unbounded in v1). */ Value* builtin_task_send(Value *arg) { STRICT_LIST_MAX(arg, 2, "task_send"); - ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "task_send", "[id, value]", make_num(0)); + ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "task_send", "[id, value]", make_bool(0)); Value *idv = arg->data.list.items[0]; /* The type guard precedes the scheduler-state check deliberately. With the * state check first, `task_send of ["x", 1]` outside a task context @@ -5579,20 +5621,20 @@ 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_num(0)); + ARG_GUARD(!idv || idv->type != VAL_NUM, "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_num(0); + if (!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 (idv->data.num != 0 && - !task_handle_resolve(idv, "task_send", &target)) return make_num(0); + if (eigs_num_arg(idv, __func__) != 0 && + !task_handle_resolve(idv, "task_send", &target)) return make_bool(0); Value *copy = val_clone_for_send(arg->data.list.items[1]); /* share-nothing */ int sent = task_deliver(target, copy); if (!sent) val_decref(copy); /* dropped to a dead task — release the copy */ - return make_num(sent ? 1 : 0); + return make_bool(sent); } /* task_recv of null — return the next message from this task's mailbox, or @@ -5628,28 +5670,28 @@ Value* builtin_task_try_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_num(0)); + ARG_GUARD(!arg || arg->type != VAL_NUM, "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_num(0); + if (!g_task_sched) return make_bool(0); int target = 0; - if (!task_handle_resolve(arg, "task_kill", &target)) return make_num(0); - return make_num(task_do_kill(target) ? 1 : 0); + if (!task_handle_resolve(arg, "task_kill", &target)) return make_bool(0); + return make_bool(task_do_kill(target)); } /* task_alive of id → 1 while the task is READY/RUNNING/SUSPENDED, else 0 * (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_num(0)); + ARG_GUARD(!arg || arg->type != VAL_NUM, "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_num(0)` is a type + * answer. Four lines above, an identical `return make_bool(0)` is a type * guard that DID convert — the pair Phase A pinned as the reason this * phase is read by hand and not swept. Do not convert this one. */ - if (!t) return make_num(0); + if (!t) return make_bool(0); int alive = (t->state == TASK_READY || t->state == TASK_RUNNING || t->state == TASK_SUSPENDED); - return make_num(alive ? 1 : 0); + return make_bool(alive); } /* task_sleep of ticks — suspend the current task until the virtual clock @@ -5666,13 +5708,15 @@ Value* builtin_task_sleep(Value *arg) { "(arena_mark…arena_reset)"); return make_null(); } - if (!g_task_sched) return make_null(); /* no scheduler: nothing to wait for */ + /* #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) { 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 */ if (no_yield_forbidden("task_sleep")) return make_null(); /* #488 */ - task_request_sleep(arg->data.num); + task_request_sleep(eigs_num_arg(arg, __func__)); return make_null(); /* placeholder: execution resumes when the clock wakes it */ } @@ -5745,7 +5789,7 @@ Value* builtin_task_sched_seed(Value *arg) { rt_error(EK_TYPE, 0, "task_sched_seed requires a number (the seed)"); return make_null(); } - task_sched_set_seed(arg->data.num); + task_sched_set_seed(eigs_num_arg(arg, __func__)); return make_null(); } @@ -5764,7 +5808,7 @@ Value* builtin_task_sched_trace(Value *arg) { rt_error(EK_TYPE, 0, "task_sched_trace takes null (read), 1 (arm) or 0 (disarm + clear)"); return make_null(); } - if (arg->data.num != 0) { + if (eigs_num_arg(arg, __func__) != 0) { g_task_trace_on = 1; } else { g_task_trace_on = 0; @@ -5917,11 +5961,13 @@ Value* builtin_nearest_in_range(Value *arg) { } Value *entities = arg->data.list.items[0]; if (!entities || entities->type != VAL_LIST) return make_null(); - double px = arg->data.list.items[1]->data.num; - double py = arg->data.list.items[2]->data.num; - double range = arg->data.list.items[3]->data.num; - double ww = arg->data.list.items[4]->data.num; - double wh = arg->data.list.items[5]->data.num; + 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__); + 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 && arg->data.list.items[6]->type == VAL_STR) px_key = arg->data.list.items[6]->data.str; @@ -5974,7 +6020,10 @@ Value* builtin_nearest_in_range(Value *arg) { if (idx >= 0 && hint_active < 0) hint_active = idx; av = (idx >= 0) ? vals[idx] : NULL; } - if (av && av->type == VAL_NUM && av->data.num != 1.0) continue; + /* #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) + || (av->type == VAL_BOOL && !av->data.boolean))) continue; Value *ex; if (hint_px >= 0 && hint_px < kcount && keys[hint_px] == ipx) { @@ -5994,10 +6043,16 @@ Value* builtin_nearest_in_range(Value *arg) { ey = (idx >= 0) ? vals[idx] : NULL; } + /* #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"); + return make_null(); + } if (!ex || !ey || ex->type != VAL_NUM || ey->type != VAL_NUM) continue; - double dx = ex->data.num - px; - double dy = ey->data.num - py; + double dx = eigs_num_arg(ex, __func__) - px; + double dy = eigs_num_arg(ey, __func__) - py; 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; @@ -6038,9 +6093,11 @@ Value* builtin_nearest_in_range_all(Value *arg) { } Value *entities = arg->data.list.items[0]; if (!entities || entities->type != VAL_LIST) return make_null(); - double range = arg->data.list.items[1]->data.num; - double ww = arg->data.list.items[2]->data.num; - double wh = arg->data.list.items[3]->data.num; + 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__); + 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 && arg->data.list.items[4]->type == VAL_STR) px_key = arg->data.list.items[4]->data.str; @@ -6094,7 +6151,8 @@ 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 && av->data.num != 1.0) ? 0 : 1; + active_arr[i] = (av && ((av->type == VAL_NUM && eigs_num_arg(av, __func__) != 1.0) + || (av->type == VAL_BOOL && !av->data.boolean))) ? 0 : 1; /* #1637 */ Value *ex; if (hint_px >= 0 && hint_px < kcount && keys[hint_px] == ipx) { @@ -6112,14 +6170,22 @@ Value* builtin_nearest_in_range_all(Value *arg) { if (idx >= 0 && hint_py < 0) hint_py = idx; ey = (idx >= 0) ? vals[idx] : 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"); + break; + } if (!ex || !ey || ex->type != VAL_NUM || ey->type != VAL_NUM) { valid_arr[i] = 0; continue; } - px_arr[i] = ex->data.num; - py_arr[i] = ey->data.num; + px_arr[i] = eigs_num_arg(ex, __func__); + py_arr[i] = eigs_num_arg(ey, __func__); valid_arr[i] = 1; } + if (g_has_error) { /* #1637 round 4: a bool coordinate raised above */ + free(px_arr); free(py_arr); free(active_arr); free(valid_arr); + return make_null(); + } Value *result = make_list(n); @@ -6184,15 +6250,15 @@ Value* builtin_sign_extend(Value *arg) { /* 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 `.data.num` off items that were never checked, so + * 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. */ Value *v0 = arg->data.list.items[0], *v1 = arg->data.list.items[1]; ARG_GUARD(!v0 || v0->type != VAL_NUM || !v1 || v1->type != VAL_NUM, "sign_extend", "two numbers", make_num(0)); - double val = v0->data.num; - int bits = (int)v1->data.num; + 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) @@ -6218,7 +6284,7 @@ Value* builtin_list_truncate(Value *arg) { rt_error(EK_TYPE, 0, "list_truncate: new_len must be a number"); return make_null(); } - int new_len = (int)len_val->data.num; + int new_len = (int)eigs_num_arg(len_val, __func__); /* #503: a negative new_len used to silently empty the list (an * undocumented soft clamp to 0). Raise instead. */ if (new_len < 0) { @@ -6242,8 +6308,10 @@ Value* builtin_list_remove_at(Value *arg) { Value *list = arg->data.list.items[0]; Value *idx_val = arg->data.list.items[1]; if (!list || list->type != VAL_LIST) return make_null(); - if (!idx_val || idx_val->type != VAL_NUM) return list; - int idx = (int)idx_val->data.num; + /* #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__); if (idx < 0 || idx >= list->data.list.count) return list; val_decref(list->data.list.items[idx]); int tail = list->data.list.count - idx - 1; @@ -6264,8 +6332,10 @@ Value* builtin_list_insert_at(Value *arg) { Value *idx_val = arg->data.list.items[1]; Value *val = arg->data.list.items[2]; if (!list || list->type != VAL_LIST) return make_null(); - if (!idx_val || idx_val->type != VAL_NUM) return list; - int idx = (int)idx_val->data.num; + /* #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__); int count = list->data.list.count; if (idx < 0 || idx > count) return list; if (idx == count) { @@ -6313,7 +6383,8 @@ Value* builtin_list_index_of(Value *arg) { ARG_GUARD(!list || list->type != VAL_LIST, "list_index_of", "a list as its first argument", make_num(-1)); for (int i = 0; i < list->data.list.count; i++) { - int equal = values_equal(list->data.list.items[i], needle); + /* #1637: the `==` comparison, raising on bool vs num like `==`. */ + int equal = values_equal_op(list->data.list.items[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); @@ -6323,24 +6394,25 @@ Value* builtin_list_index_of(Value *arg) { return make_num(-1); } -/* list_contains of [list, value] — 1 if any element structurally equals - * value (same values_equal scan as list_index_of), else 0. Bad args give 0, +/* list_contains of [list, value] — true if any element structurally equals + * value (same scan as list_index_of), else false. Bad args give false, * mirroring contains. */ Value* builtin_list_contains(Value *arg) { STRICT_LIST_MAX(arg, 2, "list_contains"); - ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "list_contains", "[list, value]", make_num(0)); + ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, "list_contains", "[list, value]", make_bool(0)); Value *list = arg->data.list.items[0]; Value *needle = arg->data.list.items[1]; - ARG_GUARD(!list || list->type != VAL_LIST, "list_contains", "a list as its first argument", make_num(0)); + ARG_GUARD(!list || list->type != VAL_LIST, "list_contains", "a list as its first argument", make_bool(0)); for (int i = 0; i < list->data.list.count; i++) { - int equal = values_equal(list->data.list.items[i], needle); + /* #1637: the `==` comparison, raising on bool vs num like `==`. */ + int equal = values_equal_op(list->data.list.items[i], needle, "list_contains"); /* #1417: structural buffer comparison can raise on a strict NaN. */ if (g_has_error) return make_null(); - if (equal) return make_num(1); + if (equal) return make_bool(1); } /* fs:ANSWER the scan completed and found nothing; 0 is the predicate's * answer, not a stand-in for a rejected argument. */ - return make_num(0); + return make_bool(0); } /* sort_by of [list, key_fn] — sort list by numeric keys from key_fn. @@ -6389,7 +6461,7 @@ Value* builtin_sort_by(Value *arg) { "gave %s)", i, kt); return make_null(); } - pairs[i].key = kv->data.num; + pairs[i].key = eigs_num_arg(kv, __func__); pairs[i].index = i; val_decref(kv); } @@ -6408,7 +6480,7 @@ 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 = (*(Value**)a)->data.num, db = (*(Value**)b)->data.num; + double da = eigs_num_arg((*(Value**)a), __func__), db = eigs_num_arg((*(Value**)b), __func__); return (da > db) - (da < db); } @@ -6458,10 +6530,10 @@ Value* builtin_dispatch(Value *arg) { rt_error(EK_TYPE, 0, "dispatch: key must be a number"); return make_null(); } - int key = (int)key_v->data.num; - if ((double)key != key_v->data.num) { + int key = (int)eigs_num_arg(key_v, __func__); + if ((double)key != eigs_num_arg(key_v, __func__)) { rt_error(EK_VALUE, 0, "dispatch key must be an integer, got %g", - key_v->data.num); + eigs_num_arg(key_v, __func__)); return make_null(); } @@ -6483,10 +6555,10 @@ Value* builtin_dispatch(Value *arg) { * making rather than the arg vector's (#720). */ if (fn->data.builtin == builtin_free_val) { if (fn_arg) val_incref(fn_arg); - Value *consumed = fn->data.builtin(fn_arg); + Value *consumed = eigs_call_builtin(fn->data.builtin, fn_arg); return consumed ? consumed : make_null(); } - Value *result = fn->data.builtin(fn_arg); + Value *result = eigs_call_builtin(fn->data.builtin, fn_arg); if (!result) return make_null(); /* #720: the inner builtin may return a borrow of fn_arg, which is a * *grandchild* of our own arg vector — one level deeper than any @@ -6586,6 +6658,142 @@ static Value* builtin_borrow_guard_selftest(Value *arg) { } #endif +/* ==== #1637: the builtin-call bool gate (eigs_bool_gate, eigenscript.h) ==== + * k_bool_policy is THE reviewed list of where a core builtin takes a bool. + * A position not declared here refuses a bool before the builtin runs, in + * every strict mode -- so a bool never reaches a builtin's own soft paths + * (`add of true` answering null, `exit of true` exiting 0, `pi of true` + * ignoring it). Positions: BA_ARG is the argument itself (`f of true`), + * 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. + * 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). */ +#define BA_ARG 0x001u +#define BA_POS(i) (1u << ((i) + 1)) +#define BA_ELEMS 0x1FEu /* every scanned list position */ +#define BA_ANY (BA_ARG | BA_ELEMS) +static const struct { const char *name; unsigned mask; const char *why; } k_bool_policy[] = { + {"print", BA_ANY, "renders any value"}, + {"write", BA_ANY, "renders any value"}, + {"str", BA_ANY, "converts any value to text (\"true\"/\"false\")"}, + {"type", BA_ANY, "names any value's type"}, + {"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"}, + {"report", BA_ANY, "the bytecode twin of OP_REPORT_NAME, which reports any value"}, + {"free_val", BA_ANY, "releases any value"}, + {"coalesce", BA_ANY, "picks the first non-null of any values"}, + {"len", BA_ELEMS, "counts a list's elements, whatever they are"}, + {"assert", BA_ARG | BA_POS(0), "the condition is a bool"}, + {"append", BA_POS(1), "a list element may be any value"}, + {"set_at", BA_POS(2), "a list element may be any value"}, + {"list_insert_at", BA_POS(2), "a list element may be any value"}, + {"fill", BA_POS(1), "a list element may be any value"}, + {"dict_set", BA_POS(2), "a dict value may be any value"}, + {"list_index_of", BA_POS(1), "searches a list for any value"}, + {"list_contains", BA_POS(1), "searches a list for any value"}, + {"json_build", BA_POS(1) | BA_POS(3) | BA_POS(5) | BA_POS(7), "JSON values may be true/false (keys may not)"}, + {"send", BA_POS(1), "a channel carries any value"}, + {"task_send", BA_POS(1), "a mailbox carries any value"}, + {"spawn", BA_ELEMS & ~BA_POS(0), "arguments for the spawned function"}, + {"task_spawn", BA_ELEMS & ~BA_POS(0), "arguments for the spawned task"}, + {"dispatch", BA_POS(2), "the argument for the dispatched function"}, + {"write_bytes", BA_POS(2), "the append flag"}, + {"shared_set", BA_POS(1), "a shared value may be any value"}, + {"db_query_value", BA_ELEMS & ~BA_POS(0), "SQL parameters: a bool binds as an SQL boolean"}, + {"db_execute", BA_ELEMS & ~BA_POS(0), "SQL parameters: a bool binds as an SQL boolean"}, + {"db_query_json", BA_ELEMS & ~BA_POS(0), "SQL parameters: a bool binds as an SQL boolean"}, +}; + +/* The policy rows resolved to function pointers, once, from the running + * state's pristine builtin layer: ~30 env lookups, no allocation (round 6: + * the round-4 lazy table build qsort'ed ~350 entries, and the free of + * qsort's work buffer made glibc consolidate every fastbin of a hot loop's + * garbage -- +7% instructions on bench/unobserved_loop, whose only bool is + * the final `print of (x > 0)`). */ +#define BOOL_POLICY_N (sizeof k_bool_policy / sizeof k_bool_policy[0]) +static BuiltinFn g_bool_policy_fn[BOOL_POLICY_N]; +static int g_bool_policy_ready; /* release-published after the fill */ +static pthread_mutex_t g_bool_gate_mu = PTHREAD_MUTEX_INITIALIZER; + +/* Host functions (eigs_register_function) are not gated; neither are + * compiled natives (eigs_native_fn_name) nor the sandbox's blocked stub. */ +static BuiltinFn g_bool_gate_host[64]; +static int g_bool_gate_host_n; /* under g_bool_gate_mu */ +static int g_bool_gate_host_overflow; + +void eigs_bool_gate_exempt(BuiltinFn fn) { + pthread_mutex_lock(&g_bool_gate_mu); + int seen = 0; + for (int i = 0; i < g_bool_gate_host_n; i++) if (g_bool_gate_host[i] == fn) seen = 1; + if (!seen) { + if (g_bool_gate_host_n < (int)(sizeof g_bool_gate_host / sizeof g_bool_gate_host[0])) + g_bool_gate_host[g_bool_gate_host_n++] = fn; + else g_bool_gate_host_overflow = 1; /* past 64 hosts: gate no host fn */ + } + pthread_mutex_unlock(&g_bool_gate_mu); +} + +static int bool_gate_is_host(BuiltinFn fn) { + if (fn == builtin_sandbox_blocked || eigs_native_fn_name(fn)) return 1; + pthread_mutex_lock(&g_bool_gate_mu); + int host = g_bool_gate_host_overflow; + for (int i = 0; !host && i < g_bool_gate_host_n; i++) host = (g_bool_gate_host[i] == fn); + pthread_mutex_unlock(&g_bool_gate_mu); + return host; +} + +/* The builtin's script name, for the error only (off every success path). */ +static const char *bool_gate_name(BuiltinFn fn) { + Env *env = g_builtin_env; + for (int i = 0; env && i < env->count; i++) { + if (!env->names[i] || !slot_is_ptr(env->values[i])) continue; + Value *v = slot_as_ptr(env->values[i]); + if (v && v->type == VAL_BUILTIN && v->data.builtin == fn) return env->names[i]; + } + return "builtin"; +} + +int eigs_bool_gate_slow(BuiltinFn fn, const Value *arg) { + if (!__atomic_load_n(&g_bool_policy_ready, __ATOMIC_ACQUIRE)) { + Env *env = g_builtin_env; + if (!env) return 0; /* no builtin layer: not a script call */ + pthread_mutex_lock(&g_bool_gate_mu); + if (!g_bool_policy_ready) { + for (size_t k = 0; k < BOOL_POLICY_N; k++) { + Value *v = env_get(env, k_bool_policy[k].name); + g_bool_policy_fn[k] = (v && v->type == VAL_BUILTIN) ? v->data.builtin : NULL; + } + __atomic_store_n(&g_bool_policy_ready, 1, __ATOMIC_RELEASE); + } + pthread_mutex_unlock(&g_bool_gate_mu); + } + unsigned mask = 0; + int listed = 0; + for (size_t k = 0; k < BOOL_POLICY_N; k++) + if (g_bool_policy_fn[k] == fn) { mask |= k_bool_policy[k].mask; listed = 1; } + if (!listed && bool_gate_is_host(fn)) return 0; + if (arg->type == VAL_BOOL) { + if (mask & BA_ARG) return 0; + rt_error(EK_TYPE, 0, "%s: does not take a bool argument", bool_gate_name(fn)); + return 1; + } + int n = arg->data.list.count < EIGS_BOOL_GATE_SCAN ? arg->data.list.count + : EIGS_BOOL_GATE_SCAN; + for (int i = 0; i < n; i++) { + const Value *it = arg->data.list.items[i]; + if (!it || it->type != VAL_BOOL || (mask & BA_POS(i))) continue; + const char *nm = bool_gate_name(fn); + rt_error(EK_TYPE, 0, "%s: argument %d is a bool, which %s does not take there", + nm, i + 1, nm); + return 1; + } + return 0; +} + void register_builtins(Env *env) { /* ---- Core language builtins (always available) ---- */ env_set_local_owned(env, "print", make_builtin(builtin_print)); @@ -6852,6 +7060,7 @@ void register_builtins(Env *env) { * race under TSan (#1137). Relaxed atomics: the value is identical from * every writer and the reader only needs a consistent int. */ __atomic_store_n(&g_builtin_binding_count, env->count, __ATOMIC_RELAXED); + } /* #1388: module code resolves builtin names against a PRISTINE layer, not the diff --git a/src/builtins_buf.c b/src/builtins_buf.c index 8e8893ac..8ab47dd9 100644 --- a/src/builtins_buf.c +++ b/src/builtins_buf.c @@ -34,8 +34,8 @@ Value* builtin_buffer(Value *arg) { if (arg && arg->type == VAL_LIST && arg->data.list.count == 2 && arg->data.list.items[0]->type == VAL_NUM && arg->data.list.items[1]->type == VAL_NUM) { - int r = (int)arg->data.list.items[0]->data.num; - int c = (int)arg->data.list.items[1]->data.num; + int r = (int)eigs_list_num(arg, 0, __func__); + int c = (int)eigs_list_num(arg, 1, __func__); if (r < 0) r = 0; if (c < 0) c = 0; long total = (long)r * (long)c; @@ -54,7 +54,7 @@ Value* builtin_buffer(Value *arg) { /* #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)arg->data.num; + if (arg && arg->type == VAL_NUM) count = (int)eigs_num_arg(arg, __func__); if (count < 0) count = 0; if (count > 10000000) count = 10000000; if (!sandbox_charge((size_t)count * sizeof(double))) return make_null(); /* #292 */ @@ -73,10 +73,11 @@ Value* builtin_reshape(Value *arg) { if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) return make_null(); Value *b = arg->data.list.items[0]; if (b->type != VAL_BUFFER) return make_null(); + BOOL_REFUSE(arg, "reshape"); /* rows/cols; the buffer is not a bool */ if (arg->data.list.items[1]->type != VAL_NUM || arg->data.list.items[2]->type != VAL_NUM) return make_null(); - int r = (int)arg->data.list.items[1]->data.num; - int c = (int)arg->data.list.items[2]->data.num; + int r = (int)eigs_list_num(arg, 1, __func__); + int c = (int)eigs_list_num(arg, 2, __func__); 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))) @@ -108,7 +109,8 @@ Value* builtin_buf_get(Value *arg) { /* fs:CHANNEL the rt_error above already raised */ return make_num(0); } - int idx = (int)arg->data.list.items[1]->data.num; + 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 */ 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); @@ -141,8 +143,9 @@ Value* builtin_buf_set(Value *arg) { rt_error(EK_TYPE, 0, "cannot store %s in a buffer (buffers hold numbers)", val_type_name(arg->data.list.items[2]->type)); return make_null(); } - int idx = (int)arg->data.list.items[1]->data.num; - double val = arg->data.list.items[2]->data.num; + int idx = (int)eigs_list_num(arg, 1, __func__); + double val = eigs_list_num(arg, 2, __func__); + 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); @@ -176,7 +179,7 @@ Value* builtin_buf_from_list(Value *arg) { v->refcount = 1; for (int i = 0; i < n; i++) { if (arg->data.list.items[i]->type == VAL_NUM) { - v->data.buffer.data[i] = arg->data.list.items[i]->data.num; + v->data.buffer.data[i] = eigs_num_arg(arg->data.list.items[i], __func__); } else { /* #1061: a non-number element silently stayed 0.0. */ const char *tn = val_type_name(arg->data.list.items[i]->type); @@ -192,14 +195,19 @@ Value* builtin_buf_from_list(Value *arg) { * conversion stops at its first numeric NUL, so later elements are not part * of that conversion; codec conversion consumes the complete list. */ static int strict_numeric_byte_list(Value *arg, const char *who, int nul_ends) { - if (!g_strict || !arg || arg->type != VAL_LIST) return 1; + if (!arg || arg->type != VAL_LIST) return 1; for (int i = 0; i < arg->data.list.count; i++) { Value *item = arg->data.list.items[i]; - if (!item || item->type != VAL_NUM) { + /* #1637: a bool byte is refused in every strict mode. */ + if (!item || (item->type != VAL_NUM && (g_strict || item->type == VAL_BOOL))) { rt_error(EK_TYPE, 0, "%s: expected numeric byte values", who); return 0; } - if (nul_ends && finite_num_to_byte(item->data.num) == 0) break; + /* 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; } return 1; } @@ -225,7 +233,7 @@ 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 = items ? (items[i] && items[i]->type == VAL_NUM ? items[i]->data.num : 0.0) + double dv = items ? (items[i] && items[i]->type == VAL_NUM ? eigs_num_arg(items[i], __func__) : 0.0) : buffer_read_num(arg, i); if (g_has_error) { free(s); return make_null(); } int b = finite_num_to_byte(dv); @@ -247,7 +255,7 @@ Value* builtin_str_from_bytes(Value *arg) { 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) ? arg->data.num : 0.0; + double d = (arg && arg->type == VAL_NUM) ? eigs_num_arg(arg, __func__) : 0.0; uint64_t bits; memcpy(&bits, &d, sizeof(bits)); Value *list = make_list(8); @@ -266,7 +274,7 @@ Value* builtin_f64_from_bytes(Value *arg) { int n = arg->data.list.count; for (int i = 0; i < 8 && i < n; i++) if (arg->data.list.items[i] && arg->data.list.items[i]->type == VAL_NUM) - bytes_in[i] = arg->data.list.items[i]->data.num; + bytes_in[i] = eigs_num_arg(arg->data.list.items[i], __func__); } else if (arg && arg->type == VAL_BUFFER) { int n = arg->data.buffer.count; for (int i = 0; i < 8 && i < n; i++) { @@ -338,7 +346,7 @@ 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 = items ? (items[i] && items[i]->type == VAL_NUM ? items[i]->data.num : 0.0) + double dv = items ? (items[i] && items[i]->type == VAL_NUM ? eigs_num_arg(items[i], __func__) : 0.0) : buffer_read_num(arg, i); if (g_has_error) { free(b); return 0; } b[i] = finite_num_to_byte(dv); @@ -551,7 +559,7 @@ static int buf_count_arg(const char *who, Value *cnt_val, long long *out) { rt_error(EK_VALUE, 0, "%s: count must be a number", who); return 0; } - long long c = (long long)cnt_val->data.num; + long long c = (long long)eigs_num_arg(cnt_val, __func__); if (c < 0) { rt_error(EK_VALUE, 0, "%s: count must be non-negative (got %lld)", who, c); @@ -567,7 +575,7 @@ static int buf_num_arg(const char *who, const char *what, Value *v, rt_error(EK_VALUE, 0, "%s: %s must be a number", who, what); return 0; } - *out = v->data.num; + *out = eigs_num_arg(v, __func__); return 1; } @@ -584,7 +592,7 @@ static int buf_window_arg(const char *who, Value *buf, Value *off_val, rt_error(EK_VALUE, 0, "%s: offset must be a number", who); return 0; } - long long off = (long long)off_val->data.num; + long long off = (long long)eigs_num_arg(off_val, __func__); long long n = buf->data.buffer.count; if (off < 0 || off > n - count) { rt_error(EK_INDEX, 0, @@ -871,8 +879,8 @@ Value* builtin_buf_deinterleave(Value *arg) { rt_error(EK_VALUE, 0, "buf_deinterleave: channel and nch must be numbers"); return make_null(); } - long long nch = (long long)nch_v->data.num; - long long channel = (long long)ch_v->data.num; + long long nch = (long long)eigs_num_arg(nch_v, __func__); + long long channel = (long long)eigs_num_arg(ch_v, __func__); if (nch < 1) { rt_error(EK_VALUE, 0, "buf_deinterleave: nch must be >= 1 (got %lld)", nch); return make_null(); diff --git a/src/builtins_host.c b/src/builtins_host.c index b2805021..255ef927 100644 --- a/src/builtins_host.c +++ b/src/builtins_host.c @@ -259,19 +259,19 @@ Value* builtin_regex_replace(Value *arg) { Value* builtin_stream_open(Value *arg) { STRICT_LIST_MAX(arg, 2, "stream_open"); ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, - "stream_open", "[path, count]", make_num(0)); + "stream_open", "[path, count]", make_bool(0)); Value *path_val = arg->data.list.items[0]; Value *count_val = arg->data.list.items[1]; ARG_GUARD(!path_val || path_val->type != VAL_STR || !count_val || count_val->type != VAL_NUM, - "stream_open", "[a string path, a number count]", make_num(0)); - if (count_val->data.num < 1 || - count_val->data.num > EIGS_TENSOR_MAX_ELEMENTS || - count_val->data.num != floor(count_val->data.num)) { + "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__))) { rt_error(EK_LIMIT, 0, "stream_open: '%s' count %.17g is outside the 1..%d element cap", - path_val->data.str, count_val->data.num, + path_val->data.str, eigs_num_arg(count_val, __func__), EIGS_TENSOR_MAX_ELEMENTS); - return make_num(0); + return make_bool(0); } if (g_stream_file) { fclose(g_stream_file); g_stream_file = NULL; } g_stream_file = xfopen_write(path_val->data.str, "wb"); @@ -279,40 +279,40 @@ Value* builtin_stream_open(Value *arg) { * reaching here with a NULL FILE* is xfopen_write failing, and the block * 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_num(0); - uint32_t count = (uint32_t)count_val->data.num; + if (!g_stream_file) return make_bool(0); + uint32_t count = (uint32_t)eigs_num_arg(count_val, __func__); 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); g_stream_file = NULL; /* fs:ANSWER a short fwrite(3) of the header — an I/O failure after the * file was already opened; 0 is the failure bit paired with the - * make_num(1) success value below. */ - return make_num(0); + * make_bool(1) success value below. */ + return make_bool(0); } - return make_num(1); + return make_bool(1); } Value* builtin_stream_write(Value *arg) { /* The two halves of the original single condition are separated because * only one of them is about the ARGUMENT: `!g_stream_file` is a stream * state, and strict must not report it as a type error. Both halves still - * answer make_num(0) with strict off, in either order, so the non-strict + * 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_num(0)); + ARG_GUARD(!arg || arg->type != VAL_NUM, "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_num(1) success value below. */ - if (!g_stream_file) return make_num(0); - double val = arg->data.num; + * make_bool(1) success value below. */ + if (!g_stream_file) return make_bool(0); + double val = eigs_num_arg(arg, __func__); if (fwrite(&val, sizeof(double), 1, g_stream_file) != 1) { fclose(g_stream_file); g_stream_file = NULL; /* fs:ANSWER a short fwrite(3) of one float64 — an I/O failure, not an * argument mistake; 0 is this builtin's documented failure bit. */ - return make_num(0); + return make_bool(0); } - return make_num(1); + return make_bool(1); } Value* builtin_stream_close(Value *arg) { @@ -320,10 +320,10 @@ Value* builtin_stream_close(Value *arg) { /* fs:ANSWER stream_close ignores `arg` entirely ((void)arg above), so this * cannot be an argument guard: no stream is open, and 0 is the failure bit * paired with the ok?1:0 value below. */ - if (!g_stream_file) return make_num(0); + if (!g_stream_file) return make_bool(0); int ok = (fclose(g_stream_file) == 0); g_stream_file = NULL; - return make_num(ok ? 1 : 0); + return make_bool(ok); } /* ---- Filesystem ---- */ @@ -337,7 +337,7 @@ Value* builtin_stream_close(Value *arg) { * rule as the proc-star / audio-capture boundary. Its return IS pinnable by * the tape (unlike a proc fd), so it is Recorded, not #148-non-replayable. */ Value* builtin_mkdir(Value *arg) { - ARG_GUARD_TAPED(!arg || arg->type != VAL_STR, "mkdir", "a string path", make_num(0)); + ARG_GUARD_TAPED(!arg || arg->type != VAL_STR, "mkdir", "a string path", make_bool(0)); TRACE_NONDET_TAKE("mkdir"); /* Simple recursive mkdir */ char *path = xstrdup(arg->data.str); @@ -353,7 +353,7 @@ Value* builtin_mkdir(Value *arg) { free(path); struct stat st; TRACE_NONDET_RECORD("mkdir", - make_num(stat(arg->data.str, &st) == 0 && S_ISDIR(st.st_mode) ? 1 : 0)); + make_bool(stat(arg->data.str, &st) == 0 && S_ISDIR(st.st_mode))); } static int ls_entry_cmp(const void *a, const void *b) { @@ -411,8 +411,8 @@ Value* builtin_exe_path(Value *arg) { /* chdir of "path" → 1 on success, 0 on failure */ Value* builtin_chdir(Value *arg) { - ARG_GUARD(!arg || arg->type != VAL_STR, "chdir", "a string path", make_num(0)); - return make_num(chdir(arg->data.str) == 0 ? 1 : 0); + ARG_GUARD(!arg || arg->type != VAL_STR, "chdir", "a string path", make_bool(0)); + return make_bool(chdir(arg->data.str) == 0); } /* mktemp of null → path to a new temporary file */ @@ -437,8 +437,8 @@ Value* builtin_mktemp(Value *arg) { /* rm of "path" → 1 on success, 0 on failure */ Value* builtin_rm(Value *arg) { - ARG_GUARD(!arg || arg->type != VAL_STR, "rm", "a string path", make_num(0)); - return make_num(unlink(arg->data.str) == 0 ? 1 : 0); + ARG_GUARD(!arg || arg->type != VAL_STR, "rm", "a string path", make_bool(0)); + return make_bool(unlink(arg->data.str) == 0); } /* Identifier frequency entry */ @@ -458,11 +458,12 @@ Value* builtin_build_corpus(Value *arg) { Value *vocab_path_val = arg->data.list.items[3]; 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 (!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)topn_val->data.num; + int top_n = (int)eigs_num_arg(topn_val, __func__); int n_files = file_list->data.list.count; /* ---- SLOT MODE (optional 6th arg: slot_count > 0) -------------------- @@ -492,7 +493,9 @@ Value* builtin_build_corpus(Value *arg) { int slot_count = 0; if (arg->data.list.count >= 6) { Value *sv = arg->data.list.items[5]; - if (sv && sv->type == VAL_NUM) slot_count = (int)sv->data.num; + double d = 0; + if (eigs_opt_num(sv, &d, "build_corpus")) slot_count = (int)d; /* #1637: a bool raises */ + if (g_has_error) return make_null(); } if (slot_count < 0) slot_count = 0; @@ -518,7 +521,9 @@ Value* builtin_build_corpus(Value *arg) { int int_count = 0; if (arg->data.list.count >= 7) { Value *iv = arg->data.list.items[6]; - if (iv && iv->type == VAL_NUM) int_count = (int)iv->data.num; + double d = 0; + if (eigs_opt_num(iv, &d, "build_corpus")) int_count = (int)d; /* #1637: a bool raises */ + if (g_has_error) return make_null(); } if (int_count < 0) int_count = 0; /* Source of truth: the size of the TokType enum. Hardcoding 54 (which is @@ -1005,7 +1010,7 @@ Value* builtin_load_file(Value *arg) { } Value* builtin_file_exists(Value *arg) { - ARG_GUARD_TAPED(!arg || arg->type != VAL_STR, "file_exists", "a string path", make_num(0)); + ARG_GUARD_TAPED(!arg || arg->type != VAL_STR, "file_exists", "a string path", make_bool(0)); /* #585: fs-dependent read — taped so replay serves the recorded answer. * TAKE short-circuits before the fopen probe under EIGS_REPLAY. */ TRACE_NONDET_TAKE("file_exists"); @@ -1016,7 +1021,7 @@ Value* builtin_file_exists(Value *arg) { * devices stay 1 as before. */ struct stat st; int ex = (stat(arg->data.str, &st) == 0); - TRACE_NONDET_RECORD("file_exists", make_num(ex)); + TRACE_NONDET_RECORD("file_exists", make_bool(ex)); } /* is_dir of path — 1 if path names a directory, 0 for a plain file, a @@ -1026,10 +1031,10 @@ Value* builtin_file_exists(Value *arg) { * file_exists, ls, mkdir, getcwd — predate the tape and are untraced; * that inconsistency is flagged on the PR, not silently copied here). */ Value* builtin_is_dir(Value *arg) { - ARG_GUARD_TAPED(!arg || arg->type != VAL_STR, "is_dir", "a string path", make_num(0)); + ARG_GUARD_TAPED(!arg || arg->type != VAL_STR, "is_dir", "a string path", make_bool(0)); struct stat st; TRACE_NONDET_RET("is_dir", - make_num(stat(arg->data.str, &st) == 0 && S_ISDIR(st.st_mode) ? 1 : 0)); + make_bool(stat(arg->data.str, &st) == 0 && S_ISDIR(st.st_mode))); } /* #1058: `is_file of path` -- 1 iff the path names a REGULAR file (S_ISREG). @@ -1039,10 +1044,10 @@ Value* builtin_is_dir(Value *arg) { * non-regular non-directory class passed both and read as "", so /dev/null * compiled to the empty program. Taped like is_dir. */ Value* builtin_is_file(Value *arg) { - ARG_GUARD_TAPED(!arg || arg->type != VAL_STR, "is_file", "a string path", make_num(0)); + ARG_GUARD_TAPED(!arg || arg->type != VAL_STR, "is_file", "a string path", make_bool(0)); struct stat st; TRACE_NONDET_RET("is_file", - make_num(stat(arg->data.str, &st) == 0 && S_ISREG(st.st_mode) ? 1 : 0)); + make_bool(stat(arg->data.str, &st) == 0 && S_ISREG(st.st_mode))); } /* rename of [old_path, new_path] — rename/replace a file. On POSIX rename(2) is @@ -1052,18 +1057,18 @@ Value* builtin_is_file(Value *arg) { Value* builtin_rename(Value *arg) { STRICT_LIST_MAX(arg, 2, "rename"); ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, - "rename", "[old_path, new_path]", make_num(0)); + "rename", "[old_path, new_path]", make_bool(0)); Value *from = arg->data.list.items[0]; Value *to = arg->data.list.items[1]; ARG_GUARD(!from || from->type != VAL_STR || !to || to->type != VAL_STR, - "rename", "two string paths", make_num(0)); - return make_num(rename(from->data.str, to->data.str) == 0 ? 1 : 0); + "rename", "two string paths", make_bool(0)); + return make_bool(rename(from->data.str, to->data.str) == 0); } /* remove_file of path — delete a file. Returns 1 on success, 0 on failure. */ Value* builtin_remove_file(Value *arg) { - ARG_GUARD(!arg || arg->type != VAL_STR, "remove_file", "a string path", make_num(0)); - return make_num(remove(arg->data.str) == 0 ? 1 : 0); + ARG_GUARD(!arg || arg->type != VAL_STR, "remove_file", "a string path", make_bool(0)); + return make_bool(remove(arg->data.str) == 0); } /* ==== BUILTIN: read_text ==== */ @@ -1142,14 +1147,14 @@ Value* builtin_read_bytes_buf(Value *arg) { rt_error(EK_VALUE, 0, "read_bytes_buf: max_bytes must be a number"); return make_null(); } - if (!(mv->data.num >= 1 && - mv->data.num <= (double)READ_BYTES_BUF_HARD_CAP)) { + if (!(eigs_num_arg(mv, __func__) >= 1 && + eigs_num_arg(mv, __func__) <= (double)READ_BYTES_BUF_HARD_CAP)) { rt_error(EK_VALUE, 0, "read_bytes_buf: max_bytes must be in 1..%lld (got %g)", - READ_BYTES_BUF_HARD_CAP, mv ? mv->data.num : 0.0); + READ_BYTES_BUF_HARD_CAP, mv ? eigs_num_arg(mv, __func__) : 0.0); return make_null(); } - cap = (long long)mv->data.num; + cap = (long long)eigs_num_arg(mv, __func__); } } if (!path || path->type != VAL_STR) return make_null(); @@ -1164,7 +1169,7 @@ Value* builtin_read_bytes_buf(Value *arg) { if (trace_replay_refuse_off_owner("read_bytes_buf")) return make_null(); if (trace_replay_take("read_bytes_buf", &tv)) { if (tv && tv->type == VAL_NUM) { - long long len = (long long)tv->data.num; + long long len = (long long)eigs_num_arg(tv, __func__); val_decref(tv); read_bytes_buf_cap_raise(path->data.str, len, cap); return make_null(); @@ -1261,21 +1266,21 @@ Value* builtin_read_line(Value *arg) { Value* builtin_write_text(Value *arg) { STRICT_LIST_MAX(arg, 2, "write_text"); ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, - "write_text", "[path, text]", make_num(0)); + "write_text", "[path, text]", make_bool(0)); Value *path_val = arg->data.list.items[0]; Value *text_val = arg->data.list.items[1]; ARG_GUARD(!path_val || path_val->type != VAL_STR || !text_val || text_val->type != VAL_STR, - "write_text", "two strings", make_num(0)); + "write_text", "two strings", make_bool(0)); FILE *f = xfopen_write(path_val->data.str, "w"); /* fs:ANSWER both arguments were accepted by the guards above; a NULL FILE* * is xfopen_write failing (missing directory, permissions, sandbox), and * the header comment documents write_text as "1 on success, 0 on failure". */ - if (!f) return make_num(0); + if (!f) return make_bool(0); size_t len = val_str_len(text_val); size_t written = fwrite(text_val->data.str, 1, len, f); int close_ok = (fclose(f) == 0); - return make_num(written == len && close_ok ? 1 : 0); + return make_bool(written == len && close_ok); } /* ==== BUILTIN: exec_capture ==== */ @@ -1323,7 +1328,7 @@ Value* builtin_exec_capture(Value *arg) { && arg->data.list.items[0] && arg->data.list.items[0]->type == VAL_LIST && arg->data.list.items[1] && arg->data.list.items[1]->type == VAL_NUM) { cmd_list = arg->data.list.items[0]; - timeout_sec = arg->data.list.items[1]->data.num; + 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, "")); } @@ -1565,7 +1570,7 @@ Value* builtin_proc_write(Value *arg) { Value *str_v = arg->data.list.items[1]; ARG_GUARD(!fd_v || fd_v->type != VAL_NUM || !str_v || str_v->type != VAL_STR, "proc_write", "[number fd, string]", make_num(-1)); - int fd = (int)fd_v->data.num; + int fd = (int)eigs_num_arg(fd_v, __func__); /* fs:ANSWER a negative descriptor is not a write that failed on its * arguments' TYPES — it is a descriptor that cannot be written, which is * the same -1 a closed fd produces below. Left soft deliberately: fds @@ -1596,7 +1601,7 @@ Value* builtin_proc_write(Value *arg) { Value* builtin_proc_read_line(Value *arg) { if (replay_blocks("proc_read_line")) return make_null(); if (!arg || arg->type != VAL_NUM) return make_null(); - int fd = (int)arg->data.num; + int fd = (int)eigs_num_arg(arg, __func__); if (fd < 0) return make_null(); size_t cap = 256, len = 0; char *buf = xmalloc(cap + 1); @@ -1643,8 +1648,8 @@ Value* builtin_proc_read(Value *arg) { Value *max_v = arg->data.list.items[1]; if (!fd_v || fd_v->type != VAL_NUM || !max_v || max_v->type != VAL_NUM) return make_null(); - int fd = (int)fd_v->data.num; - int max = (int)max_v->data.num; + int fd = (int)eigs_num_arg(fd_v, __func__); + int max = (int)eigs_num_arg(max_v, __func__); if (fd < 0 || max <= 0) return make_null(); if (max > 10 * 1024 * 1024) max = 10 * 1024 * 1024; char *buf = xmalloc((size_t)max + 1); @@ -1673,8 +1678,8 @@ Value* builtin_proc_read_buf(Value *arg) { Value *max_v = arg->data.list.items[1]; if (!fd_v || fd_v->type != VAL_NUM || !max_v || max_v->type != VAL_NUM) return make_null(); - int fd = (int)fd_v->data.num; - int max = (int)max_v->data.num; + int fd = (int)eigs_num_arg(fd_v, __func__); + int max = (int)eigs_num_arg(max_v, __func__); if (fd < 0 || max <= 0) return make_null(); if (max > 10 * 1024 * 1024) max = 10 * 1024 * 1024; unsigned char *buf = xmalloc((size_t)max); @@ -1702,7 +1707,7 @@ Value* builtin_proc_read_buf(Value *arg) { Value* builtin_proc_close(Value *arg) { if (replay_blocks("proc_close")) return make_null(); if (!arg || arg->type != VAL_NUM) return make_null(); - int fd = (int)arg->data.num; + int fd = (int)eigs_num_arg(arg, __func__); if (fd >= 0) close(fd); return make_null(); } @@ -1712,7 +1717,7 @@ Value* builtin_proc_wait(Value *arg) { * status", and the refusal belongs to the replay layer. */ if (replay_blocks("proc_wait")) return make_num(-1); ARG_GUARD(!arg || arg->type != VAL_NUM, "proc_wait", "a numeric pid", make_num(-1)); - pid_t pid = (pid_t)arg->data.num; + pid_t pid = (pid_t)eigs_num_arg(arg, __func__); /* fs:ANSWER a non-positive pid names no process, so there is no exit * status to report. A pid is a runtime value from proc_open, not a * literal, so this is state rather than a type mistake. */ @@ -1740,7 +1745,7 @@ 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)arg->data.num : 0; + int n = (arg && arg->type == VAL_NUM) ? (int)eigs_num_arg(arg, __func__) : 0; if (n <= 0 || n > 256) TRACE_NONDET_RET("random_hex", make_str("")); int bytes_needed = (n + 1) / 2; unsigned char raw[128]; @@ -1771,10 +1776,15 @@ Value* builtin_write_bytes(Value *arg) { Value *data = arg->data.list.items[1]; ARG_GUARD(!path_val || path_val->type != VAL_STR, "write_bytes", "a string path as its first argument", make_num(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; - if (arg->data.list.count >= 3 && arg->data.list.items[2] && - arg->data.list.items[2]->type == VAL_NUM) - append = (arg->data.list.items[2]->data.num != 0.0); + if (arg->data.list.count >= 3 && arg->data.list.items[2]) { + Value *fl = arg->data.list.items[2]; + 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); + } int n = 0; Value **items = NULL; @@ -1798,12 +1808,13 @@ 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 (g_strict && items && (!items[i] || items[i]->type != VAL_NUM)) { + if (items && (!items[i] || items[i]->type != VAL_NUM) + && (g_strict || (items[i] && items[i]->type == 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 = items ? (items[i] && items[i]->type == VAL_NUM ? items[i]->data.num : 0.0) + double dv = items ? (items[i] && items[i]->type == VAL_NUM ? eigs_num_arg(items[i], __func__) : 0.0) : buffer_read_num(data, i); if (g_has_error) { free(out); return make_null(); } out[i] = finite_num_to_byte(dv); diff --git a/src/builtins_tensor.c b/src/builtins_tensor.c index 950ee1b8..e0dcd343 100644 --- a/src/builtins_tensor.c +++ b/src/builtins_tensor.c @@ -256,19 +256,22 @@ static double* tensor_to_flat(Value *v, int *rows, int *cols, * the default, so reject that lossy conversion before allocating; the * explicit EIGS_STRICT=0 path deliberately retains the old stand-in. * Check even when the first element or row gives no usable shape. */ - if (g_strict && v && v->type == VAL_LIST) { + /* #1637: a bool element is refused in every strict mode. */ +#define FLAT_BAD(e) ((e)->type != VAL_NUM && (g_strict || (e)->type == VAL_BOOL)) + if (v && v->type == VAL_LIST) { int bad = 0; if (ndim == 1) { for (int i = 0; i < v->data.list.count; i++) - if (v->data.list.items[i]->type != VAL_NUM) { bad = 1; break; } + if (FLAT_BAD(v->data.list.items[i])) { bad = 1; break; } } else { for (int r = 0; r < v->data.list.count && !bad; r++) { Value *row = v->data.list.items[r]; - if (row->type != VAL_LIST) { bad = 1; break; } + if (row->type != VAL_LIST) { bad = g_strict || row->type == VAL_BOOL; if (bad) break; continue; } for (int c = 0; c < row->data.list.count; c++) - if (row->data.list.items[c]->type != VAL_NUM) { bad = 1; break; } + if (FLAT_BAD(row->data.list.items[c])) { bad = 1; break; } } } +#undef FLAT_BAD if (bad) { rt_error(EK_TYPE, 0, "%s: expected a tensor containing only numbers", who); return NULL; @@ -287,13 +290,13 @@ static double* tensor_to_flat(Value *v, int *rows, int *cols, } if (ndim == 1) { for (int i = 0; i < *cols; i++) - out[i] = (v->data.list.items[i]->type == VAL_NUM) ? v->data.list.items[i]->data.num : 0.0; + out[i] = (v->data.list.items[i]->type == VAL_NUM) ? eigs_num_arg(v->data.list.items[i], __func__) : 0.0; } else { for (int r = 0; r < *rows; r++) { Value *row = v->data.list.items[r]; int rc = (row->type == VAL_LIST) ? row->data.list.count : 0; for (int c = 0; c < *cols && c < rc; c++) - out[r * (*cols) + c] = (row->data.list.items[c]->type == VAL_NUM) ? row->data.list.items[c]->data.num : 0.0; + out[r * (*cols) + c] = (row->data.list.items[c]->type == VAL_NUM) ? eigs_num_arg(row->data.list.items[c], __func__) : 0.0; } } return out; @@ -354,6 +357,12 @@ static Value* flat_to_like(Value *src, double *data, int rows, int cols) { static int tensor_total(Value *v) { if (!v) return 0; if (v->type == VAL_NUM) 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 + * a count into a silent 0. Raised in every strict mode (#1637). */ + 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_BUFFER) return v->data.buffer.count; /* #1093 */ if (v->type != VAL_LIST) return 0; int total = 0; @@ -366,7 +375,7 @@ 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)++] = v->data.num; return 1; } + if (v->type == VAL_NUM) { out[(*idx)++] = eigs_num_arg(v, __func__); return 1; } if (v->type == VAL_BUFFER) { /* #1093 */ for (int i = 0; i < v->data.buffer.count; i++) { double x = buffer_read_num(v, i); @@ -467,13 +476,15 @@ static Value* buf_scalar_elementwise(Value *buf, double sc, BinOpFn fn, int buf_ static Value* tensor_elementwise(Value *a, Value *b, BinOpFn fn) { /* Shared by add/subtract/multiply/divide/pow (and by its own recursion on - * nested elements), so the guard names that surface rather than one call. */ + * nested elements), so the guard names that surface rather than one call. + * `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)); /* scalar op scalar */ if (a->type == VAL_NUM && b->type == VAL_NUM) - return make_num(fn(a->data.num, b->data.num)); + return make_num(fn(eigs_num_arg(a, __func__), eigs_num_arg(b, __func__))); /* #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 @@ -482,9 +493,9 @@ static Value* tensor_elementwise(Value *a, Value *b, BinOpFn fn) { 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, b->data.num, fn, 1); + 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, a->data.num, fn, 0); + return buf_scalar_elementwise(b, eigs_num_arg(a, __func__), 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(); } @@ -655,7 +666,8 @@ Value* builtin_tensor_pow(Value *arg) { /* ---- Element-wise unary op ---- */ typedef double (*UnaryOpFn)(double); static Value* tensor_unary(Value *v, UnaryOpFn fn) { - if (v->type == VAL_NUM) return make_num(fn(v->data.num)); + 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__))); /* #1093: a buffer is a flat numeric tensor — same kernel, buffer out. */ if (v->type == VAL_BUFFER) { Value *out = make_buffer_like(v); @@ -765,7 +777,7 @@ Value* builtin_tensor_matmul(Value *arg) { "result is not a number (NaN has no defined value)"); break; } - res->data.buffer.data[i] = slot_null().d; + res->data.buffer.data[i] = SLOT_NUM_RAW(slot_null()); } } return res; @@ -991,7 +1003,7 @@ Value* builtin_tensor_scatter_add(Value *arg) { rt_error(EK_TYPE, 0, "scatter_add: index %d is %s (expected a number)", i, val_type_name(iv->type)); return make_null(); } - di = iv->data.num; + di = eigs_num_arg(iv, __func__); } else { /* #1417: an index is a scalar read too. Guard before the * double-to-int conversion: casting a stored NaN is undefined @@ -1006,11 +1018,11 @@ Value* builtin_tensor_scatter_add(Value *arg) { rt_error(EK_TYPE, 0, "scatter_add: value %d is %s (expected a number)", i, val_type_name(vv->type)); return make_null(); } - v = vv->data.num; + v = eigs_num_arg(vv, __func__); } else if (values->type == VAL_BUFFER) { v = values->data.buffer.data[i]; } else { - v = values->data.num; + v = eigs_num_arg(values, __func__); } /* Check the double before converting it to int. In particular, * an out-of-range floating-to-integer conversion is undefined C @@ -1050,6 +1062,7 @@ Value* builtin_tensor_scatter_add(Value *arg) { /* ==== BUILTIN: softmax ==== */ 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); /* flat-buffer fast path (#973): row-wise on the shape, 1-D is one row; @@ -1074,6 +1087,7 @@ Value* builtin_tensor_softmax(Value *arg) { /* ==== BUILTIN: log_softmax ==== */ Value* builtin_tensor_log_softmax(Value *arg) { + BOOL_REFUSE(arg, "log_softmax"); /* Accept: log_softmax of tensor OR log_softmax of [tensor, dim]. * #973: the [tensor, dim] form is recognised only as exactly [list, num]. * The old test ("first element is a list") was satisfied by EVERY 2-D @@ -1122,9 +1136,10 @@ Value* builtin_tensor_log_softmax(Value *arg) { /* ==== BUILTIN: relu ==== */ /* relu of tensor → element-wise max(0, x). Works on 1D or 2D. */ 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 = arg->data.num; + double x = eigs_num_arg(arg, __func__); return make_num(x < 0.0 ? 0.0 : x); } /* #1093: buffers go through the same flatten path and come back as @@ -1142,9 +1157,10 @@ Value* builtin_tensor_relu(Value *arg) { /* ==== BUILTIN: leaky_relu ==== */ /* leaky_relu of tensor → element-wise max(0.01*x, x). Works on 1D or 2D. */ 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 = arg->data.num; + double x = eigs_num_arg(arg, __func__); return make_num(x < 0.0 ? 0.01 * x : x); } /* flat-buffer fast path (#973), the twin of relu's. */ @@ -1303,6 +1319,7 @@ Value* builtin_tensor_norm(Value *arg) { /* zeros of n → a BUFFER of n zeros (#1093); zeros of [rows, cols] → 2D list */ Value* builtin_tensor_zeros(Value *arg) { STRICT_LIST_MAX(arg, 2, "zeros"); + BOOL_REFUSE(arg, "zeros"); if (!arg) return make_null(); /* #1093 (breaking, documented): `zeros of n` is the FLAT numeric * container — a VAL_BUFFER of n doubles, not a list of n boxed numbers. @@ -1313,7 +1330,7 @@ Value* builtin_tensor_zeros(Value *arg) { * (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)arg->data.num; + int64_t n64 = (int64_t)eigs_num_arg(arg, __func__); if (n64 < 0) n64 = 0; if (n64 > 10000000) n64 = 10000000; /* #292: cap like fill/buffer (was uncapped → x_oom/abort) */ Value *out = make_shaped_buffer(0, (int)n64); @@ -1323,8 +1340,8 @@ Value* builtin_tensor_zeros(Value *arg) { if (arg->type == VAL_LIST && arg->data.list.count >= 2 && arg->data.list.items[0]->type == VAL_NUM && arg->data.list.items[1]->type == VAL_NUM) { - int64_t rows64 = (int64_t)arg->data.list.items[0]->data.num; - int64_t cols64 = (int64_t)arg->data.list.items[1]->data.num; + int64_t rows64 = (int64_t)eigs_list_num(arg, 0, __func__); + int64_t cols64 = (int64_t)eigs_list_num(arg, 1, __func__); if (rows64 < 0) rows64 = 0; if (cols64 < 0) cols64 = 0; /* #292: bound each dim before multiplying (no int64 overflow), then the @@ -1385,7 +1402,7 @@ static int flat_is_vector(Value *v) { static int flat_index_at(Value *v, int i) { if (v->type == VAL_LIST) return (v->data.list.items[i]->type == VAL_NUM) - ? (int)v->data.list.items[i]->data.num : -1; + ? (int)eigs_num_arg(v->data.list.items[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 @@ -1441,7 +1458,7 @@ Value* builtin_tensor_gather(Value *arg) { * 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)indices->data.num; + int idx = (int)eigs_num_arg(indices, __func__); 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); @@ -1486,6 +1503,12 @@ Value* builtin_tensor_gather(Value *arg) { for (int i = 0; i < n; i++) { Value *row = tensor->data.list.items[i]; if (row->type != VAL_LIST) { /* not a matrix row — shape, not index */ + /* #1637: a bool where a row belongs raises, in every mode. */ + if (row->type == VAL_BOOL) { + val_decref(out); + eigs_num_arg_slow(row, "gather"); + return make_null(); + } list_append_owned(out, make_num(0.0)); continue; } @@ -1495,7 +1518,7 @@ Value* builtin_tensor_gather(Value *arg) { i, val_type_name(indices->data.list.items[i]->type)); return make_null(); } - int idx = (int)indices->data.list.items[i]->data.num; + int idx = (int)eigs_num_arg(indices->data.list.items[i], __func__); if (idx < 0 || idx >= row->data.list.count) { val_decref(out); rt_error(EK_INDEX, 0, @@ -1503,21 +1526,25 @@ Value* builtin_tensor_gather(Value *arg) { idx, i, row->data.list.count); return make_null(); } - list_append_owned(out, make_num(row->data.list.items[idx]->type == VAL_NUM - ? row->data.list.items[idx]->data.num : 0.0)); + /* #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_elem_num(row->data.list.items[idx], "gather"); + if (g_has_error) { val_decref(out); return make_null(); } + list_append_owned(out, make_num(cell)); } return out; } /* 1D tensor, scalar index */ if (tensor->type == VAL_LIST && indices->type == VAL_NUM) { - int idx = (int)indices->data.num; + int idx = (int)eigs_num_arg(indices, __func__); 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); return make_null(); } - return make_num(tensor->data.list.items[idx]->type == VAL_NUM - ? tensor->data.list.items[idx]->data.num : 0.0); + double cell = eigs_elem_num(tensor->data.list.items[idx], "gather"); /* #1637 round 4 */ + if (g_has_error) return make_null(); + return make_num(cell); } /* 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 @@ -1545,10 +1572,10 @@ Value* call_eigs_fn(Value *fn, Value *arg) { * read it below (the VM sites guard the same way). */ if (fn->data.builtin == builtin_free_val) { if (arg) val_incref(arg); - Value *consumed = fn->data.builtin(arg); + Value *consumed = eigs_call_builtin(fn->data.builtin, arg); return consumed ? consumed : make_null(); } - Value *result = fn->data.builtin(arg); + Value *result = eigs_call_builtin(fn->data.builtin, arg); if (!result) return make_null(); vm_borrow_compensate(arg, result, 0, fn, NULL); return result; @@ -1621,6 +1648,7 @@ 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 */ 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 @@ -1630,9 +1658,9 @@ Value* builtin_random_normal(Value *arg) { int argc = arg->data.list.count; if (argc == 3) { /* 2D: [rows, cols, scale] */ - int rows = (int)arg->data.list.items[0]->data.num; - int cols = (int)arg->data.list.items[1]->data.num; - double scale = arg->data.list.items[2]->data.num; + 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); @@ -1650,8 +1678,8 @@ Value* builtin_random_normal(Value *arg) { } if (argc == 2) { /* 1D: [len, scale] */ - int len = (int)arg->data.list.items[0]->data.num; - double scale = arg->data.list.items[1]->data.num; + 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 */ @@ -1710,7 +1738,7 @@ static double numerical_loss(Value *loss_fn, Value *arg, const char *who, if (!*loss_valid) return 0.0; Value *loss = call_eigs_fn(loss_fn, arg); if (loss && loss->type == VAL_NUM) { - double result = loss->data.num; + double result = eigs_num_arg(loss, __func__); val_decref(loss); return result; } @@ -1728,12 +1756,45 @@ static double numerical_loss(Value *loss_fn, Value *arg, const char *who, * loss_fn is a VAL_FN that takes null and returns a scalar loss. * param is a 1D or 2D tensor (VAL_LIST). * Returns gradient tensor matching param shape. */ +/* #1637: the cells a numeric-gradient / SGD builtin reads (and, for the + * numerical_grad family, writes in place) must be numbers. A bool cell used + * to read as 0, and numerical_grad_rows/_cols then wrote the perturbed value + * into it in place -- into the immortal true/false singleton. So every cell + * and every row is checked up front, before anything is mutated, and ANY + * non-number -- a bool, a string, or a null where a row or cell belongs -- + * raises a type error in EVERY strict mode, EIGS_STRICT=0 included (owner + * decision, round 5: fail loud, the #975 direction; v0.44.0 skipped null + * rows and read non-number cells as 0). Index lists get the same check, so + * a bool index is refused, never skipped as "-1". */ +static int tensor_cells_numeric(const Value *v, const char *who) { + if (!v || v->type != VAL_LIST) return 1; + for (int i = 0; i < v->data.list.count; i++) { + const Value *e = v->data.list.items[i]; + if (e && e->type == VAL_LIST) { + for (int c = 0; c < e->data.list.count; c++) + if (!e->data.list.items[c] || e->data.list.items[c]->type != VAL_NUM) { + eigs_num_arg(e->data.list.items[c], who); + return 0; + } + } else if (!e || e->type != VAL_NUM) { + eigs_num_arg(e, who); + return 0; + } + } + return 1; +} + Value* builtin_numerical_grad(Value *arg) { STRICT_LIST_MAX(arg, 3, "numerical_grad"); if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) return make_null(); Value *loss_fn = arg->data.list.items[0]; Value *param = arg->data.list.items[1]; - double eps = (arg->data.list.items[2]->type == VAL_NUM) ? arg->data.list.items[2]->data.num : 0.001; + /* #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_opt_num(arg->data.list.items[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; int loss_valid = 1; @@ -1768,7 +1829,7 @@ Value* builtin_numerical_grad(Value *arg) { Value *grad = make_list(len); for (int i = 0; i < len; i++) { Value *orig = param->data.list.items[i]; - double old_val = (orig->type == VAL_NUM) ? orig->data.num : 0.0; + double old_val = eigs_num_arg(orig, __func__); val_incref(orig); /* guard while displaced from its slot */ Value *pp = make_num(old_val + eps); /* birth ref doubles as slot ref */ param->data.list.items[i] = pp; @@ -1798,7 +1859,7 @@ Value* builtin_numerical_grad(Value *arg) { Value *grad_row = make_list(cols); for (int c = 0; c < cols; c++) { Value *orig = row->data.list.items[c]; - double old_val = (orig->type == VAL_NUM) ? orig->data.num : 0.0; + double old_val = eigs_num_arg(orig, __func__); val_incref(orig); /* guard while displaced from its slot */ Value *pp = make_num(old_val + eps); /* birth ref doubles as slot ref */ row->data.list.items[c] = pp; @@ -1826,7 +1887,12 @@ Value* builtin_sgd_update(Value *arg) { if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) return make_null(); Value *param = arg->data.list.items[0]; Value *grad = arg->data.list.items[1]; - double lr = (arg->data.list.items[2]->type == VAL_NUM) ? arg->data.list.items[2]->data.num : 0.01; + /* #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_opt_num(arg->data.list.items[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(); /* #1093: both operands flat buffers — update the doubles in place. */ if (param->type == VAL_BUFFER && grad->type == VAL_BUFFER) { @@ -1846,8 +1912,8 @@ Value* builtin_sgd_update(Value *arg) { ? param->data.list.count : grad->data.list.count; for (int i = 0; i < len; i++) { Value *old = param->data.list.items[i]; - double pv = (old->type == VAL_NUM) ? old->data.num : 0.0; - double gv = (grad->data.list.items[i]->type == VAL_NUM) ? grad->data.list.items[i]->data.num : 0.0; + double pv = eigs_num_arg(old, __func__); + double gv = eigs_num_arg(grad->data.list.items[i], __func__); param->data.list.items[i] = make_num_permanent(pv - lr * gv); val_decref(old); } @@ -1863,8 +1929,8 @@ Value* builtin_sgd_update(Value *arg) { ? pr->data.list.count : gr->data.list.count; for (int c = 0; c < cols; c++) { Value *old = pr->data.list.items[c]; - double pv = (old->type == VAL_NUM) ? old->data.num : 0.0; - double gv = (gr->data.list.items[c]->type == VAL_NUM) ? gr->data.list.items[c]->data.num : 0.0; + double pv = eigs_num_arg(old, __func__); + double gv = eigs_num_arg(gr->data.list.items[c], __func__); pr->data.list.items[c] = make_num_permanent(pv - lr * gv); val_decref(old); } @@ -1884,7 +1950,12 @@ Value* builtin_numerical_grad_rows(Value *arg) { Value *loss_fn = arg->data.list.items[0]; Value *matrix = arg->data.list.items[1]; Value *row_indices = arg->data.list.items[2]; - double eps = (arg->data.list.items[3]->type == VAL_NUM) ? arg->data.list.items[3]->data.num : 0.001; + /* #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_opt_num(arg->data.list.items[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; int loss_valid = 1; @@ -1953,12 +2024,12 @@ Value* builtin_numerical_grad_rows(Value *arg) { for (int c = 0; c < cols && c < row->data.list.count; c++) { Value *cell = row->data.list.items[c]; - double old_val = (cell->type == VAL_NUM) ? cell->data.num : 0.0; - cell->data.num = old_val + eps; + double old_val = eigs_num_arg(cell, __func__); + VAL_NUM_RAW(cell) = old_val + eps; double loss_plus = numerical_loss(loss_fn, nul, "numerical_grad_rows", &loss_valid); - cell->data.num = old_val - eps; + VAL_NUM_RAW(cell) = old_val - eps; double loss_minus = numerical_loss(loss_fn, nul, "numerical_grad_rows", &loss_valid); - cell->data.num = old_val; + VAL_NUM_RAW(cell) = old_val; /* gradient — release the zero placeholder this slot held */ val_decref(grad_row->data.list.items[c]); grad_row->data.list.items[c] = make_num((loss_plus - loss_minus) / (2.0 * eps)); @@ -1978,7 +2049,12 @@ Value* builtin_sgd_update_rows(Value *arg) { Value *matrix = arg->data.list.items[0]; Value *grad = arg->data.list.items[1]; Value *row_indices = arg->data.list.items[2]; - double lr = (arg->data.list.items[3]->type == VAL_NUM) ? arg->data.list.items[3]->data.num : 0.01; + /* #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_opt_num(arg->data.list.items[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(); /* #1093: shaped-buffer matrix + shaped-buffer gradient, index vector as a * list or a buffer — update the named rows' doubles in place. */ @@ -2014,8 +2090,8 @@ Value* builtin_sgd_update_rows(Value *arg) { ? mrow->data.list.count : grow->data.list.count; for (int c = 0; c < cols; c++) { Value *old = mrow->data.list.items[c]; - double pv = (old->type == VAL_NUM) ? old->data.num : 0.0; - double gv = (grow->data.list.items[c]->type == VAL_NUM) ? grow->data.list.items[c]->data.num : 0.0; + double pv = eigs_num_arg(old, __func__); + double gv = eigs_num_arg(grow->data.list.items[c], __func__); mrow->data.list.items[c] = make_num_permanent(pv - lr * gv); val_decref(old); } @@ -2034,7 +2110,12 @@ Value* builtin_numerical_grad_cols(Value *arg) { Value *loss_fn = arg->data.list.items[0]; Value *matrix = arg->data.list.items[1]; Value *col_indices = arg->data.list.items[2]; - double eps = (arg->data.list.items[3]->type == VAL_NUM) ? arg->data.list.items[3]->data.num : 0.001; + /* #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_opt_num(arg->data.list.items[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; int loss_valid = 1; @@ -2100,7 +2181,7 @@ Value* builtin_numerical_grad_cols(Value *arg) { if (!row || row->type != VAL_LIST || col >= row->data.list.count) continue; Value *orig = row->data.list.items[col]; - double old_val = (orig->type == VAL_NUM) ? orig->data.num : 0.0; + double old_val = eigs_num_arg(orig, __func__); val_incref(orig); /* guard while displaced from its slot */ /* +eps */ Value *pp = make_num(old_val + eps); /* birth ref doubles as slot ref */ @@ -2134,7 +2215,12 @@ Value* builtin_sgd_update_cols(Value *arg) { Value *matrix = arg->data.list.items[0]; Value *grad = arg->data.list.items[1]; Value *col_indices = arg->data.list.items[2]; - double lr = (arg->data.list.items[3]->type == VAL_NUM) ? arg->data.list.items[3]->data.num : 0.01; + /* #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_opt_num(arg->data.list.items[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(); /* #1093: shaped-buffer matrix + gradient, list-or-buffer index vector. */ if (matrix->type == VAL_BUFFER && grad->type == VAL_BUFFER @@ -2171,8 +2257,8 @@ Value* builtin_sgd_update_cols(Value *arg) { if (!grow || grow->type != VAL_LIST || col >= grow->data.list.count) continue; Value *old = mrow->data.list.items[col]; - double pv = (old->type == VAL_NUM) ? old->data.num : 0.0; - double gv = (grow->data.list.items[col]->type == VAL_NUM) ? grow->data.list.items[col]->data.num : 0.0; + double pv = eigs_num_arg(old, __func__); + double gv = eigs_num_arg(grow->data.list.items[col], __func__); mrow->data.list.items[col] = make_num_permanent(pv - lr * gv); val_decref(old); } @@ -2183,12 +2269,12 @@ Value* builtin_sgd_update_cols(Value *arg) { Value* builtin_tensor_save(Value *arg) { STRICT_LIST_MAX(arg, 2, "tensor_save"); ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 2, - "tensor_save", "[tensor, path]", make_num(0)); + "tensor_save", "[tensor, path]", make_bool(0)); Value *tensor = arg->data.list.items[0]; Value *path_val = arg->data.list.items[1]; ARG_GUARD(!tensor || (tensor->type != VAL_LIST && tensor->type != VAL_BUFFER) || !path_val || path_val->type != VAL_STR, /* #1093 */ - "tensor_save", "[a list or buffer tensor, a string path]", make_num(0)); + "tensor_save", "[a list or buffer tensor, a string path]", make_bool(0)); int rows, cols; int ndim = tensor_dims(tensor, &rows, &cols); @@ -2196,13 +2282,13 @@ Value* builtin_tensor_save(Value *arg) { * neither a number nor a list — i.e. the argument is a list but not a 1D * or 2D tensor, which is a shape/type mistake in the tensor argument and * not an I/O failure (no file has been opened yet at this point). */ - ARG_GUARD(ndim == 0, "tensor_save", "a non-empty 1D or 2D tensor", make_num(0)); + ARG_GUARD(ndim == 0, "tensor_save", "a non-empty 1D or 2D tensor", make_bool(0)); if ((int64_t)rows * (int64_t)cols > EIGS_TENSOR_MAX_ELEMENTS) { rt_error(EK_LIMIT, 0, "tensor_save: '%s' has %lld elements, over the %d-element cap", path_val->data.str, (long long)rows * cols, EIGS_TENSOR_MAX_ELEMENTS); - return make_num(0); + return make_bool(0); } /* Flatten before opening the file: a strict #1416 rejection must not @@ -2214,14 +2300,14 @@ Value* builtin_tensor_save(Value *arg) { * to return for that shape. Preserve the historical save format: write * its header and the empty data/observer sections. A NULL for any other * shape remains a conversion failure. */ - if (!flat && cols != 0) return make_num(0); - if (!flat && g_has_error) return make_num(0); + if (!flat && cols != 0) return make_bool(0); + if (!flat && g_has_error) return make_bool(0); FILE *f = xfopen_write(path_val->data.str, "wb"); /* fs:ANSWER both arguments were accepted by the guards above; a NULL FILE* * is xfopen_write failing, and 0 is this builtin's failure bit (the success - * path ends in make_num(1)). */ - if (!f) { free(flat); return make_num(0); } + * path ends in make_bool(1)). */ + if (!f) { free(flat); return make_bool(0); } uint32_t header[4] = { (uint32_t)ndim, (uint32_t)rows, (uint32_t)cols, 1 /* flags: has observer */ }; fwrite(header, sizeof(uint32_t), 4, f); @@ -2242,7 +2328,7 @@ Value* builtin_tensor_save(Value *arg) { } fclose(f); - return make_num(1); + return make_bool(1); } #endif /* !EIGENSCRIPT_FREESTANDING */ @@ -2302,8 +2388,8 @@ Value* builtin_tensor_load(Value *arg) { verdict->data.list.count == 2 && verdict->data.list.items[0]->type == VAL_NUM && verdict->data.list.items[1]->type == VAL_NUM) { - uint32_t rows = (uint32_t)verdict->data.list.items[0]->data.num; - uint32_t cols = (uint32_t)verdict->data.list.items[1]->data.num; + uint32_t rows = (uint32_t)eigs_list_num(verdict, 0, __func__); + uint32_t cols = (uint32_t)eigs_list_num(verdict, 1, __func__); val_decref(verdict); tensor_load_limit_raise(arg->data.str, rows, cols); return make_null(); diff --git a/src/chunk.c b/src/chunk.c index fbd480bd..6a02c14a 100644 --- a/src/chunk.c +++ b/src/chunk.c @@ -191,7 +191,7 @@ void chunk_patch_jump(EigsChunk *chunk, int offset) { * same and compare ==). Non-NUM/STR constants are never deduped. */ static uint32_t const_hash_value(const Value *v) { if (v->type == VAL_NUM) { - double d = v->data.num; + double d = VAL_NUM_RAW(v); if (d == 0.0) d = 0.0; /* fold -0.0 into +0.0, matching == */ uint64_t bits; memcpy(&bits, &d, sizeof bits); @@ -210,7 +210,7 @@ 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 a->data.num == b->data.num; + return VAL_NUM_RAW(a) == VAL_NUM_RAW(b); if (a->type == VAL_STR && b->type == VAL_STR) return strcmp(a->data.str, b->data.str) == 0; return 0; @@ -335,7 +335,7 @@ const char *op_name(uint8_t op) { if (op >= OP_COUNT) return "???"; 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_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_BAND) N(OP_BOR) N(OP_BXOR) N(OP_SHL) N(OP_SHR) N(OP_NEG) N(OP_NOT) N(OP_BNOT) @@ -418,7 +418,7 @@ void chunk_disassemble(EigsChunk *chunk, const char *label) { if (op == OP_CONST && arg < (uint16_t)chunk->const_count) { Value *v = chunk->constants[arg]; if (v->type == VAL_NUM) - fprintf(stderr, " (%.6g)", v->data.num); + fprintf(stderr, " (%.6g)", VAL_NUM_RAW(v)); else if (v->type == VAL_STR) fprintf(stderr, " (\"%s\")", v->data.str); } @@ -506,7 +506,7 @@ static int op_verify_operands(uint8_t op8, VerifyRole roles[3]) { roles[0] = VR_RAW; roles[1] = VR_RAW; roles[2] = VR_NAME; return 3; /* Operand-free opcodes — every one listed, so a new opcode cannot * silently walk wrong. */ - case OP_NULL: case OP_NUM_ZERO: case OP_NUM_ONE: + 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_BAND: case OP_BOR: case OP_BXOR: case OP_SHL: case OP_SHR: case OP_NEG: case OP_NOT: case OP_BNOT: @@ -614,6 +614,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_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: case OP_IMPORT: case OP_MATCH: case OP_LISTCOMP_BEGIN: diff --git a/src/compiler.c b/src/compiler.c index 31ffa0b1..0976cd06 100644 --- a/src/compiler.c +++ b/src/compiler.c @@ -305,6 +305,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_TRUE: case OP_FALSE: case OP_GET_LOCAL: case OP_GET_NAME: case OP_DUP: case OP_PREDICATE: case OP_LISTCOMP_BEGIN: case OP_REPORT_SLOT: case OP_REPORT_NAME: @@ -789,7 +790,7 @@ static void collect_referenced_names_skip(ASTNode *node, ASTNode *skip, NameSet * error here instead of a silent no-op. */ case AST_NUM: case AST_STR: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: @@ -933,7 +934,7 @@ static void collect_referenced_names(ASTNode *node, NameSet *out) { * error here instead of a silent no-op. */ case AST_NUM: case AST_STR: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BREAK: case AST_CONTINUE: case AST_IMPORT: @@ -1073,7 +1074,7 @@ static void scan_for_captures(ASTNode *node, NameSet *out) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_PROGRAM: case AST_INTERROGATE: case AST_PREDICATE: @@ -1180,7 +1181,7 @@ static int ast_has_closure(ASTNode *node) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_PROGRAM: case AST_PREDICATE: case AST_BREAK: @@ -1255,7 +1256,7 @@ static int subtree_overwrite_safe(ASTNode *n, NameSet *bound, Env *env) { case AST_INTERROGATE: SAFE(n->data.interrogate.expr); SAFE(n->data.interrogate.at_expr); SAFE(n->data.interrogate.when_expr); break; /* Leaves with no binding effect. */ - case AST_IDENT: case AST_NUM: case AST_STR: case AST_NULL: + case AST_IDENT: case AST_NUM: case AST_STR: case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: break; /* These take the fallback the deleted `default:` supplied. Enumerated @@ -1468,7 +1469,7 @@ static void scan_for_interrogated(ASTNode *node, NameSet *out) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_FUNC: case AST_PROGRAM: case AST_PREDICATE: @@ -1590,7 +1591,7 @@ static void scan_for_env_bound(ASTNode *node, NameSet *out) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_FUNC: case AST_PROGRAM: case AST_INTERROGATE: @@ -1657,7 +1658,7 @@ static void collect_module_names_walk(ASTNode *node, NameSet *out) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_RELATION: @@ -1794,7 +1795,7 @@ static int scan_dispatch_rebind(ASTNode *n) { * CFLAGS) makes a new ASTType a build error here. */ case AST_NUM: case AST_STR: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: @@ -1982,7 +1983,7 @@ static int cond_is_observer_based(const ASTNode *n) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_ASSIGN: case AST_IF: case AST_LOOP: @@ -2085,6 +2086,10 @@ static void compile_node_inner(Compiler *c, ASTNode *node) { emit(c, OP_NULL, node->line); break; + case AST_BOOL: /* #1637 */ + emit(c, node->data.num != 0.0 ? OP_TRUE : OP_FALSE, node->line); + break; + case AST_IDENT: { /* A reference to the state_at builtin means assignment history * will be queried at runtime — enable recording. (Aliasing that @@ -3836,7 +3841,7 @@ static int obs_ast_scan_d(ASTNode *n, ObsLoadList *L, int depth) { for (int j = 0; j < n->data.match.body_counts[i]; j++) if (obs_ast_scan_d(n->data.match.bodies[i][j], L, depth + 1)) return 1; } return 0; - case AST_NUM: case AST_STR: case AST_NULL: case AST_BREAK: case AST_CONTINUE: return 0; + case AST_NUM: case AST_STR: case AST_NULL: case AST_BOOL: case AST_BREAK: case AST_CONTINUE: return 0; } return 1; /* an unknown node kind arms the gate, never the reverse */ } diff --git a/src/eigenlsp.c b/src/eigenlsp.c index e39a35de..f6ed59fe 100644 --- a/src/eigenlsp.c +++ b/src/eigenlsp.c @@ -59,8 +59,8 @@ static const char *keyword_docs[][2] = { {"match", "Pattern matching: match expr:"}, {"case", "Match case: case pattern:"}, {"unobserved", "Unobserved block (suppresses entropy tracking)"}, - {"true", "Boolean true (1)"}, - {"false", "Boolean false (0)"}, + {"true", "Boolean true (type bool)"}, + {"false", "Boolean false (type bool)"}, {NULL, NULL} }; @@ -501,7 +501,7 @@ static void walk_ast_symbols(ASTNode *node, const TokenList *tokens, case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_LISTCOMP: case AST_INTERROGATE: case AST_PREDICATE: @@ -637,7 +637,7 @@ static void collect_references(ASTNode *node, const char *name, Location *locs, case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_INTERROGATE: case AST_PREDICATE: case AST_BREAK: diff --git a/src/eigenscript.c b/src/eigenscript.c index e7d9fcbb..58c483c5 100644 --- a/src/eigenscript.c +++ b/src/eigenscript.c @@ -236,6 +236,8 @@ const char* tok_type_name(TokType t) { case TOK_FOR: return "'for'"; case TOK_IN: return "'in'"; case TOK_NULL: return "'null'"; + case TOK_TRUE: return "'true'"; + case TOK_FALSE: return "'false'"; case TOK_UNOBSERVED: return "'unobserved'"; case TOK_REPORT: return "'report'"; case TOK_REPORT_VALUE: return "'report_value'"; @@ -314,6 +316,7 @@ const char* val_type_name(ValType t) { case VAL_DICT: return "dict"; case VAL_BUFFER: return "buffer"; case VAL_TEXT_BUILDER: return "text_builder"; + case VAL_BOOL: return "bool"; /* No `default:` — -Werror=switch (Makefile CFLAGS) makes a new * ValType a build error here instead of printing "?". */ } @@ -432,7 +435,7 @@ 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(v->data.num); + case VAL_NUM: return entropy_of_num(VAL_NUM_RAW(v)); case VAL_STR: return entropy_of_cstr(v->data.str); case VAL_JSON_RAW: return entropy_of_cstr(v->data.str); case VAL_LIST: { @@ -459,6 +462,11 @@ static double compute_entropy_impl(Value *v) { return eigs_native_fn_name(v->data.builtin) ? 1.0 : 0.0; case VAL_BUFFER: return log2((double)v->data.buffer.count + 1.0); case VAL_TEXT_BUILDER: return log2((double)v->data.text_builder.len + 1.0); + /* #1637: a bool is one bit. false sits at 0.0 and true at 1.0, the + * binary-entropy floor and ceiling — the values entropy_of_num gave + * the 0/1 a comparison used to produce, so a flag binding's entropy + * trajectory is unchanged. No existing constant moves. */ + case VAL_BOOL: return v->data.boolean ? 1.0 : 0.0; } /* No `default:` above, and no fallthrough value here: with -Werror=switch a * new ValType is a BUILD failure at this switch rather than a silent 0.0. @@ -778,7 +786,7 @@ void observer_slot_update(Env *e, int idx, Value *newval) { * the relative-delta step is only defined for a scalar trajectory). */ if (newval && newval->type == VAL_NUM) { ObserverSlot *s = env_obs_slot(e, idx); - if (s) observer_slot_record_value(s, newval->data.num); + if (s) observer_slot_record_value(s, VAL_NUM_RAW(newval)); } else { /* #861: a non-numeric assignment ends the numeric trajectory's claim * on this binding — predicates fall back to the entropy channel until @@ -841,7 +849,7 @@ 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, newval->data.num); + observer_slot_sample_num_gated(e, idx, VAL_NUM_RAW(newval)); return; } if (!e || idx < 0) return; @@ -1374,7 +1382,10 @@ int observer_entropy_now(Env *e, int idx, double *out) { EigsSlot s = e->values[idx]; if (slot_is_num(s)) { kind = 1; - num = s.d; + num = SLOT_NUM_RAW(s); + } else if (slot_is_bool(s)) { + held = make_bool(slot_as_bool(s)); /* #1637: immortal; incref no-op */ + kind = 2; } else if (slot_is_ptr(s)) { Value *v = slot_as_ptr(s); if (v && v->type != VAL_NULL) { @@ -1428,8 +1439,8 @@ Value *observer_slot_trajectory(const ObserverSlot *s) { 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, "observed", make_num(s ? (s->used != 0) : 0)); - dict_set_owned(out, "numeric", make_num(s ? (s->v_used != 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. */ @@ -1459,10 +1470,10 @@ int observer_slot_from_trajectory(ObserverSlot *out, Value *dict) { { Value *w = dict_get(dict, "window"); if (w) { - if (w->type != VAL_NUM || w->data.num < OBSERVER_WINDOW_MIN || - w->data.num > OBSERVER_WINDOW_MAX || w->data.num != (int)w->data.num) + 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)) return 0; - out->win_override = (uint8_t)(int)w->data.num; + out->win_override = (uint8_t)(int)VAL_NUM_RAW(w); } } const int N = observer_slot_window(out); @@ -1481,8 +1492,8 @@ int observer_slot_from_trajectory(ObserverSlot *out, Value *dict) { memset(out, 0, sizeof *out); return 0; } - out->v_window[out->v_window_count] = a->data.num; - out->vr_window[out->v_window_count] = b->data.num; + out->v_window[out->v_window_count] = VAL_NUM_RAW(a); + out->vr_window[out->v_window_count] = VAL_NUM_RAW(b); out->v_window_count++; } out->v_window_head = (uint8_t)(out->v_window_count % N); @@ -1496,19 +1507,33 @@ int observer_slot_from_trajectory(ObserverSlot *out, Value *dict) { memset(out, 0, sizeof *out); return 0; } - out->dh_window[out->dh_window_count++] = a->data.num; + out->dh_window[out->dh_window_count++] = VAL_NUM_RAW(a); } out->dh_window_head = (uint8_t)(out->dh_window_count % N); - Value *v; - if ((v = dict_get(dict, "entropy")) && v->type == VAL_NUM) out->entropy = v->data.num; - if ((v = dict_get(dict, "dH")) && v->type == VAL_NUM) out->dH = v->data.num; - if ((v = dict_get(dict, "last_entropy")) && v->type == VAL_NUM) out->last_entropy = v->data.num; - if ((v = dict_get(dict, "last_value")) && v->type == VAL_NUM) out->last_value = v->data.num; - v = dict_get(dict, "observed"); - out->used = (v && v->type == VAL_NUM) ? (v->data.num != 0.0) : 1; - v = dict_get(dict, "numeric"); - out->v_used = (v && v->type == VAL_NUM) ? (v->data.num != 0.0) - : (out->v_window_count > 0); + /* #1637: the scalar fields are numbers and the two flags are bools, as + * `trajectory of x` writes them. Absent keeps the default; any other + * type is not a snapshot (the old code read a bool flag as "not a + * number" and defaulted `observed: false` to observed). */ + static const char *const nums[] = {"entropy", "dH", "last_entropy", "last_value"}; + double *dsts[] = {&out->entropy, &out->dH, &out->last_entropy, &out->last_value}; + for (int k = 0; k < 4; k++) { + Value *v = dict_get(dict, nums[k]); + if (!v) continue; + if (v->type != VAL_NUM) { + 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); + } + Value *fo = dict_get(dict, "observed"), *fn = dict_get(dict, "numeric"); + if ((fo && fo->type != VAL_BOOL) || (fn && fn->type != VAL_BOOL)) { + free(out->v_window); free(out->vr_window); free(out->dh_window); + memset(out, 0, sizeof *out); + return 0; + } + out->used = fo ? fo->data.boolean : 1; + out->v_used = fn ? fn->data.boolean : (out->v_window_count > 0); /* A full slot re-fed through the classifiers needs obs_age > 0 so the * partial-window fallbacks behave like a live slot's. */ out->obs_age = out->dh_window_count + out->v_window_count; @@ -1701,6 +1726,7 @@ void free_value(Value *v) { case VAL_NUM: case VAL_BUILTIN: case VAL_NULL: + case VAL_BOOL: break; } free(v); @@ -1720,7 +1746,7 @@ Value* make_num(double n) { v = from_arena ? arena_alloc(sizeof(Value)) : xcalloc(1, sizeof(Value)); } v->type = VAL_NUM; - v->data.num = n; + VAL_NUM_RAW(v) = n; v->refcount = 1; v->arena = from_arena; return v; @@ -1743,7 +1769,7 @@ Value* make_num_permanent(double n) { n = num_guard(n); Value *v = xcalloc(1, sizeof(Value)); v->type = VAL_NUM; - v->data.num = n; + VAL_NUM_RAW(v) = n; v->refcount = 1; v->arena = 0; return v; @@ -1751,6 +1777,67 @@ Value* make_num_permanent(double n) { /* Forward decl of the VAL_NULL singleton — defined below near make_null. */ static Value g_null_singleton; +/* #1637: the two VAL_BOOL singletons — immortal like null (arena=1 makes + * every incref/decref a no-op). make_bool hands out one of these; no other + * VAL_BOOL Value is ever allocated. */ +static Value g_true_singleton = { .type = VAL_BOOL, .data = { .boolean = 1 }, .refcount = 1000000, .arena = 1 }; +static Value g_false_singleton = { .type = VAL_BOOL, .data = { .boolean = 0 }, .refcount = 1000000, .arena = 1 }; +Value* make_bool(int b) { return b ? &g_true_singleton : &g_false_singleton; } + +static const char *num_who(const char *who) { + if (!who) return "builtin"; + 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); + 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; +} + +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) { + 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; + } + return eigs_num_arg(list->data.list.items[i], who); +} + +double eigs_elem_num(const Value *v, const char *who) { + if (v && v->type == VAL_NUM) return VAL_NUM_RAW(v); + if (g_strict || (v && v->type == VAL_BOOL)) return eigs_num_arg_slow(v, who); + return 0.0; +} + +int eigs_opt_num(const Value *v, double *out, const char *who) { + if (!v || v->type == VAL_NULL) return 0; + if (v->type == VAL_NUM) { *out = VAL_NUM_RAW(v); return 1; } + eigs_num_arg(v, who); + return 0; +} + +/* Two levels: the argument, an element of the argument list, or an element + * of a list argument (`audio_mix of [[true, 1], [1]]`'s samples). Called on a + * failed guard only, so the walk is off the success path. */ +int eigs_arg_has_bool(const Value *arg) { + if (!arg) return 0; + if (arg->type == VAL_BOOL) return 1; + if (arg->type == VAL_LIST) + for (int i = 0; i < arg->data.list.count; i++) { + const Value *e = arg->data.list.items[i]; + if (!e) continue; + if (e->type == VAL_BOOL) return 1; + if (e->type == VAL_LIST) + for (int j = 0; j < e->data.list.count; j++) + if (e->data.list.items[j] && e->data.list.items[j]->type == VAL_BOOL) + return 1; + } + return 0; +} /* ---- NaN-boxing boundary shims ---- * @@ -1773,22 +1860,24 @@ EigsSlot slot_from_value(Value *v) { /* #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 * is dead. */ - double n = v->data.num; + double n = VAL_NUM_RAW(v); val_decref(v); return slot_from_num(n); } + 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/bool -> make_num + * - immediate number -> make_num * - immediate null -> g_null_singleton + * - immediate bool -> the true/false singleton (#1637) * - heap/tracked pointer -> incref and return */ Value* slot_to_value(EigsSlot s) { - if (slot_is_num(s)) return make_num(s.d); + if (slot_is_num(s)) return make_num(SLOT_NUM_RAW(s)); if (slot_is_null(s)) return &g_null_singleton; - if (slot_is_bool(s)) return make_num(slot_as_bool(s) ? 1.0 : 0.0); + if (slot_is_bool(s)) return make_bool(slot_as_bool(s)); if (slot_is_heap(s)) { Value *v = slot_as_ptr(s); val_incref(v); @@ -1818,7 +1907,7 @@ typedef struct { static Value *promote_arena_scalar(Value *v) { if (v->type == VAL_NUM) { if (!sandbox_charge(sizeof(Value))) return NULL; - return make_num_permanent(v->data.num); + return make_num_permanent(VAL_NUM_RAW(v)); } if (v->type == VAL_STR || v->type == VAL_JSON_RAW) { size_t n = val_str_len(v); @@ -1845,8 +1934,9 @@ Value* promote_if_arena(Value *v) { * preserves callers' non-NULL contract without another allocation. */ return h ? h : &g_null_singleton; } - if (v->type == VAL_NULL) { - /* VAL_NULL has a single immortal singleton (g_null_singleton, arena=1). + if (v->type == VAL_NULL || v->type == VAL_BOOL) { + /* VAL_NULL has a single immortal singleton (g_null_singleton, arena=1); + * VAL_BOOL has two (#1637). * Don't allocate a heap copy — incref/decref are already no-ops on it, * and a heap VAL_NULL leaks via slot_bridge_wrap's pointer-drop. */ return v; @@ -2481,7 +2571,7 @@ static Value *module_ns_project(Value *d, Env *e, const char *key, uint32_t h) { /* Exclusive untracked mirror — refresh in place, no allocation. * Same exclusivity test as dict_set_cached_immediate: a mirror * anyone else holds a ref to must not be mutated under them. */ - cur->data.num = s.d; + VAL_NUM_RAW(cur) = SLOT_NUM_RAW(s); env_shared_unlock(e); slot_decref(s); return cur; @@ -2717,8 +2807,9 @@ static Value *chan_clone_rec(Value *v, int depth) { * behavior) rather than overflow the stack. */ if (depth > CHAN_CLONE_MAX_DEPTH) { val_incref(v); return v; } switch (v->type) { - case VAL_NUM: return make_num(v->data.num); + case VAL_NUM: return make_num(VAL_NUM_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); case VAL_LIST: { int n = v->data.list.count; @@ -2920,7 +3011,7 @@ int is_truthy(Value *v) { if (!v) return 0; switch (v->type) { case VAL_NULL: return 0; - case VAL_NUM: return v->data.num != 0.0; + case VAL_NUM: return VAL_NUM_RAW(v) != 0.0; 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; @@ -2929,6 +3020,7 @@ int is_truthy(Value *v) { case VAL_DICT: eigs_module_ns_sync(v); return v->data.dict.count > 0; case VAL_BUFFER: return v->data.buffer.count > 0; case VAL_TEXT_BUILDER: return v->data.text_builder.len > 0; + case VAL_BOOL: return v->data.boolean; } return 0; } @@ -2941,22 +3033,42 @@ int is_truthy(Value *v) { * (no coercion — consistent with the comparison operators). The depth * guard prevents runaway recursion on self-referential containers; beyond * it we fall back to identity. */ -static int values_equal_impl(Value *a, Value *b, int depth) { +/* #1637 item 6: a bool met by a number — at any depth of the two values — + * RAISES instead of answering "not equal", so `(pred of x) == 1` written + * against the old 1/0 predicates fails loudly rather than silently flipping. + * There is NO non-raising mode: the membership builtins (list_contains, + * list_index_of) search with this same comparison and raise the same way, or + * an old 1/0 membership check would flip from found to not-found. `op` names + * 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; if (!a || !b) return 0; - if (a->type != b->type) return 0; + if (a->type != b->type) { + if ((a->type == VAL_BOOL && b->type == VAL_NUM) || + (a->type == VAL_NUM && 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); + else + rt_error(EK_TYPE, 0, "%s: cannot compare %s and %s (a bool is not a number)", + op, val_type_name(a->type), val_type_name(b->type)); + } + return 0; + } if (depth > 64) return a == b; switch (a->type) { - case VAL_NUM: return a->data.num == b->data.num; + case VAL_NUM: return VAL_NUM_RAW(a) == VAL_NUM_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; case VAL_LIST: { if (a->data.list.count != b->data.list.count) return 0; for (int i = 0; i < a->data.list.count; i++) if (!values_equal_impl(a->data.list.items[i], - b->data.list.items[i], depth + 1)) + b->data.list.items[i], depth + 1, op)) return 0; return 1; } @@ -2967,7 +3079,7 @@ static int values_equal_impl(Value *a, Value *b, int depth) { for (int i = 0; i < a->data.dict.count; i++) { Value *bv = dict_get(b, a->data.dict.keys[i]); if (!bv) return 0; - if (!values_equal_impl(a->data.dict.vals[i], bv, depth + 1)) + if (!values_equal_impl(a->data.dict.vals[i], bv, depth + 1, op)) return 0; } return 1; @@ -2997,7 +3109,8 @@ static int values_equal_impl(Value *a, Value *b, int depth) { return a == b; /* unreachable for valid ValType values */ } -int values_equal(Value *a, Value *b) { return values_equal_impl(a, b, 0); } +int values_equal(Value *a, Value *b) { return values_equal_impl(a, b, 0, "=="); } +int values_equal_op(Value *a, Value *b, const char *op) { return values_equal_impl(a, b, 0, op); } /* THE number->text rule, in one place (#875). * @@ -3038,7 +3151,7 @@ char* value_to_string(Value *v) { switch (v->type) { case VAL_NULL: return xstrdup("null"); case VAL_NUM: { - eigs_num_text(buf, sizeof(buf), v->data.num); + eigs_num_text(buf, sizeof(buf), VAL_NUM_RAW(v)); return xstrdup(buf); } case VAL_STR: return xstrdup(v->data.str); @@ -3093,6 +3206,7 @@ char* value_to_string(Value *v) { return xstrdup(buf); case VAL_TEXT_BUILDER: return xstrdup(v->data.text_builder.data ? v->data.text_builder.data : ""); + case VAL_BOOL: return xstrdup(v->data.boolean ? "true" : "false"); } return xstrdup("?"); } @@ -4289,6 +4403,7 @@ static int gc_value_is_node(Value *v) { case VAL_BUILTIN: case VAL_BUFFER: case VAL_TEXT_BUILDER: + case VAL_BOOL: return 0; } return 0; /* unreachable for valid ValType values */ diff --git a/src/eigenscript.h b/src/eigenscript.h index ff399fc6..617f1e18 100644 --- a/src/eigenscript.h +++ b/src/eigenscript.h @@ -145,7 +145,7 @@ typedef enum { TOK_IS, TOK_OF, TOK_DEFINE, TOK_AS, TOK_IF, TOK_ELSE, TOK_ELIF, TOK_LOOP, TOK_WHILE, TOK_RETURN, TOK_AND, TOK_OR, TOK_NOT, - TOK_FOR, TOK_IN, TOK_NULL, + TOK_FOR, TOK_IN, TOK_NULL, TOK_TRUE, TOK_FALSE, /* #1637: inside the run, so `d.true` stays a field */ TOK_WHAT, TOK_WHO, TOK_WHEN, TOK_WHERE, TOK_WHY, TOK_HOW, TOK_PREV, TOK_AT, TOK_CONVERGED, TOK_STABLE, TOK_IMPROVING, TOK_OSCILLATING, TOK_DIVERGING, TOK_EQUILIBRIUM, @@ -195,7 +195,8 @@ typedef enum { AST_INTERROGATE, AST_PREDICATE, 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_SLICE, + AST_BOOL /* #1637: `true`/`false`; the value is data.num (1 or 0) */ } ASTType; typedef struct ASTNode ASTNode; @@ -250,18 +251,30 @@ struct ASTNode { /* ---- Value types ---- */ typedef enum { - VAL_NUM, VAL_STR, VAL_LIST, VAL_FN, VAL_BUILTIN, VAL_NULL, VAL_JSON_RAW, VAL_DICT, VAL_BUFFER, VAL_TEXT_BUILDER + VAL_NUM, VAL_STR, VAL_LIST, VAL_FN, VAL_BUILTIN, VAL_NULL, VAL_JSON_RAW, VAL_DICT, 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 } ValType; typedef struct Value Value; typedef Value* (*BuiltinFn)(Value* arg); +/* #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_)) + /* EigsSlot union — full inline helpers in value_slot.h, which is * included below after the Value struct is fully declared. We need the * raw union here because Env::values is EigsSlot*. */ #ifndef EIGENSCRIPT_EIGSSLOT_UNION_DEFINED #define EIGENSCRIPT_EIGSSLOT_UNION_DEFINED -typedef union { double d; uint64_t u; } EigsSlot; +typedef union { double d_; uint64_t u; } EigsSlot; #endif /* Hash index for O(1) variable lookup. Sits alongside the linear @@ -383,7 +396,15 @@ struct Env { struct Value { ValType type; union { - double num; + /* #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 + * reviewed list tools/num_read_allowlist.txt, pinned by count + * (tools/num_read_check.sh). */ + double num_; + int boolean; /* VAL_BOOL: 0 or 1 (only the two singletons exist) */ /* VAL_STR / VAL_JSON_RAW payload: NUL-terminated bytes. * * #1183: the member is `char *const` on purpose. Every other sequence @@ -1261,10 +1282,83 @@ extern __thread EigsThread *eigs_current; * Deliberately a macro rather than a helper: each call site keeps its own * early `return`, which is what makes the conversion reviewable one guard at * a time instead of a control-flow rewrite. */ +/* #1637: a bool a builtin does not take raises in EVERY strict mode. The + * EIGS_STRICT=0 opt-out keeps the soft stand-in for other wrong types, but a + * soft answer for a bool is a silent wrong value (`abs of true` was 0, so true + * read as 0 -- the old 1/0 idiom flipping without a word). So a failed guard + * raises under EIGS_STRICT=0 too when the argument, or a top-level element of + * the argument list, is a bool. The oracle is tests/test_bool_fuzz.sh: true + * 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: 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 + * argument is a bool, or one of the first EIGS_BOOL_GATE_SCAN top-level + * elements of an argument list is, the builtin's declared bool policy + * (k_bool_policy in builtins.c: which positions may hold a bool) decides; an + * undeclared position raises a type error in every strict mode and the + * builtin is not called. Names outside the core registry (host functions + * from eigs_register_function, compiled natives) are not gated. The fast + * path is a type test, plus at most EIGS_BOOL_GATE_SCAN loads for a list. */ +#define EIGS_BOOL_GATE_SCAN 8 +int eigs_bool_gate_slow(BuiltinFn fn, const Value *arg); +void eigs_bool_gate_exempt(BuiltinFn fn); /* a host function: never gated */ +static inline int eigs_bool_gate(BuiltinFn fn, const Value *arg) { + if (!arg) return 0; + if (arg->type == VAL_BOOL) return eigs_bool_gate_slow(fn, arg); + if (arg->type != VAL_LIST) return 0; + int n = arg->data.list.count < EIGS_BOOL_GATE_SCAN ? arg->data.list.count + : EIGS_BOOL_GATE_SCAN; + for (int i = 0; i < n; i++) { + const Value *e = arg->data.list.items[i]; + if (e && e->type == VAL_BOOL) return eigs_bool_gate_slow(fn, arg); + } + return 0; +} +static inline Value *eigs_call_builtin(BuiltinFn fn, Value *arg) { + return eigs_bool_gate(fn, arg) ? NULL : fn(arg); +} +/* #1637: for a builtin that reads a number without a typed guard (its other + * wrong types answer null): a bool there raises in every strict mode. */ +#define BOOL_REFUSE(v, who) \ + do { \ + if (eigs_arg_has_bool(v)) { \ + rt_error(EK_TYPE, 0, "%s: expected a number, got a bool", (who)); \ + return make_null(); \ + } \ + } while (0) +#define EIGS_GUARD_RAISES(want) \ + ((void)(want), g_strict || eigs_arg_has_bool(arg)) + #define ARG_GUARD(cond, who, want, soft) \ do { \ if (cond) { \ - if (g_strict) { \ + if (EIGS_GUARD_RAISES(want)) { \ rt_error(EK_TYPE, 0, "%s: expected %s", (who), (want)); \ return make_null(); \ } \ @@ -1283,7 +1377,7 @@ extern __thread EigsThread *eigs_current; #define ARG_GUARD_TAPED(cond, who, want, soft) \ do { \ if (cond) { \ - if (g_strict) { \ + if (EIGS_GUARD_RAISES(want)) { \ rt_error(EK_TYPE, 0, "%s: expected %s", (who), (want)); \ return make_null(); \ } \ @@ -1298,7 +1392,7 @@ extern __thread EigsThread *eigs_current; #define ARG_GUARD_PRETAKE(cond, who, want, soft) \ do { \ if (cond) { \ - if (g_strict) { \ + if (EIGS_GUARD_RAISES(want)) { \ rt_error(EK_TYPE, 0, "%s: expected %s", (who), (want)); \ return make_null(); \ } \ @@ -1353,7 +1447,7 @@ extern __thread EigsThread *eigs_current; #define STRICT_REQUIRE(cond, who, want) \ do { \ - if (g_strict && (cond)) { \ + if ((cond) && EIGS_GUARD_RAISES(want)) { \ rt_error(EK_TYPE, 0, "%s: expected %s", (who), (want)); \ return make_null(); \ } \ @@ -1624,6 +1718,8 @@ Value* make_str_len(const char *s, size_t n); /* #1183: caller knows strlen(s) Value* make_str_owned(char *s); Value* make_str_owned_len(char *s, size_t n); /* #1183: caller knows strlen(s) */ 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_list_heap(int capacity); Value* make_text_builder(void); @@ -2056,6 +2152,9 @@ int is_truthy(Value *v); /* Structural equality for == / != (recursive for lists/dicts/buffers; * identity for functions/builtins; no cross-type coercion). */ int values_equal(Value *a, Value *b); +/* #1637: both raise on a bool meeting a number; `op` names the operator or + * builtin in the message. */ +int values_equal_op(Value *a, Value *b, const char *op); char* value_to_string(Value *v); /* #875: THE number->text rule. Every producer of number text calls this — * `str of`, all three JSON encoders, the SIGUSR1 observer dump — so a copy diff --git a/src/eigs_embed.c b/src/eigs_embed.c index aef9e82d..11f59924 100644 --- a/src/eigs_embed.c +++ b/src/eigs_embed.c @@ -301,6 +301,7 @@ EigsValue *eigs_get_global(const char *name) { EigsValue *eigs_value_new_num(double n) { return make_num(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); } EigsValue *eigs_value_new_list(int capacity) { return make_list(capacity > 0 ? capacity : 0); } EigsValue *eigs_value_new_dict(int capacity) { return make_dict(capacity > 0 ? capacity : 0); } @@ -318,6 +319,7 @@ EigsValueType eigs_value_type(EigsValue *v) { case VAL_FN: case VAL_BUILTIN: return EIGS_TYPE_FN; case VAL_BUFFER: return EIGS_TYPE_BUFFER; + case VAL_BOOL: return EIGS_TYPE_BOOL; /* #1637 */ /* No public embed-API mapping. Enumerated rather than covered by a * `default:` so -Werror=switch forces a new ValType to choose one. */ case VAL_JSON_RAW: @@ -326,8 +328,17 @@ EigsValueType eigs_value_type(EigsValue *v) { return EIGS_TYPE_OTHER; /* unreachable for valid ValType values */ } +int eigs_value_as_bool(EigsValue *v) { + return (v && v->type == VAL_BOOL) ? v->data.boolean : 0; +} + +/* #1637: a bool answers NaN, not 0.0, so a host that reads a bool as a + * number sees the mistake propagate (NaN poisons the arithmetic) instead of a + * 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) { - return (v && v->type == VAL_NUM) ? v->data.num : 0.0; + if (v && v->type == VAL_BOOL) return NAN; + return (v && v->type == VAL_NUM) ? VAL_NUM_RAW(v) : 0.0; } const char *eigs_value_as_string(EigsValue *v) { @@ -419,6 +430,20 @@ void eigs_trace_record_nondet(const char *name, EigsValue *v) { if (g_trace_enabled) trace_nondet_value(name, (Value *)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_LIST, VAL_LIST}, {EIGS_TYPE_DICT, VAL_DICT}, + {EIGS_TYPE_BUFFER, VAL_BUFFER}, {EIGS_TYPE_BOOL, VAL_BOOL}, + }; + unsigned vk = 0, seen = 0; + for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) + if (kinds & EIGS_KIND(map[i].e)) { vk |= 1u << map[i].v; seen |= EIGS_KIND(map[i].e); } + if (kinds & ~seen) return 0; /* FN / OTHER cannot be on a tape */ + return trace_declare_kind(name, vk); +} + /* ---- Async abort (see eigs_embed.h) -------------------------------- */ void eigs_set_abort_flag(volatile int *flag) { @@ -438,6 +463,7 @@ void eigs_register_function(const char *name, EigsHostFn fn) { * refuses to enter opaque host code with incomplete history. */ obs_flag_store(eval_host_callbacks, 1); eigs_obs_enable_runtime(); + eigs_bool_gate_exempt((BuiltinFn)fn); /* #1637: host functions are not bool-gated */ Value *bv = make_builtin((BuiltinFn)fn); /* #1388: a registered function joins the builtin layer too, so module * code sees it (and the host's own rebinding of the name stays out of diff --git a/src/eigs_embed.h b/src/eigs_embed.h index ed16d16e..95ec339c 100644 --- a/src/eigs_embed.h +++ b/src/eigs_embed.h @@ -154,19 +154,22 @@ typedef enum { EIGS_TYPE_DICT, EIGS_TYPE_FN, EIGS_TYPE_BUFFER, - EIGS_TYPE_OTHER + EIGS_TYPE_OTHER, + EIGS_TYPE_BOOL /* #1637; appended so the earlier values keep their numbers */ } EigsValueType; EigsValue *eigs_value_new_num(double n); 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 */ EigsValue *eigs_value_new_list(int capacity); EigsValue *eigs_value_new_dict(int capacity); void eigs_value_retain(EigsValue *v); void eigs_value_release(EigsValue *v); EigsValueType eigs_value_type(EigsValue *v); -double eigs_value_as_num(EigsValue *v); /* 0.0 if not num */ +double eigs_value_as_num(EigsValue *v); /* NaN for a bool (#1637), 0.0 for any other non-num */ +int eigs_value_as_bool(EigsValue *v); /* #1637: 1 for true, 0 for false or not a bool */ const char *eigs_value_as_string(EigsValue *v); /* NULL if not str; borrowed */ int eigs_value_list_len(EigsValue *v); @@ -250,7 +253,11 @@ void eigs_value_buffer_set(EigsValue *v, int i, double x); /* OOB: no- * logged-and-tolerated. * * Host builtins participate through the take/record pair — the same - * contract the runtime's own nondet builtins use: + * contract the runtime's own nondet builtins use — after declaring their + * return kinds once, at registration: + * + * eigs_register_function("my_sensor", my_sensor); + * eigs_trace_declare_kind("my_sensor", EIGS_KIND(EIGS_TYPE_NUM)); * * EigsValue *my_sensor(EigsValue *arg) { * EigsValue *v; @@ -279,6 +286,15 @@ int eigs_set_replay_tape(const char *bytes, size_t len, int strict); int eigs_replay_advance_session(void); int eigs_replay_take(const char *name, EigsValue **out); /* 1 = served */ void eigs_trace_record_nondet(const char *name, EigsValue *v); +/* #1637: declare, when registering a host nondet function, the kinds its + * recorded value can have: EIGS_KIND(EIGS_TYPE_NUM) | EIGS_KIND(EIGS_TYPE_NULL). + * Replay refuses (exit 3) a record of any other kind, and a host name that + * was never declared -- a hand-edited `my_sensor=true` cannot replay as a + * bool into code that reads a number. Returns 0 (nothing changed) for an + * empty name, no kinds, a FN/OTHER kind, or a runtime builtin's name (those + * are declared by the runtime). Declaring again replaces the kinds. */ +#define EIGS_KIND(t) (1u << (unsigned)(t)) +int eigs_trace_declare_kind(const char *name, unsigned kinds); /* ---- Async abort ----------------------------------------------------- * Register a flag the host may set from an interrupt/signal context (a diff --git a/src/embed_smoke.c b/src/embed_smoke.c index 6ea856a5..b7f45ccc 100644 --- a/src/embed_smoke.c +++ b/src/embed_smoke.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -193,8 +194,8 @@ static void test_worker_exit_lifecycle(void) { EigsValue *handle = eigs_get_global("worker"); CHECK(handle && handle->type == VAL_DICT, "exit fixture owns join handle"); if (handle && handle->type == VAL_DICT) { - f.handle_id = (int)dict_get(handle, "_handle_id")->data.num; - f.handle_gen = (uint32_t)dict_get(handle, "_handle_gen")->data.num; + f.handle_id = (int)VAL_NUM_RAW(dict_get(handle, "_handle_id")); + f.handle_gen = (uint32_t)VAL_NUM_RAW(dict_get(handle, "_handle_gen")); pthread_t requester; int made = pthread_create(&requester, NULL, exit_after_join_claim, &f) == 0; CHECK(made, "exit fixture starts request coordinator"); @@ -278,10 +279,15 @@ static EigsValue *host_add(EigsValue *arg) { if (eigs_value_list_len(arg) != 2) return eigs_value_new_null(); EigsValue *a = eigs_value_list_get(arg, 0); EigsValue *b = eigs_value_list_get(arg, 1); - double sum = eigs_value_as_num(a) + eigs_value_as_num(b); + /* Check the types: eigs_value_as_num answers NaN for a bool and 0.0 for + * other non-numbers, so an unchecked read turns `host_add of [true, 1]` + * into a wrong number. */ + EigsValue *r = (eigs_value_type(a) == EIGS_TYPE_NUM && eigs_value_type(b) == EIGS_TYPE_NUM) + ? eigs_value_new_num(eigs_value_as_num(a) + eigs_value_as_num(b)) + : eigs_value_new_null(); eigs_value_release(a); eigs_value_release(b); - return eigs_value_new_num(sum); + return r; } /* #1434: runs an eval that raises on its line 5, swallows that error, then @@ -323,18 +329,18 @@ int main(void) { int line_save = g_trace_current_line; g_trace_current_line = 10; - s.d = 11.0; trace_assign(NM, s); + SLOT_NUM_RAW(s) = 11.0; trace_assign(NM, s); g_trace_current_line = 20; - s.d = 22.0; trace_assign(NM, s); + SLOT_NUM_RAW(s) = 22.0; trace_assign(NM, s); - CHECK(trace_query_prev(NM, &out) && out.d == 11.0, + CHECK(trace_query_prev(NM, &out) && SLOT_NUM_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) && out.d == 11.0, + CHECK(trace_query_at(0, NM, 15, &out) && SLOT_NUM_RAW(out) == 11.0, "#830: what-at from a non-compiler producer (line-10 value)"); - CHECK(trace_query_at(0, NM, 99, &out) && out.d == 22.0, + CHECK(trace_query_at(0, NM, 99, &out) && SLOT_NUM_RAW(out) == 22.0, "#830: what-at past the last non-compiler assignment"); - CHECK(trace_query_at(2, NM, 99, &out) && out.d == 2.0, + CHECK(trace_query_at(2, NM, 99, &out) && SLOT_NUM_RAW(out) == 2.0, "#830: when-at counts non-compiler assignments"); g_trace_current_line = line_save; @@ -477,6 +483,19 @@ int main(void) { CHECK(r && eigs_value_type(r) == EIGS_TYPE_NUM, "FFI result is num"); CHECK(r && eigs_value_as_num(r) == 7.0, "host_add(3,4) == 7"); eigs_value_release(r); + /* #1637: a bool reads as NaN through eigs_value_as_num (never 0.0); the + * typed host function refuses it; other non-numbers keep 0.0. */ + r = eigs_eval_string("5 > 3"); + CHECK(r && eigs_value_type(r) == EIGS_TYPE_BOOL && isnan(eigs_value_as_num(r)), + "eigs_value_as_num of a bool is NaN"); + CHECK(r && eigs_value_as_bool(r) == 1, "eigs_value_as_bool reads the bool"); + eigs_value_release(r); + r = eigs_eval_string("\"s\""); + CHECK(r && eigs_value_as_num(r) == 0.0, "eigs_value_as_num of a string stays 0.0"); + eigs_value_release(r); + r = eigs_eval_string("host_add of [true, 1]"); + CHECK(r && eigs_value_type(r) == EIGS_TYPE_NULL, "host_add refuses a bool operand"); + eigs_value_release(r); /* --- #1387: embed API errors do not inherit an eval's last line. -- */ { @@ -812,7 +831,7 @@ int main(void) { "nf_matrix[0] == 10 and nf_matrix[1] == 20 and " "nf_matrix[2] == 30 and nf_matrix[3] == 40 and " "nf_list_matrix == [[10, 20], [30, 40]]"); - CHECK(r != NULL && eigs_value_as_num(r) == 1.0 && !eigs_has_error(), + CHECK(r != NULL && eigs_value_as_bool(r) && !eigs_has_error(), "#1417 failed index read performs no callback or matrix write"); eigs_value_release(r); } @@ -880,6 +899,7 @@ int main(void) { /* --- Trace tape seam: record via the sink, replay from memory. --- */ eigs_register_function("host_sensor", host_sensor); + eigs_trace_declare_kind("host_sensor", EIGS_KIND(EIGS_TYPE_NUM)); /* #1637 */ eigs_set_trace_sink(tape_sink, NULL); r = eigs_eval_string("s1 is host_sensor of []\n" "s2 is host_sensor of []\n" @@ -894,7 +914,7 @@ int main(void) { { static const char *const NM = "native_after_callback"; EigsSlot s; - s.d = 1441.0; + SLOT_NUM_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, diff --git a/src/ext_db.c b/src/ext_db.c index ea10ce4d..9c25f737 100644 --- a/src/ext_db.c +++ b/src/ext_db.c @@ -10,8 +10,18 @@ #define DB_MAX_PARAMS 16 +/* Postgres's boolean type OID (pinned by the wire protocol; see the + * DB_OID_* block below, which carries the same value). */ +#define DB_PARAM_OID_BOOL 16 + +/* types[i] is each parameter's type OID: 0 lets the server infer it from the + * SQL (strings and numbers, text format), DB_PARAM_OID_BOOL binds a bool as + * an SQL boolean (#1637: a bool read from a boolean column must be writable + * back, and must not become the text 'true'/'false' an untyped context + * would keep). */ static int db_build_query(Value *arg, const char **sql, int *nparams, - const char **params, char numbuf[DB_MAX_PARAMS][64]) { + const char **params, char numbuf[DB_MAX_PARAMS][64], + unsigned int types[DB_MAX_PARAMS]) { if (arg && arg->type == VAL_STR) { *sql = arg->data.str; *nparams = 0; @@ -38,13 +48,17 @@ static int db_build_query(Value *arg, const char **sql, int *nparams, } for (int i = 0; i < *nparams; i++) { Value *v = arg->data.list.items[i + 1]; - if (v && v->type == VAL_STR) { + types[i] = 0; + if (v && v->type == VAL_BOOL) { + params[i] = v->data.boolean ? "true" : "false"; + 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", v->data.num); + snprintf(numbuf[i], 64, "%g", eigs_num_arg(v, __func__)); params[i] = numbuf[i]; } else { - rt_error(EK_TYPE, 0, "db: parameter %d is not a string or number (got %s)", + rt_error(EK_TYPE, 0, "db: parameter %d is not a string, number or bool (got %s)", i + 1, v ? val_type_name(v->type) : "null"); return 0; } @@ -56,11 +70,12 @@ static PGresult* db_exec_from_arg(Value *arg) { const char *sql = ""; const char *params[DB_MAX_PARAMS]; char numbuf[DB_MAX_PARAMS][64]; + unsigned int types[DB_MAX_PARAMS]; int nparams = 0; - if (!db_build_query(arg, &sql, &nparams, params, numbuf)) return NULL; + if (!db_build_query(arg, &sql, &nparams, params, numbuf, types)) return NULL; if (nparams == 0) return PQexec(g_db_conn, sql); - return PQexecParams(g_db_conn, sql, nparams, NULL, params, NULL, NULL, 0); + return PQexecParams(g_db_conn, sql, nparams, (const Oid *)types, params, NULL, NULL, 0); } Value* builtin_db_connect(Value *arg) { @@ -279,7 +294,7 @@ Value* builtin_db_query_value(Value *arg) { Value *result = NULL; switch (db_classify(oid)) { case DBT_BOOL: - result = make_num(text[0] == 't' ? 1 : 0); + 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) */ diff --git a/src/ext_gfx.c b/src/ext_gfx.c index c9bcae8e..8651045d 100644 --- a/src/ext_gfx.c +++ b/src/ext_gfx.c @@ -389,7 +389,7 @@ static const char* scancode_name(int sc) { /* ---- Builtins ---- */ -/* #1007: the drawing surface reads RUNS of list elements as `.data.num` +/* #1007: the drawing surface reads RUNS of list elements as `eigs_num_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 @@ -448,31 +448,31 @@ Value* builtin_gfx_open(Value *arg) { * so "you called it wrong" and "this machine has no SDL" were the * same value. */ ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 3, - "gfx_open", "[number width, number height, title]", make_num(0)); - /* #1007: width and height were read as `.data.num` with no type check + "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 * 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(arg->data.list.items[0]->type != VAL_NUM || arg->data.list.items[1]->type != VAL_NUM, - "gfx_open", "[number width, number height, title]", make_num(0)); - int w = (int)arg->data.list.items[0]->data.num; - int h = (int)arg->data.list.items[1]->data.num; + "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__); const char *title = arg->data.list.items[2]->type == VAL_STR ? arg->data.list.items[2]->data.str : "EigenScript"; if (!load_sdl2()) { fprintf(stderr, "gfx_open: cannot load libSDL2\n"); - return make_num(0); /* fs:ANSWER 0 is gfx_open's open-failed result -- line 422 returns 1 only after a renderer exists; libSDL2 absent is environment state, not a bad argument */ + return make_bool(0); /* fs:ANSWER 0 is gfx_open's open-failed result -- line 422 returns 1 only after a renderer exists; libSDL2 absent is environment state, not a bad argument */ } if (p_SDL_Init(MY_SDL_INIT_VIDEO) < 0) { fprintf(stderr, "gfx_open: SDL_Init failed: %s\n", p_SDL_GetError()); - return make_num(0); /* fs:ANSWER SDL_Init failed -- same 0-vs-1 open result as line 422, detail printed on stderr above */ + return make_bool(0); /* fs:ANSWER SDL_Init failed -- same 0-vs-1 open result as line 422, detail printed on stderr above */ } g_window = p_SDL_CreateWindow(title, MY_SDL_WINDOWPOS_CENTERED, MY_SDL_WINDOWPOS_CENTERED, w, h, MY_SDL_WINDOW_RESIZABLE); if (!g_window) { fprintf(stderr, "gfx_open: SDL_CreateWindow failed: %s\n", p_SDL_GetError()); - return make_num(0); /* fs:ANSWER SDL_CreateWindow failed -- 0 open result; g_window stays NULL so every later gfx builtin no-ops */ + return make_bool(0); /* fs:ANSWER SDL_CreateWindow failed -- 0 open result; g_window stays NULL so every later gfx builtin no-ops */ } g_renderer = p_SDL_CreateRenderer(g_window, -1, MY_SDL_RENDERER_ACCELERATED | MY_SDL_RENDERER_PRESENTVSYNC); if (!g_renderer) { @@ -482,10 +482,10 @@ Value* builtin_gfx_open(Value *arg) { fprintf(stderr, "gfx_open: SDL_CreateRenderer failed\n"); p_SDL_DestroyWindow(g_window); g_window = NULL; - return make_num(0); /* fs:ANSWER SDL_CreateRenderer failed on both attempts -- 0 open result; the window is destroyed and g_window NULLed first */ + return make_bool(0); /* fs:ANSWER SDL_CreateRenderer failed on both attempts -- 0 open result; the window is destroyed and g_window NULLed first */ } p_SDL_SetRenderDrawBlendMode(g_renderer, MY_SDL_BLENDMODE_BLEND); - return make_num(1); + return make_bool(1); } /* gfx_close of null */ @@ -528,9 +528,9 @@ Value* builtin_gfx_clear(Value *arg) { 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)arg->data.list.items[0]->data.num; - g = (int)arg->data.list.items[1]->data.num; - b = (int)arg->data.list.items[2]->data.num; + 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); p_SDL_RenderClear(g_renderer); @@ -547,14 +547,14 @@ Value* builtin_gfx_rect(Value *arg) { 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)arg->data.list.items[0]->data.num; - rect.y = (int)arg->data.list.items[1]->data.num; - rect.w = (int)arg->data.list.items[2]->data.num; - rect.h = (int)arg->data.list.items[3]->data.num; - int r = (int)arg->data.list.items[4]->data.num; - int g = (int)arg->data.list.items[5]->data.num; - int b = (int)arg->data.list.items[6]->data.num; - int a = (arg->data.list.count >= 8) ? (int)arg->data.list.items[7]->data.num : 255; + 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 */ @@ -569,13 +569,13 @@ Value* builtin_gfx_line(Value *arg) { "[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)arg->data.list.items[0]->data.num; - int y1 = (int)arg->data.list.items[1]->data.num; - int x2 = (int)arg->data.list.items[2]->data.num; - int y2 = (int)arg->data.list.items[3]->data.num; - int r = (int)arg->data.list.items[4]->data.num; - int g = (int)arg->data.list.items[5]->data.num; - int b = (int)arg->data.list.items[6]->data.num; + 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 */ @@ -589,11 +589,11 @@ Value* builtin_gfx_point(Value *arg) { "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)arg->data.list.items[0]->data.num; - int y = (int)arg->data.list.items[1]->data.num; - int r = (int)arg->data.list.items[2]->data.num; - int g = (int)arg->data.list.items[3]->data.num; - int b = (int)arg->data.list.items[4]->data.num; + 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 */ @@ -608,13 +608,13 @@ Value* builtin_gfx_circle(Value *arg) { "[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)arg->data.list.items[0]->data.num; - int cy = (int)arg->data.list.items[1]->data.num; - int radius = (int)arg->data.list.items[2]->data.num; - int r = (int)arg->data.list.items[3]->data.num; - int g = (int)arg->data.list.items[4]->data.num; - int b = (int)arg->data.list.items[5]->data.num; - int a = (arg->data.list.count >= 7) ? (int)arg->data.list.items[6]->data.num : 255; + 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++) { @@ -635,15 +635,15 @@ Value* builtin_gfx_rrect(Value *arg) { "[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)arg->data.list.items[0]->data.num; - int y = (int)arg->data.list.items[1]->data.num; - int w = (int)arg->data.list.items[2]->data.num; - int h = (int)arg->data.list.items[3]->data.num; - int rad = (int)arg->data.list.items[4]->data.num; - int r = (int)arg->data.list.items[5]->data.num; - int g = (int)arg->data.list.items[6]->data.num; - int b = (int)arg->data.list.items[7]->data.num; - int a = (arg->data.list.count >= 9) ? (int)arg->data.list.items[8]->data.num : 255; + 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; @@ -689,10 +689,10 @@ Value* builtin_gfx_clip(Value *arg) { return make_null(); /* fs:VOID the clip was cleared -- gfx_clip's normal successful answer */ } SDL_Rect clip; - clip.x = (int)arg->data.list.items[0]->data.num; - clip.y = (int)arg->data.list.items[1]->data.num; - clip.w = (int)arg->data.list.items[2]->data.num; - clip.h = (int)arg->data.list.items[3]->data.num; + 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 */ } @@ -721,8 +721,8 @@ Value* builtin_gfx_read(Value *arg) { if (!g_renderer || !p_SDL_RenderReadPixels) TRACE_NONDET_RECORD("gfx_read", make_null()); SDL_Rect r; - r.x = (int)arg->data.list.items[0]->data.num; - r.y = (int)arg->data.list.items[1]->data.num; + r.x = (int)eigs_list_num(arg, 0, __func__); + r.y = (int)eigs_list_num(arg, 1, __func__); r.w = 1; r.h = 1; Uint32 px = 0; @@ -743,14 +743,14 @@ Value* builtin_gfx_present(Value *arg) { return make_null(); /* fs:VOID gfx_present answers null on every path -- this is the return value, not a stand-in for a rejected argument */ } -/* Attach keyboard modifier state as shift/ctrl/alt (0/1) dict fields. +/* Attach keyboard modifier state as shift/ctrl/alt bool dict fields (#1637). * KMOD_SHIFT = 0x0003, KMOD_CTRL = 0x00C0, KMOD_ALT = 0x0300. * Key events read the mask from keysym.mod; mouse/wheel events (#568) * pass SDL_GetModState() — SDL keeps it current at mouse-event time. */ static void poll_set_mods(Value *d, int mod) { - dict_set_owned(d, "shift", make_num((mod & 0x03) ? 1 : 0)); - dict_set_owned(d, "ctrl", make_num((mod & 0xC0) ? 1 : 0)); - dict_set_owned(d, "alt", make_num((mod & 0x300) ? 1 : 0)); + dict_set_owned(d, "shift", make_bool((mod & 0x03) != 0)); + dict_set_owned(d, "ctrl", make_bool((mod & 0xC0) != 0)); + dict_set_owned(d, "alt", make_bool((mod & 0x300) != 0)); } static int poll_mod_state(void) { @@ -854,7 +854,7 @@ Value* builtin_gfx_ticks(Value *arg) { Value* builtin_gfx_delay(Value *arg) { ARG_GUARD(!arg || arg->type != VAL_NUM, "gfx_delay", "number milliseconds", make_null()); - if (g_sdl_lib) p_SDL_Delay((Uint32)arg->data.num); + if (g_sdl_lib) p_SDL_Delay((Uint32)eigs_num_arg(arg, __func__)); 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 */ } @@ -985,7 +985,7 @@ Value* builtin_gfx_text(Value *arg) { * 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)items[6]->data.num : 1; + * int scale = (count >= 7) ? (int)eigs_num_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. @@ -1002,13 +1002,13 @@ Value* builtin_gfx_text(Value *arg) { "[number x, number y, string text, number r, number g, number b] and an optional number scale", 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)arg->data.list.items[0]->data.num; - int y = (int)arg->data.list.items[1]->data.num; + int x = (int)eigs_list_num(arg, 0, __func__); + int y = (int)eigs_list_num(arg, 1, __func__); const char *text = arg->data.list.items[2]->data.str; - int r = (int)arg->data.list.items[3]->data.num; - int g = (int)arg->data.list.items[4]->data.num; - int b = (int)arg->data.list.items[5]->data.num; - int scale = (arg->data.list.count >= 7) ? (int)arg->data.list.items[6]->data.num : 1; + 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 @@ -1075,7 +1075,7 @@ Value* builtin_gfx_text_width(Value *arg) { text = arg->data.list.items[0]->data.str; if (arg->data.list.count >= 2) { if (arg->data.list.items[1]->type == VAL_NUM) - scale = (int)arg->data.list.items[1]->data.num; + scale = (int)eigs_list_num(arg, 1, __func__); else bad_scale = 1; } @@ -1113,10 +1113,10 @@ Value* builtin_gfx_text_height(Value *arg) { && arg->data.list.items[0]->type == VAL_NUM), "gfx_text_height", "number scale, [number scale] or null"); if (arg && arg->type == VAL_NUM) { - scale = (int)arg->data.num; + scale = (int)eigs_num_arg(arg, __func__); } else if (arg && arg->type == VAL_LIST && arg->data.list.count >= 1 && arg->data.list.items[0]->type == VAL_NUM) { - scale = (int)arg->data.list.items[0]->data.num; + scale = (int)eigs_list_num(arg, 0, __func__); } if (scale < 1) scale = 1; if (ttf_available()) { @@ -1225,7 +1225,7 @@ static int16_t* audio_convert_samples(Value *samples, int *out_n) { free(buf); return NULL; } - double s = v->data.num; + double s = eigs_num_arg(v, __func__); if (s > 1.0) s = 1.0; if (s < -1.0) s = -1.0; buf[i] = (int16_t)(s * 32767); @@ -1309,7 +1309,7 @@ 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 `.data.num` type-pun as gfx_open. That it is an + /* The same unchecked `eigs_num_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` * before reading it. */ @@ -1333,8 +1333,8 @@ Value* builtin_audio_open(Value *arg) { 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)arg->data.list.items[0]->data.num; - want.channels = (int)arg->data.list.items[1]->data.num; + want.freq = (int)eigs_list_num(arg, 0, __func__); + want.channels = (int)eigs_list_num(arg, 1, __func__); } g_audio_device = p_SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); @@ -1364,7 +1364,7 @@ Value* builtin_audio_pause(Value *arg) { STRICT_REQUIRE(arg && arg->type != VAL_NULL && arg->type != VAL_NUM, "audio_pause", "number flag (1 = pause, 0 = unpause) or 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)arg->data.num : 1; + int pause = (arg && arg->type == VAL_NUM) ? (int)eigs_num_arg(arg, __func__) : 1; p_SDL_PauseAudioDevice(g_audio_device, 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 */ } @@ -1479,8 +1479,8 @@ Value* builtin_audio_capture_open(Value *arg) { 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)arg->data.list.items[0]->data.num; - want.channels = (int)arg->data.list.items[1]->data.num; + want.freq = (int)eigs_list_num(arg, 0, __func__); + want.channels = (int)eigs_list_num(arg, 1, __func__); } if (g_capture_device) { @@ -1606,8 +1606,8 @@ Value* builtin_audio_stream_open(Value *arg) { if (arg && arg->type == VAL_LIST && arg->data.list.count >= 2 && arg->data.list.items[0]->type == VAL_NUM && arg->data.list.items[1]->type == VAL_NUM) { - want.freq = (int)arg->data.list.items[0]->data.num; - want.channels = (int)arg->data.list.items[1]->data.num; + want.freq = (int)eigs_list_num(arg, 0, __func__); + want.channels = (int)eigs_list_num(arg, 1, __func__); } if (g_stream_device) { @@ -1632,7 +1632,7 @@ Value* builtin_audio_stream_push(Value *arg) { /* #1007 round 3: above the device check, for the reason audio_play's is. */ STRICT_REQUIRE(gfx_bad_samples(arg), "audio_stream_push", "a list or buffer of samples, or null"); - if (!g_stream_device) return make_num(0); /* fs:ANSWER BUILTINS.md audio_stream_push: "0 on a closed device"; g_stream_device == 0 is device state, not an argument */ + if (!g_stream_device) return make_bool(0); /* fs:ANSWER BUILTINS.md audio_stream_push: "0 on a closed device"; g_stream_device == 0 is device state, not an argument */ int n = 0; int16_t *buf = audio_convert_samples(arg, &n); if (!buf) { @@ -1641,15 +1641,15 @@ Value* builtin_audio_stream_push(Value *arg) { * audio_convert_samples, while an empty or over-64MB clip has not and * is a documented "nothing to play". */ /* 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_bool(0); /* fs:ANSWER an empty or over-64MB clip plays nothing, so 0 (no * channel) is the result, not a laundered argument mistake. */ - return make_num(0); + return make_bool(0); } int rc = p_SDL_QueueAudio(g_stream_device, buf, (Uint32)((size_t)n * sizeof(int16_t))); free(buf); - return make_num(rc == 0 ? 1 : 0); + return make_bool(rc == 0); } /* audio_stream_queued of null — samples still buffered (not yet played) @@ -1698,7 +1698,7 @@ Value* builtin_audio_music_play(Value *arg) { /* #1007: both #971 deferral markers converted. Above load_sdl2(), the [135] rule. */ ARG_GUARD(!arg || arg->type != VAL_LIST || arg->data.list.count < 1 || arg->data.list.items[0]->type != VAL_STR, - "audio_music_play", "[string path, number loops]", make_num(0)); + "audio_music_play", "[string path, number loops]", make_bool(0)); /* 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 @@ -1706,10 +1706,10 @@ Value* builtin_audio_music_play(Value *arg) { "audio_music_play", "[string path, number loops]"); const char *path = arg->data.list.items[0]->data.str; int loops = (arg->data.list.count >= 2 && arg->data.list.items[1]->type == VAL_NUM) - ? (int)arg->data.list.items[1]->data.num : -1; - if (!load_sdl2()) return make_num(0); /* fs:ANSWER the header's documented "0 on failure (missing mixer lib ...)" -- libSDL2 absent is environment state, not an argument */ + ? (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_num(0); /* fs:ANSWER SDL2_mixer not loadable -- the documented missing-mixer-lib 0 */ + if (!load_sdl_mixer()) return make_bool(0); /* fs:ANSWER SDL2_mixer not loadable -- the documented missing-mixer-lib 0 */ if (!g_mixer_open) { p_Mix_Init(MY_MIX_INIT_MP3); /* Mix_OpenAudioDevice (not the legacy Mix_OpenAudio) so the music @@ -1717,7 +1717,7 @@ Value* builtin_audio_music_play(Value *arg) { if (p_Mix_OpenAudioDevice(44100, MY_AUDIO_S16SYS, 2, 2048, NULL, 0) < 0) { fprintf(stderr, "audio_music: Mix_OpenAudioDevice failed: %s\n", p_SDL_GetError ? p_SDL_GetError() : "?"); - return make_num(0); /* fs:ANSWER Mix_OpenAudioDevice failed (no audio device) -- documented 0, detail on stderr above */ + return make_bool(0); /* fs:ANSWER Mix_OpenAudioDevice failed (no audio device) -- documented 0, detail on stderr above */ } g_mixer_open = 1; } @@ -1726,14 +1726,14 @@ Value* builtin_audio_music_play(Value *arg) { if (!g_music) { fprintf(stderr, "audio_music: cannot load '%s': %s\n", path, p_SDL_GetError ? p_SDL_GetError() : "?"); - return make_num(0); /* fs:ANSWER Mix_LoadMUS failed -- the documented unreadable/undecodable-file 0 */ + return make_bool(0); /* fs:ANSWER Mix_LoadMUS failed -- the documented unreadable/undecodable-file 0 */ } if (p_Mix_PlayMusic(g_music, loops) < 0) { fprintf(stderr, "audio_music: play failed: %s\n", p_SDL_GetError ? p_SDL_GetError() : "?"); - return make_num(0); /* fs:ANSWER Mix_PlayMusic failed -- documented 0; line 1378 returns 1 only when the track is actually playing */ + return make_bool(0); /* fs:ANSWER Mix_PlayMusic failed -- documented 0; line 1378 returns 1 only when the track is actually playing */ } - return make_num(1); + return make_bool(1); } /* audio_music_volume of v — music volume 0..128 */ @@ -1747,10 +1747,10 @@ Value* builtin_audio_music_volume(Value *arg) { "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)arg->data.num; + 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 && arg->data.list.items[0]->type == VAL_NUM) - v = (int)arg->data.list.items[0]->data.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; p_Mix_VolumeMusic(v); @@ -1800,7 +1800,7 @@ Value* builtin_audio_play_loop(Value *arg) { || arg->data.list.items[1]->type != 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 = arg->data.list.items[1]->data.num; + double loops_d = eigs_list_num(arg, 1, __func__); int loops; if (loops_d == -1.0) loops = -1; else { @@ -1812,8 +1812,12 @@ Value* builtin_audio_play_loop(Value *arg) { } /* #1007 round 3: the samples slot, beside the loops slot and above the * device check for the same reachability reason. */ - STRICT_REQUIRE(gfx_bad_samples(arg->data.list.items[0]), "audio_play_loop", - "[list or buffer of samples, number loops]"); + /* #1637: a non-number sample element is refused here too, above the + * device check (a bool in every strict mode, through STRICT_REQUIRE). */ + STRICT_REQUIRE(gfx_bad_samples(arg->data.list.items[0]) + || (arg->data.list.items[0]->type == VAL_LIST + && !gfx_list_all_num(arg->data.list.items[0])), + "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 */ Value *samples = arg->data.list.items[0]; int n = 0; @@ -1839,9 +1843,9 @@ Value* builtin_audio_volume(Value *arg) { 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 */ Value *ch_v = arg->data.list.items[0]; Value *vol_v = arg->data.list.items[1]; - int c = (int)ch_v->data.num - 1; + int c = (int)eigs_num_arg(ch_v, "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 = vol_v->data.num; + double vol = eigs_num_arg(vol_v, "audio_volume"); if (isnan(vol) || vol < 0.0) vol = 0.0; if (vol > 4.0) vol = 4.0; p_SDL_LockAudioDevice(g_audio_device); @@ -1858,7 +1862,7 @@ Value* builtin_audio_stop(Value *arg) { 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)arg->data.num - 1; + 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 */ p_SDL_LockAudioDevice(g_audio_device); int ok = g_audio_ch[c].active; @@ -1921,9 +1925,9 @@ Value* builtin_audio_sine(Value *arg) { arg->data.list.items[1]->type != VAL_NUM || arg->data.list.items[2]->type != VAL_NUM, "audio_sine", "[number freq, number duration, number amplitude]", make_list(0)); - double freq = arg->data.list.items[0]->data.num; - double dur = arg->data.list.items[1]->data.num; - double amp = arg->data.list.items[2]->data.num; + double freq = eigs_list_num(arg, 0, __func__); + double dur = eigs_list_num(arg, 1, __func__); + double amp = eigs_list_num(arg, 2, __func__); int rate = g_audio_freq > 0 ? g_audio_freq : 44100; int n = (int)(dur * rate); if (n <= 0 || n > rate * 30) return make_list(0); @@ -1956,9 +1960,9 @@ Value* builtin_audio_saw(Value *arg) { arg->data.list.items[1]->type != VAL_NUM || arg->data.list.items[2]->type != VAL_NUM, "audio_saw", "[number freq, number duration, number amplitude]", make_list(0)); - double freq = arg->data.list.items[0]->data.num; - double dur = arg->data.list.items[1]->data.num; - double amp = arg->data.list.items[2]->data.num; + double freq = eigs_list_num(arg, 0, __func__); + double dur = eigs_list_num(arg, 1, __func__); + double amp = eigs_list_num(arg, 2, __func__); int rate = g_audio_freq > 0 ? g_audio_freq : 44100; int n = (int)(dur * rate); if (n <= 0 || n > rate * 30) return make_list(0); @@ -1991,9 +1995,9 @@ Value* builtin_audio_square(Value *arg) { arg->data.list.items[1]->type != VAL_NUM || arg->data.list.items[2]->type != VAL_NUM, "audio_square", "[number freq, number duration, number amplitude]", make_list(0)); - double freq = arg->data.list.items[0]->data.num; - double dur = arg->data.list.items[1]->data.num; - double amp = arg->data.list.items[2]->data.num; + double freq = eigs_list_num(arg, 0, __func__); + double dur = eigs_list_num(arg, 1, __func__); + double amp = eigs_list_num(arg, 2, __func__); int rate = g_audio_freq > 0 ? g_audio_freq : 44100; int n = (int)(dur * rate); if (n <= 0 || n > rate * 30) return make_list(0); @@ -2029,11 +2033,11 @@ Value* builtin_audio_sweep(Value *arg) { arg->data.list.items[3]->type != VAL_NUM || arg->data.list.items[4]->type != VAL_NUM, "audio_sweep", "[number freq_start, number freq_end, number duration, number amplitude, number waveform]", make_list(0)); - double f0 = arg->data.list.items[0]->data.num; - double f1 = arg->data.list.items[1]->data.num; - double dur = arg->data.list.items[2]->data.num; - double amp = arg->data.list.items[3]->data.num; - int wave = (int)arg->data.list.items[4]->data.num; + 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__); int rate = g_audio_freq > 0 ? g_audio_freq : 44100; int n = (int)(dur * rate); if (n <= 0 || n > rate * 30) return make_list(0); @@ -2073,8 +2077,8 @@ Value* builtin_audio_noise(Value *arg) { ARG_GUARD(arg->data.list.items[0]->type != VAL_NUM || arg->data.list.items[1]->type != VAL_NUM, "audio_noise", "[number duration, number amplitude]", make_list(0)); - double dur = arg->data.list.items[0]->data.num; - double amp = arg->data.list.items[1]->data.num; + double dur = eigs_list_num(arg, 0, __func__); + double amp = eigs_list_num(arg, 1, __func__); int rate = g_audio_freq > 0 ? g_audio_freq : 44100; int n = (int)(dur * rate); if (n <= 0 || n > rate * 30) return make_list(0); @@ -2108,8 +2112,8 @@ 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 && a->data.list.items[i]->type == VAL_NUM) ? a->data.list.items[i]->data.num : 0; - double sb = (i < b->data.list.count && b->data.list.items[i]->type == VAL_NUM) ? b->data.list.items[i]->data.num : 0; + double sa = (i < a->data.list.count && a->data.list.items[i]->type == VAL_NUM) ? eigs_num_arg(a->data.list.items[i], __func__) : 0; + double sb = (i < b->data.list.count && b->data.list.items[i]->type == VAL_NUM) ? eigs_num_arg(b->data.list.items[i], __func__) : 0; double mixed = sa + sb; if (mixed > 1.0) mixed = 1.0; if (mixed < -1.0) mixed = -1.0; @@ -2141,12 +2145,12 @@ Value* builtin_audio_gain(Value *arg) { * beside it was not, which is the same next-line asymmetry as gfx_open. */ ARG_GUARD(arg->data.list.items[1]->type != VAL_NUM, "audio_gain", "[samples, number volume]", make_list(0)); - double vol = arg->data.list.items[1]->data.num; + double vol = eigs_list_num(arg, 1, __func__); int n = samples->data.list.count; Value *out = make_list(n); for (int i = 0; i < n; i++) { - double s = (samples->data.list.items[i]->type == VAL_NUM) ? samples->data.list.items[i]->data.num : 0; + double s = (samples->data.list.items[i]->type == VAL_NUM) ? eigs_num_arg(samples->data.list.items[i], __func__) : 0; s *= vol; if (s > 1.0) s = 1.0; if (s < -1.0) s = -1.0; @@ -2184,10 +2188,10 @@ 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 = arg->data.list.items[1]->data.num; - double decay = arg->data.list.items[2]->data.num; - double sustain = arg->data.list.items[3]->data.num; - double release = arg->data.list.items[4]->data.num; + 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__); int n = samples->data.list.count; int rate = g_audio_freq > 0 ? g_audio_freq : 44100; @@ -2216,7 +2220,7 @@ 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 = (samples->data.list.items[i]->type == VAL_NUM) ? samples->data.list.items[i]->data.num : 0; + double s = (samples->data.list.items[i]->type == VAL_NUM) ? eigs_num_arg(samples->data.list.items[i], __func__) : 0; s *= env; list_append_owned(out, make_num(s)); } @@ -2240,11 +2244,11 @@ Value* builtin_gfx_fb(Value *arg) { "[buffer fb, number w, number h, number x, number y, number scale]", make_null()); Value *buf = arg->data.list.items[0]; - int w = (int)arg->data.list.items[1]->data.num; - int h = (int)arg->data.list.items[2]->data.num; - int dx = (int)arg->data.list.items[3]->data.num; - int dy = (int)arg->data.list.items[4]->data.num; - int sc = (int)arg->data.list.items[5]->data.num; + 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 diff --git a/src/ext_http.c b/src/ext_http.c index b096075f..c4fe2959 100644 --- a/src/ext_http.c +++ b/src/ext_http.c @@ -589,7 +589,9 @@ Value* builtin_http_early_bind(Value *arg) { return make_null(); } int port = 5000; - if (arg && arg->type == VAL_NUM) port = (int)arg->data.num; + 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); @@ -655,7 +657,9 @@ Value* builtin_http_serve(Value *arg) { return make_null(); } int port = 5000; - if (arg && arg->type == VAL_NUM) port = (int)arg->data.num; + 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); @@ -944,7 +948,7 @@ Value* builtin_shared_incr(Value *arg) { Server *s = eigs_http_active; if (!s) return make_null(); - double delta = delta_v->data.num; + double delta = eigs_num_arg(delta_v, __func__); long cap = shared_max_bytes(); pthread_mutex_lock(&s->shared_mu); @@ -957,7 +961,7 @@ Value* builtin_shared_incr(Value *arg) { * 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 = parsed->data.num; + if (!bad) cur = eigs_num_arg(parsed, __func__); val_decref(parsed); if (bad) { pthread_mutex_unlock(&s->shared_mu); @@ -1017,13 +1021,13 @@ Value* builtin_shared_get(Value *arg) { } Value* builtin_shared_has(Value *arg) { - if (!arg || arg->type != VAL_STR) return make_num(0); /* fs:TODO #971 guards a non-string key; deferred: ext_http is a variant-only build (make http) */ + if (!arg || arg->type != VAL_STR) return make_bool(0); /* fs:TODO #971 guards a non-string key; deferred: ext_http is a variant-only build (make http) */ Server *s = eigs_http_active; - if (!s) return make_num(0); /* fs:ANSWER 0 means "key not present" -- the same value line 739 returns when shared_find misses; with no active server there is no shared store, so nothing is present */ + if (!s) return make_bool(0); /* fs:ANSWER 0 means "key not present" -- the same value line 739 returns when shared_find misses; with no active server there is no shared store, so nothing is present */ pthread_mutex_lock(&s->shared_mu); int idx = shared_find(s, arg->data.str); pthread_mutex_unlock(&s->shared_mu); - return make_num(idx >= 0 ? 1 : 0); + return make_bool(idx >= 0); } Value* builtin_shared_delete(Value *arg) { diff --git a/src/ext_net.c b/src/ext_net.c index f7c3803e..82a8b7e7 100644 --- a/src/ext_net.c +++ b/src/ext_net.c @@ -110,7 +110,7 @@ static double net_register_sock(int fd, EigsNetSockKind kind) { static EigsNetSock* net_lookup(Value *v, EigsNetSockKind kind) { if (!v || v->type != VAL_NUM) return NULL; - EigsNetSock *s = net_unpack(v->data.num, NULL, NULL); + EigsNetSock *s = net_unpack(eigs_num_arg(v, __func__), NULL, NULL); if (!s || s->kind != kind) return NULL; return s; } @@ -143,7 +143,10 @@ static int net_num_at(Value *arg, int idx, int fallback) { if (!arg || arg->type != VAL_LIST || idx >= arg->data.list.count) return fallback; Value *v = arg->data.list.items[idx]; - return (v && v->type == VAL_NUM) ? (int)v->data.num : 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_opt_num(v, &d, "net") ? (int)d : fallback; } /* ---- builtins ----------------------------------------------------- */ @@ -158,7 +161,7 @@ Value* builtin_net_listen(Value *arg) { return make_null(); } TRACE_NONDET_TAKE("net_listen"); - int port = (int)arg->data.num; + 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); @@ -206,6 +209,7 @@ Value* builtin_net_accept(Value *arg) { 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); + if (g_has_error) return make_null(); /* #1637: a bool timeout raised */ arg = arg->data.list.items[0]; } if (!arg || arg->type != VAL_NUM) { @@ -239,6 +243,7 @@ Value* builtin_net_dial(Value *arg) { } int port = net_num_at(arg, 1, -1); int timeout_ms = net_num_at(arg, 2, -1); + 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()); @@ -288,6 +293,7 @@ Value* builtin_net_recv(Value *arg) { Value *conn = arg->data.list.items[0]; int max = net_num_at(arg, 1, 0); int timeout_ms = net_num_at(arg, 2, -1); + 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. */ if (max < 1) { @@ -353,9 +359,7 @@ Value* builtin_net_send(Value *arg) { for (int i = 0; i < n; i++) { double dv = data->type == VAL_BUFFER ? buffer_read_num(data, i) - : (data->data.list.items[i] && - data->data.list.items[i]->type == VAL_NUM - ? data->data.list.items[i]->data.num : 0.0); + : eigs_elem_num(data->data.list.items[i], "net_send"); /* #1637 round 4 */ if (g_has_error) { free(owned); return make_null(); } owned[i] = finite_num_to_byte(dv); } @@ -378,7 +382,7 @@ Value* builtin_net_close(Value *arg) { } int nidx = 0; uint32_t ngen = 0; - EigsNetSock *s = net_unpack(arg->data.num, &nidx, &ngen); + EigsNetSock *s = net_unpack(eigs_num_arg(arg, __func__), &nidx, &ngen); if (s) { close(s->fd); free(s); diff --git a/src/ext_store.c b/src/ext_store.c index a81bc89e..726b01ee 100644 --- a/src/ext_store.c +++ b/src/ext_store.c @@ -161,7 +161,7 @@ static void store_json_encode(Value *v, strbuf *out) { } switch (v->type) { case VAL_NUM: { - double n = v->data.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 @@ -178,6 +178,9 @@ static void store_json_encode(Value *v, strbuf *out) { eigs_json_escape_string(out, v->data.str); break; } + case VAL_BOOL: /* #1637: JSON's own literal; decodes back to a bool */ + strbuf_append(out, v->data.boolean ? "true" : "false"); + break; case VAL_LIST: { strbuf_append_char(out, '['); for (int i = 0; i < v->data.list.count; i++) { @@ -339,7 +342,7 @@ static Value* store_buffer_from_tag(Value *dict) { * 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 = rv->data.num, cd = cv->data.num; + 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; @@ -374,7 +377,7 @@ static Value* store_buffer_from_tag(Value *dict) { for (int i = 0; i < count; i++) { Value *e = body->data.list.items[i]; double d = 0; - if (e->type == VAL_NUM) d = e->data.num; + if (e->type == VAL_NUM) d = eigs_num_arg(e, __func__); else store_nonfinite_sentinel(e->data.str, &d); buf->data.buffer.data[i] = d; } @@ -427,10 +430,11 @@ static Value* store_json_parse_value(const char *s, int *pos) { if (s[*pos] == '{') return store_json_parse_object(s, pos); if (s[*pos] == '-' || isdigit(s[*pos])) return store_json_parse_number(s, pos); if (strncmp(s + *pos, "null", 4) == 0) { *pos += 4; return make_null(); } - if (strncmp(s + *pos, "true", 4) == 0) { *pos += 4; return make_num(1); } - /* fs:LITERAL the JSON token `false` decodes to the number 0 — this is the + /* #1637: JSON true/false decode to the bool values. */ + if (strncmp(s + *pos, "true", 4) == 0) { *pos += 4; return make_bool(1); } + /* fs:LITERAL the JSON token `false` decodes to the bool false — this is the * value being constructed, not a guard; no argument is being rejected. */ - if (strncmp(s + *pos, "false", 5) == 0) { *pos += 5; return make_num(0); } + if (strncmp(s + *pos, "false", 5) == 0) { *pos += 5; return make_bool(0); } return NULL; /* unknown token */ } @@ -716,7 +720,7 @@ static int store_load_catalog(Store *store) { free(visited); return -1; } - uint32_t root = (uint32_t)rv->data.num; + uint32_t root = (uint32_t)eigs_num_arg(rv, __func__); if (root == 0 || root >= store->page_count) { fprintf(stderr, "store_open: collection '%s' has invalid root %u\n", name, root); free(visited); @@ -784,9 +788,9 @@ static Store* get_store_why(Value *v, int *why, int *out_id) { if (!v || v->type != VAL_DICT) return NULL; Value *sv = dict_get(v, "_store_id"); if (!sv || sv->type != VAL_NUM) return NULL; - *out_id = (int)sv->data.num; + *out_id = (int)eigs_num_arg(sv, __func__); Value *gv = dict_get(v, "_store_gen"); - uint32_t gen = (gv && gv->type == VAL_NUM) ? (uint32_t)gv->data.num : 0; + uint32_t gen = (gv && gv->type == VAL_NUM) ? (uint32_t)eigs_num_arg(gv, __func__) : 0; return (Store*)handle_lookup(*out_id, gen, HANDLE_STORE, why); } @@ -948,9 +952,9 @@ 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) ? dict_get(arg, "_store_id") : NULL; - int hid = (id_val && id_val->type == VAL_NUM) ? (int)id_val->data.num : -1; + int hid = (id_val && id_val->type == VAL_NUM) ? (int)eigs_num_arg(id_val, __func__) : -1; Value *g_val = (arg && arg->type == VAL_DICT) ? dict_get(arg, "_store_gen") : NULL; - uint32_t hgen = (g_val && g_val->type == VAL_NUM) ? (uint32_t)g_val->data.num : 0; + uint32_t hgen = (g_val && g_val->type == VAL_NUM) ? (uint32_t)eigs_num_arg(g_val, __func__) : 0; handle_release(hid, hgen); store->dirty = 1; /* Force flush */ store_flush_catalog(store); @@ -1011,8 +1015,8 @@ static Value* builtin_store_put(Value *arg) { } else { Value *rv = dict_get(col_info, "root"); Value *nv = dict_get(col_info, "next_id"); - root_page = (uint32_t)(rv ? rv->data.num : 0); - next_id = (int)(nv ? nv->data.num : 1); + root_page = (uint32_t)(rv ? eigs_num_arg(rv, __func__) : 0); + next_id = (int)(nv ? eigs_num_arg(nv, __func__) : 1); } /* Determine key */ @@ -1149,7 +1153,7 @@ static Value* builtin_store_get(Value *arg) { 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)key_val->data.num); + snprintf(key_buf, STORE_MAX_KEY_LEN, "%d", (int)eigs_num_arg(key_val, __func__)); } else { return make_null(); } @@ -1158,7 +1162,7 @@ static Value* builtin_store_get(Value *arg) { if (!col_info || col_info->type != VAL_DICT) return make_null(); Value *rv = dict_get(col_info, "root"); if (!rv) return make_null(); - uint32_t pg = (uint32_t)rv->data.num; + uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__); size_t target_key_len = strlen(key_buf); @@ -1216,7 +1220,7 @@ static int store_locate(Store *store, const char *collection, if (!col_info || col_info->type != VAL_DICT) return 0; Value *rv = dict_get(col_info, "root"); if (!rv) return 0; - uint32_t pg = (uint32_t)rv->data.num; + uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__); size_t target_key_len = strlen(key_buf); for (int _hops = 0; pg != 0 && _hops < STORE_MAX_PAGE_HOPS; _hops++) { @@ -1292,7 +1296,7 @@ static Value* builtin_store_delete(Value *arg) { /* fs:CHANNEL the arity/type failure is already signalled by the * unconditional rt_error above (EK_TYPE latch, catchable — asserted by * CV2-68); this return is the post-raise placeholder, not the answer. */ - return make_num(0); + return make_bool(0); } Store *store = store_arg(arg->data.list.items[0], "store_delete"); /* fs:CHANNEL store_arg is resolve-OR-RAISE (#1146): it has already @@ -1300,12 +1304,12 @@ static Value* builtin_store_delete(Value *arg) { * closed" — by the time it returns NULL, and rt_error LATCHES rather * than unwinding, so this return is the post-raise placeholder, not * "deleted nothing". Same shape as the arity guard above. */ - if (!store) return make_num(0); - if (store_replay_blocks("store_delete")) return make_num(0); + if (!store) return make_bool(0); + if (store_replay_blocks("store_delete")) return make_bool(0); Value *col_val = arg->data.list.items[1]; Value *key_val = arg->data.list.items[2]; ARG_GUARD(!col_val || col_val->type != VAL_STR, - "store_delete", "[store handle, string collection, key]", make_num(0)); + "store_delete", "[store handle, string collection, key]", make_bool(0)); const char *collection = col_val->data.str; char key_buf[STORE_MAX_KEY_LEN]; @@ -1313,12 +1317,12 @@ static Value* builtin_store_delete(Value *arg) { 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)key_val->data.num); + snprintf(key_buf, STORE_MAX_KEY_LEN, "%d", (int)eigs_num_arg(key_val, __func__)); } 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 * if/else chain's control flow is provably unchanged. */ - ARG_GUARD(1, "store_delete", "a string or number key", make_num(0)); + ARG_GUARD(1, "store_delete", "a string or number key", make_bool(0)); } StoreLoc loc; @@ -1327,7 +1331,7 @@ static Value* builtin_store_delete(Value *arg) { * to the end, or on-disk corruption that aborted the scan without having * marked anything deleted) — 0 is store_delete's documented "deleted * nothing", the value CV2-69 pins for a missing key. */ - if (!store_locate(store, collection, key_buf, &loc)) return make_num(0); + if (!store_locate(store, collection, key_buf, &loc)) return make_bool(0); if (store_tombstone(store, &loc) != 0) { /* The record is still live on disk; answering 1 would report a @@ -1336,9 +1340,9 @@ static Value* builtin_store_delete(Value *arg) { /* fs:CHANNEL the IO failure is signalled by the unconditional rt_error * above (EK_IO latch, catchable); this return is the post-raise * placeholder, not "deleted nothing". */ - return make_num(0); + return make_bool(0); } - return make_num(1); + return make_bool(1); } /* store_query([handle, collection]) -> list of all records */ @@ -1359,7 +1363,7 @@ static Value* builtin_store_query(Value *arg) { if (!col_info || col_info->type != VAL_DICT) return make_list(0); Value *rv = dict_get(col_info, "root"); if (!rv) return make_list(0); - uint32_t pg = (uint32_t)rv->data.num; + uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__); Value *results = make_list(16); @@ -1417,7 +1421,7 @@ static Value* builtin_store_count(Value *arg) { /* 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)rv->data.num; + uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__); int count = 0; for (int _hops = 0; pg != 0 && _hops < STORE_MAX_PAGE_HOPS; _hops++) { @@ -1445,7 +1449,7 @@ static Value* builtin_store_update(Value *arg) { /* fs:CHANNEL the arity/type failure is already signalled by the * unconditional rt_error above (EK_TYPE latch, catchable); this return * is the post-raise placeholder, not "updated nothing". */ - return make_num(0); + return make_bool(0); } Value *handle = arg->data.list.items[0]; Value *col_val = arg->data.list.items[1]; @@ -1457,11 +1461,11 @@ static Value* builtin_store_update(Value *arg) { * rt_error'd by the time it returns NULL, and rt_error latches rather * than unwinding, so this return is the post-raise placeholder, not * "no record updated". */ - if (!store) return make_num(0); - if (store_replay_blocks("store_update")) return make_num(0); + if (!store) return make_bool(0); + if (store_replay_blocks("store_update")) return make_bool(0); ARG_GUARD(!col_val || col_val->type != VAL_STR, "store_update", "[store handle, string collection, key, record]", - make_num(0)); + make_bool(0)); const char *collection = col_val->data.str; char key_buf[STORE_MAX_KEY_LEN]; @@ -1469,12 +1473,12 @@ static Value* builtin_store_update(Value *arg) { 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)key_val->data.num); + snprintf(key_buf, STORE_MAX_KEY_LEN, "%d", (int)eigs_num_arg(key_val, __func__)); } 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 * if/else chain's control flow is provably unchanged. */ - ARG_GUARD(1, "store_update", "a string or number key", make_num(0)); + ARG_GUARD(1, "store_update", "a string or number key", make_bool(0)); } /* Locate the old record before touching it. #1006: the replace below is @@ -1485,7 +1489,7 @@ static Value* builtin_store_update(Value *arg) { /* fs:ANSWER no live record carries this key, so there was nothing to * update — 0 is store_update's documented "no row updated"; 1 is produced * only when the replacement actually landed. */ - if (!store_locate(store, collection, key_buf, &loc)) return make_num(0); + if (!store_locate(store, collection, key_buf, &loc)) return make_bool(0); /* Reject a non-dict record BEFORE anything is removed. store_put raises * the same EK_TYPE for it, but by then the old record was already gone @@ -1501,7 +1505,7 @@ static Value* builtin_store_update(Value *arg) { /* fs:CHANNEL the type failure is signalled by the unconditional * rt_error above (EK_TYPE latch, catchable); this return is the * post-raise placeholder, not "no row updated". */ - return make_num(0); + return make_bool(0); } if (store_tombstone(store, &loc) != 0) { @@ -1510,7 +1514,7 @@ static Value* builtin_store_update(Value *arg) { * above (EK_IO latch, catchable); this return is the post-raise * placeholder, not "no row updated". Nothing was written, so the * record is untouched. */ - return make_num(0); + return make_bool(0); } /* Set _id on new record so store_put reuses the same key. */ @@ -1561,10 +1565,10 @@ static Value* builtin_store_update(Value *arg) { * error, the un-restorable case, or the `!g_has_error` backstop just * above — so this return is the post-raise placeholder. It is * deliberately not 1: the replacement did not land. */ - return make_num(0); + return make_bool(0); } val_decref(put_result); - return make_num(1); + return make_bool(1); } /* store_collections(handle) -> list of collection names */ @@ -1587,7 +1591,7 @@ static Value* builtin_store_drop(Value *arg) { /* fs:CHANNEL the arity/type failure is already signalled by the * unconditional rt_error above (EK_TYPE latch, catchable — asserted by * CV2-74); this return is the post-raise placeholder, not "not dropped". */ - return make_num(0); + return make_bool(0); } Store *store = store_arg(arg->data.list.items[0], "store_drop"); /* fs:CHANNEL store_arg is resolve-OR-RAISE (#1146): it has already @@ -1595,22 +1599,22 @@ static Value* builtin_store_drop(Value *arg) { * than unwinding, so this return is the post-raise placeholder, not * store_drop's documented "no" (that is the documented-answer zero * below). */ - if (!store) return make_num(0); - if (store_replay_blocks("store_drop")) return make_num(0); + if (!store) return make_bool(0); + if (store_replay_blocks("store_drop")) return make_bool(0); Value *col_val = arg->data.list.items[1]; ARG_GUARD(!col_val || col_val->type != VAL_STR, - "store_drop", "[store handle, string collection]", make_num(0)); + "store_drop", "[store handle, string collection]", make_bool(0)); const char *collection = col_val->data.str; Value *col_info = dict_get(store->catalog, collection); /* fs:ANSWER there is no such collection to drop, so nothing was dropped — * 0 is store_drop's documented "no" against the 1 produced on success. */ - if (!col_info || col_info->type != VAL_DICT) return make_num(0); + if (!col_info || col_info->type != VAL_DICT) return make_bool(0); /* Mark all pages in chain as free */ Value *rv = dict_get(col_info, "root"); if (rv) { - uint32_t pg = (uint32_t)rv->data.num; + uint32_t pg = (uint32_t)eigs_num_arg(rv, __func__); for (int _hops = 0; pg != 0 && _hops < STORE_MAX_PAGE_HOPS; _hops++) { Page page; if (store_read_page(store, pg, &page) != 0) break; @@ -1638,7 +1642,7 @@ static Value* builtin_store_drop(Value *arg) { store->catalog = new_catalog; store->dirty = 1; store_flush_catalog(store); - return make_num(1); + return make_bool(1); } /* ================================================================ diff --git a/src/jit.c b/src/jit.c index ab89d236..029be167 100644 --- a/src/jit.c +++ b/src/jit.c @@ -521,7 +521,8 @@ static void ensure_layout(void) { * thread an advance-tracker register. * * Stage 4 supported set: - * OP_NULL/NUM_ZERO/NUM_ONE — inline immediate push. + * OP_NULL/NUM_ZERO/NUM_ONE/ + * OP_TRUE/OP_FALSE — inline immediate push. * OP_LINE — inline 32-bit store to current_line. * OP_CONST [idx:16] — pre-encoded slot push + inline incref for * heap (skipped statically when arena==1). @@ -542,8 +543,8 @@ static void ensure_layout(void) { * check — no separate divisor guard. * OP_EQ / OP_NE / OP_LT / * OP_GT / OP_LE / OP_GE — load both, type-check (immediate-num), - * ucomisd + cmovcc to materialize 0.0 or - * 1.0 bits as the result slot. No output + * ucomisd + cmovcc to materialize the + * false/true bool slot bits (#1637). No output * overflow check needed (bounded). Type- * check is the only bail. * OP_POP — peephole `dec %ecx`, valid only when the @@ -573,7 +574,8 @@ static int jit_supported_prefix(const struct EigsChunk *chunk, *stop_offset = 0; while (i < chunk->code_len) { uint8_t op = chunk->code[i]; - if (op == OP_NULL || op == OP_NUM_ZERO || op == OP_NUM_ONE) { + if (op == OP_NULL || op == OP_NUM_ZERO || op == OP_NUM_ONE || + op == OP_TRUE || op == OP_FALSE) { i += 1; ops++; non_line_ops++; } else if (op == OP_CONST) { if (i + 3 > chunk->code_len) { *stop_op = op; *stop_offset = i; break; } @@ -740,7 +742,7 @@ static int jit_supported_prefix(const struct EigsChunk *chunk, i += 1; ops++; non_line_ops++; *has_bail_op = 1; *extra_size += 320; /* Stage 5e loaders + decrefs */ - /* Result is bit-exact 0.0 or 1.0 (an immediate-num), so + /* #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) { @@ -1492,10 +1494,6 @@ static uint8_t *emit_store_rdx_at_disp_stack(uint8_t *w, int32_t disp) { *w++ = 0x48; *w++ = 0x89; *w++ = 0x94; *w++ = 0xCB; return emit_u32(w, (uint32_t)disp); } -/* xor %rdx, %rdx (3 bytes: 48 31 D2) — clear %rdx (bits of +0.0). */ -static uint8_t *emit_xor_rdx_rdx(uint8_t *w) { - *w++ = 0x48; *w++ = 0x31; *w++ = 0xD2; return w; -} /* ucomisd %xmm0, %xmm1 (4 bytes: 66 0F 2E C8) — AT&T order: compares * %xmm1 (= a) with %xmm0 (= b). Sets ZF/PF/CF per IEEE-754 ordered @@ -1505,11 +1503,6 @@ static uint8_t *emit_ucomisd_xmm0_xmm1(uint8_t *w) { *w++ = 0x66; *w++ = 0x0F; *w++ = 0x2E; *w++ = 0xC8; return w; } -/* xor %rax, %rax (3 bytes: 48 31 C0) — clears %rax (bits of +0.0). */ -static uint8_t *emit_xor_rax_rax(uint8_t *w) { - *w++ = 0x48; *w++ = 0x31; *w++ = 0xC0; return w; -} - /* movabs $imm64, %rdx (10 bytes) */ static uint8_t *emit_movabs_rdx(uint8_t *w, uint64_t imm) { *w++ = 0x48; *w++ = 0xBA; @@ -1864,6 +1857,29 @@ static uint8_t *emit_je_rel8(uint8_t *w, uint8_t **patch) { static uint8_t *emit_jmp_rel8(uint8_t *w, uint8_t **patch) { *w++ = 0xEB; *patch = w; *w++ = 0x00; return w; } +/* #1637 bool fast paths. test $1, %al (2 bytes): ZF=1 iff the low bit is + * clear — for a TAG_BOOL slot, iff it is false. */ +static uint8_t *emit_test_1_al(uint8_t *w) { + *w++ = 0xA8; *w++ = 0x01; return w; +} +/* xor $1, %rax (4 bytes): flips a TAG_BOOL slot between false and true. */ +static uint8_t *emit_xor_1_rax(uint8_t *w) { + *w++ = 0x48; *w++ = 0x83; *w++ = 0xF0; *w++ = 0x01; return w; +} +/* Resolve a rel8 placeholder to `target`; sets *abort on overflow (the + * caller abandons the compile, as for every other rel8 site). */ +static void patch_rel8_to(uint8_t *p, uint8_t *target, int *abort_flag) { + long rel = (long)(target - (p + 1)); + if (rel > 127 || rel < -128) { *abort_flag = 1; return; } + *p = (uint8_t)(int8_t)rel; +} +/* Branch to *patch unless %rax holds a TAG_BOOL slot. Clobbers %rsi. */ +static uint8_t *emit_jump_unless_bool_rax(uint8_t *w, uint8_t **patch) { + w = emit_mov_rax_rsi(w); + w = emit_shr_48_rsi(w); + w = emit_cmp_imm32_esi(w, 0xFFF9); /* TAG_BOOL >> 48 */ + return emit_jnz_rel8(w, patch); +} /* jle rel8 (2 bytes) — jump if signed less-or-equal (take the free_value * arm when the decremented refcount reached zero). */ static uint8_t *emit_jle_rel8(uint8_t *w, uint8_t **patch) { @@ -2271,8 +2287,8 @@ static uint8_t *emit_load_numeric_operand(uint8_t *w, int is_b, 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)offsetof(Value, data.num)) - : emit_mov_disp32_rdi_to_rdx(w, (int32_t)offsetof(Value, data.num)); + 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); /* .imm: bits (immediate or loaded data.num) → xmm. */ { int rel = (int)(w - imm_after); @@ -2701,10 +2717,15 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, /* Unconditional jumps suppress the trailing `mov $i, %r13d` * advance writeback because the next bytes are unreachable. */ int skip_post_op_advance = 0; - if (op == OP_NULL || op == OP_NUM_ZERO || op == OP_NUM_ONE) { + if (op == OP_NULL || op == OP_NUM_ZERO || op == OP_NUM_ONE || + op == OP_TRUE || op == OP_FALSE) { uint64_t bits; if (op == OP_NULL) { bits = 0xFFF8000000000000ULL; /* SLOT_NULL_BITS */ + } else if (op == OP_TRUE) { + bits = SLOT_TRUE_BITS; /* #1637 */ + } else if (op == OP_FALSE) { + bits = SLOT_FALSE_BITS; } else if (op == OP_NUM_ZERO) { bits = 0; /* IEEE-754 +0.0 */ } else { @@ -2724,7 +2745,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, if (v->type == VAL_NUM) { /* Immediate-num: same shape as NUM_ZERO. (#262 Step E: a * constant num never carries observer state.) */ - memcpy(&bits, &v->data.num, 8); + memcpy(&bits, &VAL_NUM_RAW(v), 8); } else { /* Heap slot: TAG_HEAP | payload. Incref unless arena. */ bits = 0xFFFB000000000000ULL | @@ -3008,7 +3029,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, * local, cache miss, strcmp-equal key, non-num or observed * field) routes to the Stage 4m helper, which repopulates * the dict cache so the next iteration hits inline. The hit - * path pushes v->data.num's raw bits — for an untracked + * path pushes VAL_NUM_RAW(v)'s raw bits — for an untracked * number that IS the immediate slot encoding, so there is * no incref/decref or allocation. Guards precede all * mutation (the fast path mutates nothing but the stack). */ @@ -3038,7 +3059,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, (uint32_t)VAL_NUM); w = emit_jne_rel32(w, &slow_p[slow_n]); slow_n++; /* #262 Step E: obs_age guard removed — nums are never tracked. */ - w = emit_mov_disp32_rax_to_rax(w, (int32_t)offsetof(Value, data.num)); + w = emit_mov_disp32_rax_to_rax(w, (int32_t)VAL_NUM_OFFSET); w = emit_store_rax_at_stack(w, g_layout.off_stack); w = emit_inc_ecx(w); w = emit_jmp_rel32(w, &done_p); @@ -3173,8 +3194,8 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, /* TOS must be an immediate num. */ w = emit_load_stack_to_rdx(w, g_layout.off_stack - 8); w = emit_immediate_num_check_rdx(w, &slow_p[slow_n]); slow_n++; - /* existing->data.num = tos.d (raw bits). */ - w = emit_mov_rdx_to_disp32_rax(w, (int32_t)offsetof(Value, data.num)); + /* VAL_NUM_RAW(existing) = tos.d (raw bits). */ + w = emit_mov_rdx_to_disp32_rax(w, (int32_t)VAL_NUM_OFFSET); w = emit_jmp_rel32(w, &done_p); for (int k = 0; k < slow_n; k++) patch_rel32(slow_p[k], w); } @@ -3322,7 +3343,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, * incref+decref both no-op) * old is VAL_NUM && rc==1 && obs_age==0 && !arena? * no → .swap - * in-place: old->data.num = tos bits; → .done + * in-place: VAL_NUM_RAW(old) = tos bits; → .done * .plain: values[slot] = tos; → .done * .swap: values[slot] = tos; incref tos; decref old * .done: */ @@ -3367,8 +3388,8 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, /* #262 Step E: obs_age guard removed — nums are never tracked. */ w = emit_testb_1_disp32_rdi(w, (int32_t)offsetof(Value, arena)); w = emit_jne_rel32(w, &swap_p[swap_n]); swap_n++; - /* in-place: existing->data.num = tos bits. */ - w = emit_mov_rax_to_disp32_rdi(w, (int32_t)offsetof(Value, data.num)); + /* in-place: VAL_NUM_RAW(existing) = tos bits. */ + w = emit_mov_rax_to_disp32_rdi(w, (int32_t)VAL_NUM_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); @@ -3435,7 +3456,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, if (ab) { JIT_BAIL_AND_RETURN(); } - /* Divisor zero check. The VM compares `b->data.num != 0.0`, + /* Divisor zero check. The VM compares `VAL_NUM_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 @@ -3578,23 +3599,15 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, if (ab) { JIT_BAIL_AND_RETURN(); } - /* Materialize the cmov operands (%rax=0.0 bits, %rdx=1.0 - * bits) BEFORE ucomisd. The materialization uses - * `xor %rax,%rax` which clobbers ZF, so it must come before - * the comparison — otherwise cmove would always fire - * (ZF=1 from the xor). movabs does not touch flags, and - * ucomisd between the xor and cmovcc is the only - * flag-affecting op left — its flags are exactly what - * cmovcc sees. The operand bit registers are dead here + /* Materialize the cmov operands (%rax=false, %rdx=true) + * BEFORE ucomisd, so ucomisd is the only flag-affecting op + * between them and cmovcc. The operand bit registers are dead here * (values live in xmm; slots reload from the stack at * commit). */ - w = emit_xor_rax_rax(w); /* rax = 0.0 bits */ - { - uint64_t one_bits; - double one = 1.0; - memcpy(&one_bits, &one, 8); - w = emit_movabs_rdx(w, one_bits); /* rdx = 1.0 bits */ - } + /* #1637: the result is a bool, mirroring NUM_CMP/EQ/NE's + * slot_from_bool. movabs leaves the flags alone. */ + w = emit_movabs_rax(w, SLOT_FALSE_BITS); /* rax = false */ + w = emit_movabs_rdx(w, SLOT_TRUE_BITS); /* rdx = true */ w = emit_ucomisd_xmm0_xmm1(w); /* cmovcc %rdx → %rax when condition holds. */ uint8_t cc_byte; @@ -3674,7 +3687,22 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, JIT_BAIL_AND_RETURN(); } uint8_t *p_t; + uint8_t *not_num = NULL, *not_done = NULL; w = emit_load_stack_to_rax(w, g_layout.off_stack - 8); + if (op == OP_NOT) { + /* #1637: a bool operand flips its low bit (CASE(NOT): + * slot_from_bool(!slot_truthy(s))). Anything else that is + * not an immediate num bails below, as before. */ + w = emit_jump_unless_bool_rax(w, ¬_num); + w = emit_xor_1_rax(w); + w = emit_store_rax_at_disp_stack(w, g_layout.off_stack - 8); + w = emit_jmp_rel8(w, ¬_done); + { + int ab8 = 0; + patch_rel8_to(not_num, w, &ab8); + if (ab8) { JIT_BAIL_AND_RETURN(); } + } + } w = emit_immediate_num_check_rax(w, &p_t); bail_patches[bail_count++] = p_t; if (op == OP_NEG) { @@ -3683,21 +3711,22 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, w = emit_xor_rdx_rax(w); w = emit_store_rax_at_disp_stack(w, g_layout.off_stack - 8); } else if (op == OP_NOT) { - /* Logical not over an immediate num: result is 1.0 iff - * the input is ±0.0, else 0.0. btr clears the sign bit - * so -0.0 and +0.0 both test as zero. Materialize the - * cmov operands BEFORE the test (xor clobbers ZF). */ + /* Logical not over an immediate num: the result is true + * iff the input is ±0.0, else false (#1637: bool bits). + * btr clears the sign bit so -0.0 and +0.0 both test as + * zero. movabs leaves the flags alone, so the test is + * what cmove sees. */ w = emit_btr_63_rax(w); - w = emit_xor_rdx_rdx(w); /* rdx = 0.0 bits */ - { - uint64_t one_bits; - double one = 1.0; - memcpy(&one_bits, &one, 8); - w = emit_movabs_rsi(w, one_bits); /* rsi = 1.0 bits */ - } + w = emit_movabs_rdx(w, SLOT_FALSE_BITS); /* rdx = false */ + w = emit_movabs_rsi(w, SLOT_TRUE_BITS); /* rsi = true */ w = emit_test_rax_rax(w); /* ZF=1 iff input was ±0 */ w = emit_cmove_rsi_rdx(w); /* rdx = ZF ? rsi : rdx */ w = emit_store_rdx_at_disp_stack(w, g_layout.off_stack - 8); + { + int ab8 = 0; + patch_rel8_to(not_done, w, &ab8); + if (ab8) { JIT_BAIL_AND_RETURN(); } + } } else { /* OP_BNOT */ /* (double)(~(int64_t)d): cvttsd2si, not, cvtsi2sd back. */ w = emit_movq_rax_xmm0(w); @@ -3806,12 +3835,32 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, int target = i + 3 + (int)off; uint8_t *p_t; + uint8_t *jif_num, *jif_decide; w = emit_load_stack_to_rax(w, g_layout.off_stack - 8); + /* #1637 bool fast path: every comparison now yields a bool, + * so without this arm each compiled `if a < b` would bail. + * ZF=1 iff the bool is false, the same falsy sense the + * number arm computes. In both arms the pop (dec %ecx) comes + * before the flag-setting test, so jne/je read the test. */ + w = emit_jump_unless_bool_rax(w, &jif_num); + if (!is_peek) w = emit_dec_ecx(w); + w = emit_test_1_al(w); + w = emit_jmp_rel8(w, &jif_decide); + { + int ab8 = 0; + patch_rel8_to(jif_num, w, &ab8); + if (ab8) { JIT_BAIL_AND_RETURN(); } + } w = emit_immediate_num_check_rax(w, &p_t); bail_patches[bail_count++] = p_t; if (!is_peek) w = emit_dec_ecx(w); w = emit_btr_63_rax(w); w = emit_test_rax_rax(w); + { + int ab8 = 0; + patch_rel8_to(jif_decide, w, &ab8); + if (ab8) { JIT_BAIL_AND_RETURN(); } + } uint8_t *skip_patch; if (take_on_falsy) { @@ -4219,6 +4268,7 @@ static void jit_compile_to_thunk(struct EigsChunk *chunk, * must also be handled here. */ switch (op) { 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_EQ: case OP_NE: case OP_LT: case OP_GT: case OP_LE: case OP_GE: case OP_BAND: case OP_BOR: case OP_BXOR: case OP_SHL: case OP_SHR: diff --git a/src/jit_smoke.c b/src/jit_smoke.c index 54c673cb..f2a55443 100644 --- a/src/jit_smoke.c +++ b/src/jit_smoke.c @@ -135,7 +135,7 @@ int jit_helper_iter_next(void) { VM *vm = eigs_current->vm; reader_receipt.calls++; if (reader_receipt.kind != 1 || vm->sp != 1 || - vm->stack[0].d != 0 || vm->frames[0].ip != reader_receipt.ip || + SLOT_NUM_RAW(vm->stack[0]) != 0 || vm->frames[0].ip != reader_receipt.ip || vm->current_line != 71) reader_receipt.bad++; if (reader_receipt.status != 1) @@ -146,7 +146,7 @@ int jit_helper_index_get(void) { VM *vm = eigs_current->vm; reader_receipt.calls++; if (reader_receipt.kind != 0 || vm->sp != 2 || - vm->stack[0].d != 0 || vm->stack[1].d != 1 || + SLOT_NUM_RAW(vm->stack[0]) != 0 || SLOT_NUM_RAW(vm->stack[1]) != 1 || vm->frames[0].ip != reader_receipt.ip || vm->current_line != 71) reader_receipt.bad++; vm->sp -= 2; @@ -169,11 +169,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 || - g_vm.stack[0].d != 42 || g_vm.stack[1].d != 0 || - g_vm.stack[2].d != 1 || g_vm.frames[0].ip != store_receipt.ip) + 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) store_receipt.bad = 1; for (int i = 0; i < 3; i++) { - if (store_receipt.values[i].d != 10 + i || + if (SLOT_NUM_RAW(store_receipt.values[i]) != 10 + i || store_receipt.counts[i] != 17) store_receipt.bad = 1; } @@ -346,15 +346,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(vm->stack[0].d == 42 && vm->stack[1].d == 0); - STORE_ASSERT(vm->stack[2].d == 1); + 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(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(values[i].d == (stored ? 1 : 10 + i)); + STORE_ASSERT(SLOT_NUM_RAW(values[i]) == (stored ? 1 : 10 + i)); STORE_ASSERT(counts[i] == 17 + stored); } helpers += store_receipt.calls; @@ -430,7 +430,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]) : - vm->stack[0].d == 42 && vm->stack[1].d == 1); + SLOT_NUM_RAW(vm->stack[0]) == 42 && SLOT_NUM_RAW(vm->stack[1]) == 1); rows++; if (!good) { fprintf(stderr, "FAIL: index-bail %s status=%d r13=%" PRIx64 @@ -522,12 +522,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 - ? vm->stack[0].d == 0 && - (status ? vm->stack[1].d == 0 : - vm->stack[1].d == 42 && vm->stack[2].d == 1 && - vm->stack[3].d == 0) - : (status ? vm->stack[0].d == 0 : - vm->stack[0].d == 42 && vm->stack[1].d == 1); + ? 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); int good = saved_r13 == UINT64_C(0x13579bdf2468ace0) && advance == expected && reader_receipt.calls == 1 && reader_receipt.bad == 0 && vm->sp == expected_sp && stack_ok && @@ -588,7 +588,7 @@ static int run_exit_cases(void) { eigs_current = thread; state->jit_entry_threshold = state->jit_iter_threshold = 1; Value limit = {.type = VAL_NUM}; - limit.data.num = 4; + VAL_NUM_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, @@ -623,12 +623,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 && vm->stack[0].d == (stop ? 1 : 4) && + int ok = vm->sp == 1 && SLOT_NUM_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, vm->stack[0].d, advance, other); + osr ? "osr" : "entry", row, vm->sp, SLOT_NUM_RAW(vm->stack[0]), advance, other); rc = 1; } if (ok && stop) bailed++; @@ -645,6 +645,81 @@ static int run_exit_cases(void) { return rc; } +/* #1637: the bool emitters, read off the native stack. Every comparison and + * NOT pushes the TAG_BOOL slot bits (never 0.0/1.0), NOT flips a bool, and + * JUMP_IF_FALSE decides on a bool without bailing — so the thunk must run to + * the end of the chunk (advance == code_len). Removing the JUMP_IF bool arm + * makes it stop at the first JUMP_IF_FALSE; a comparison emitting numbers + * fails the slot-bits rows. */ +static int run_bool_cases(void) { + int rc = 0, checked = 0; + EigsState *state = calloc(1, sizeof *state); + EigsThread *thread = calloc(1, sizeof *thread); + VM *vm = calloc(1, sizeof *vm); + if (!state || !thread || !vm) { + free(state); free(thread); free(vm); + fprintf(stderr, "FAIL bool allocation\n"); + return 1; + } + EigsExitScope open = {.refs = 1}; + thread->state = state; + thread->vm = vm; + vm->owner = thread; + eigs_current = thread; + thread->exit_scope = state->exit_scope = &open; + uint8_t code[] = { + OP_TRUE, OP_NOT, /* false */ + OP_FALSE, OP_NOT, /* true */ + OP_NUM_ZERO, OP_NOT, /* true */ + OP_NUM_ONE, OP_NUM_ZERO, OP_LT, /* 1 < 0: false */ + OP_NUM_ZERO, OP_NUM_ONE, OP_LT, /* 0 < 1: true */ + OP_TRUE, OP_JUMP_IF_FALSE, 1, 0, OP_NUM_ONE, /* not taken: push 1 */ + OP_FALSE, OP_JUMP_IF_FALSE, 1, 0, OP_NUM_ZERO, /* taken: skip */ + }; + const uint64_t want[] = {SLOT_FALSE_BITS, SLOT_TRUE_BITS, SLOT_TRUE_BITS, + SLOT_FALSE_BITS, SLOT_TRUE_BITS, 0x3FF0000000000000ULL}; + const int nwant = (int)(sizeof want / sizeof want[0]); + EigsChunk chunk = {0}; + chunk.code = code; + chunk.code_len = sizeof code; + chunk.exec_count = 1; + jit_try_compile_chunk(&chunk); + if (!chunk.jit_code || chunk.jit_state != 2) { + fprintf(stderr, "FAIL bool: chunk did not compile\n"); + rc = 1; + } else { + vm->sp = 1; + vm->stack[0] = slot_null(); + vm->frame_count = 1; + vm->frames[0].chunk = &chunk; + vm->frames[0].ip = chunk.code; + chunk.jit_advance = -99; + ((JitChunkFn)chunk.jit_code)(); + if (chunk.jit_advance != (int)sizeof code || vm->sp != 1 + nwant) { + fprintf(stderr, "FAIL bool: advance=%d (want %d) sp=%d (want %d)\n", + chunk.jit_advance, (int)sizeof code, vm->sp, 1 + nwant); + rc = 1; + } else { + for (int k = 0; k < nwant; k++) { + checked++; + if (vm->stack[1 + k].u != want[k]) { + fprintf(stderr, "FAIL bool: slot %d = %016llx, want %016llx\n", k, + (unsigned long long)vm->stack[1 + k].u, + (unsigned long long)want[k]); + rc = 1; + } + } + } + } + if (checked != nwant) rc = 1; + printf("JIT bool: slots=%d/%d status=%s\n", checked, nwant, rc ? "FAIL" : "PASS"); + jit_unregister_chunk(&chunk); + jit_thread_destroy(thread); + eigs_current = NULL; + free(vm); free(thread); free(state); + return rc; +} + int main(void) { int rc = 0; rc |= run_case(42); @@ -656,6 +731,7 @@ int main(void) { rc |= run_index_bail_cases(); rc |= run_reader_bail_cases(); rc |= run_exit_cases(); + rc |= run_bool_cases(); if (rc == 0) printf("\nJIT smoke: all cases passed.\n"); return rc; } diff --git a/src/lexer.c b/src/lexer.c index 4337a107..57b5fa22 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -162,6 +162,7 @@ static TokType keyword_type(const char *word) { break; case 'f': if (strcmp(word, "for") == 0) return TOK_FOR; + if (strcmp(word, "false") == 0) return TOK_FALSE; /* #1637 */ break; case 'h': if (strcmp(word, "how") == 0) return TOK_HOW; @@ -202,6 +203,7 @@ static TokType keyword_type(const char *word) { break; case 't': if (strcmp(word, "try") == 0) return TOK_TRY; + if (strcmp(word, "true") == 0) return TOK_TRUE; /* #1637 */ break; case 'u': if (strcmp(word, "unobserved") == 0) return TOK_UNOBSERVED; @@ -322,6 +324,8 @@ const char* tok_base_string(TokType t) { case TOK_FOR: return "for "; case TOK_IN: return "in "; case TOK_NULL: return "null "; + case TOK_TRUE: return "true "; + case TOK_FALSE: return "false "; case TOK_WHAT: return "what "; case TOK_WHO: return "who "; case TOK_WHEN: return "when "; diff --git a/src/lint.c b/src/lint.c index d47b0e8a..6852b94a 100644 --- a/src/lint.c +++ b/src/lint.c @@ -246,7 +246,7 @@ static void collect_refs(ASTNode *node, LintContext *ctx) { * error here instead of a silent no-op. */ case AST_NUM: case AST_STR: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: @@ -311,7 +311,7 @@ static void collect_assigns(ASTNode *node, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_RELATION: @@ -369,7 +369,7 @@ static int cond_has_bare_predicate(const ASTNode *n) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_ASSIGN: case AST_IF: case AST_LOOP: @@ -431,7 +431,7 @@ static void collect_loop_assign_names(ASTNode *n, const char *seen[], int cap, i case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_RELATION: @@ -531,7 +531,7 @@ static void check_empty_blocks(ASTNode *node, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -640,7 +640,7 @@ static void check_dup_keys(ASTNode *node, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_UNARY: case AST_RELATION: case AST_BLOCK: @@ -740,7 +740,7 @@ static void check_builtin_shadow(ASTNode *node, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -870,7 +870,7 @@ static void check_disc_interrog(ASTNode *node, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -947,7 +947,7 @@ static void check_func_unreachable(ASTNode *node, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -1016,7 +1016,7 @@ static void check_is_conditions(ASTNode *node, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -1118,7 +1118,7 @@ static void check_unused_params(ASTNode *node, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -1228,7 +1228,7 @@ static void w015_collect_locals(ASTNode *n, char *locals[], int *count) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_RELATION: @@ -1296,7 +1296,7 @@ static void w015_flag(ASTNode *n, char *const module[], int module_n, case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_RELATION: @@ -1503,7 +1503,7 @@ static void w023_walk(ASTNode *n, int mode, const char *name, int *flags, break; case AST_IDENT: N(n->data.ident.name); break; case AST_IMPORT: N(n->data.import.module_name); break; - case AST_NUM: case AST_STR: case AST_NULL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: break; + case AST_NUM: case AST_STR: case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: break; } #undef N #undef W @@ -1565,7 +1565,7 @@ static void w023_collect_binders(ASTNode *n, W023Names *locals) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_RELATION: @@ -1723,7 +1723,7 @@ static void w023_scan_sequence(ASTNode **stmts, int count, W023Names *bound, } break; case AST_IMPORT: w023_record_current_scope_import(n, bound); break; - case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: case AST_BINOP: case AST_UNARY: case AST_RELATION: case AST_RETURN: case AST_LIST: case AST_INDEX: case AST_LISTCOMP: case AST_PROGRAM: case AST_INTERROGATE: case AST_PREDICATE: case AST_DICT: case AST_DOT: case AST_BREAK: case AST_CONTINUE: case AST_DOT_ASSIGN: case AST_LAMBDA: case AST_INDEX_ASSIGN: case AST_SLICE: break; + case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_RELATION: case AST_RETURN: case AST_LIST: case AST_INDEX: case AST_LISTCOMP: case AST_PROGRAM: case AST_INTERROGATE: case AST_PREDICATE: case AST_DICT: case AST_DOT: case AST_BREAK: case AST_CONTINUE: case AST_DOT_ASSIGN: case AST_LAMBDA: case AST_INDEX_ASSIGN: case AST_SLICE: break; } } } @@ -1890,7 +1890,7 @@ static void w016_scan(ASTNode *n, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BREAK: case AST_CONTINUE: case AST_IMPORT: @@ -2049,7 +2049,7 @@ static void w017_scan(ASTNode *n, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: @@ -2073,7 +2073,7 @@ static void check_one_element_arg_list(ASTNode *ast, LintContext *ctx) { int _lfe_i, _lfe_j; \ (void)_lfe_i; (void)_lfe_j; \ switch ((n)->type) { \ - case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: \ + case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: case AST_BOOL: \ case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: case AST_IMPORT: \ break; \ case AST_BINOP: \ @@ -2292,7 +2292,7 @@ static void w022_collect_bindings(ASTNode *n, W022Table *t) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_RELATION: @@ -2418,7 +2418,7 @@ static void w020_facts(ASTNode *n, W020Facts *f) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_IF: @@ -2478,7 +2478,7 @@ static void w020_facts(ASTNode *n, W020Facts *f) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_ASSIGN: case AST_RELATION: case AST_FUNC: @@ -2552,7 +2552,7 @@ static void w020_scan(ASTNode *n, LintContext *ctx) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -2814,7 +2814,7 @@ static void w018_scan(ASTNode *n, LintContext *ctx, W018Scope *sc) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: @@ -3092,7 +3092,7 @@ static void w024_collect(ASTNode *n, W024Loop *lp, int nested) { for (int i = 0; i < n->data.program.count; i++) w024_collect(n->data.program.stmts[i], lp, nested); break; - case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: + case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: case AST_IMPORT: break; } @@ -3117,7 +3117,7 @@ static int w024_index_varies(ASTNode *ix, W024Loop *lp) { w024_index_varies(ix->data.binop.right, lp); case AST_UNARY: return w024_index_varies(ix->data.unary.operand, lp); - case AST_NUM: case AST_STR: case AST_NULL: case AST_ASSIGN: case AST_RELATION: + case AST_NUM: case AST_STR: case AST_NULL: case AST_BOOL: case AST_ASSIGN: case AST_RELATION: case AST_IF: case AST_LOOP: case AST_FUNC: case AST_RETURN: case AST_BLOCK: case AST_LIST: case AST_INDEX: case AST_LISTCOMP: case AST_FOR: case AST_PROGRAM: case AST_INTERROGATE: case AST_PREDICATE: case AST_TRY: case AST_DICT: case AST_DOT: @@ -3147,7 +3147,7 @@ static int w024_elem_walks(ASTNode *n, W024Loop *lp) { w024_elem_walks(n->data.index.target, lp); case AST_DOT: return w024_elem_walks(n->data.dot.target, lp); - case AST_NUM: case AST_STR: case AST_NULL: case AST_ASSIGN: case AST_RELATION: + case AST_NUM: case AST_STR: case AST_NULL: case AST_BOOL: case AST_ASSIGN: case AST_RELATION: case AST_IF: case AST_LOOP: case AST_FUNC: case AST_RETURN: case AST_BLOCK: case AST_LIST: case AST_LISTCOMP: case AST_FOR: case AST_PROGRAM: case AST_BINOP: case AST_UNARY: case AST_INTERROGATE: case AST_PREDICATE: case AST_TRY: @@ -3183,7 +3183,7 @@ static ASTNode *w024_proj_node(ASTNode *n, W024Loop *lp); * other than one entity's quantity? */ static int w024_operand_inert(ASTNode *n, W024Loop *lp) { if (!n) return 0; - if (n->type == AST_NUM || n->type == AST_STR || n->type == AST_NULL) return 1; + if (n->type == AST_NUM || n->type == AST_STR || n->type == AST_NULL || n->type == AST_BOOL) return 1; if (n->type == AST_IDENT) { if (lp->for_var && strcmp(n->data.ident.name, lp->for_var) == 0) return 0; for (int i = 0; i < lp->count; i++) @@ -3534,7 +3534,7 @@ static void w024_walk(ASTNode *n, int fn_depth, LintContext *ctx) { case AST_INTERROGATE: w024_walk(n->data.interrogate.expr, fn_depth, ctx); break; - case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: + case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: case AST_IMPORT: break; } diff --git a/src/lint_host.c b/src/lint_host.c index 8f860029..1c611af3 100644 --- a/src/lint_host.c +++ b/src/lint_host.c @@ -545,7 +545,7 @@ static void w021_collect_imports(ASTNode *n, W021Imports *im) { case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -634,7 +634,7 @@ static void w021_walk(ASTNode *node, LintContext *ctx, const W021Imports *im, case AST_NUM: case AST_STR: case AST_IDENT: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_BINOP: case AST_UNARY: case AST_ASSIGN: @@ -1092,7 +1092,7 @@ static void e003_walk(ASTNode *n, E003 *e, LintContext *ctx, int mode) { * error here instead of a silent no-op. */ case AST_NUM: case AST_STR: - case AST_NULL: + case AST_NULL: case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: diff --git a/src/lsp_builtin_index.h b/src/lsp_builtin_index.h index 08fbad02..c95d1b3e 100644 --- a/src/lsp_builtin_index.h +++ b/src/lsp_builtin_index.h @@ -165,7 +165,7 @@ static const char *builtin_docs[][2] = { {"keys", "keys — builtin; see docs/BUILTINS.md"}, {"leaky_relu", "leaky_relu of tensor → element-wise max(0.01*x, x). Works on 1D or 2D."}, {"len", "len — builtin; see docs/BUILTINS.md"}, - {"list_contains", "list_contains of [list, value] — 1 if any element structurally equals"}, + {"list_contains", "list_contains of [list, value] — true if any element structurally equals"}, {"list_index_of", "list_index_of of [list, value] — index of the first element structurally"}, {"list_insert_at", "list_insert_at of [list, index, value] — insert value at index, shift tail"}, {"list_remove_at", "list_remove_at of [list, index] — remove element at index, shift tail down."}, diff --git a/src/model_infer.c b/src/model_infer.c index df5a3d9e..b25aa189 100644 --- a/src/model_infer.c +++ b/src/model_infer.c @@ -519,11 +519,15 @@ int g_training_samples = 0; Value* builtin_eigen_model_loaded(Value *arg) { (void)arg; - return make_num(g_model.loaded ? 1 : 0); + return make_bool(g_model.loaded); } Value* builtin_eigen_generate(Value *arg) { STRICT_LIST_MAX(arg, 4, "eigen_generate"); + /* #1637: a bool token id (or option) raises in every mode, before the + * tape and before the no-model answer -- the ids were only read, and the + * bool only refused, once a model was loaded. */ + BOOL_REFUSE(arg, "eigen_generate"); /* Input: [prompt_ids_list, temperature, max_tokens] * Output: list of generated token IDs * @@ -563,10 +567,13 @@ Value* builtin_eigen_generate(Value *arg) { * 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->data.list.items[1]->type == VAL_NUM) temperature = arg->data.list.items[1]->data.num; - if (arg->data.list.items[2]->type == VAL_NUM) max_tokens = (int)arg->data.list.items[2]->data.num; - if (arg->data.list.count >= 4 && arg->data.list.items[3]->type == VAL_NUM) - top_p = arg->data.list.items[3]->data.num; + /* #1637: optional numbers -- null keeps the default, a bool raises. */ + double mt = max_tokens; + eigs_opt_num(arg->data.list.items[1], &temperature, __func__); + eigs_opt_num(arg->data.list.items[2], &mt, __func__); + if (arg->data.list.count >= 4) eigs_opt_num(arg->data.list.items[3], &top_p, __func__); + if (g_has_error) return make_null(); + max_tokens = (int)mt; if (!g_model.loaded) TRACE_NONDET_RECORD("eigen_generate", make_list(0)); @@ -601,8 +608,9 @@ Value* builtin_eigen_generate(Value *arg) { int *prompt_ids = xcalloc(prompt_len, sizeof(int)); for (int i = 0; i < prompt_len; i++) { Value *v = prompt_list->data.list.items[i]; - prompt_ids[i] = (v->type == VAL_NUM) ? (int)v->data.num : 0; + prompt_ids[i] = (int)eigs_num_arg(v, "eigen_generate"); /* #1637: a bool raises */ } + if (g_has_error) { free(prompt_ids); return make_null(); } int out_len = 0; int *output_ids = generate_response(prompt_ids, prompt_len, &g_model, temperature, max_tokens, top_p, &out_len); @@ -633,6 +641,7 @@ Value* builtin_eigen_eval_loss(Value *arg) { * Forward only -- no backward, no weight update, no requantise, no observer * state, and g_model_age/g_training_samples are untouched, so scoring a * checkpoint never mutates it. */ + BOOL_REFUSE(arg, "eigen_eval_loss"); /* #1637: before the no-model answer */ if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) { fprintf(stderr, "eigen_eval_loss: requires [prompt_ids, target_id]\n"); /* #1008: guarded in place with a constant condition so the stderr @@ -651,7 +660,7 @@ Value* builtin_eigen_eval_loss(Value *arg) { 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)); } - int target_id = (int)arg->data.list.items[1]->data.num; + int target_id = (int)eigs_list_num(arg, 1, __func__); /* 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 @@ -684,10 +693,11 @@ Value* builtin_eigen_eval_loss(Value *arg) { int *prompt_ids = xcalloc(prompt_len, sizeof(int)); for (int i = 0; i < prompt_len; i++) { Value *v = prompt_list->data.list.items[i]; - int t = (v->type == VAL_NUM) ? (int)v->data.num : 0; + int t = (int)eigs_num_arg(v, "eigen_eval_loss"); /* #1637: a bool raises */ if (t < 0 || t >= vocab_size) t = 0; prompt_ids[i] = t; } + if (g_has_error) { free(prompt_ids); return make_null(); } float *logits = xcalloc(vocab_size, sizeof(float)); native_forward(prompt_ids, prompt_len, &g_model, logits); diff --git a/src/model_train.c b/src/model_train.c index f652129b..493b44ca 100644 --- a/src/model_train.c +++ b/src/model_train.c @@ -1437,6 +1437,9 @@ Value* builtin_native_train_step(Value *arg) { if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) { return make_str("{\"status\": \"error\", \"error\": \"requires [input_ids, output_ids, lr]\"}"); } + /* #1637: a bool id or rate raises in every mode, before the no-model + * answer (the ids were only read once a model was loaded). */ + BOOL_REFUSE(arg, "native_train_step"); if (!g_model.loaded) { return make_str("{\"status\": \"error\", \"error\": \"Model not loaded\"}"); } @@ -1448,7 +1451,8 @@ Value* builtin_native_train_step(Value *arg) { } float lr = 0.001f; - if (arg->data.list.items[2]->type == VAL_NUM) lr = arg->data.list.items[2]->data.num; + if (arg->data.list.items[2]->type == VAL_BOOL) { eigs_num_arg(arg->data.list.items[2], __func__); return make_null(); } /* #1637 */ + if (arg->data.list.items[2]->type == VAL_NUM) lr = eigs_list_num(arg, 2, __func__); else if (arg->data.list.items[2]->type == VAL_STR) lr = strtod(arg->data.list.items[2]->data.str, NULL); if (lr <= 0 || lr > 1) lr = 0.001f; @@ -1465,12 +1469,13 @@ Value* builtin_native_train_step(Value *arg) { int *output_ids = xcalloc(output_len > 0 ? output_len : 1, sizeof(int)); for (int i = 0; i < input_len; i++) { Value *v = in_list->data.list.items[i]; - input_ids[i] = (v->type == VAL_NUM) ? (int)v->data.num : 0; + input_ids[i] = (int)eigs_num_arg(v, "native_train_step"); /* #1637: a bool raises */ } for (int i = 0; i < output_len; i++) { Value *v = out_list->data.list.items[i]; - output_ids[i] = (v->type == VAL_NUM) ? (int)v->data.num : 0; + output_ids[i] = (int)eigs_num_arg(v, "native_train_step"); } + if (g_has_error) { free(input_ids); free(output_ids); return make_null(); } float loss = 0.0f; int tokens_trained = 0; diff --git a/src/parser.c b/src/parser.c index e92d8438..04682aba 100644 --- a/src/parser.c +++ b/src/parser.c @@ -523,6 +523,7 @@ void free_ast(ASTNode *node) { * error here instead of a silently leaked child. */ case AST_NUM: case AST_NULL: + case AST_BOOL: case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: @@ -814,6 +815,13 @@ static ASTNode* parse_primary(Parser *p) { return make_node(AST_NULL, t->line); } + 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; + return b; + } + if (tok_is_report(t->type)) { p_advance(p); if (p_cur(p)->type != TOK_OF) p_report_error(t, 0); diff --git a/src/tape_read.c b/src/tape_read.c index 74dc5768..61efaf8e 100644 --- a/src/tape_read.c +++ b/src/tape_read.c @@ -268,7 +268,7 @@ static int tape_parse(Tape *t, long len) { uint64_t stream_id = 0; if (p[0] != 'V' && strchr("BLASNO", p[0])) { if (p[1] != ' ' || *body < '0' || *body > '9') { - fprintf(stderr, "step: malformed v5 stream id in '%s'; refusing to step\n", p); + fprintf(stderr, "step: malformed v%d stream id in '%s'; refusing to step\n", TRACE_FORMAT_VERSION, p); free(streams); return 0; } char *id_end = body; @@ -280,7 +280,7 @@ static int tape_parse(Tape *t, long len) { id_end++; } if (id == UINT64_MAX || *id_end != ' ' || !id_end[1]) { - fprintf(stderr, "step: malformed v5 stream id in '%s'; refusing to step\n", p); + fprintf(stderr, "step: malformed v%d stream id in '%s'; refusing to step\n", TRACE_FORMAT_VERSION, p); free(streams); return 0; } body = id_end + 1; diff --git a/src/task.c b/src/task.c index 6369d81c..ca7157e7 100644 --- a/src/task.c +++ b/src/task.c @@ -604,7 +604,8 @@ static Value *task_start(Task *t) { Value *fn = t->entry_fn; if (fn->type == VAL_BUILTIN) { /* builtins never suspend — run direct */ Value *a = t->argc == 1 ? t->args[0] : make_null(); - return fn->data.builtin(a); + Value *r = eigs_call_builtin(fn->data.builtin, a); /* #1637 bool gate */ + return r ? r : make_null(); } Env *call_env = env_new(fn->data.fn.closure); /* #989: same re-collect carve-out as every other entry point — a diff --git a/src/trace.c b/src/trace.c index 7eaa8877..d3427047 100644 --- a/src/trace.c +++ b/src/trace.c @@ -1912,8 +1912,8 @@ static int replay_record_prefix(ReplayContext *ctx, char kind, uint64_t *id, cha } static void replay_malformed_id(ReplayContext *ctx) { - fprintf(stderr, "trace: malformed v5 stream id in record '%s'; refusing to replay\n", - ctx->line ? ctx->line : ""); + fprintf(stderr, "trace: malformed v%d stream id in record '%s'; refusing to replay\n", + TRACE_FORMAT_VERSION, ctx->line ? ctx->line : ""); #if EIGENSCRIPT_FREESTANDING abort(); #else @@ -1922,8 +1922,8 @@ static void replay_malformed_id(ReplayContext *ctx) { } static void replay_malformed_value(const char *value) { - fprintf(stderr, "trace: malformed v5 stream id or N value '%s'; refusing to replay\n", - value ? value : ""); + fprintf(stderr, "trace: malformed v%d stream id or N value '%s'; refusing to replay\n", + TRACE_FORMAT_VERSION, value ? value : ""); #if EIGENSCRIPT_FREESTANDING abort(); #else @@ -1931,6 +1931,121 @@ static void replay_malformed_value(const char *value) { #endif } +/* #1637: THE return-kind table of every taped builtin -- each name that + * records an N value (TRACE_NONDET_*, ARG_GUARD_TAPED/PRETAKE, + * trace_nondet_value). A replayed value of a kind its builtin cannot return + * (a hand-edited `file_exists=1`, a `random_normal=true`) would hand the + * program a value no live run could produce, at exit 0; it is refused + * instead. tools/tape_kinds_check.sh fails when a taped name is missing here + * or a row names nothing taped. Names a host records through the embedding + * 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_STR_ TK(VAL_STR) +#define TK_BOOL_ TK(VAL_BOOL) +#define TK_NULL_ TK(VAL_NULL) +#define TK_LIST_ TK(VAL_LIST) +#define TK_BUF_ TK(VAL_BUFFER) +static const struct { const char *name; unsigned kinds; } k_tape_kinds[] = { + {"args", TK_LIST_}, + {"audio_capture_open", TK_NUM_}, + {"audio_capture_read", TK_BUF_ | TK_NULL_}, + {"audio_stream_queued", TK_NUM_}, + {"clock_unix", TK_NUM_}, + {"eigen_generate", TK_LIST_ | TK_STR_}, /* str: the recorded over-length refusal */ + {"env_get", TK_STR_}, + {"exe_path", TK_STR_}, + {"file_exists", TK_BOOL_}, + {"getcwd", TK_STR_}, + {"gfx_read", TK_LIST_ | TK_NULL_}, + {"heap_inuse", TK_NUM_}, + {"http_post", TK_STR_}, + {"http_request_body", TK_STR_}, + {"http_request_headers", TK_STR_}, + {"http_session_id", TK_STR_}, + {"is_dir", TK_BOOL_}, + {"is_file", TK_BOOL_}, + {"ls", TK_LIST_}, + {"mkdir", TK_BOOL_}, + {"monotonic_ms", TK_NUM_}, + {"monotonic_ns", TK_NUM_}, + {"net_accept", TK_NUM_ | TK_NULL_}, + {"net_dial", TK_NUM_ | TK_NULL_}, + {"net_listen", TK_NUM_ | TK_NULL_}, + {"net_port", TK_NUM_ | TK_NULL_}, + {"net_recv", TK_BUF_ | TK_NULL_}, + {"net_send", TK_NUM_}, + {"random", TK_NUM_}, + {"random_hex", TK_STR_}, + {"random_int", TK_NUM_}, + {"random_normal", TK_LIST_ | TK_NULL_}, + {"read_bytes", TK_LIST_ | TK_NULL_}, + {"read_bytes_buf", TK_BUF_ | TK_NULL_ | TK_NUM_}, /* num: the recorded over-cap size */ + {"read_line", TK_STR_ | TK_NULL_}, + {"read_text", TK_STR_}, + {"tensor_load", TK_LIST_ | TK_NULL_}, /* the [rows, cols] over-cap verdict */ +}; + +/* Host-declared kinds (eigs_trace_declare_kind). Small, append-only. */ +static struct { char *name; unsigned kinds; } *g_host_kinds; +static int g_host_kinds_n, g_host_kinds_cap; +static pthread_mutex_t g_host_kinds_mu = PTHREAD_MUTEX_INITIALIZER; + +static unsigned core_tape_kinds(const char *fn) { + for (size_t i = 0; i < sizeof k_tape_kinds / sizeof k_tape_kinds[0]; i++) + if (strcmp(fn, k_tape_kinds[i].name) == 0) return k_tape_kinds[i].kinds; + return 0; +} + +int trace_declare_kind(const char *name, unsigned kinds) { + if (!name || !*name || !kinds || core_tape_kinds(name)) return 0; + pthread_mutex_lock(&g_host_kinds_mu); + for (int i = 0; i < g_host_kinds_n; i++) + if (strcmp(g_host_kinds[i].name, name) == 0) { + g_host_kinds[i].kinds = kinds; + pthread_mutex_unlock(&g_host_kinds_mu); + return 1; + } + if (g_host_kinds_n == g_host_kinds_cap) { + int nc = g_host_kinds_cap ? g_host_kinds_cap * 2 : 8; + void *nk = realloc(g_host_kinds, (size_t)nc * sizeof *g_host_kinds); + if (!nk) { pthread_mutex_unlock(&g_host_kinds_mu); return 0; } + g_host_kinds = nk; g_host_kinds_cap = nc; + } + g_host_kinds[g_host_kinds_n].name = xstrdup(name); + g_host_kinds[g_host_kinds_n].kinds = kinds; + g_host_kinds_n++; + pthread_mutex_unlock(&g_host_kinds_mu); + return 1; +} + +static unsigned replay_expected_kinds(const char *fn) { + if (!fn) return 0; + unsigned k = core_tape_kinds(fn); + if (k) return k; + pthread_mutex_lock(&g_host_kinds_mu); + for (int i = 0; i < g_host_kinds_n; i++) + if (strcmp(g_host_kinds[i].name, fn) == 0) { k = g_host_kinds[i].kinds; break; } + pthread_mutex_unlock(&g_host_kinds_mu); + return k; +} + +/* "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, + VAL_BUFFER, VAL_NULL}; + size_t used = 0; int first = 1; + buf[0] = '\0'; + for (size_t i = 0; i < sizeof order / sizeof order[0]; i++) { + if (!(kinds & TK(order[i]))) continue; + int w = snprintf(buf + used, n - used, "%s%s", first ? "a " : " or ", + val_type_name(order[i])); + if (w < 0 || (size_t)w >= n - used) break; + used += (size_t)w; first = 0; + } +} + static int replay_value_is_marker(const char *value) { return value && (value[0] == '<' || (unsigned char)value[0] == 0xE2); } @@ -2192,11 +2307,14 @@ static Value *parse_value_p(const char **p) { if (strncmp(*p, "null", 4) == 0 && at_token_boundary((*p)[4])) { *p += 4; return make_null(); } + /* #1637 (format v6): true/false are the bool values. A v5 tape wrote + * a predicate's answer as 1/0; it is refused by the version check, never + * read as a number here. */ if (strncmp(*p, "true", 4) == 0 && at_token_boundary((*p)[4])) { - *p += 4; return make_num(1.0); + *p += 4; return make_bool(1); } if (strncmp(*p, "false", 5) == 0 && at_token_boundary((*p)[5])) { - *p += 5; return make_num(0.0); + *p += 5; return make_bool(0); } if (c == '"') { @@ -2554,6 +2672,30 @@ int trace_replay_take(const char *fn, Value **out) { Value *v = parse_value(hit->value); int marker = replay_value_is_marker(hit->value); if (!v && !marker) replay_malformed_value(hit->value); + { + /* #1637: the record's kind must be one its builtin can return. */ + const char *who = fn ? fn : hit->name; + unsigned want = replay_expected_kinds(who); + if (!want || (v && !(want & TK(v->type)))) { + char wtxt[96]; + kinds_text(want, wtxt, sizeof wtxt); + if (!want) + fprintf(stderr, "trace: tape format v%d record 'N %llu %s=%s': %s has no " + "declared return kind (a host name declares it with " + "eigs_trace_declare_kind); refusing to replay\n", TRACE_FORMAT_VERSION, + (unsigned long long)hit->stream_id, hit->name, hit->value, who); + else + fprintf(stderr, "trace: tape format v%d record 'N %llu %s=%s': %s returns %s, " + "the tape holds a %s; refusing to replay\n", TRACE_FORMAT_VERSION, + (unsigned long long)hit->stream_id, hit->name, hit->value, + who, wtxt, val_type_name(v->type)); +#if EIGENSCRIPT_FREESTANDING + abort(); +#else + _exit(3); +#endif + } + } free(hit->name); free(hit->value); free(hit); if (!v) { replay_take_unlock(); return 0; } *out = v; @@ -2693,7 +2835,7 @@ static void write_string(const char *s) { static void write_value_ptr(Value *v) { if (!v) { tp_puts("null"); return; } switch (v->type) { - case VAL_NUM: tp_printf("%.17g", v->data.num); break; + case VAL_NUM: tp_printf("%.17g", VAL_NUM_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; @@ -2703,13 +2845,14 @@ static void write_value_ptr(Value *v) { case VAL_BUFFER: tp_printf("", v->data.buffer.count); break; case VAL_JSON_RAW: tp_puts(""); break; case VAL_TEXT_BUILDER: tp_puts(""); break; + case VAL_BOOL: tp_puts(v->data.boolean ? "true" : "false"); break; /* No `default:` — -Werror=switch (Makefile CFLAGS) forces a new * ValType to choose its tape rendering here. */ } } static void write_slot(EigsSlot s) { - if (slot_is_num(s)) { tp_printf("%.17g", s.d); return; } + if (slot_is_num(s)) { tp_printf("%.17g", SLOT_NUM_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; } @@ -2826,8 +2969,9 @@ static void write_value_ptr_full(Value *v, int *budget) { if (*budget <= 0) return; if (!v) { wf_puts("null", budget); return; } switch (v->type) { - case VAL_NUM: wf_printf(budget, "%.17g", v->data.num); break; + case VAL_NUM: wf_printf(budget, "%.17g", VAL_NUM_RAW(v)); break; case VAL_NULL: wf_puts("null", budget); break; + case VAL_BOOL: wf_puts(v->data.boolean ? "true" : "false", budget); break; case VAL_STR: write_string_full(v->data.str, budget); break; case VAL_LIST: { wf_putc('[', budget); diff --git a/src/trace.h b/src/trace.h index a7e0b55b..79885687 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; +typedef union { double d_; uint64_t u; } EigsSlot; /* d_: read via SLOT_NUM_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; * 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 5 /* v5: stream/state/correspondence declarations */ +#define TRACE_FORMAT_VERSION 6 /* v6: true/false are bool values (#1637) */ #define TRACE_STREAM_KEY_MAX 1024 /* B payload parser shared with the non-executing tape reader. key_hex points @@ -435,6 +435,11 @@ int trace_replay_refuse_off_owner(const char *fn); * Hot-path cost when both disabled: two predicted-not-taken loads + branches. * Each call site must have `Value` defined (i.e. include eigenscript.h * before trace.h). */ +/* #1637: declare the return kinds (a ValType bit set, 1u << VAL_X) of a + * host-recorded nondet name; replay refuses a record of another kind, and a + * name declared nowhere. 0 = refused (empty name, no kinds, a core name). */ +int trace_declare_kind(const char *name, unsigned kinds); + #define TRACE_NONDET_RET(name, expr) do { \ Value *_tr_v; \ if (__builtin_expect(g_replay_enabled, 0)) { \ diff --git a/src/value_slot.h b/src/value_slot.h index 45a840cf..3b782f35 100644 --- a/src/value_slot.h +++ b/src/value_slot.h @@ -30,7 +30,7 @@ #ifndef EIGENSCRIPT_EIGSSLOT_UNION_DEFINED #define EIGENSCRIPT_EIGSSLOT_UNION_DEFINED -typedef union { double d; uint64_t u; } EigsSlot; +typedef union { double d_; uint64_t u; } EigsSlot; /* d_: read via SLOT_NUM_RAW (#1637) */ #endif #define SLOT_QNAN_MASK 0xFFF8000000000000ULL @@ -60,14 +60,17 @@ static inline int slot_is_numeric(EigsSlot s) { return slot_is_num(s); } /* Accessors (caller must check the predicate first) */ -static inline double slot_as_num(EigsSlot s) { return s.d; } +/* #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 + * 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_as_heap slot_as_ptr /* Constructors */ -static inline EigsSlot slot_from_num(double d) { EigsSlot s; s.d = d; return s; } +static inline EigsSlot slot_from_num(double d) { EigsSlot s; SLOT_NUM_RAW(s) = d; return s; } static inline EigsSlot slot_from_heap(Value *v) { EigsSlot s; s.u = TAG_HEAP | ((uint64_t)(uintptr_t)v & 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; } @@ -127,7 +130,7 @@ static inline void slot_decref(EigsSlot s) { * are falsy; everything else (heap, tracked) defers to caller-side * is_truthy() for now (Phase A keeps semantics identical). */ static inline int slot_truthy_immediate(EigsSlot s, int *out_decided) { - if (slot_is_num(s)) { *out_decided = 1; return s.d != 0.0; } + if (slot_is_num(s)) { *out_decided = 1; return SLOT_NUM_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; } *out_decided = 0; diff --git a/src/vm.c b/src/vm.c index 6d562eb4..71a7d7bc 100644 --- a/src/vm.c +++ b/src/vm.c @@ -115,13 +115,13 @@ 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(s.d, buf, nbuf); return; } + if (slot_is_num(s)) { obs_dump_num(SLOT_NUM_RAW(s), buf, nbuf); return; } 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(v->data.num, buf, nbuf); break; + case VAL_NUM: obs_dump_num(VAL_NUM_RAW(v), buf, nbuf); break; case VAL_STR: { size_t o = 0, i; buf[o++] = '"'; @@ -149,6 +149,7 @@ static void obs_dump_value(EigsSlot s, char *buf, size_t nbuf) { /* No `default:` — -Werror=switch (Makefile CFLAGS) forces a new * ValType to choose its observer-dump rendering here. */ case VAL_NULL: snprintf(buf, nbuf, "null"); break; + case VAL_BOOL: snprintf(buf, nbuf, "%s", v->data.boolean ? "true" : "false"); break; } return; } @@ -210,7 +211,9 @@ static void obs_dump_scope(const char *scope, Env *e, int shared, * The band classifies the same refreshed view. */ ObserverSlot qs = *os; if (slot_is_num(s)) { - qs.entropy = observer_entropy_of_num(s.d); + qs.entropy = observer_entropy_of_num(SLOT_NUM_RAW(s)); + } else if (slot_is_bool(s)) { + qs.entropy = compute_entropy(make_bool(slot_as_bool(s))); /* #1637 */ } else if (slot_is_ptr(s)) { Value *sv0 = slot_as_ptr(s); if (sv0 && sv0->type != VAL_NULL) @@ -711,7 +714,7 @@ static inline int dict_set_cached_immediate(Value *dict, const char *key, uint32 if (existing && existing->type == VAL_NUM && existing->refcount == 1 && !existing->arena) { - existing->data.num = num; + VAL_NUM_RAW(existing) = num; return 1; } } @@ -785,6 +788,9 @@ static inline EigsSlot slot_bridge_wrap(Value *v) { val_decref(v); return slot_null(); } + /* #1637: a bool is always one of the two immortal singletons, so the + * pointer can be dropped for the TAG_BOOL immediate with no decref. */ + if (v->type == VAL_BOOL) return slot_from_bool(v->data.boolean); /* #262 Step E: nums never carry observer state → never TAG_TRACKED. */ return slot_from_heap(v); } @@ -792,13 +798,13 @@ static inline EigsSlot slot_bridge_wrap(Value *v) { static inline Value *slot_bridge_unwrap(EigsSlot s) { if (slot_is_num(s)) { /* Materialize immediate -> fresh Value. Caller owns one ref. */ - return make_num(s.d); + return make_num(SLOT_NUM_RAW(s)); } if (slot_is_null(s)) { return make_null(); } if (slot_is_bool(s)) { - return make_num(slot_as_bool(s) ? 1.0 : 0.0); + return make_bool(slot_as_bool(s)); } /* Heap or tracked: just unwrap the pointer (ref already in slot). */ return slot_as_ptr(s); @@ -835,9 +841,9 @@ 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(s.d); + if (slot_is_num(s)) v = make_num(SLOT_NUM_RAW(s)); else if (slot_is_null(s)) v = make_null(); - else if (slot_is_bool(s)) v = make_num(slot_as_bool(s) ? 1.0 : 0.0); + else if (slot_is_bool(s)) v = make_bool(slot_as_bool(s)); else v = make_null(); g_vm.stack[idx] = slot_from_heap(v); return v; @@ -868,7 +874,7 @@ 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. */ static inline int slot_truthy(EigsSlot s) { - if (slot_is_num(s)) return s.d != 0.0; + if (slot_is_num(s)) return SLOT_NUM_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)); @@ -878,10 +884,10 @@ static inline int slot_truthy(EigsSlot s) { * 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 = s.d; return 1; } + 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 = v->data.num; return 1; } + if (v->type == VAL_NUM) { *out = VAL_NUM_RAW(v); return 1; } } return 0; } @@ -1170,7 +1176,7 @@ static inline void vm_store_local_slot(Env *e, int slot, EigsSlot tos) { Value *existing = slot_as_ptr(ex_s); if (existing && existing->type == VAL_NUM && existing->refcount == 1 && !existing->arena) { - existing->data.num = tos.d; + VAL_NUM_RAW(existing) = SLOT_NUM_RAW(tos); return; } } @@ -1385,7 +1391,7 @@ int jit_helper_local_idx_get(int slot, int idx) { if (i < target->data.list.count) { Value *item = target->data.list.items[i]; if (item && item->type == VAL_NUM) { - vm_push_slot(slot_from_num(item->data.num)); + vm_push_slot(slot_from_num(VAL_NUM_RAW(item))); } else { val_incref(item); vm_push(item); @@ -1441,7 +1447,7 @@ int jit_helper_local_dot_get(EigsChunk *chunk, int slot, int name_idx) { Value *v = dict_get_cached(target, key, h); if (v) { if (v->type == VAL_NUM) { - vm_push_slot(slot_from_num(v->data.num)); + vm_push_slot(slot_from_num(VAL_NUM_RAW(v))); } else { val_incref(v); vm_push(v); @@ -1495,7 +1501,7 @@ int jit_helper_local_idx_dot_get(EigsChunk *chunk, int slot, Value *v = dict_get_cached(dict, key, h); if (v) { if (v->type == VAL_NUM) { - vm_push_slot(slot_from_num(v->data.num)); + vm_push_slot(slot_from_num(VAL_NUM_RAW(v))); } else { val_incref(v); vm_push(v); @@ -1553,7 +1559,7 @@ void jit_helper_dot_set(EigsChunk *chunk, int name_idx) { if (slot_is_num(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, val_s.d)) { + dict_set_cached_immediate(target, key, h, SLOT_NUM_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; @@ -1586,7 +1592,7 @@ void jit_helper_dot_get(EigsChunk *chunk, int name_idx) { Value *v = dict_get_cached(target, key, h); if (v) { if (v->type == VAL_NUM) { - double n = v->data.num; + double n = VAL_NUM_RAW(v); val_decref(target); vm_push_slot(slot_from_num(n)); return; @@ -1627,7 +1633,7 @@ void jit_helper_local_dot_set(EigsChunk *chunk, int slot, int name_idx) { } EigsSlot tos = g_vm.stack[g_vm.sp - 1]; if (slot_is_num(tos) && - dict_set_cached_immediate(target, key, h, tos.d)) { + dict_set_cached_immediate(target, key, h, SLOT_NUM_RAW(tos))) { return; } Value *val = vm_slot_lift(g_vm.sp - 1); @@ -1667,6 +1673,17 @@ void jit_helper_observe_assign(EigsChunk *chunk, int name_idx) { * so the JIT helper does not bounce through two more out-of-line functions on * every assignment. The public routine owns allocation/window-growth and is * still the cold fallback. */ +/* #1637: the observer sees a bool binding through its immortal singleton + * (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. */ +static inline int obs_slot_observable(EigsSlot s) { + return slot_is_ptr(s) || slot_is_bool(s); +} +static inline Value *obs_slot_value(EigsSlot s) { + return slot_is_bool(s) ? make_bool(slot_as_bool(s)) : slot_as_ptr(s); +} + static inline void jit_sample_num_gated(Env *e, int slot, double v) { ObserverSlot *s = env_obs_slot(e, slot); int n = s && s->win_override ? s->win_override : g_obs_window; @@ -1707,15 +1724,15 @@ void jit_helper_observe_assign_local(int slot) { Env *e = frame->fn_env; 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, s.d); - else if (slot_is_ptr(s)) observer_slot_sample_gated(e, slot, slot_as_ptr(s)); + if (slot_is_num(s)) jit_sample_num_gated(e, slot, SLOT_NUM_RAW(s)); + 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, s.d); + observer_slot_update_num(e, slot, SLOT_NUM_RAW(s)); g_last_obs_slot_env = e; g_last_obs_slot_idx = slot; - } else if (slot_is_ptr(s)) { - observer_slot_update(e, slot, slot_as_ptr(s)); + } else if (obs_slot_observable(s)) { + observer_slot_update(e, slot, obs_slot_value(s)); g_last_obs_slot_env = e; g_last_obs_slot_idx = slot; } } @@ -1759,21 +1776,21 @@ void jit_helper_observe_name_post(EigsChunk *chunk, int name_idx) { EigsSlot s = g_vm.stack[g_vm.sp - 1]; /* #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. Skip null/bool. */ - if (!slot_is_num(s) && !slot_is_ptr(s)) return; + * slot from whichever (#1637: a bool too). Skip null. */ + if (!slot_is_num(s) && !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, s.d); - else observer_slot_sample_gated(oe, oidx, slot_as_ptr(s)); + 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)); return; } if (oe && oidx >= 0) { - if (slot_is_num(s)) observer_slot_update_num(oe, oidx, s.d); - else observer_slot_update(oe, oidx, slot_as_ptr(s)); + if (slot_is_num(s)) observer_slot_update_num(oe, oidx, SLOT_NUM_RAW(s)); + else observer_slot_update(oe, oidx, obs_slot_value(s)); g_last_obs_slot_env = oe; g_last_obs_slot_idx = oidx; /* #262 Phase-3 D2: slot-source the temporal snapshot — mirror of the @@ -1805,7 +1822,7 @@ int jit_helper_iter_next(void) { if (!iterable) return 1; Value *idx_v = state->data.list.items[1]; if (!idx_v || idx_v->type != VAL_NUM) return 1; - int idx = (int)idx_v->data.num; + int idx = (int)VAL_NUM_RAW(idx_v); int len = 0; if (iterable->type == VAL_LIST) len = iterable->data.list.count; else if (iterable->type == VAL_BUFFER) len = iterable->data.buffer.count; @@ -1815,7 +1832,7 @@ int jit_helper_iter_next(void) { if (state->data.list.count >= 3) { Value *snap_v = state->data.list.items[2]; if (snap_v && snap_v->type == VAL_NUM) { - int snap = (int)snap_v->data.num; + int snap = (int)VAL_NUM_RAW(snap_v); if (snap < len) len = snap; } } @@ -1830,7 +1847,7 @@ int jit_helper_iter_next(void) { /* In-place idx bump when the slot is an exclusive plain VAL_NUM * (matches the interpreter's NUM_REUSE fast path). */ if (idx_v->type == VAL_NUM && idx_v->refcount == 1 && !idx_v->arena) { - idx_v->data.num = (double)(idx + 1); + VAL_NUM_RAW(idx_v) = (double)(idx + 1); } else { val_decref(idx_v); /* #873: heap-force when the state list must outlive an active @@ -1908,7 +1925,7 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) { case OP_CONST: { uint16_t idx = read_u16(ip); ip += 2; /* Scanner guarantees VAL_NUM. */ - mini[msp++] = slot_from_num(c->constants[idx]->data.num); + mini[msp++] = slot_from_num(VAL_NUM_RAW(c->constants[idx])); break; } case OP_NUM_ZERO: @@ -1942,7 +1959,7 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) { * null/num input — these are borrows, so wrap by hand. */ mini[msp++] = slot_null(); } else if (v->type == VAL_NUM) { - mini[msp++] = slot_from_num(v->data.num); + mini[msp++] = slot_from_num(VAL_NUM_RAW(v)); } else { mini[msp++] = slot_from_heap(v); /* borrow, no incref */ } @@ -1963,7 +1980,7 @@ static int vm_leaf_accessor_exec(EigsChunk *c, int argc) { Value *r = target->data.list.items[i]; if (!r) return 0; if (r->type == VAL_NUM) - mini[msp++] = slot_from_num(r->data.num); + mini[msp++] = slot_from_num(VAL_NUM_RAW(r)); else if (r->type == VAL_NULL) mini[msp++] = slot_null(); else @@ -2076,7 +2093,7 @@ static void loop_iter_store(Env *env, double n) { LoopIterCache *c = &g_loop_iter_cache; if (c->env == env && c->version == env->binding_version && slot_is_num(env->values[c->slot])) { - env->values[c->slot].d = n; + SLOT_NUM_RAW(env->values[c->slot]) = n; if (env->assign_counts) env->assign_counts[c->slot]++; return; @@ -2205,12 +2222,12 @@ void jit_helper_index_set(void) { EigsSlot tgt_s = g_vm.stack[g_vm.sp - 3]; if (slot_is_num(idx_s) && slot_is_ptr(tgt_s)) { Value *target = slot_as_ptr(tgt_s); - int i, _ok = vm_index_is_int(idx_s.d, &i); + int i, _ok = vm_index_is_int(SLOT_NUM_RAW(idx_s), &i); if (target->type == VAL_BUFFER && slot_is_num(val_s)) { if (_ok && vm_index_resolve(&i, target->data.buffer.count)) { - target->data.buffer.data[i] = val_s.d; + target->data.buffer.data[i] = SLOT_NUM_RAW(val_s); } else { - if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx_s.d); + if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_NUM_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); } @@ -2226,7 +2243,7 @@ void jit_helper_index_set(void) { if (existing && existing->type == VAL_NUM && existing->refcount == 1 && !existing->arena) { - existing->data.num = val_s.d; + VAL_NUM_RAW(existing) = SLOT_NUM_RAW(val_s); } else { val_decref(existing); /* #873: inside an arena window a plain make_num is @@ -2234,11 +2251,11 @@ void jit_helper_index_set(void) { * after arena_reset. Heap-force for heap targets. */ target->data.list.items[i] = (g_arena.active && !target->arena) - ? make_num_permanent(val_s.d) - : make_num(val_s.d); + ? make_num_permanent(SLOT_NUM_RAW(val_s)) + : make_num(SLOT_NUM_RAW(val_s)); } } else { - if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx_s.d); + if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_NUM_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); } @@ -2252,8 +2269,8 @@ 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) { int i; - if (!vm_index_is_int(idx->data.num, &i)) { - rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx->data.num); + 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)); } else if (vm_index_resolve(&i, target->data.list.count)) { /* #873: promote an arena value stored into a heap list — * same contract as list_append / the env store paths. */ @@ -2274,12 +2291,12 @@ void jit_helper_index_set(void) { } } else if (target->type == VAL_BUFFER && idx->type == VAL_NUM) { int i; - if (!vm_index_is_int(idx->data.num, &i)) { - rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx->data.num); + 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)); } 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->data.num; + target->data.buffer.data[i] = VAL_NUM_RAW(val); } 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)", @@ -2300,15 +2317,15 @@ int jit_helper_index_get(void) { g_vm.sp -= 2; if (slot_is_num(idx_s) && slot_is_ptr(tgt_s)) { Value *target = slot_as_ptr(tgt_s); - int i, _ok = vm_index_is_int(idx_s.d, &i); + int i, _ok = vm_index_is_int(SLOT_NUM_RAW(idx_s), &i); if (target->type == VAL_LIST) { if (_ok && vm_index_resolve(&i, target->data.list.count)) { Value *r = target->data.list.items[i]; if (r && r->type == VAL_NUM) { /* See CASE(INDEX_GET) for the use-after-free story — - * snapshot r->data.num before slot_decref(tgt_s) + * snapshot VAL_NUM_RAW(r) before slot_decref(tgt_s) * potentially frees r via the num freelist. */ - double rv = r->data.num; + double rv = VAL_NUM_RAW(r); slot_decref(tgt_s); vm_push_slot(slot_from_num(rv)); } else { @@ -2317,7 +2334,7 @@ int jit_helper_index_get(void) { vm_push(r); } } else { - if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx_s.d); + if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_NUM_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); @@ -2332,7 +2349,7 @@ int jit_helper_index_get(void) { slot_decref(tgt_s); vm_push_slot(slot_from_num(v)); } else { - if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx_s.d); + if (!_ok) rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", SLOT_NUM_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); @@ -2350,8 +2367,8 @@ int jit_helper_index_get(void) { Value *result = make_null(); if (target->type == VAL_LIST && idx->type == VAL_NUM) { int i; - if (!vm_index_is_int(idx->data.num, &i)) { - rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx->data.num); + 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)); } else if (vm_index_resolve(&i, target->data.list.count)) { result = target->data.list.items[i]; val_incref(result); @@ -2365,8 +2382,8 @@ int jit_helper_index_get(void) { if (v) { result = v; val_incref(result); } } else if (target->type == VAL_STR && idx->type == VAL_NUM) { int i; - if (!vm_index_is_int(idx->data.num, &i)) { - rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx->data.num); + 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)); } 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); @@ -2377,8 +2394,8 @@ int jit_helper_index_get(void) { } } else if (target->type == VAL_BUFFER && idx->type == VAL_NUM) { int i; - if (!vm_index_is_int(idx->data.num, &i)) - rt_error(EK_VALUE, g_vm.current_line, "index must be an integer, got %g", idx->data.num); + 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)); else if (vm_index_resolve(&i, target->data.buffer.count)) result = make_num(buffer_read_num(target, i)); else @@ -2735,7 +2752,7 @@ int jit_helper_call(EigsChunk *caller_chunk, int argc, int resume_off) { Env *saved = g_builtin_call_env; g_builtin_call_env = frame->env; int consumes_arg = (fn_val->data.builtin == builtin_free_val); - Value *result = fn_val->data.builtin(arg); + Value *result = eigs_call_builtin(fn_val->data.builtin, arg); g_builtin_call_env = saved; frame->ip = thunk_entry_ip; @@ -3035,7 +3052,8 @@ static int vm_handler_in_range(int base) { 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"; } - return "num"; /* immediate double / bool */ + if (slot_is_bool(s)) return "bool"; /* #1637 */ + return "num"; /* immediate double */ } /* #408: forward decls — the copying-stack save/restore and the "who is @@ -3118,6 +3136,7 @@ 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_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_BAND] = &&lbl_BAND, [OP_BOR] = &&lbl_BOR, [OP_BXOR] = &&lbl_BXOR, @@ -3267,7 +3286,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, /* 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(v->data.num)); + vm_push_slot(slot_from_num(VAL_NUM_RAW(v))); } else { val_incref(v); vm_push(v); @@ -3290,6 +3309,16 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, DISPATCH(); } + CASE(TRUE): { /* #1637 */ + vm_push_slot(slot_true()); + DISPATCH(); + } + + CASE(FALSE): { /* #1637 */ + vm_push_slot(slot_false()); + DISPATCH(); + } + /* ---- Arithmetic ---- */ /* NUM_REUSE — retained for slow-path fallbacks that allocate via @@ -3314,7 +3343,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, if (slot_is_ptr(_as)) { \ Value *_a = slot_as_ptr(_as); \ if (NUM_REUSE(_a)) { \ - _a->data.num = _r; \ + VAL_NUM_RAW(_a) = _r; \ slot_decref(_bs); \ g_vm.sp--; \ DISPATCH(); \ @@ -3323,7 +3352,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)) { \ - _b->data.num = _r; \ + VAL_NUM_RAW(_b) = _r; \ g_vm.stack[g_vm.sp - 2] = _bs; \ slot_decref(_as); \ g_vm.sp--; \ @@ -3342,9 +3371,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, /* 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(a->data.num + b->data.num); - if (NUM_REUSE(a)) { a->data.num = r; vm_push(a); val_decref(b); DISPATCH(); } - if (NUM_REUSE(b)) { b->data.num = r; vm_push(b); val_decref(a); DISPATCH(); } + 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)); } else if (a->type == VAL_STR && b->type == VAL_STR) { int la = val_str_len(a), lb = val_str_len(b); @@ -3395,11 +3424,11 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, ARITH_FAST(OP, UF); \ Value *b = vm_pop(); Value *a = vm_pop(); \ if (a->type == VAL_NUM && b->type == VAL_NUM) { \ - double r = num_guard(a->data.num OP b->data.num); \ - if ((UF) && r == 0.0 && a->data.num != 0.0 && b->data.num != 0.0) \ + 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) \ g_math_flags |= EIGS_MATH_UNDERFLOW; \ - if (NUM_REUSE(a)) { a->data.num = r; vm_push(a); val_decref(b); DISPATCH(); } \ - if (NUM_REUSE(b)) { b->data.num = r; vm_push(b); val_decref(a); DISPATCH(); } \ + 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)); \ } else { \ rt_error(EK_TYPE, current_line, "cannot apply '%s' to %s and %s", \ @@ -3423,17 +3452,17 @@ 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 (b->data.num == 0.0) { + if (VAL_NUM_RAW(b) == 0.0) { rt_error(EK_VALUE, current_line, "division by zero"); vm_push(make_num(0)); } else { - double r = num_guard(a->data.num / b->data.num); + double r = num_guard(VAL_NUM_RAW(a) / VAL_NUM_RAW(b)); /* divisor already known nonzero (the raise above), so a zero * quotient from a nonzero dividend is underflow. */ - if (r == 0.0 && a->data.num != 0.0) + if (r == 0.0 && VAL_NUM_RAW(a) != 0.0) g_math_flags |= EIGS_MATH_UNDERFLOW; - if (NUM_REUSE(a)) { a->data.num = r; vm_push(a); val_decref(b); DISPATCH(); } - if (NUM_REUSE(b)) { b->data.num = r; vm_push(b); val_decref(a); DISPATCH(); } + 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)); } } else { @@ -3447,10 +3476,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, CASE(MOD): { Value *b = vm_pop(); Value *a = vm_pop(); - if (a->type == VAL_NUM && b->type == VAL_NUM && b->data.num != 0.0) { - double r = num_guard(fmod(a->data.num, b->data.num)); - if (NUM_REUSE(a)) { a->data.num = r; vm_push(a); val_decref(b); DISPATCH(); } - if (NUM_REUSE(b)) { b->data.num = r; vm_push(b); val_decref(a); DISPATCH(); } + 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)); } else { if (!(a->type == VAL_NUM && b->type == VAL_NUM)) @@ -3479,11 +3508,11 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, double _r = (double)((int64_t)_ad OP (RHS)); \ if (slot_is_ptr(_as)) { \ Value *_a = slot_as_ptr(_as); \ - if (NUM_REUSE(_a)) { _a->data.num = _r; slot_decref(_bs); g_vm.sp--; DISPATCH(); } \ + 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)) { _b->data.num = _r; g_vm.stack[g_vm.sp - 2] = _bs; slot_decref(_as); g_vm.sp--; DISPATCH(); } \ + 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); \ @@ -3521,13 +3550,13 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, if (__builtin_expect(slot_as_double(s, &d), 1)) { if (slot_is_ptr(s)) { Value *v = slot_as_ptr(s); - if (NUM_REUSE(v)) { v->data.num = -d; DISPATCH(); } + if (NUM_REUSE(v)) { VAL_NUM_RAW(v) = -d; DISPATCH(); } } slot_decref(s); g_vm.stack[g_vm.sp - 1] = slot_from_num(-d); DISPATCH(); } - rt_error(EK_TYPE, current_line, "cannot negate non-numeric"); + 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); DISPATCH(); @@ -3537,7 +3566,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, EigsSlot s = g_vm.stack[g_vm.sp - 1]; int t = slot_truthy(s); slot_decref(s); - g_vm.stack[g_vm.sp - 1] = slot_from_num(t ? 0.0 : 1.0); + g_vm.stack[g_vm.sp - 1] = slot_from_bool(!t); /* #1637 */ DISPATCH(); } @@ -3548,7 +3577,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, double r = (double)(~(int64_t)d); if (slot_is_ptr(s)) { Value *v = slot_as_ptr(s); - if (NUM_REUSE(v)) { v->data.num = r; DISPATCH(); } + if (NUM_REUSE(v)) { VAL_NUM_RAW(v) = r; DISPATCH(); } } slot_decref(s); g_vm.stack[g_vm.sp - 1] = slot_from_num(r); @@ -3570,23 +3599,18 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, 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)) { - double _r = (_ad == _bd) ? 1.0 : 0.0; - if (slot_is_ptr(_as)) { - Value *_a = slot_as_ptr(_as); - if (NUM_REUSE(_a)) { _a->data.num = _r; slot_decref(_bs); g_vm.sp--; DISPATCH(); } - } - if (slot_is_ptr(_bs)) { - Value *_b = slot_as_ptr(_bs); - if (NUM_REUSE(_b)) { _b->data.num = _r; g_vm.stack[g_vm.sp - 2] = _bs; slot_decref(_as); g_vm.sp--; DISPATCH(); } - } + /* #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. */ + int _r = (_ad == _bd); 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_bool(_r); g_vm.sp--; DISPATCH(); } Value *b = vm_pop(); Value *a = vm_pop(); - int eq = values_equal(a, b); - vm_push(make_num(eq ? 1.0 : 0.0)); + int eq = values_equal_op(a, b, "=="); /* raises on bool vs num */ + vm_push_slot(slot_from_bool(eq)); val_decref(a); val_decref(b); DISPATCH(); } @@ -3596,23 +3620,18 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, 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)) { - double _r = (_ad != _bd) ? 1.0 : 0.0; - if (slot_is_ptr(_as)) { - Value *_a = slot_as_ptr(_as); - if (NUM_REUSE(_a)) { _a->data.num = _r; slot_decref(_bs); g_vm.sp--; DISPATCH(); } - } - if (slot_is_ptr(_bs)) { - Value *_b = slot_as_ptr(_bs); - if (NUM_REUSE(_b)) { _b->data.num = _r; g_vm.stack[g_vm.sp - 2] = _bs; slot_decref(_as); g_vm.sp--; DISPATCH(); } - } + /* #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. */ + int _r = (_ad != _bd); 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_bool(_r); g_vm.sp--; DISPATCH(); } Value *b = vm_pop(); Value *a = vm_pop(); - int eq = values_equal(a, b); - vm_push(make_num(eq ? 0.0 : 1.0)); + int eq = values_equal_op(a, b, "!="); /* raises on bool vs num */ + vm_push_slot(slot_from_bool(!eq)); val_decref(a); val_decref(b); DISPATCH(); } @@ -3623,17 +3642,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, 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)) { \ - double _r = (_ad OP _bd) ? 1.0 : 0.0; \ - if (slot_is_ptr(_as)) { \ - Value *_a = slot_as_ptr(_as); \ - if (NUM_REUSE(_a)) { _a->data.num = _r; slot_decref(_bs); g_vm.sp--; DISPATCH(); } \ - } \ - if (slot_is_ptr(_bs)) { \ - Value *_b = slot_as_ptr(_bs); \ - if (NUM_REUSE(_b)) { _b->data.num = _r; g_vm.stack[g_vm.sp - 2] = _bs; slot_decref(_as); g_vm.sp--; DISPATCH(); } \ - } \ + /* #1637: a bool immediate; no NUM_REUSE (see CASE(EQ)). */ \ + int _r = (_ad OP _bd); \ 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_bool(_r); \ g_vm.sp--; \ DISPATCH(); \ } \ @@ -3642,9 +3654,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, Value *_a = slot_as_ptr(_as), *_b = slot_as_ptr(_bs); \ if (_a->type == VAL_STR && _b->type == VAL_STR) { \ int _cmp = strcmp(_a->data.str ? _a->data.str : "", _b->data.str ? _b->data.str : ""); \ - double _r = (_cmp OP 0) ? 1.0 : 0.0; \ + int _r = (_cmp OP 0); \ 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_bool(_r); \ g_vm.sp--; \ DISPATCH(); \ } \ @@ -3658,7 +3670,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, rt_error(EK_TYPE, current_line, "cannot compare %s and %s with '%s'", \ _tna, _tnb, OPNAME); \ } \ - g_vm.stack[g_vm.sp - 2] = slot_from_num(0.0); \ + g_vm.stack[g_vm.sp - 2] = slot_from_bool(0); \ g_vm.sp--; \ DISPATCH(); \ } @@ -4037,9 +4049,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(result_s.d); + 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_num(slot_as_bool(result_s) ? 1.0 : 0.0); + if (slot_is_bool(result_s)) return make_bool(slot_as_bool(result_s)); return slot_as_ptr(result_s); } frame = &g_vm.frames[g_vm.frame_count - 1]; @@ -4197,7 +4209,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, Env *saved = g_builtin_call_env; g_builtin_call_env = frame->env; int consumes_arg = (fn_val->data.builtin == builtin_free_val); - Value *result = fn_val->data.builtin(arg); + Value *result = eigs_call_builtin(fn_val->data.builtin, arg); g_builtin_call_env = saved; if (!result) { @@ -4403,9 +4415,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(result_s.d); + 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_num(slot_as_bool(result_s) ? 1.0 : 0.0); + if (slot_is_bool(result_s)) return make_bool(slot_as_bool(result_s)); return slot_as_ptr(result_s); } frame = &g_vm.frames[g_vm.frame_count - 1]; @@ -4485,9 +4497,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, 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(result_s.d); + 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_num(slot_as_bool(result_s) ? 1.0 : 0.0); + if (slot_is_bool(result_s)) return make_bool(slot_as_bool(result_s)); return slot_as_ptr(result_s); } frame = &g_vm.frames[g_vm.frame_count - 1]; @@ -4571,7 +4583,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, g_vm.sp -= 2; if (slot_is_num(idx_s) && slot_is_ptr(tgt_s)) { Value *target = slot_as_ptr(tgt_s); - int i, _ok = vm_index_is_int(idx_s.d, &i); + int i, _ok = vm_index_is_int(SLOT_NUM_RAW(idx_s), &i); if (target->type == VAL_LIST) { if (_ok && vm_index_resolve(&i, target->data.list.count)) { Value *r = target->data.list.items[i]; @@ -4580,13 +4592,13 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, * if the stack slot was the sole owner, slot_decref * frees the list and cascades into free_value(r), * which memcpy's the num-freelist next pointer over - * r->data. Reading r->data.num after that yields + * r->data. Reading VAL_NUM_RAW(r) after that yields * freelist garbage (denormal pointer-as-double, or * the prior head — often 0 — for the first freed * item). Surfaced by `(call())[i]` inline in an * arg list; bound-to-local form hides the bug by * keeping refcount > 1. */ - double rv = r->data.num; + double rv = VAL_NUM_RAW(r); slot_decref(tgt_s); vm_push_slot(slot_from_num(rv)); } else { @@ -4595,7 +4607,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, vm_push(r); } } else { - if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx_s.d); + if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_NUM_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); @@ -4609,7 +4621,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, slot_decref(tgt_s); vm_push_slot(slot_from_num(v)); } else { - if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx_s.d); + if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_NUM_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); @@ -4626,8 +4638,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, Value *result = make_null(); if (target->type == VAL_LIST && idx->type == VAL_NUM) { int i; - if (!vm_index_is_int(idx->data.num, &i)) { - rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx->data.num); + 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 = target->data.list.items[i]; val_incref(result); @@ -4639,8 +4651,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, if (v) { result = v; val_incref(result); } } else if (target->type == VAL_STR && idx->type == VAL_NUM) { int i; - if (!vm_index_is_int(idx->data.num, &i)) { - rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx->data.num); + 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); @@ -4649,8 +4661,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, } } else if (target->type == VAL_BUFFER && idx->type == VAL_NUM) { int i; - if (!vm_index_is_int(idx->data.num, &i)) - rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx->data.num); + 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 @@ -4671,12 +4683,12 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, EigsSlot tgt_s = g_vm.stack[g_vm.sp - 3]; if (slot_is_num(idx_s) && slot_is_ptr(tgt_s)) { Value *target = slot_as_ptr(tgt_s); - int i, _ok = vm_index_is_int(idx_s.d, &i); + int i, _ok = vm_index_is_int(SLOT_NUM_RAW(idx_s), &i); if (target->type == VAL_BUFFER && slot_is_num(val_s)) { if (_ok && vm_index_resolve(&i, target->data.buffer.count)) { - target->data.buffer.data[i] = val_s.d; + target->data.buffer.data[i] = SLOT_NUM_RAW(val_s); } else { - if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx_s.d); + if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s)); else rt_error(EK_INDEX, current_line, "buffer index %d out of range (length %d)", i, target->data.buffer.count); } @@ -4693,7 +4705,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, if (existing && existing->type == VAL_NUM && existing->refcount == 1 && !existing->arena) { - existing->data.num = val_s.d; + VAL_NUM_RAW(existing) = SLOT_NUM_RAW(val_s); } else { val_decref(existing); /* #873: heap-force into heap targets while an @@ -4701,11 +4713,11 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, * jit_helper_index_set). */ target->data.list.items[i] = (g_arena.active && !target->arena) - ? make_num_permanent(val_s.d) - : make_num(val_s.d); + ? make_num_permanent(SLOT_NUM_RAW(val_s)) + : make_num(SLOT_NUM_RAW(val_s)); } } else { - if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx_s.d); + if (!_ok) rt_error(EK_VALUE, current_line, "index must be an integer, got %g", SLOT_NUM_RAW(idx_s)); else rt_error(EK_INDEX, current_line, "index %d out of range (list length %d)", i, target->data.list.count); } @@ -4719,8 +4731,8 @@ 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) { int i; - if (!vm_index_is_int(idx->data.num, &i)) { - rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx->data.num); + 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)) { /* #873: promote an arena value stored into a heap list * (mirror of jit_helper_index_set's general arm). */ @@ -4741,12 +4753,12 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, } } else if (target->type == VAL_BUFFER && idx->type == VAL_NUM) { int i; - if (!vm_index_is_int(idx->data.num, &i)) { - rt_error(EK_VALUE, current_line, "index must be an integer, got %g", idx->data.num); + 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)) { 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->data.num; + target->data.buffer.data[i] = VAL_NUM_RAW(val); } else { /* #1061: the last fail-soft numeric context. A non-number * store was silently DROPPED (the old element stayed), so @@ -4778,7 +4790,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, Value *v = dict_get_cached(target, key, h); if (v) { if (v->type == VAL_NUM) { - double n = v->data.num; + double n = VAL_NUM_RAW(v); val_decref(target); vm_push_slot(slot_from_num(n)); DISPATCH(); @@ -4811,7 +4823,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, if (slot_is_num(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, val_s.d)) { + dict_set_cached_immediate(t, key, h, SLOT_NUM_RAW(val_s))) { g_vm.sp -= 1; g_vm.stack[g_vm.sp - 1] = val_s; slot_decref(tgt_s); @@ -4849,7 +4861,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, /* Hot DMG path: untracked VAL_NUM field -> push immediate, * skipping the incref/decref pair on every register read. */ if (v->type == VAL_NUM) { - vm_push_slot(slot_from_num(v->data.num)); + vm_push_slot(slot_from_num(VAL_NUM_RAW(v))); } else { val_incref(v); vm_push(v); @@ -4884,7 +4896,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, 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, tos.d)) { + dict_set_cached_immediate(target, key, h, SLOT_NUM_RAW(tos))) { DISPATCH(); } Value *val = vm_slot_lift(g_vm.sp - 1); @@ -4923,7 +4935,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, Value *item = target->data.list.items[i]; /* Plain VAL_NUM -> immediate; skip incref/decref pair. */ if (item && item->type == VAL_NUM) { - vm_push_slot(slot_from_num(item->data.num)); + vm_push_slot(slot_from_num(VAL_NUM_RAW(item))); } else { val_incref(item); vm_push(item); @@ -4976,7 +4988,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, /* Hot DMG path: register dict[field] returning a * plain VAL_NUM -> push immediate. */ if (v->type == VAL_NUM) { - vm_push_slot(slot_from_num(v->data.num)); + vm_push_slot(slot_from_num(VAL_NUM_RAW(v))); } else { val_incref(v); vm_push(v); @@ -5022,7 +5034,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, * 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, tos.d)) { + dict_set_cached_immediate(dict, key, h, SLOT_NUM_RAW(tos))) { DISPATCH(); } Value *val = vm_slot_lift(g_vm.sp - 1); @@ -5062,7 +5074,7 @@ 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 ->data.num off a + * bare ITER_NEXT with any list on TOS, and reading VAL_NUM_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 || !state->data.list.items[1] || @@ -5071,7 +5083,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, DISPATCH(); } Value *iterable = state->data.list.items[0]; - int idx = (int)state->data.list.items[1]->data.num; + int idx = (int)VAL_NUM_RAW(state->data.list.items[1]); int len = 0; if (iterable->type == VAL_LIST) len = iterable->data.list.count; else if (iterable->type == VAL_BUFFER) len = iterable->data.buffer.count; @@ -5080,7 +5092,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, * remove from reading past the end. See make_iter_state. */ if (state->data.list.count >= 3 && state->data.list.items[2]->type == VAL_NUM) { - int snap = (int)state->data.list.items[2]->data.num; + int snap = (int)VAL_NUM_RAW(state->data.list.items[2]); if (snap < len) len = snap; } @@ -5095,7 +5107,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, * an exclusive plain VAL_NUM (avoids per-iter make_num/free). */ Value *idx_v = state->data.list.items[1]; if (NUM_REUSE(idx_v)) { - idx_v->data.num = (double)(idx + 1); + VAL_NUM_RAW(idx_v) = (double)(idx + 1); } else { val_decref(idx_v); state->data.list.items[1] = @@ -5265,10 +5277,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, 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, s.d); + observer_slot_update_num(e, (int)slot, SLOT_NUM_RAW(s)); g_last_obs_slot_env = e; g_last_obs_slot_idx = (int)slot; - } else if (slot_is_ptr(s)) { - observer_slot_update(e, (int)slot, slot_as_ptr(s)); + } else if (obs_slot_observable(s)) { + observer_slot_update(e, (int)slot, obs_slot_value(s)); g_last_obs_slot_env = e; g_last_obs_slot_idx = (int)slot; } } else { @@ -5279,8 +5291,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, EigsSlot s = g_vm.stack[g_vm.sp - 1]; Env *e = frame->fn_env; eigs_obs_count_call(); - if (slot_is_num(s)) observer_slot_sample_num_gated(e, (int)slot, s.d); - else if (slot_is_ptr(s)) observer_slot_sample_gated(e, (int)slot, slot_as_ptr(s)); + if (slot_is_num(s)) observer_slot_sample_num_gated(e, (int)slot, SLOT_NUM_RAW(s)); + else if (obs_slot_observable(s)) observer_slot_sample_gated(e, (int)slot, obs_slot_value(s)); } DISPATCH(); } @@ -5508,8 +5520,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, EigsSlot s = g_vm.stack[g_vm.sp - 1]; /* #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; skip null/bool. */ - if (slot_is_num(s) || slot_is_ptr(s)) { + * slot from whichever (#1637: a bool too); skip null. */ + if (slot_is_num(s) || 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; } @@ -5517,11 +5529,11 @@ 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, s.d); - else observer_slot_sample_gated(oe, oidx, slot_as_ptr(s)); + 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)); } else if (oe && oidx >= 0) { - if (slot_is_num(s)) observer_slot_update_num(oe, oidx, s.d); - else observer_slot_update(oe, oidx, slot_as_ptr(s)); + if (slot_is_num(s)) observer_slot_update_num(oe, oidx, SLOT_NUM_RAW(s)); + else observer_slot_update(oe, oidx, obs_slot_value(s)); g_last_obs_slot_env = oe; g_last_obs_slot_idx = oidx; /* #262 Phase-3 D2: slot-source the temporal snapshot for @@ -5544,7 +5556,10 @@ 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(v->data.num); } + if (v && v->type == VAL_NUM) { result = make_num(VAL_NUM_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); } @@ -5559,6 +5574,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, v->type == VAL_FN ? "function" : v->type == VAL_BUILTIN ? "builtin" : v->type == VAL_BUFFER ? "buffer" : + v->type == VAL_BOOL ? "bool" : v->type == VAL_NULL ? "none" : "unknown"); break; /* #262 Step E: when/where/why/how on a value-based operand have no @@ -5635,8 +5651,16 @@ 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 *line_v = vm_pop(); - int line = 0; - if (line_v && line_v->type == VAL_NUM) line = (int)line_v->data.num; + /* #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) { + 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); val_decref(line_v); Value *result = make_null(); const char *name = chunk->const_interns[name_idx]; @@ -5657,7 +5681,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, 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 ? ord_v->data.num : 0.0; + double ord_d = ord_is_num ? VAL_NUM_RAW(ord_v) : 0.0; val_decref(ord_v); const char *name = chunk->const_interns[name_idx]; @@ -5726,7 +5750,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, if (!slot_is_ptr(tgt_s) || slot_as_ptr(tgt_s)->type != VAL_LIST) { rt_error(EK_TYPE, current_line, "destructuring requires a list, got %s", - slot_is_ptr(tgt_s) ? val_type_name(slot_as_ptr(tgt_s)->type) : "number"); + slot_type_name(tgt_s)); /* #1637: names bool/none, not "number" */ slot_decref(tgt_s); for (int i = 0; i < n; i++) vm_push_slot(slot_null()); DISPATCH(); @@ -5764,7 +5788,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, g_vm.sp -= 3; if (!slot_is_ptr(tgt_s)) { - rt_error(EK_TYPE, g_vm.current_line, "cannot slice number"); + rt_error(EK_TYPE, g_vm.current_line, "cannot slice %s", slot_type_name(tgt_s)); /* #1637 */ slot_decref(start_s); slot_decref(end_s); slot_decref(tgt_s); vm_push_slot(slot_null()); DISPATCH(); @@ -5789,7 +5813,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, /* 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_JSON_RAW: case VAL_DICT: case VAL_TEXT_BUILDER: + case VAL_JSON_RAW: 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)); slot_decref(start_s); slot_decref(end_s); slot_decref(tgt_s); @@ -5806,9 +5830,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, #define READ_SLICE_BOUND(slot, defval, outvar) \ do { \ if (slot_is_num(slot)) { \ - if (!vm_index_is_int(slot.d, &(outvar))) { \ + if (!vm_index_is_int(SLOT_NUM_RAW(slot), &(outvar))) { \ rt_error(EK_VALUE, g_vm.current_line, \ - "slice bound must be an integer, got %g", slot.d);\ + "slice bound must be an integer, got %g", SLOT_NUM_RAW(slot));\ bound_err = 1; \ } \ } else if (slot_is_ptr(slot)) { \ @@ -5816,10 +5840,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, if (bv->type == VAL_NULL) { \ (outvar) = (defval); \ } else if (bv->type == VAL_NUM) { \ - if (!vm_index_is_int(bv->data.num, &(outvar))) { \ + if (!vm_index_is_int(VAL_NUM_RAW(bv), &(outvar))) { \ rt_error(EK_VALUE, g_vm.current_line, \ "slice bound must be an integer, got %g", \ - bv->data.num); \ + VAL_NUM_RAW(bv)); \ bound_err = 1; \ } \ } else { \ @@ -5828,6 +5852,15 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, val_type_name(bv->type)); \ bound_err = 1; \ } \ + } else if (slot_is_null(slot)) { \ + (outvar) = (defval); /* an omitted bound */ \ + } else { \ + /* #1637: an immediate bool (or any other non-number \ + * slot) is not a bound -- never the default one. */ \ + rt_error(EK_TYPE, g_vm.current_line, \ + "slice bound must be an integer or null, got %s", \ + slot_type_name(slot)); \ + bound_err = 1; \ } \ } while (0) @@ -5916,7 +5949,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, } int result = observer_predicate_at(g_last_obs_slot_env, g_last_obs_slot_idx, kind, 0); - vm_push(make_num(result ? 1.0 : 0.0)); + vm_push_slot(slot_from_bool(result)); /* #1637 */ DISPATCH(); } @@ -5937,7 +5970,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, vm_desc_unrecorded(chunk, current_line, "slot")) { vm_push_slot(slot_null()); DISPATCH(); } } int result = observer_predicate_at(e, (int)slot, kind, 1); - vm_push(make_num(result ? 1.0 : 0.0)); + vm_push_slot(slot_from_bool(result)); /* #1637 */ DISPATCH(); } @@ -5966,7 +5999,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, vm_desc_unrecorded(chunk, current_line, name)) { vm_push_slot(slot_null()); DISPATCH(); } } int result = observer_predicate_at(oe, oidx, kind, 1); - vm_push(make_num(result ? 1.0 : 0.0)); + vm_push_slot(slot_from_bool(result)); /* #1637 */ DISPATCH(); } @@ -6408,10 +6441,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, double key_d; if (slot_is_num(key_s)) { - key_d = key_s.d; + 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 = slot_as_ptr(key_s)->data.num; + key_d = VAL_NUM_RAW(slot_as_ptr(key_s)); } else { slot_decref(arg_s); slot_decref(key_s); slot_decref(table_s); rt_error(EK_TYPE, current_line, "dispatch: key must be a number"); @@ -6460,7 +6493,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, Env *saved = g_builtin_call_env; g_builtin_call_env = frame->env; int consumes_arg = (fn->data.builtin == builtin_free_val); - Value *result = fn->data.builtin(arg); + Value *result = eigs_call_builtin(fn->data.builtin, arg); g_builtin_call_env = saved; if (!result) { result = make_null(); @@ -6553,9 +6586,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(result_s.d); + 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_num(slot_as_bool(result_s) ? 1.0 : 0.0); + if (slot_is_bool(result_s)) return make_bool(slot_as_bool(result_s)); return slot_as_ptr(result_s); } frame = &g_vm.frames[g_vm.frame_count - 1]; diff --git a/src/vm.h b/src/vm.h index 1ab8ab57..1cc147a2 100644 --- a/src/vm.h +++ b/src/vm.h @@ -227,6 +227,8 @@ typedef enum { OP_SET_LOCAL_INTERNAL, /*obs:NONE*/ /* [slot:16] internal TOS store; no history/tape. * 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. */ OP_COUNT /* sentinel — number of opcodes */ } OpCode; diff --git a/tests/bool_fuzz_anyvalue.txt b/tests/bool_fuzz_anyvalue.txt new file mode 100644 index 00000000..0205bc28 --- /dev/null +++ b/tests/bool_fuzz_anyvalue.txt @@ -0,0 +1,76 @@ +# tests/bool_fuzz_anyvalue.txt -- reviewed slots that TAKE a bool (#1637). +# name|slot|reason. tests/test_bool_fuzz.sh: a bool in any slot not listed +# here must raise, in both strict modes; an entry that no call reaches with a +# return is stale and fails the run. The C side of the same decision is +# k_bool_policy in src/builtins.c (the builtin-call bool gate); container +# ELEMENTS (slot aNe0) are not gated there and are listed only here. +# Slots: u = the whole argument, eN = element N of a list argument, +# e0.dD = a bool D brackets deep, aN = argument N of the reviewed call, +# aN.dD[.key] = a numeric literal D brackets deep in argument N (the dict key +# when it is a dict value). +print|u|renders any value +print|e0|renders any value +print|e1|renders any value +write|u|renders any value +write|e0|renders any value +write|e1|renders any value +str|u|converts any value to text +str|e0|converts any value to text +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 +json_encode|u|JSON has true and false +json_encode|e0|JSON has true and false +json_encode|e1|JSON has true and false +observe|u|the observer measures bools (entropy 0 or 1) +observe|e0|the observer measures any value +observe|e1|the observer measures any value +free_val|u|releases any value +free_val|e0|releases any value +free_val|e1|releases any value +coalesce|u|picks the first non-null of any values +coalesce|a0|picks the first non-null of any values +coalesce|a1|picks the first non-null of any values +len|e0|counts elements whatever they are +len|e1|counts elements whatever they are +assert|u|the condition is a bool +assert|a0|the condition is a bool +append|a1|a list element may be any value +list_insert_at|a2|a list element may be any value +fill|a1|a list element may be any value +dict_set|a2|a dict value may be any value +send|a1|a channel carries any value +task_send|a1|a mailbox carries any value +json_build|e1|JSON values may be true and false (position 1 is a value) +write_bytes|a2|the append flag is a bool (or the older nonzero number) +concat|a0.d1|joins two lists; elements may be any value +concat|a1.d1|joins two lists; elements may be any value +list_slice|a0.d1|slices a list; elements may be any value +copy_into|a2.d1|copies list elements; elements may be any value +str|e0.d2|converts any value to text +str|e0.d3|converts any value to text +json_encode|e0.d2|JSON has true and false +json_encode|e0.d3|JSON has true and false +json_build|e0.d2|a JSON value may be a list holding true/false +json_build|e0.d3|a JSON value may be a list holding true/false +has_key|a0.d1.k|a dict value may be any value +store_put|a2.d1.value|a stored record value may be any value +store_update|a3.d1.value|a stored record value may be any value +nearest_in_range|a0.d2.active|the entity's active flag is a bool (false skips it) +nearest_in_range_all|a0.d2.active|the entity's active flag is a bool (false skips it) +channel|e0.d2|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +channel|e0.d3|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +clock_unix|e0.d2|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +clock_unix|e0.d3|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +heap_inuse|e0.d2|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +heap_inuse|e0.d3|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +mktemp|e0.d2|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +mktemp|e0.d3|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +monotonic_ms|e0.d2|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +monotonic_ms|e0.d3|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +monotonic_ns|e0.d2|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +monotonic_ns|e0.d3|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +random|e0.d2|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +random|e0.d3|nullary and nondeterministic: the argument is ignored and two calls differ, so the deep probe has no control to compare with +shared_set|a1|the http shared store holds any JSON value; a bool round-trips through shared_get as a bool diff --git a/tests/bool_fuzz_gen.eigs b/tests/bool_fuzz_gen.eigs new file mode 100644 index 00000000..a19ec434 --- /dev/null +++ b/tests/bool_fuzz_gen.eigs @@ -0,0 +1,299 @@ +# bool_fuzz_gen.eigs -- writes the #1637 bool fuzz probe programs. +# +# Run by tests/test_bool_fuzz.sh; never on its own. For EVERY builtin and +# extension name the binary's `--api --json` lists, it writes one probe +# program that puts `true`, then `false`, into every argument slot it can +# name, each call inside try/catch with a marker line: +# u the whole argument: f of true +# e0 e1 an element of a list argument: f of ([true, 1]), f of ([1, true]) +# aN argument N of the reviewed valid call in +# tests/strict_shape_cases.json, replaced whole +# e0.d2 e0.d3 a bool two and three brackets deep: f of ([[true, 1], 1]) +# aN.dD every numeric literal at bracket depth D (1-3) inside argument N +# of the reviewed call, one at a time: [[2, 3]] -> [[true, 3]] +# (D = 2), so a cell a builtin reads deep in a tensor is reached; +# aN.dD.key when the literal is a dict value ({"px": 1}) +# ctl:* controls (not probes): the reviewed call itself, and the e0.dD +# shapes with the number 7. A depth-2/3 probe whose result equals +# its control's was not read (the bool sat where the builtin does +# not look); the runner decides. +# A returned call prints `@k:V:`. +# plus one program of VM operand positions (index, slice bounds, range, +# for-in, arithmetic). The runner decides which calls are violations. +# +# usage: eigenscript bool_fuzz_gen.eigs API_JSON CASES_JSON TMPDIR WAV OUTDIR +# writes OUTDIR/p.eigs and OUTDIR/manifest.txt, one line per probe call: +# |||| + +argv is args of null +api is json_decode of (read_text of argv[0]) +cases is json_decode of (read_text of argv[1]) +tmp is argv[2] +wav is argv[3] +outdir is argv[4] + +# @DESCRIPTOR@ (sandbox_run's chunk) is produced by a C helper that only +# tools/strict_differential.sh builds; null stands in -- every bool slot of +# sandbox_run is refused before the descriptor is read. +define subst(s) as: + local a is str_replace of [s, "@TMP@", tmp] + a is str_replace of [a, "@DESCRIPTOR@", "null"] + return str_replace of [a, "@WAV@", wav] + +define is_numlit(s) as: + local t is trim of s + if (len of t) == 0: + return false + local i is 0 + local seen is false + if (char_at of [t, 0]) == "-": + i is 1 + loop while i < (len of t): + local c is char_at of [t, i] + if (c >= "0") and (c <= "9"): + seen is true + else: + if c != ".": + return false + i is i + 1 + return seen + +# Every numeric literal inside a list or dict literal of an argument, with its +# nesting depth: [[start, length, depth], ...]. Depth counts the enclosing +# [ and { brackets (parentheses only group); string literals are skipped. +# A literal starts at a digit (or a minus right before one) that does not +# continue an identifier or another number. +# The dict key a literal is the value of: `{"px": 1}` -> "px"; "" when the +# literal is not right after `"key":`. +define dict_key_before(s, at) as: + local j is at - 1 + loop while (j >= 0) and ((char_at of [s, j]) == " "): + j is j - 1 + if (j < 0) or ((char_at of [s, j]) != ":"): + return "" + j is j - 1 + loop while (j >= 0) and ((char_at of [s, j]) == " "): + j is j - 1 + if (j < 1) or ((char_at of [s, j]) != "\""): + return "" + local e is j + j is j - 1 + loop while (j >= 0) and ((char_at of [s, j]) != "\""): + j is j - 1 + if j < 0: + return "" + return substr of [s, j + 1, e - j - 1] + +define num_literal_spans(s) as: + local out is [] + local depth is 0 + local n is len of s + local i is 0 + loop while i < n: + local c is char_at of [s, i] + if c == "\"": + i is i + 1 + loop while i < n: + local q is char_at of [s, i] + if q == "\\": + i is i + 2 + else: + if q == "\"": + break + i is i + 1 + i is i + 1 + else: + if (c == "[") or (c == "{"): + depth is depth + 1 + i is i + 1 + else: + if (c == "]") or (c == "}"): + depth is depth - 1 + i is i + 1 + else: + local prev is " " + if i > 0: + prev is char_at of [s, i - 1] + local starts is false + if (c >= "0") and (c <= "9"): + starts is true + if c == "-": + if (i + 1) < n: + local nx is char_at of [s, i + 1] + if (nx >= "0") and (nx <= "9"): + starts is true + local ident_prev is ((prev >= "a") and (prev <= "z")) or ((prev >= "A") and (prev <= "Z")) or ((prev >= "0") and (prev <= "9")) or (prev == "_") or (prev == ".") + if starts and (not ident_prev) and (depth > 0): + local st is i + i is i + 1 + loop while i < n: + local d is char_at of [s, i] + if ((d >= "0") and (d <= "9")) or (d == ".") or (d == "e"): + i is i + 1 + else: + break + append of [out, [st, i - st, depth, dict_key_before of [s, st]]] + else: + i is i + 1 + return out + +names is [] +for n in api["builtins"]: + append of [names, n] +for grp in keys of api["extensions"]: + for n in api["extensions"][grp]: + append of [names, n] + +rows is {} +for c in cases: + rows[c["name"]] is c + +manifest is [] +pidx is 0 + +define wrap(lines, k, call) as: + append of [lines, "try:"] + append of [lines, " __r" + (str of k) + " is " + call] + append of [lines, " print of (\"@" + (str of k) + ":V:\" + (sha256 of (str of __r" + (str of k) + ")))"] + append of [lines, "catch __x:"] + append of [lines, " print of (\"@" + (str of k) + ":R:\" + __x.kind)"] + +# `report` is a reserved form, not a function (`report of ` only; a +# literal there is a compile error, E005), so no call of it can be written. +skip is ["report"] + +for f in names: + if list_contains of [skip, f]: + append of [manifest, "skip|0|" + f + "|reserved-form|-"] + continue + lines is [] + calls is [] + # presence: a name of a capability this binary was built without is + # undefined; the runner counts it absent, never as a raise. + append of [lines, "try:"] + append of [lines, " print of (\"@P:\" + (type of " + f + "))"] + append of [lines, "catch __x:"] + append of [lines, " print of \"@P:absent\""] + for v in ["true", "false"]: + append of [calls, [f + " of " + v, "u", v]] + has_row is has_key of [rows, f] + # exit ends the process and throw raises by definition, so neither can + # carry a control call; their deep probes are not generated + deep is not (list_contains of [["exit", "throw"], f]) + if (not has_row) and deep: + # controls for the deep probes: the same shape with the number 7 + append of [calls, [f + " of ([[7, 1], 1])", "ctl:d2", "-", "-"]] + append of [calls, [f + " of ([[[7, 1]], 1])", "ctl:d3", "-", "-"]] + for v in ["true", "false"]: + append of [calls, [f + " of ([" + v + ", 1])", "e0", v]] + append of [calls, [f + " of ([1, " + v + "])", "e1", v]] + if deep: + append of [calls, [f + " of ([[" + v + ", 1], 1])", "e0.d2", v, "d2"]] + append of [calls, [f + " of ([[[" + v + ", 1]], 1])", "e0.d3", v, "d3"]] + if has_row: + row is rows[f] + setup is subst of row["setup"] + if (len of setup) > 0: + # A setup that needs a capability this build lacks must not end + # the program before its markers: it runs inside try. + append of [lines, "try:"] + for sl in split of [setup, "\n"]: + append of [lines, " " + sl] + append of [lines, " __setup_ok is 1"] + append of [lines, "catch __x:"] + append of [lines, " print of \"@S:failed\""] + a is [] + for x in row["args"]: + append of [a, subst of x] + if deep: + append of [calls, [f + " of [" + (join of [a, ", "]) + "]", "ctl:case", "-", "-"]] + i is 0 + loop while i < (len of a): + # every numeric literal at depth 1-3 inside argument i, from the + # case's own nesting: aN.dD (D = 1 an element, 2 an element of an + # element, 3 one level deeper) + spans is num_literal_spans of a[i] + for v in ["true", "false"]: + b is [] + for x in a: + append of [b, x] + b[i] is v + append of [calls, [f + " of [" + (join of [b, ", "]) + "]", "a" + (str of i), v]] + for sp in spans: + if sp[2] <= 3: + src is a[i] + b[i] is (substr of [src, 0, sp[0]]) + v + (substr of [src, sp[0] + sp[1], (len of src) - sp[0] - sp[1]]) + slot is "a" + (str of i) + ".d" + (str of sp[2]) + if (len of sp[3]) > 0: + slot is slot + "." + sp[3] + append of [calls, [f + " of [" + (join of [b, ", "]) + "]", slot, v, "case"]] + i is i + 1 + ctl is {} + k is 0 + for c in calls: + if starts_with of [c[1], "ctl:"]: + ctl[substr of [c[1], 4, (len of c[1]) - 4]] is str of k + k is k + 1 + k is 0 + for c in calls: + wrap of [lines, k, c[0]] + cref is "-" + if (len of c) > 3: + if has_key of [ctl, c[3]]: + cref is ctl[c[3]] + append of [manifest, "p" + (str of pidx) + "|" + (str of k) + "|" + f + "|" + c[1] + "|" + c[2] + "|" + cref] + k is k + 1 + append of [lines, "print of \"@END\""] + write_text of [outdir + "/p" + (str of pidx) + ".eigs", (join of [lines, "\n"]) + "\n"] + pidx is pidx + 1 + +# VM operand positions: one program, setup first. +vm_setup is "xs is [10, 20, 30, 40]\ns is \"abcd\"\nb is buffer of 4\nd is {\"k\": 1}\n" +vm_calls is [] +for v in ["true", "false"]: + append of [vm_calls, ["xs[" + v + "]", "index-list", v]] + append of [vm_calls, ["s[" + v + "]", "index-str", v]] + append of [vm_calls, ["b[" + v + "]", "index-buffer", v]] + append of [vm_calls, ["d[" + v + "]", "index-dict", v]] + append of [vm_calls, ["xs[" + v + ":]", "slice-start", v]] + append of [vm_calls, ["xs[:" + v + "]", "slice-end", v]] + append of [vm_calls, ["s[" + v + ":]", "slice-str-start", v]] + append of [vm_calls, ["s[:" + v + "]", "slice-str-end", v]] + append of [vm_calls, ["range of " + v, "range-unary", v]] + append of [vm_calls, ["range of [" + v + ", 3]", "range-start", v]] + append of [vm_calls, ["range of [0, " + v + "]", "range-end", v]] + append of [vm_calls, ["range of [0, 3, " + v + "]", "range-step", v]] + append of [vm_calls, [v + " + 1", "arith-add", v]] + 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-mod", v]] + append of [vm_calls, ["0 - " + v, "arith-neg", v]] + append of [vm_calls, [v + " < 1", "order", v]] + append of [vm_calls, [v + " == 1", "eq-num", v]] + append of [vm_calls, ["xs * " + v, "repeat-list", v]] + append of [vm_calls, ["s * " + v, "repeat-str", v]] + append of [vm_calls, ["what is xs at " + v, "at-line", v]] + append of [vm_calls, ["what is xs when " + v, "when-ordinal", v]] +lines is [vm_setup] +k is 0 +for c in vm_calls: + wrap of [lines, k, c[0]] + append of [manifest, "p" + (str of pidx) + "|" + (str of k) + "|@vm|" + c[1] + "|" + c[2]] + k is k + 1 +# Statement forms: an index store, a slice store and a for-in over a bool. +for v in ["true", "false"]: + for stmt in [["xs[" + v + "] is 9", "store-list"], ["b[" + v + "] is 9", "store-buffer"], ["for __i in " + v + ":\n __z is __i", "for-in"]]: + append of [lines, "try:"] + append of [lines, " " + stmt[0]] + append of [lines, " print of \"@" + (str of k) + ":V:stmt\""] + append of [lines, "catch __x:"] + append of [lines, " print of (\"@" + (str of k) + ":R:\" + __x.kind)"] + append of [manifest, "p" + (str of pidx) + "|" + (str of k) + "|@vm|" + stmt[1] + "|" + v] + k is k + 1 +append of [lines, "print of \"@END\""] +write_text of [outdir + "/p" + (str of pidx) + ".eigs", (join of [lines, "\n"]) + "\n"] +pidx is pidx + 1 + +write_text of [outdir + "/manifest.txt", (join of [manifest, "\n"]) + "\n"] +print of ("GEN: names=" + (str of (len of names)) + " programs=" + (str of pidx) + " calls=" + (str of (len of manifest))) diff --git a/tests/bool_lib_predicates_cases.txt b/tests/bool_lib_predicates_cases.txt new file mode 100644 index 00000000..559bdfd5 --- /dev/null +++ b/tests/bool_lib_predicates_cases.txt @@ -0,0 +1,46 @@ +# tests/bool_lib_predicates_cases.txt -- data for tests/test_bool_lib_predicates.sh (#1637). +# Three record kinds, one per line, fields split on '|': +# case|module.name|setup|arg1;;arg2;;... an argument tuple that reaches an +# answer the generic battery misses (setup: one EigenScript line; a +# literal \n separates lines; may be empty) +# one_sided|module.name|reason answers only one of true/false +# skip|name|reason the battery cannot call it +skip|wait_until|sleeps between polls by design +skip|git_worktree_clean|lib/pkg.eigs is a CLI script: loading it runs the dispatcher +skip|is_valid_name_part|lib/pkg.eigs is a CLI script: loading it runs the dispatcher +case|complex.eq_near||[1, 2];;[1, 2];;0.001 +case|complex.eq_near||[1, 2];;[3, 2];;0.001 +case|experiment.is_measurement_stable||[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];;3 +case|experiment.is_measurement_stable||[1, 9, 2, 8, 3, 7, 4, 6, 5, 5];;3 +case|args.get_flag||[["--v", 1]];;"--v" +case|args.has_flag||[["--v", 1]];;"--v" +case|functional.complement||(x) => x > 1;;0 +case|functional.complement||(x) => x > 1;;5 +case|geometry.collinear||[0, 0];;[1, 1];;[3, 1] +case|geometry.on_segment||[0, 0];;[5, 5];;[1, 1] +case|geometry.segments_intersect||[0, 0];;[1, 0];;[0, 1];;[1, 1] +case|geometry.point_in_triangle||[5, 5];;[0, 0];;[1, 0];;[0, 1] +case|geometry.point_in_polygon||[0.5, 0.5];;[[0, 0], [1, 0], [1, 1], [0, 1]] +case|geometry.point_in_polygon||[2, 2];;[[0, 0], [1, 0], [1, 1], [0, 1]] +case|geometry.polygon_is_clockwise||[[0, 0], [0, 1], [1, 1], [1, 0]] +case|geometry.point_in_circle||5;;5;;0;;0;;1 +case|json.json_has||"{\"a\": 1}";;"a" +case|json.json_has||"{\"a\": 1}";;"b" +case|lab.is_stable|__x is new_experiment of "t"\nfor __i in range of 12:\n record of [__x, "v", 1.0]|__x;;"v" +case|list.any||[1, 2, 3];;(x) => x > 1 +case|list.all||[1, 2, 3];;(x) => x > 1 +case|map.map_has|__m is map_set of [map_new of null, "k", 1]|__m;;"k" +case|observer_slots.is_stable|for __i in range of 12:\n feed of [0, 5]|0 +case|set.set_has|__s is set_from of ([1, 2])|__s;;1 +case|state.sm_can_send|__sm is sm_add_transition of [sm_new of "idle", "idle", "go", "run"]|__sm;;"go" +case|state.sm_is|__sm is sm_new of "idle"|__sm;;"idle" +case|test_runner.is_test_file||"test_x.eigs" +case|utf8.utf8_validate||(str_from_bytes of ([255])) +one_sided|json.json_has|pre-existing bug, not #1637: it tests `type of val == "null"` but that type is "none", so it always answers true +one_sided|observer.is_converged|the argument is a fresh parameter binding with no trajectory (#262) +one_sided|observer.is_stable|the argument is a fresh parameter binding with no trajectory (#262) +one_sided|observer.is_improving|the argument is a fresh parameter binding with no trajectory (#262) +one_sided|observer.is_diverging|the argument is a fresh parameter binding with no trajectory (#262) +one_sided|observer.is_oscillating|the argument is a fresh parameter binding with no trajectory (#262) +one_sided|observer_slots.is_diverging|a slot fed a bounded constant cannot read diverging +one_sided|sanitize.check_openai|true only with an OpenAI key in the environment diff --git a/tests/bool_meta_snippets.txt b/tests/bool_meta_snippets.txt new file mode 100644 index 00000000..01aa08a0 --- /dev/null +++ b/tests/bool_meta_snippets.txt @@ -0,0 +1,54 @@ +# tests/bool_meta_snippets.txt -- data for tests/test_bool_meta_diff.sh (#1637). +# One snippet per line: setup|expression. Setup may be empty; a literal \n +# separates its lines. Each runs as a VM program and through lib/eigen.eigs's +# eigen_run, and both must print the same `[type of v, v]` or `raised `. +|1 < 2 +|2 <= 1 +|3 > 2 +|3 >= 4 +|3 == 3 +|3 != 3 +|"a" < "b" +|"x" == "x" +|[1, 2] == [1, 2] +|null == false +|"true" == true +|not 0 +|not 7 +|not true +|not false +|not "" +|true +|false +|type of (1 < 2) +|type of true +|true and 5 +|false or 6 +|0 or false +|contains of ["abc", "b"] +|starts_with of ["abc", "x"] +|has_key of [{"a": 1}, "a"] +|converged +|stable +|improving +|diverging +|oscillating +|equilibrium +|type of converged +x is 5|converged of x +x is 1\nx is 2\nx is 3|improving of x +x is 1\nx is 2|equilibrium +x is 4|type of (stable of x) +|true == 1 +|false != 0 +|1 == true +|true + 1 +|[true] == [1] +|1 < true +|[2, true] == [2, 1] +|[1, true] == [2, 1] +|[10, 20][1 < 2] +|range of true +x is 0\nloop while x < 3:\n x is x + 1|x +z is 0\nloop while false:\n z is 1|z +|[y for y in [1, 2, 3] if y > 1] diff --git a/tests/handles_mt_fixtures/handles_reuse.expected b/tests/handles_mt_fixtures/handles_reuse.expected index 5691964e..6625510a 100644 --- a/tests/handles_mt_fixtures/handles_reuse.expected +++ b/tests/handles_mt_fixtures/handles_reuse.expected @@ -1 +1 @@ -first=STALE stale=ERR:value fresh=FRESH same_slot=1 +first=STALE stale=ERR:value fresh=FRESH same_slot=true diff --git a/tests/handles_mt_fixtures/handles_store_stale.expected b/tests/handles_mt_fixtures/handles_store_stale.expected index 9ea86733..4e1158ef 100644 --- a/tests/handles_mt_fixtures/handles_store_stale.expected +++ b/tests/handles_mt_fixtures/handles_store_stale.expected @@ -1 +1 @@ -same_slot=1 get=store_get: stale store handle (slot 1 no longer holds the store this handle names) put=ERR-stale query=ERR-stale count=ERR-stale fresh=FRESH +same_slot=true get=store_get: stale store handle (slot 1 no longer holds the store this handle names) put=ERR-stale query=ERR-stale count=ERR-stale fresh=FRESH diff --git a/tests/handles_mt_fixtures/handles_task_stale.expected b/tests/handles_mt_fixtures/handles_task_stale.expected index 48e58fb5..444fe7f0 100644 --- a/tests/handles_mt_fixtures/handles_task_stale.expected +++ b/tests/handles_mt_fixtures/handles_task_stale.expected @@ -1 +1 @@ -same_slot=1 alive=ERR-stale join=ERR-stale kill=ERR-stale fresh=7 +same_slot=true alive=ERR-stale join=ERR-stale kill=ERR-stale fresh=7 diff --git a/tests/observer_corpus/golden/iLambdaAi__test_report_alignment.out b/tests/observer_corpus/golden/iLambdaAi__test_report_alignment.out index d55f1311..454127e6 100644 --- a/tests/observer_corpus/golden/iLambdaAi__test_report_alignment.out +++ b/tests/observer_corpus/golden/iLambdaAi__test_report_alignment.out @@ -2,23 +2,23 @@ Verifies report of x agrees with predicate vocabulary --- RA1: Diverging (low H -> high H) --- -0 +false moving --- RA2: Improving (primed, high H -> low H) --- -0 +false moving --- RA3: Converged (low H basin, dH~0) --- -0 +false stable --- RA4: Oscillating (sign change in dH) --- -0 +false moving --- RA5: Equilibrium (dH~0, moderate H) --- -0 +false stable === REPORT-PREDICATE ALIGNMENT COMPLETE === diff --git a/tests/observer_corpus/golden/iLambdaAi__test_stable_band.out b/tests/observer_corpus/golden/iLambdaAi__test_stable_band.out index 14ed1493..3084b305 100644 --- a/tests/observer_corpus/golden/iLambdaAi__test_stable_band.out +++ b/tests/observer_corpus/golden/iLambdaAi__test_stable_band.out @@ -1,12 +1,12 @@ === STABLE BAND TEST === --- SB1: Stable is reachable (small drift, moderate H) --- -0 +false moving 0.15374218032876188 -0.002748882576939643 --- SB2: Converged is NOT stable --- -0 +false stable === STABLE BAND COMPLETE === diff --git a/tests/roads/importer_scope.out b/tests/roads/importer_scope.out index e3ab955d..13e04634 100644 --- a/tests/roads/importer_scope.out +++ b/tests/roads/importer_scope.out @@ -1,2 +1,2 @@ ["outer", 1, 1] -["helper_has_outer", 1, 1] +["helper_has_outer", 1, true] diff --git a/tests/roads/resolution_errors.out b/tests/roads/resolution_errors.out index 8dedb170..61bc6bc4 100644 --- a/tests/roads/resolution_errors.out +++ b/tests/roads/resolution_errors.out @@ -1,4 +1,4 @@ -["no_parent", 1, 1] -["no_cwd", 1, 1] -["loaded_error", 1, 1] -["import_error", 1, 1] +["no_parent", 1, true] +["no_cwd", 1, true] +["loaded_error", 1, true] +["import_error", 1, true] diff --git a/tests/roads/resolution_order.out b/tests/roads/resolution_order.out index 1b6ef8fe..c5c44546 100644 --- a/tests/roads/resolution_order.out +++ b/tests/roads/resolution_order.out @@ -1,4 +1,4 @@ ["nearby", 1, 1] ["package", 1, 2] ["root_relative", 1, 3] -["root_error", 1, 1] +["root_error", 1, true] diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index 4f69fd77..963cc36c 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -640,8 +640,8 @@ check_binary_fingerprint HFL_OUT=$($EIGS_TMO ./eigenscript ../tests/test_host_frame_line.eigs "$TMPDIR/eigs-host-frame.err"); HFL_RC=$? HFL_ERR=$(cat "$TMPDIR/eigs-host-frame.err") TOTAL=$((TOTAL + 1)) -if [ "$HFL_RC" -eq 0 ] && [ "$HFL_OUT" = "0" ] && [ -z "$HFL_ERR" ]; then - PASS=$((PASS + 1)); echo " PASS: sandbox policy errors return ok=0 without stderr" +if [ "$HFL_RC" -eq 0 ] && [ "$HFL_OUT" = "[false, false]" ] && [ -z "$HFL_ERR" ]; then + PASS=$((PASS + 1)); echo " PASS: sandbox policy errors return ok=false without stderr" else FAIL=$((FAIL + 1)); echo " FAIL: sandbox policy error containment (rc=$HFL_RC, stdout=$HFL_OUT)" [ -n "$HFL_ERR" ] && echo "$HFL_ERR" @@ -844,13 +844,13 @@ check "T06 Division" "$(echo "$OUTPUT" | grep -A1 'T06' | tail -1)" "25" check "T07 String Concat" "$(echo "$OUTPUT" | grep -A1 'T07' | tail -1)" "hello world" check "T08 Boolean And" "$(echo "$OUTPUT" | grep -A1 'T08' | tail -1)" "1" check "T09 Boolean Or" "$(echo "$OUTPUT" | grep -A1 'T09' | tail -1)" "1" -check "T10 Boolean Not" "$(echo "$OUTPUT" | grep -A1 'T10' | tail -1)" "1" -check "T11 Greater Than" "$(echo "$OUTPUT" | grep -A1 'T11' | tail -1)" "1" -check "T12 Less Than" "$(echo "$OUTPUT" | grep -A1 'T12' | tail -1)" "1" -check "T13 Equality (42==42)" "$(echo "$OUTPUT" | grep -A1 'T13:' | tail -1)" "1" -check "T13b Inequality (42==99)" "$(echo "$OUTPUT" | grep -A1 'T13b' | tail -1)" "0" -check "T13c String Equality" "$(echo "$OUTPUT" | grep -A1 'T13c' | tail -1)" "1" -check "T13d String Inequality" "$(echo "$OUTPUT" | grep -A1 'T13d' | tail -1)" "0" +check "T10 Boolean Not" "$(echo "$OUTPUT" | grep -A1 'T10' | tail -1)" "true" +check "T11 Greater Than" "$(echo "$OUTPUT" | grep -A1 'T11' | tail -1)" "true" +check "T12 Less Than" "$(echo "$OUTPUT" | grep -A1 'T12' | tail -1)" "true" +check "T13 Equality (42==42)" "$(echo "$OUTPUT" | grep -A1 'T13:' | tail -1)" "true" +check "T13b Inequality (42==99)" "$(echo "$OUTPUT" | grep -A1 'T13b' | tail -1)" "false" +check "T13c String Equality" "$(echo "$OUTPUT" | grep -A1 'T13c' | tail -1)" "true" +check "T13d String Inequality" "$(echo "$OUTPUT" | grep -A1 'T13d' | tail -1)" "false" check "T14 If Statement" "$(echo "$OUTPUT" | grep -A1 'T14' | tail -1)" "big" check "T15 If-Else" "$(echo "$OUTPUT" | grep -A1 'T15' | tail -1)" "small" check "T16 While Loop" "$(echo "$OUTPUT" | grep -A1 'T16' | tail -1)" "5" @@ -963,27 +963,27 @@ RA_OUTPUT=$(./eigenscript ../tests/test_report_alignment.eigs 2>&1) RA1_D=$(echo "$RA_OUTPUT" | grep -A2 'RA1:' | tail -2 | head -1) RA1_R=$(echo "$RA_OUTPUT" | grep -A2 'RA1:' | tail -1) -check "RA1 diverging predicate" "$RA1_D" "1" # #861: linear runaway, raw same-sign +check "RA1 diverging predicate" "$RA1_D" "true" # #861: linear runaway, raw same-sign check "RA1 report=diverging" "$RA1_R" "diverging" RA2_I=$(echo "$RA_OUTPUT" | grep -A2 'RA2:' | tail -2 | head -1) RA2_R=$(echo "$RA_OUTPUT" | grep -A2 'RA2:' | tail -1) -check "RA2 improving predicate" "$RA2_I" "1" +check "RA2 improving predicate" "$RA2_I" "true" check "RA2 report=improving" "$RA2_R" "improving" RA3_C=$(echo "$RA_OUTPUT" | grep -A2 'RA3:' | tail -2 | head -1) RA3_R=$(echo "$RA_OUTPUT" | grep -A2 'RA3:' | tail -1) -check "RA3 converged predicate" "$RA3_C" "1" +check "RA3 converged predicate" "$RA3_C" "true" check "RA3 report=converged" "$RA3_R" "converged" RA4_O=$(echo "$RA_OUTPUT" | grep -A2 'RA4:' | tail -2 | head -1) RA4_R=$(echo "$RA_OUTPUT" | grep -A2 'RA4:' | tail -1) -check "RA4 oscillating predicate" "$RA4_O" "1" +check "RA4 oscillating predicate" "$RA4_O" "true" check "RA4 report=oscillating" "$RA4_R" "oscillating" RA5_E=$(echo "$RA_OUTPUT" | grep -A2 'RA5:' | tail -2 | head -1) RA5_R=$(echo "$RA_OUTPUT" | grep -A2 'RA5:' | tail -1) -check "RA5 equilibrium predicate" "$RA5_E" "1" # #861: balanced jitter, NOT converged +check "RA5 equilibrium predicate" "$RA5_E" "true" # #861: balanced jitter, NOT converged check "RA5 report=equilibrium" "$RA5_R" "equilibrium" echo "" @@ -1024,10 +1024,10 @@ echo "[7/15] Halting: Settled Constant (#861)" HS_OUTPUT=$(./eigenscript ../tests/test_halting_stall.eigs 2>&1) HS_CONV=$(echo "$HS_OUTPUT" | grep -A1 'HS1:' | tail -1) -check "HS1 converged=1 at moderate H (#861: dead zone gone)" "$HS_CONV" "1" +check "HS1 converged=true at moderate H (#861: dead zone gone)" "$HS_CONV" "true" HS_EQ=$(echo "$HS_OUTPUT" | grep -A2 'HS1:' | tail -1) -check "HS2 equilibrium=1 at dH~0" "$HS_EQ" "1" +check "HS2 equilibrium=true at dH~0" "$HS_EQ" "true" HS_H=$(echo "$HS_OUTPUT" | grep -A1 'HS2:' | tail -1) TOTAL=$((TOTAL + 1)) @@ -1050,13 +1050,13 @@ echo "[8/15] Stable Band" SB_OUTPUT=$(./eigenscript ../tests/test_stable_band.eigs 2>&1) SB1_S=$(echo "$SB_OUTPUT" | grep -A1 'SB1:' | tail -1) -check "SB1 stable=0 (#861: linear drift is diverging)" "$SB1_S" "0" +check "SB1 stable=false (#861: linear drift is diverging)" "$SB1_S" "false" SB1_R=$(echo "$SB_OUTPUT" | grep -A2 'SB1:' | tail -1) check "SB1 report=diverging (#861)" "$SB1_R" "diverging" SB2_S=$(echo "$SB_OUTPUT" | grep -A1 'SB2:' | tail -1) -check "SB2 stable=1 (#861: converged implies stable)" "$SB2_S" "1" +check "SB2 stable=true (#861: converged implies stable)" "$SB2_S" "true" SB2_R=$(echo "$SB_OUTPUT" | grep -A2 'SB2:' | tail -1) check "SB2 report=converged" "$SB2_R" "converged" @@ -1065,15 +1065,15 @@ echo "" echo "[8b] Windowed Converged" WC_OUTPUT=$(./eigenscript ../tests/test_windowed_converged.eigs 2>&1) WC1=$(echo "$WC_OUTPUT" | grep -A1 'WC1:' | tail -1) -check "WC1 short trajectory cannot converge" "$WC1" "0" +check "WC1 short trajectory cannot converge" "$WC1" "false" WC2=$(echo "$WC_OUTPUT" | grep -A1 'WC2:' | tail -1) -check "WC2 full N quiet window converges" "$WC2" "1" +check "WC2 full N quiet window converges" "$WC2" "true" WC3=$(echo "$WC_OUTPUT" | grep -A1 'WC3:' | tail -1) -check "WC3 single transient breaks convergence" "$WC3" "0" +check "WC3 single transient breaks convergence" "$WC3" "false" WC4=$(echo "$WC_OUTPUT" | grep -A2 'WC4:' | tail -1) -check "WC4 newton sqrt CERTIFIES converged (#861: dead zone gone)" "$WC4" "converged=1 equilibrium=1" +check "WC4 newton sqrt CERTIFIES converged (#861: dead zone gone)" "$WC4" "converged=true equilibrium=true" WC5=$(echo "$WC_OUTPUT" | grep -A1 'WC5:' | tail -1) -check "WC5 rebind-from-temp loop converges (issue #260)" "$WC5" "converged=1 equilibrium=1" +check "WC5 rebind-from-temp loop converges (issue #260)" "$WC5" "converged=true equilibrium=true" echo "" echo "[8c] Predicate Matrix" @@ -1317,17 +1317,17 @@ echo "" echo "[15/15] try_parse Validation" TP_OUTPUT=$(./eigenscript ../tests/test_try_parse.eigs 2>&1) -check "TP_V1 valid assignment" "$(echo "$TP_OUTPUT" | grep -A1 'TP_V1:' | tail -1)" "1" -check "TP_V2 valid define" "$(echo "$TP_OUTPUT" | grep -A1 'TP_V2:' | tail -1)" "1" -check "TP_V3 valid if" "$(echo "$TP_OUTPUT" | grep -A1 'TP_V3:' | tail -1)" "1" -check "TP_V4 valid for" "$(echo "$TP_OUTPUT" | grep -A1 'TP_V4:' | tail -1)" "1" -check "TP_I1 rejects x is )" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I1:' | tail -1)" "0" -check "TP_I2 rejects if without colon" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I2:' | tail -1)" "0" -check "TP_I3 rejects empty string" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I3:' | tail -1)" "0" -check "TP_I4 rejects bracket garbage" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I4:' | tail -1)" "0" -check "TP_I5 rejects unknown char @" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I5:' | tail -1)" "0" -check "TP_I6 rejects unterminated string" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I6:' | tail -1)" "0" -check "TP_I7 rejects lone !" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I7:' | tail -1)" "0" +check "TP_V1 valid assignment" "$(echo "$TP_OUTPUT" | grep -A1 'TP_V1:' | tail -1)" "true" +check "TP_V2 valid define" "$(echo "$TP_OUTPUT" | grep -A1 'TP_V2:' | tail -1)" "true" +check "TP_V3 valid if" "$(echo "$TP_OUTPUT" | grep -A1 'TP_V3:' | tail -1)" "true" +check "TP_V4 valid for" "$(echo "$TP_OUTPUT" | grep -A1 'TP_V4:' | tail -1)" "true" +check "TP_I1 rejects x is )" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I1:' | tail -1)" "false" +check "TP_I2 rejects if without colon" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I2:' | tail -1)" "false" +check "TP_I3 rejects empty string" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I3:' | tail -1)" "false" +check "TP_I4 rejects bracket garbage" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I4:' | tail -1)" "false" +check "TP_I5 rejects unknown char @" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I5:' | tail -1)" "false" +check "TP_I6 rejects unterminated string" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I6:' | tail -1)" "false" +check "TP_I7 rejects lone !" "$(echo "$TP_OUTPUT" | grep -A1 'TP_I7:' | tail -1)" "false" echo "" echo "[16/16] Error Messages" @@ -1545,7 +1545,7 @@ SMOKE SMOKE_OUTPUT=$(./eigenscript "$SMOKE_FILE" 2>&1) rm -f "$SMOKE_FILE" - check "TR1 v2 model loads" "$(echo "$SMOKE_OUTPUT" | grep -A1 'TR1:' | tail -1)" "1" + check "TR1 v2 model loads" "$(echo "$SMOKE_OUTPUT" | grep -A1 'TR1:' | tail -1)" "true" check "TR2 generate returns list" "$(echo "$SMOKE_OUTPUT" | grep -A1 'TR2:' | tail -1)" "list" check "TR3 generate length matches max_tokens" "$(echo "$SMOKE_OUTPUT" | grep -A1 'TR3:' | tail -1)" "4" check "TR4 train returns string (JSON)" "$(echo "$SMOKE_OUTPUT" | grep -A1 'TR4:' | tail -1)" "str" @@ -1567,7 +1567,7 @@ V0TEST V0_OUTPUT=$(./eigenscript "$V0_FILE" 2>&1) rm -f "$V0_FILE" V0_LOADED=$(echo "$V0_OUTPUT" | tail -1) - check "TR6 old model rejected" "$V0_LOADED" "0" + check "TR6 old model rejected" "$V0_LOADED" "false" if echo "$V0_OUTPUT" | grep -q "format mismatch"; then echo " PASS: TR7 old rejection prints format mismatch"; PASS=$((PASS + 1)) else diff --git a/tests/sections/0f5-trace-context.sh b/tests/sections/0f5-trace-context.sh index 99d39ed1..f67da13f 100644 --- a/tests/sections/0f5-trace-context.sh +++ b/tests/sections/0f5-trace-context.sh @@ -2,7 +2,7 @@ echo "[0f5] Owned replay contexts and explicit host session advance (#1286)" check_binary_fingerprint TOTAL=$((TOTAL + 1)) TRACE_CONTEXT_OUT=$(bash "$TESTS_DIR/test_trace_context.sh" 2>&1); TRACE_CONTEXT_RC=$? -if rc_ok "$TRACE_CONTEXT_RC" "$TRACE_CONTEXT_OUT" && grep -qx 'trace context: 41 passed, 0 failed (41 declared)' <<< "$TRACE_CONTEXT_OUT"; then +if rc_ok "$TRACE_CONTEXT_RC" "$TRACE_CONTEXT_OUT" && grep -qx 'trace context: 44 passed, 0 failed (44 declared)' <<< "$TRACE_CONTEXT_OUT"; then PASS=$((PASS + 1)); echo " PASS: replay source/session ownership" else FAIL=$((FAIL + 1)); echo " FAIL: stream context contract (rc=$TRACE_CONTEXT_RC)" diff --git a/tests/sections/0fb-bool-type.sh b/tests/sections/0fb-bool-type.sh new file mode 100644 index 00000000..265604b7 --- /dev/null +++ b/tests/sections/0fb-bool-type.sh @@ -0,0 +1,136 @@ +echo "[0fb] Boolean type (#1637)" +check_eigs_suite "bool type: semantics 1-7, default tier" test_bool.eigs "All tests passed" +EIGS_JIT_OFF=1 check_eigs_suite "bool type: semantics 1-7, interpreter tier" test_bool.eigs "All tests passed" +# A bool never takes a builtin's EIGS_STRICT=0 soft path (#1637). +EIGS_STRICT=0 check_eigs_suite "bool type: semantics 1-7, EIGS_STRICT=0" test_bool.eigs "All tests passed" +# true and false in every argument slot of every builtin `--api --json` lists, +# and in the VM operand positions, raise in both strict modes unless the slot +# is on the reviewed any-value list (tests/test_bool_fuzz.sh; it prints its +# own verdict and examined counts). +check_binary_fingerprint +TOTAL=$((TOTAL + 1)) +BOOL_FZ_OUT=$(bash "$TESTS_DIR/test_bool_fuzz.sh" ./eigenscript 2>&1); BOOL_FZ_RC=$? +if [ "$BOOL_FZ_RC" = 0 ] && grep -q "^BOOL_FUZZ: examined=.* PASS$" <<< "$BOOL_FZ_OUT"; then + PASS=$((PASS + 1)); echo " PASS: a bool in any builtin slot or VM operand raises ($(grep '^BOOL_FUZZ: examined' <<< "$BOOL_FZ_OUT"))" +else + FAIL=$((FAIL + 1)); echo " FAIL: a bool in any builtin slot or VM operand raises (rc=$BOOL_FZ_RC)" + printf '%s\n' "$BOOL_FZ_OUT" +fi +unset BOOL_FZ_OUT BOOL_FZ_RC +# Forced JIT: every chunk compiles on first entry and every loop OSRs at once, +# so the comparison/NOT/JUMP_IF bool emitters run. A row that compiled nothing +# measured the interpreter, so it must also show compiled=N>0. +check_binary_fingerprint +TOTAL=$((TOTAL + 1)) +BOOL_JIT_OUT=$(EIGS_JIT_STATS=1 EIGS_JIT_ENTRY_THRESHOLD=1 EIGS_JIT_ITER_THRESHOLD=1 \ + EIGS_JIT_OSR_THRESHOLD=1 $EIGS_TMO ./eigenscript ../tests/test_bool.eigs &1) +BOOL_JIT_RC=$? +if rc_ok "$BOOL_JIT_RC" "$BOOL_JIT_OUT" && grep -q "All tests passed" <<< "$BOOL_JIT_OUT" && + [ "$(lar_jit_witness JIT "$BOOL_JIT_OUT")" = ok ]; then + PASS=$((PASS + 1)) + echo " PASS: bool type: semantics 1-7, forced-JIT tier (chunks compiled)" +else + FAIL=$((FAIL + 1)) + echo " FAIL: bool type: semantics 1-7, forced-JIT tier (rc=$BOOL_JIT_RC)" + printf '%s\n' "$BOOL_JIT_OUT" | eigs_failure_output +fi +unset BOOL_JIT_OUT BOOL_JIT_RC +# The class gate (#1637 round 4): the number members are renamed, so a raw +# read is a VAL_NUM_RAW / SLOT_NUM_RAW token; every token sits in a reviewed +# function with its exact count, and every builtin call passes the bool gate +# (tools/num_read_check.sh). +TOTAL=$((TOTAL + 1)) +BOOL_NR_OUT=$(bash "$TESTS_DIR/../tools/num_read_check.sh" 2>&1); BOOL_NR_RC=$? +if [ "$BOOL_NR_RC" = 0 ] && grep -q '^num-read: PASS$' <<< "$BOOL_NR_OUT"; then + PASS=$((PASS + 1)); echo " PASS: number reads are typed ($(grep '^num-read: examined' <<< "$BOOL_NR_OUT"))" +else + FAIL=$((FAIL + 1)); echo " FAIL: number reads are typed (rc=$BOOL_NR_RC)" + printf '%s\n' "$BOOL_NR_OUT" +fi +unset BOOL_NR_OUT BOOL_NR_RC +# Tape (#1637, format v6): a recorded bool replays as a bool, from the tape — +# the probed file is deleted between record and replay, so a live re-probe +# would answer false. A v5 header is refused (exit 3), never read as 1/0. +check_binary_fingerprint +BOOL_TP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/eigs_bool_tape.XXXXXX") +printf 'probe\n' > "$BOOL_TP_DIR/probe.txt" +printf 'x is file_exists of "%s"\nprint of [x, type of x, is_dir of "%s"]\n' \ + "$BOOL_TP_DIR/probe.txt" "$BOOL_TP_DIR/probe.txt" > "$BOOL_TP_DIR/p.eigs" +BOOL_TP_REC=$(EIGS_TRACE="$BOOL_TP_DIR/p.tape" $EIGS_TMO ./eigenscript "$BOOL_TP_DIR/p.eigs" &1) +rm -f "$BOOL_TP_DIR/probe.txt" +BOOL_TP_REP=$(EIGS_REPLAY="$BOOL_TP_DIR/p.tape" $EIGS_TMO ./eigenscript "$BOOL_TP_DIR/p.eigs" &1); BOOL_TP_RC=$? +TOTAL=$((TOTAL + 1)) +if [ "$BOOL_TP_RC" = 0 ] && [ "$BOOL_TP_REC" = '[true, "bool", false]' ] && + [ "$BOOL_TP_REP" = "$BOOL_TP_REC" ] && grep -q '^N 0 file_exists=true$' "$BOOL_TP_DIR/p.tape"; then + PASS=$((PASS + 1)); echo " PASS: a recorded bool replays as a bool, from the tape" +else + FAIL=$((FAIL + 1)); echo " FAIL: a recorded bool replays as a bool (rc=$BOOL_TP_RC rec='$BOOL_TP_REC' rep='$BOOL_TP_REP')" +fi +sed '1s/^V 6 /V 5 /' "$BOOL_TP_DIR/p.tape" > "$BOOL_TP_DIR/p5.tape" +BOOL_TP_OUT=$(EIGS_REPLAY="$BOOL_TP_DIR/p5.tape" $EIGS_TMO ./eigenscript "$BOOL_TP_DIR/p.eigs" &1); BOOL_TP_RC=$? +TOTAL=$((TOTAL + 1)) +if [ "$BOOL_TP_RC" = 3 ] && grep -q "tape format v5, this binary reads v6" <<< "$BOOL_TP_OUT"; then + PASS=$((PASS + 1)); echo " PASS: a v5 (pre-bool) tape is refused with exit 3" +else + FAIL=$((FAIL + 1)); echo " FAIL: a v5 (pre-bool) tape is refused with exit 3 (rc=$BOOL_TP_RC)" + printf '%s\n' "$BOOL_TP_OUT" | eigs_failure_output +fi +# Tamper: a v6 tape whose bool record was hand-edited to a number (and a +# number record edited to a bool) is refused with exit 3, naming the builtin, +# the record and the tape version -- never replayed as the other kind. +printf 'x is file_exists of "%s"\nr is random of null\nn is random_normal of [2, 1.0]\nprint of [x, type of x, type of r, type of n]\n' \ + "$BOOL_TP_DIR/p.eigs" > "$BOOL_TP_DIR/t.eigs" +EIGS_TRACE="$BOOL_TP_DIR/t.tape" $EIGS_TMO ./eigenscript "$BOOL_TP_DIR/t.eigs" /dev/null 2>&1 +sed 's/^\(N [0-9]* file_exists=\)true$/\11/' "$BOOL_TP_DIR/t.tape" > "$BOOL_TP_DIR/t1.tape" +sed 's/^\(N [0-9]* random=\).*$/\1true/' "$BOOL_TP_DIR/t.tape" > "$BOOL_TP_DIR/t2.tape" +sed 's/^\(N [0-9]* random_normal=\).*$/\1true/' "$BOOL_TP_DIR/t.tape" > "$BOOL_TP_DIR/t3.tape" +for BOOL_TP_CASE in "t1:file_exists returns a bool, the tape holds a num" "t2:random returns a num, the tape holds a bool" \ + "t3:random_normal returns a list or null, the tape holds a bool"; do + TOTAL=$((TOTAL + 1)) + BOOL_TP_T=${BOOL_TP_CASE%%:*}; BOOL_TP_WANT=${BOOL_TP_CASE#*:} + BOOL_TP_OUT=$(EIGS_REPLAY="$BOOL_TP_DIR/$BOOL_TP_T.tape" $EIGS_TMO ./eigenscript "$BOOL_TP_DIR/t.eigs" &1); BOOL_TP_RC=$? + if ! cmp -s "$BOOL_TP_DIR/t.tape" "$BOOL_TP_DIR/$BOOL_TP_T.tape" && [ "$BOOL_TP_RC" = 3 ] && + grep -q "tape format v6 record 'N [0-9]* .*': $BOOL_TP_WANT; refusing to replay" <<< "$BOOL_TP_OUT"; then + PASS=$((PASS + 1)); echo " PASS: a tampered tape is refused: $BOOL_TP_WANT" + else + FAIL=$((FAIL + 1)); echo " FAIL: a tampered tape is refused: $BOOL_TP_WANT (rc=$BOOL_TP_RC)" + printf '%s\n' "$BOOL_TP_OUT" | eigs_failure_output + fi +done +rm -rf "$BOOL_TP_DIR" +# Completeness: every taped builtin has a k_tape_kinds row, and every row +# names a taped builtin (tools/tape_kinds_check.sh). +TOTAL=$((TOTAL + 1)) +BOOL_TK_OUT=$(bash "$TESTS_DIR/../tools/tape_kinds_check.sh" 2>&1); BOOL_TK_RC=$? +if [ "$BOOL_TK_RC" = 0 ] && grep -q '^tape-kinds: PASS$' <<< "$BOOL_TK_OUT"; then + PASS=$((PASS + 1)); echo " PASS: every taped builtin declares its return kinds ($(grep '^tape-kinds: taped' <<< "$BOOL_TK_OUT"))" +else + FAIL=$((FAIL + 1)); echo " FAIL: every taped builtin declares its return kinds (rc=$BOOL_TK_RC)" + printf '%s\n' "$BOOL_TK_OUT" +fi +unset BOOL_TK_OUT BOOL_TK_RC +unset BOOL_TP_DIR BOOL_TP_REC BOOL_TP_REP BOOL_TP_OUT BOOL_TP_RC BOOL_TP_CASE BOOL_TP_T BOOL_TP_WANT +# Every public lib predicate, run on a battery plus true/false cases, answers +# with a bool (tests/test_bool_lib_predicates.sh measures; it reads no code). +check_binary_fingerprint +TOTAL=$((TOTAL + 1)) +BOOL_LIB_OUT=$(bash "$TESTS_DIR/test_bool_lib_predicates.sh" ./eigenscript 2>&1); BOOL_LIB_RC=$? +if [ "$BOOL_LIB_RC" = 0 ] && grep -q "^BOOL_LIB_PREDICATES: .* PASS$" <<< "$BOOL_LIB_OUT"; then + PASS=$((PASS + 1)); echo " PASS: lib predicates answer bools ($(grep '^BOOL_LIB_PREDICATES' <<< "$BOOL_LIB_OUT"))" +else + FAIL=$((FAIL + 1)); echo " FAIL: lib predicates answer bools (rc=$BOOL_LIB_RC)" + printf '%s\n' "$BOOL_LIB_OUT" | eigs_failure_output +fi +unset BOOL_LIB_OUT BOOL_LIB_RC +# lib/eigen.eigs's eigen_run agrees with the VM on every bool-producing form +# (tests/test_bool_meta_diff.sh runs each snippet through both). +check_binary_fingerprint +TOTAL=$((TOTAL + 1)) +BOOL_META_OUT=$(bash "$TESTS_DIR/test_bool_meta_diff.sh" ./eigenscript 2>&1); BOOL_META_RC=$? +if [ "$BOOL_META_RC" = 0 ] && grep -q "^BOOL_META_DIFF: .* PASS$" <<< "$BOOL_META_OUT"; then + PASS=$((PASS + 1)); echo " PASS: eigen_run matches the VM on bools ($(grep '^BOOL_META_DIFF' <<< "$BOOL_META_OUT"))" +else + FAIL=$((FAIL + 1)); echo " FAIL: eigen_run matches the VM on bools (rc=$BOOL_META_RC)" + printf '%s\n' "$BOOL_META_OUT" | eigs_failure_output +fi +unset BOOL_META_OUT BOOL_META_RC diff --git a/tests/sections/0fc-db-params.sh b/tests/sections/0fc-db-params.sh new file mode 100644 index 00000000..60546436 --- /dev/null +++ b/tests/sections/0fc-db-params.sh @@ -0,0 +1,20 @@ +echo "[0fc] db parameters bind a bool as an SQL boolean (#1637)" +# Needs the libpq headers to build the server-db objects; no server is used. +# The build runs into build/server-db only, so the suite's src/eigenscript +# alias is not re-pointed. +if [ ! -f /usr/include/postgresql/libpq-fe.h ]; then + section_skip "no libpq headers (/usr/include/postgresql/libpq-fe.h): the server-db objects cannot be built here" +else + TOTAL=$((TOTAL + 1)) + DBP_BUILD=$(make --no-print-directory -C "$TESTS_DIR/.." build/server-db/test_db_params 2>&1); DBP_RC=$? + DBP_OUT="" + [ "$DBP_RC" = 0 ] && { DBP_OUT=$($EIGS_TMO "$TESTS_DIR/../build/server-db/test_db_params" 2>&1); DBP_RC=$?; } + if rc_ok "$DBP_RC" "$DBP_OUT" && grep -qx 'db params: 7 passed, 0 failed (7 declared)' <<< "$DBP_OUT"; then + PASS=$((PASS + 1)); echo " PASS: a bool db parameter binds as SQL boolean text with OID 16" + else + FAIL=$((FAIL + 1)); echo " FAIL: db parameter binding (rc=$DBP_RC)" + printf '%s\n%s\n' "$DBP_BUILD" "$DBP_OUT" | sed 's/^/ /' | tail -30 + fi + unset DBP_BUILD DBP_OUT DBP_RC +fi +echo "" diff --git a/tests/sections/47i-model-context-window.sh b/tests/sections/47i-model-context-window.sh index 0ddd738d..34217b6c 100644 --- a/tests/sections/47i-model-context-window.sh +++ b/tests/sections/47i-model-context-window.sh @@ -12,7 +12,8 @@ if [ "$CW_PROBE_RC" -eq 1 ] && section_skip "binary built without EIGENSCRIPT_EXT_MODEL" else TOTAL=$((TOTAL + 1)) - if ! rc_ok "$CW_PROBE_RC" "$CW_PROBE_OUT" || [ "$CW_PROBE_OUT" != 0 ]; then + # #1637: eigen_model_loaded answers a bool (`false` before a load). + if ! rc_ok "$CW_PROBE_RC" "$CW_PROBE_OUT" || [ "$CW_PROBE_OUT" != false ]; then FAIL=$((FAIL + 1)) echo " FAIL: model capability probe (rc=$CW_PROBE_RC)" printf '%s\n' "$CW_PROBE_OUT" diff --git a/tests/strict_shape_cases.json b/tests/strict_shape_cases.json index b3145edf..563fd7c5 100644 --- a/tests/strict_shape_cases.json +++ b/tests/strict_shape_cases.json @@ -183,7 +183,7 @@ "\"@WAV@\"", "0" ], - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "setup": "", "observe": "print of shape_result", "cleanup": "audio_music_stop of null", @@ -215,12 +215,12 @@ "backend": "mixer", "backend_absent": { "setup": "", - "valid_assert": "shape_result == 0", + "valid_assert": "shape_result == false", "observe": "print of shape_result", "unchanged": "# Entry rejection is checked before any absent-backend operation.", "outputs": { - "absent-sdl": "0\nshape-complete", - "absent-mixer": "audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0)\n0\nshape-complete" + "absent-sdl": "false\nshape-complete", + "absent-mixer": "audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0)\nfalse\nshape-complete" }, "claim": "Documented missing-backend or closed-device outcome, not a device-operation positive." } @@ -230,7 +230,7 @@ "32" ], "valid_assert": "shape_result == null", - "setup": "shape_music is audio_music_play of [\"@WAV@\", 0 - 1]\nassert of [shape_music == 1, \"shape control\"]", + "setup": "shape_music is audio_music_play of [\"@WAV@\", 0 - 1]\nassert of [shape_music == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "audio_music_stop of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -835,9 +835,9 @@ ], "valid_assert": "(len of shape_result) == 3 and shape_result[0] > 0 and shape_result[2] == 1", "setup": "write_text of [\"@TMP@/input.eigs\", \"x is 1\\n\"]", - "observe": "assert of [(file_exists of \"@TMP@/tokens\") == 1, \"shape control\"]\nassert of [(file_exists of \"@TMP@/vocab\") == 1, \"shape control\"]\nprint of \"observed\"", + "observe": "assert of [(file_exists of \"@TMP@/tokens\"), \"shape control\"]\nassert of [(file_exists of \"@TMP@/vocab\"), \"shape control\"]\nprint of \"observed\"", "cleanup": "", - "unchanged": "assert of [(file_exists of \"@TMP@/tokens\") == 0, \"shape control\"]\nassert of [(file_exists of \"@TMP@/vocab\") == 0, \"shape control\"]", + "unchanged": "assert of [(not (file_exists of \"@TMP@/tokens\")), \"shape control\"]\nassert of [(not (file_exists of \"@TMP@/vocab\")), \"shape control\"]", "optional_args": [ [ "[\"@TMP@/input.eigs\"]", @@ -932,7 +932,7 @@ "\"b\"" ], "setup": "", - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "observe": "print of shape_result", "unchanged": "# Pure input: no mutable argument or external state.", "cleanup": "", @@ -961,7 +961,7 @@ "\"k\"" ], "setup": "d is {\"k\": 2}", - "valid_assert": "(has_key of [d, \"k\"]) == 0", + "valid_assert": "(not (has_key of [d, \"k\"]))", "observe": "print of d", "unchanged": "assert of [d.k == 2, \"dictionary mutated on rejection\"]", "cleanup": "", @@ -1032,7 +1032,7 @@ "1" ], "valid_assert": "shape_result == (0 - 1)", - "setup": "assert of [(eigen_model_loaded of null) == 0, \"shape control\"]", + "setup": "assert of [(not (eigen_model_loaded of null)), \"shape control\"]", "observe": "print of shape_result", "cleanup": "", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1061,7 +1061,7 @@ "0" ], "valid_assert": "(len of shape_result) == 0", - "setup": "assert of [(eigen_model_loaded of null) == 0, \"shape control\"]", + "setup": "assert of [(not (eigen_model_loaded of null)), \"shape control\"]", "observe": "print of shape_result", "cleanup": "", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1095,7 +1095,7 @@ "\"bc\"" ], "setup": "", - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "observe": "print of shape_result", "unchanged": "# Pure input: no mutable argument or external state.", "cleanup": "", @@ -1158,7 +1158,7 @@ "255" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1208,7 +1208,7 @@ "0" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1253,7 +1253,7 @@ "8" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1300,7 +1300,7 @@ "1" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]\nshape_pixels is buffer of 1", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]\nshape_pixels is buffer of 1", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1345,7 +1345,7 @@ "30" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1385,7 +1385,7 @@ "16", "\"shape\"" ], - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "setup": "", "observe": "print of shape_result", "cleanup": "gfx_close of null", @@ -1411,11 +1411,11 @@ "backend": "sdl", "backend_absent": { "setup": "", - "valid_assert": "shape_result == 0", + "valid_assert": "shape_result == false", "observe": "print of shape_result", "unchanged": "# Entry rejection is checked before any absent-backend operation.", "outputs": { - "absent-sdl": "gfx_open: cannot load libSDL2\n0\nshape-complete" + "absent-sdl": "gfx_open: cannot load libSDL2\nfalse\nshape-complete" }, "claim": "Documented missing-backend or closed-device outcome, not a device-operation positive." } @@ -1429,7 +1429,7 @@ "30" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1469,7 +1469,7 @@ "0" ], "valid_assert": "(len of shape_result) == 3", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]\ngfx_clear of [10, 20, 30]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]\ngfx_clear of [10, 20, 30]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1515,7 +1515,7 @@ "255" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1572,7 +1572,7 @@ "255" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1628,7 +1628,7 @@ "1" ], "valid_assert": "shape_result == null", - "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == 1, \"shape control\"]", + "setup": "shape_window is gfx_open of [16, 16, \"shape\"]\nassert of [shape_window == true, \"shape control\"]", "observe": "print of shape_result", "cleanup": "gfx_close of null", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -1734,7 +1734,7 @@ "\"k\"" ], "setup": "", - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "observe": "print of shape_result", "unchanged": "# Pure input: no mutable argument or external state.", "cleanup": "", @@ -1956,7 +1956,7 @@ "2" ], "setup": "", - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "observe": "print of shape_result", "unchanged": "# Pure input: no mutable argument or external state.", "cleanup": "", @@ -2098,7 +2098,7 @@ "0.001" ], "valid_assert": "shape_result == \"{\\\"status\\\": \\\"error\\\", \\\"error\\\": \\\"Model not loaded\\\"}\"", - "setup": "assert of [(eigen_model_loaded of null) == 0, \"shape control\"]", + "setup": "assert of [(not (eigen_model_loaded of null)), \"shape control\"]", "observe": "print of shape_result", "cleanup": "", "unchanged": "# No observable mutable argument; entry-guard ordering is independently checked from source.", @@ -2561,11 +2561,11 @@ "\"@TMP@/from\"", "\"@TMP@/to\"" ], - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "setup": "write_text of [\"@TMP@/from\", \"abc\"]", "observe": "assert of [(read_text of \"@TMP@/to\") == \"abc\", \"shape control\"]\nprint of \"observed\"", "cleanup": "", - "unchanged": "assert of [(read_text of \"@TMP@/from\") == \"abc\", \"shape control\"]\nassert of [(file_exists of \"@TMP@/to\") == 0, \"shape control\"]", + "unchanged": "assert of [(read_text of \"@TMP@/from\") == \"abc\", \"shape control\"]\nassert of [(not (file_exists of \"@TMP@/to\")), \"shape control\"]", "optional_args": [], "requirements": [ "owned_temp" @@ -2605,7 +2605,7 @@ "10000" ], "setup": "descriptor is @DESCRIPTOR@", - "valid_assert": "shape_result.ok == 1 and shape_result.result == 42", + "valid_assert": "shape_result.ok == true and shape_result.result == 42", "observe": "print of shape_result.result", "unchanged": "# Pure input: no mutable argument or external state.", "cleanup": "", @@ -2760,7 +2760,7 @@ "\"same\"" ], "setup": "", - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "observe": "print of shape_result", "unchanged": "# Pure input: no mutable argument or external state.", "cleanup": "", @@ -2879,7 +2879,7 @@ "setup": "", "observe": "assert of [(shared_get of \"shape-key\") == 3, \"shape control\"]\nprint of \"observed\"", "cleanup": "", - "unchanged": "assert of [(shared_has of \"shape-key\") == 0, \"shape control\"]", + "unchanged": "assert of [(not (shared_has of \"shape-key\")), \"shape control\"]", "optional_args": [], "requirements": [ "http_compiled", @@ -2952,7 +2952,7 @@ "\"ab\"" ], "setup": "", - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "observe": "print of shape_result", "unchanged": "# Pure input: no mutable argument or external state.", "cleanup": "", @@ -2991,7 +2991,7 @@ "\"items\"", "shape_key" ], - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "setup": "shape_store is store_open of \"@TMP@/store\"\nassert of [(type of shape_store) == \"dict\", \"shape control\"]\nshape_key is store_put of [shape_store, \"items\", {\"value\": 1}]\nassert of [(type of shape_key) == \"str\", \"shape control\"]", "observe": "assert of [(store_count of [shape_store, \"items\"]) == 0, \"shape control\"]\nprint of \"observed\"", "cleanup": "store_close of shape_store", @@ -3017,7 +3017,7 @@ "shape_store", "\"items\"" ], - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "setup": "shape_store is store_open of \"@TMP@/store\"\nassert of [(type of shape_store) == \"dict\", \"shape control\"]\nshape_key is store_put of [shape_store, \"items\", {\"value\": 1}]\nassert of [(type of shape_key) == \"str\", \"shape control\"]", "observe": "assert of [(store_count of [shape_store, \"items\"]) == 0, \"shape control\"]\nprint of \"observed\"", "cleanup": "store_close of shape_store", @@ -3125,7 +3125,7 @@ "shape_key", "{\"value\": 2}" ], - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "setup": "shape_store is store_open of \"@TMP@/store\"\nassert of [(type of shape_store) == \"dict\", \"shape control\"]\nshape_key is store_put of [shape_store, \"items\", {\"value\": 1}]\nassert of [(type of shape_key) == \"str\", \"shape control\"]", "observe": "shape_after is store_get of [shape_store, \"items\", shape_key]\nassert of [shape_after[\"value\"] == 2, \"shape control\"]\nprint of \"observed\"", "cleanup": "store_close of shape_store", @@ -3166,11 +3166,11 @@ "\"@TMP@/stream\"", "1" ], - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "setup": "", "observe": "print of shape_result", "cleanup": "stream_close of null", - "unchanged": "assert of [(file_exists of \"@TMP@/stream\") == 0, \"shape control\"]", + "unchanged": "assert of [(not (file_exists of \"@TMP@/stream\")), \"shape control\"]", "optional_args": [], "requirements": [ "owned_temp" @@ -3222,7 +3222,7 @@ "7" ], "setup": "define completed(n) as:\n return 3\ntask_id is task_spawn of completed", - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "observe": "print of shape_result\nprint of (task_try_recv of null)", "unchanged": "assert of [(task_try_recv of null) == null, \"task send enqueued on rejection\"]", "cleanup": "assert of [(task_join of task_id) == 3, \"owned task join\"]", @@ -3236,9 +3236,9 @@ "\"@TMP@/tensor\"" ], "setup": "", - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "observe": "assert of [(tensor_load of \"@TMP@/tensor\") == [[2, 3]], \"tensor round trip\"]\nprint of \"observed\"", - "unchanged": "assert of [(file_exists of \"@TMP@/tensor\") == 0, \"tensor file created on rejection\"]", + "unchanged": "assert of [(not (file_exists of \"@TMP@/tensor\")), \"tensor file created on rejection\"]", "cleanup": "", "requirements": [ "owned_temp" @@ -3297,7 +3297,7 @@ "setup": "", "observe": "assert of [(read_text of \"@TMP@/bytes\") == \"AB\", \"shape control\"]\nprint of \"observed\"", "cleanup": "", - "unchanged": "assert of [(file_exists of \"@TMP@/bytes\") == 0, \"shape control\"]", + "unchanged": "assert of [(not (file_exists of \"@TMP@/bytes\")), \"shape control\"]", "optional_args": [ [ "\"@TMP@/bytes\"", @@ -3323,11 +3323,11 @@ "\"@TMP@/text\"", "\"abc\"" ], - "valid_assert": "shape_result == 1", + "valid_assert": "shape_result == true", "setup": "", "observe": "assert of [(read_text of \"@TMP@/text\") == \"abc\", \"shape control\"]\nprint of \"observed\"", "cleanup": "", - "unchanged": "assert of [(file_exists of \"@TMP@/text\") == 0, \"shape control\"]", + "unchanged": "assert of [(not (file_exists of \"@TMP@/text\")), \"shape control\"]", "optional_args": [], "requirements": [ "owned_temp" diff --git a/tests/strict_shape_descriptor.c b/tests/strict_shape_descriptor.c index b9a5cc60..bd147f43 100644 --- a/tests/strict_shape_descriptor.c +++ b/tests/strict_shape_descriptor.c @@ -25,7 +25,7 @@ int main(void) { chunk->fn_count || chunk->local_count || chunk->param_count || chunk->const_count != 1 || !chunk->constants[0] || chunk->constants[0]->type != VAL_NUM || - chunk->constants[0]->data.num != 42) goto done; + VAL_NUM_RAW(chunk->constants[0]) != 42) goto done; descriptor = make_list(3); Value *code = make_list(chunk->code_len); diff --git a/tests/test_arena_promotion_ordinary.c b/tests/test_arena_promotion_ordinary.c index 2f13310e..b1a0bb8f 100644 --- a/tests/test_arena_promotion_ordinary.c +++ b/tests/test_arena_promotion_ordinary.c @@ -28,7 +28,7 @@ int main(void) { int source_cap = source->data.list.capacity, child_cap = child->data.list.capacity; Value *copy_num = promote_if_arena(number), *copy_text = promote_if_arena(text); CHECK("number is a distinct heap copy", copy_num != number && !copy_num->arena && copy_num->type == VAL_NUM); - CHECK("number retains finite value", copy_num->data.num == 3); + CHECK("number retains finite value", VAL_NUM_RAW(copy_num) == 3); CHECK("string is a distinct heap copy", copy_text != text && !copy_text->arena && copy_text->type == VAL_STR); CHECK("string retains exact bytes and length", val_str_len(copy_text) == 4 && !memcmp(copy_text->data.str,"tiny",4)); Value *copy = promote_if_arena(source); @@ -38,7 +38,7 @@ int main(void) { CHECK("repeated child keeps one copied identity", inner == copy->data.list.items[1] && inner != child); CHECK("child is a heap list", !inner->arena && inner->type == VAL_LIST && inner->data.list.count == 2); CHECK("copied child owns two root edges", inner->refcount == 2); - CHECK("nested number retains value", inner->data.list.items[0]->type == VAL_NUM && inner->data.list.items[0]->data.num == 3); + CHECK("nested number retains value", inner->data.list.items[0]->type == VAL_NUM && VAL_NUM_RAW(inner->data.list.items[0]) == 3); CHECK("nested string retains bytes", val_str_len(inner->data.list.items[1]) == 4 && !memcmp(inner->data.list.items[1]->data.str,"tiny",4)); CHECK("heap child preserves identity", copy->data.list.items[2] == heap); CHECK("heap child acquires one copied edge", heap->refcount == 2); @@ -57,7 +57,7 @@ int main(void) { g_gc_enabled = 0; val_decref(copy_num); val_decref(copy_text); val_decref(copy); CHECK("copied graph releases heap edge", heap->refcount == 1); - CHECK("independently owned heap child remains valid", heap->data.list.count == 1 && heap->data.list.items[0]->data.num == 7); + CHECK("independently owned heap child remains valid", heap->data.list.count == 1 && VAL_NUM_RAW(heap->data.list.items[0]) == 7); val_decref(heap); arena_reset_to_mark(); /* No arena pointer is inspected afterward. */ g_gc_enabled = old_gc; diff --git a/tests/test_args_import.sh b/tests/test_args_import.sh index 8142842a..86205a94 100644 --- a/tests/test_args_import.sh +++ b/tests/test_args_import.sh @@ -40,7 +40,7 @@ print of (args.get_flag of [p, "--verbose"]) print of (args.get_positional of p)' --output b --verbose 2>&1) RC=$? set -e -if [ "$RC" -eq 0 ] && str_has_line "$OUT" 'b' && str_has_line "$OUT" '1' && str_has_line "$OUT" '[]'; then +if [ "$RC" -eq 0 ] && str_has_line "$OUT" 'b' && str_has_line "$OUT" 'true' && str_has_line "$OUT" '[]'; then ok "import args: --key value + boolean flag" else bad "import args: --key value + boolean flag" "rc=$RC out=$OUT" diff --git a/tests/test_audio.eigs b/tests/test_audio.eigs index 4a5ec10e..f7b0d0a0 100644 --- a/tests/test_audio.eigs +++ b/tests/test_audio.eigs @@ -58,15 +58,15 @@ if ok != 0: short_clip is audio_sine of [440, 0.01, 0.5] # 3 loops × 441 samples = 1323 n is audio_play_loop of [short_clip, 3] - assert_eq of [(n > 0), 1, "audio_play_loop returns a channel id"] + assert_eq of [(n > 0), true, "audio_play_loop returns a channel id"] # 1 loop is the no-op identity n1 is audio_play_loop of [short_clip, 1] - assert_eq of [(n1 > 0), 1, "audio_play_loop 1 loop plays"] + assert_eq of [(n1 > 0), true, "audio_play_loop 1 loop plays"] # Invalid: 0 loops, non-number, empty samples — all 0. loops=-1 is # now the infinite form (GAP-002) — the mixer rewinds, no memory cost. assert_eq of [audio_play_loop of [short_clip, 0], 0, "loops=0 rejected"] ch_inf is audio_play_loop of [short_clip, -1] - assert_eq of [(ch_inf > 0), 1, "loops=-1 plays forever on a channel (GAP-002)"] + assert_eq of [(ch_inf > 0), true, "loops=-1 plays forever on a channel (GAP-002)"] assert_eq of [audio_stop of ch_inf, 1, "infinite channel stoppable"] assert_eq of [audio_play_loop of [short_clip, "many"], 0, "non-number loops rejected"] assert_eq of [audio_play_loop of [[], 5], 0, "empty samples rejected"] @@ -78,9 +78,9 @@ if ok != 0: # longer multiplies memory, so 800 loops of a 5s clip now just plays # (the old queue mode had to reject it at the 256 MiB byte budget). big_clip is audio_sine of [220, 5, 0.5] - assert_eq of [(audio_play_loop of [big_clip, 800]) > 0, 1, "800 loops of a 5s clip plays (one copy + rewind)"] + assert_eq of [(audio_play_loop of [big_clip, 800]) > 0, true, "800 loops of a 5s clip plays (one copy + rewind)"] # Loops at the very edge still succeed. - assert_eq of [(audio_play_loop of [short_clip, 10000]) > 0, 1, "loops=10000 at the ceiling still plays (#152)"] + assert_eq of [(audio_play_loop of [short_clip, 10000]) > 0, true, "loops=10000 at the ceiling still plays (#152)"] audio_clear of null audio_close of null else: @@ -181,7 +181,7 @@ else: # ---- F-DS-17: live audio stream (push/queue playback for musical typing). # Ungated safety first: with no stream open, push/queued are 0 and # clear/close are safe no-ops (mirrors the capture "no device" contract). -assert_eq of [audio_stream_push of (audio_sine of [440, 0.01, 0.5]), 0, "push with no stream open returns 0"] +assert_eq of [audio_stream_push of (audio_sine of [440, 0.01, 0.5]), false, "push with no stream open returns 0"] assert_eq of [audio_stream_queued of null, 0, "queued with no stream open is 0"] assert_eq of [audio_stream_clear of null, null, "clear with no stream is safe"] assert_eq of [audio_stream_close of null, null, "close with no stream is safe"] @@ -194,7 +194,7 @@ if sdev != 0: # Push a ~50ms block; the device drains at real time, so essentially # all of it is still buffered microseconds later. blk is audio_sine of [220, 0.05, 0.5] - assert_eq of [audio_stream_push of blk, 1, "push a float-list block returns 1"] + assert_eq of [audio_stream_push of blk, true, "push a float-list block returns 1"] assert_true of [(audio_stream_queued of null) >= 1, "queued reflects the pushed block"] # Buffer blocks accepted too (the bulk-render fast path, like #578). bb is buffer of 441 @@ -202,10 +202,10 @@ if sdev != 0: loop while bbi < 441: bb[bbi] is 0.1 bbi += 1 - assert_eq of [audio_stream_push of bb, 1, "push a buffer block returns 1"] + assert_eq of [audio_stream_push of bb, true, "push a buffer block returns 1"] # Bad shapes rejected while open; the device stays usable. - assert_eq of [audio_stream_push of [], 0, "empty block rejected"] - assert_eq of [audio_stream_push of 42, 0, "non-list/buffer block rejected"] + assert_eq of [audio_stream_push of [], false, "empty block rejected"] + assert_eq of [audio_stream_push of 42, false, "non-list/buffer block rejected"] # Clear flushes the queue. audio_stream_clear of null assert_eq of [audio_stream_queued of null, 0, "clear empties the stream queue"] @@ -235,11 +235,11 @@ d is audio_open of [44100, 1] if d != 0: clip is audio_sine of [440, 0.05, 0.5] ch1 is audio_play of clip - assert_eq of [(ch1 > 0), 1, "audio_play returns a channel id"] + assert_eq of [(ch1 > 0), true, "audio_play returns a channel id"] chinf is audio_play_loop of [clip, 0 - 1] - assert_eq of [(chinf > 0), 1, "loops == -1 (infinite) accepted"] + assert_eq of [(chinf > 0), true, "loops == -1 (infinite) accepted"] ch3 is audio_play_loop of [clip, 3] - assert_eq of [(ch3 > 0) and (ch3 != ch1) and (ch3 != chinf), 1, "channels are distinct"] + assert_eq of [(ch3 > 0) and (ch3 != ch1) and (ch3 != chinf), true, "channels are distinct"] q is audio_queue_size of null assert_eq of [q, (len of clip) * 2 * 4, "queue counts finite channels only (1 + 3 passes)"] assert_eq of [audio_volume of [chinf, 0.25], 1, "live volume on an active channel"] @@ -260,10 +260,10 @@ if d != 0: bclip[bi] is 0.1 bi += 1 chb is audio_play of bclip - assert_eq of [(chb > 0), 1, "audio_play accepts a buffer (#578)"] + assert_eq of [(chb > 0), true, "audio_play accepts a buffer (#578)"] assert_eq of [audio_queue_size of null, nb * 2, "buffer clip queued at full length"] chbl is audio_play_loop of [bclip, 3] - assert_eq of [(chbl > 0), 1, "audio_play_loop accepts a buffer (#578)"] + assert_eq of [(chbl > 0), true, "audio_play_loop accepts a buffer (#578)"] assert_eq of [audio_stop of chbl, 1, "buffer loop channel stoppable"] # Buffer values are clamped like list values (out-of-range -> +/-1). hot is buffer of 100 @@ -272,7 +272,7 @@ if d != 0: hot[hi] is 9.5 hi += 1 chh is audio_play of hot - assert_eq of [(chh > 0), 1, "out-of-range buffer samples clamp, still play"] + assert_eq of [(chh > 0), true, "out-of-range buffer samples clamp, still play"] # Empty buffer is still rejected (0), matching the empty-list rule. assert_eq of [audio_play of (buffer of 0), 0, "empty buffer rejected"] diff --git a/tests/test_auth.eigs b/tests/test_auth.eigs index 1210cf59..0f45fed2 100644 --- a/tests/test_auth.eigs +++ b/tests/test_auth.eigs @@ -14,14 +14,14 @@ assert of [(require_auth of null) == "", "AUTH01 require_auth accepts CRLF beare _test_headers is (exec_capture of ["printf", "authorization: bearer eigen_test\\r\\n"])[1] ok_json is auth_check of null -assert of [(contains of [ok_json, "\"status\": \"ok\""]) == 1, "AUTH02 auth_check accepts case-insensitive bearer header"] +assert of [(contains of [ok_json, "\"status\": \"ok\""]), "AUTH02 auth_check accepts case-insensitive bearer header"] _test_headers is (exec_capture of ["printf", "Authorization: Bearer wrong\\r\\n"])[1] bad_json is require_auth of null -assert of [(contains of [bad_json, "invalid or missing token"]) == 1, "AUTH03 require_auth rejects wrong bearer token"] +assert of [(contains of [bad_json, "invalid or missing token"]), "AUTH03 require_auth rejects wrong bearer token"] _auth_token is "" missing_json is require_auth of null -assert of [(contains of [missing_json, "not authenticated"]) == 1, "AUTH04 require_auth rejects missing session token"] +assert of [(contains of [missing_json, "not authenticated"]), "AUTH04 require_auth rejects missing session token"] print of "All auth tests passed" diff --git a/tests/test_biology.eigs b/tests/test_biology.eigs index c26e2d16..6c121651 100644 --- a/tests/test_biology.eigs +++ b/tests/test_biology.eigs @@ -75,7 +75,7 @@ ps is punnett_square of ["Aa", "Aa"] assert_near of [ps["AA"], 0.25, 0.001, "Punnett AA"] assert_near of [ps["Aa"], 0.5, 0.001, "Punnett Aa (heterozygote, both boxes)"] assert_near of [ps["aa"], 0.25, 0.001, "Punnett aa"] -assert_eq of [has_key of [ps, "aA"], 0, "Punnett has no separate aA genotype"] +assert_eq of [has_key of [ps, "aA"], false, "Punnett has no separate aA genotype"] # ============================================================ # DNA / RNA diff --git a/tests/test_bool.eigs b/tests/test_bool.eigs new file mode 100644 index 00000000..4578d24c --- /dev/null +++ b/tests/test_bool.eigs @@ -0,0 +1,298 @@ +# #1637: the boolean type. Each block pins one of the decided semantics +# (issue #1637, "Decided semantics" 1-7). Every row compares TYPE as well as +# value, so a comparison that silently produces the number 1 again is a FAIL, +# not a pass by coincidence of truthiness. +load_file of "lib/test.eigs" + +# same: equal type AND equal value. A plain `==` would raise on bool vs num +# (semantics 6), which is right for programs and wrong for a test oracle that +# must REPORT the mismatch. +define same(actual, expected, desc) as: + local ta is type of actual + local te is type of expected + if ta != te: + assert_eq of [f"{ta}:{actual}", f"{te}:{expected}", desc] + return null + assert_eq of [actual, expected, desc] + +# raises: the thunk must raise, and the message must contain `needle`. +define raises(thunk, needle, desc) as: + local got is "" + try: + local r is thunk of null + got is f"" + catch e: + got is e.message + assert_true of [(index_of of [got, needle]) >= 0, f"{desc}: {got}"] + +T is 1 == 1 +F is 1 == 2 + +# ---- 1. a distinct type ---- +same of [type of T, "bool", "type of a comparison is bool"] +same of [type of F, "bool", "type of a false comparison is bool"] + +# ---- 2. comparisons, not, and observer predicates produce bool on EVERY path ---- +same of [1 < 2, T, "number < (immediate path)"] +same of [2 <= 1, F, "number <= (immediate path)"] +same of [3 > 2, T, ">"] +same of [3 >= 4, F, ">="] +same of [5 == 5, T, "== numbers"] +same of [5 != 5, F, "!= numbers"] +same of ["a" < "b", T, "string < (lex path)"] +same of ["b" <= "a", F, "string <= (lex path)"] +same of ["x" == "x", T, "== strings (slow path)"] +same of ["x" != "y", T, "!= strings (slow path)"] +same of [[1, 2] == [1, 2], T, "== lists (structural path)"] +same of [{"a": 1} != {"a": 1}, F, "!= dicts (structural path)"] +same of [null == null, T, "null == null"] +# A builtin's result is a heap number with refcount 1 — the operand the old +# NUM_REUSE branch overwrote with the 0/1 result in place. +same of [(len of [1, 2, 3]) < 5, T, "heap-number operand < (old NUM_REUSE path)"] +same of [(len of [1, 2, 3]) == 3, T, "heap-number operand == (old NUM_REUSE path)"] +same of [(len of [1, 2, 3]) != 3, F, "heap-number operand != (old NUM_REUSE path)"] +same of [4 > (len of [1, 2]), T, "heap-number right operand >"] +same of [not 0, T, "not 0"] +same of [not 7, F, "not 7"] +same of [not T, F, "not true"] +same of [not F, T, "not false"] +same of [not "", T, "not empty string"] +same of [not null, T, "not null"] + +obs is 100 +i is 0 +loop while i < 30: + obs is 5 + i is i + 1 +same of [type of (converged of obs), "bool", "a named observer predicate returns a bool"] +same of [type of (stable of obs), "bool", "named stable returns a bool"] + +# ---- 3. printing and strings ---- +same of [str of T, "true", "str of true"] +same of [str of F, "false", "str of false"] +same of [f"{T}/{F}", "true/false", "f-string interpolation"] +same of [str of [T, F], "[true, false]", "a list of bools prints as true/false"] + +# ---- 4. truthiness is unchanged; and/or return an operand ---- +seen is "none" +if T: + seen is "then" +same of [seen, "then", "true is truthy"] +seen is "none" +if F: + seen is "then" +else: + seen is "else" +same of [seen, "else", "false is falsy"] +same of [F or 5, 5, "false or 5 returns the operand 5"] +same of [T and 7, 7, "true and 7 returns the operand 7"] +same of [0 or F, F, "0 or false returns false"] +same of [T or 9, T, "true or 9 returns true"] + +# ---- 5. arithmetic on a bool raises, naming the type ---- +raises of [(_) => T + 1, "bool", "true + 1"] +raises of [(_) => 1 + T, "bool", "1 + true"] +raises of [(_) => T - F, "bool", "true - false"] +raises of [(_) => T * 2, "bool", "true * 2"] +raises of [(_) => 2 / T, "bool", "2 / true"] +raises of [(_) => -T, "bool", "-true"] +raises of [(_) => sum of [1 < 2, 3 < 4], "bool", "sum of a list of comparisons"] +raises of [(_) => abs of T, "abs", "abs of a bool"] +raises of [(_) => sqrt of F, "sqrt", "sqrt of a bool"] +raises of [(_) => max of [T, 3], "max", "max over a bool element"] +# Round 2 (#1637): numeric builtins that answered a bool with null/[] or read +# it as 0. The [0fb] section runs this file under EIGS_STRICT=0 as well; the +# full enumeration is tests/test_bool_fuzz.sh. +raises of [(_) => usleep of T, "usleep: does not take a bool argument", "usleep of a bool"] +# Round 4 (#1637): a bool two and three levels deep, in a cell gather reads. +raises of [(_) => gather of [[T, 3], 0], "gather: expected a number, got bool", "gather of a bool cell"] +raises of [(_) => gather of [[[T, 3]], [0]], "gather: expected a number, got bool", "gather of a bool cell, depth 3"] +raises of [(_) => nearest_in_range of [[{"px": T, "py": 0}], 0, 0, 4, 8, 8, "px", "py", "active"], "nearest_in_range: expected a number, got bool", "nearest_in_range of a bool coordinate"] +raises of [(_) => nearest_in_range_all of [[{"px": 1, "py": F}], 4, 8, 8, "px", "py", "active"], "nearest_in_range_all: expected a number, got bool", "nearest_in_range_all of a bool coordinate"] +raises of [(_) => zeros of T, "zeros", "zeros of a bool"] +raises of [(_) => fill of [T, 3], "fill", "fill with a bool count"] +raises of [(_) => sum of [T, T, 1], "bool", "sum counting bools"] +raises of [(_) => abs of T, "abs", "abs of a bool (no soft 0)"] +same of [len of (fill of [2, T]), 2, "fill with a bool VALUE is fine"] + +# ---- 6. equality between a bool and a number raises; other mixes unequal ---- +raises of [(_) => T == 1, "cannot compare bool and num with '=='", "true == 1"] +raises of [(_) => F != 0, "cannot compare bool and num with '!='", "false != 0"] +raises of [(_) => 1 == T, "cannot compare num and bool with '=='", "1 == true"] +raises of [(_) => (1 < 2) == 1, "bool", "the old (cmp) == 1 idiom"] +raises of [(_) => [T] == [1], "bool", "a bool meets a number inside a list"] +# Ordering is defined for numbers and strings only; bools are not ordered. +raises of [(_) => T < F, "cannot compare bool and bool with '<'", "true < false"] +raises of [(_) => F >= 0, "cannot compare bool and num with '>='", "false >= 0"] +same of [null == F, F, "null == false is false"] +same of ["true" == T, F, "\"true\" == true is false"] +same of [T == T, T, "true == true"] +same of [T != F, T, "true != false"] +same of [F == (3 < 2), T, "false == false"] +# Membership searches with the `==` comparison, so it raises the same way +# (#1637 item 6): an old 1/0 membership check must not flip found -> not-found. +raises of [(_) => list_contains of [[1], 1 == 1], "list_contains: cannot compare num and bool", "list_contains of a bool in a number list"] +raises of [(_) => list_contains of [[0], 1 == 2], "list_contains: cannot compare num and bool", "list_contains of false in [0]"] +raises of [(_) => list_contains of [[true], 1], "list_contains: cannot compare bool and num", "list_contains of a number in a bool list"] +raises of [(_) => list_index_of of [[1, 2], 1 == 1], "list_index_of: cannot compare num and bool", "list_index_of of a bool in a number list"] +raises of [(_) => list_contains of [[[1]], [true]], "list_contains", "list_contains: bool vs num inside a nested element"] +same of [list_contains of [[true, false], false], true, "list_contains finds a bool among bools"] +same of [list_index_of of [["x", false], false], 1, "list_index_of finds a bool past a string"] +same of [list_contains of [["true", null], true], false, "list_contains: other mixed types stay unequal"] +same of [list_contains of [[null], false], false, "list_contains: null is not false"] +same of [list_index_of of [["1"], 1], -1, "list_index_of: a string is not a number"] +# Bools stay distinct from 1/0 wherever a value is keyed by its text. +keyed is {} +keyed[str of T] is "bool" +keyed[str of 1] is "num" +same of [len of (keys of keyed), 2, "str of true and str of 1 are distinct keys"] + +# ---- keywords (item 2): `true`/`false` are literals of the same values ---- +same of [true, T, "the true literal is the value a comparison produces"] +same of [false, F, "the false literal is the value a comparison produces"] +same of [type of true, "bool", "type of true"] +same of [type of false, "bool", "type of false"] +same of [str of true, "true", "str of the true literal"] +same of [not false, true, "not false"] +same of [[true, false] == [1 < 2, 2 < 1], true, "literals inside a list compare structurally"] +raises of [(_) => true + 0, "bool", "true + 0 with the literal"] +raises of [(_) => false == 0, "bool", "false == 0 with the literal"] +# A keyword in the TOK_IS..TOK_LOCAL run stays a field name after `.`. +fields is {"true": "yes", "false": "no"} +same of [fields.true, "yes", "d.true still parses as a field"] +same of [fields.false, "no", "d.false still parses as a field"] +fields.true is "set" +same of [fields["true"], "set", "d.true is assignable as a field"] +# The self-interpreter (lib/eigen.eigs) agrees with the VM. +load_file of "lib/eigen.eigs" +same of [eigen_run of "true", true, "eigen_run: the true literal"] +same of [eigen_run of "false", false, "eigen_run: the false literal"] +same of [eigen_run of "1 < 2", true, "eigen_run: a comparison is a bool"] +same of [eigen_run of "not 0", true, "eigen_run: not is a bool"] +same of [eigen_run of "2 == 3", false, "eigen_run: == is a bool"] +raises of [(_) => eigen_run of "true == 1", "bool", "eigen_run: bool == num raises"] + +# ---- builtins and lib (item 4): predicates and two-state statuses are bools ---- +define is_bool(v, desc) as: + same of [type of v, "bool", desc] +is_bool of [has_key of [{"a": 1}, "a"], "has_key"] +is_bool of [has_key of [{"a": 1}, "b"], "has_key (absent)"] +is_bool of [contains of ["abc", "b"], "contains"] +is_bool of [starts_with of ["abc", "a"], "starts_with"] +is_bool of [ends_with of ["abc", "x"], "ends_with"] +is_bool of [list_contains of [[1, 2], 2], "list_contains"] +is_bool of [secure_equals of ["a", "a"], "secure_equals"] +is_bool of [try_parse of "x is 1", "try_parse (valid)"] +is_bool of [try_parse of "x is is", "try_parse (invalid)"] +is_bool of [try_parse of "", "try_parse (empty)"] +is_bool of [task_alive of 987654, "task_alive (unknown id)"] +is_bool of [task_send of [987654, 1], "task_send (unknown id)"] +is_bool of [task_kill of 987654, "task_kill (unknown id)"] +ch is channel of null +is_bool of [channel_closed of ch, "channel_closed"] +is_bool of [file_exists of "lib/test.eigs", "file_exists"] +is_bool of [is_dir of "lib", "is_dir"] +is_bool of [is_file of "lib/test.eigs", "is_file"] +is_bool of [seed_random of 7, "seed_random"] +scratch_file is mktemp of null +scratch is scratch_file + "_d" +is_bool of [mkdir of scratch, "mkdir"] +is_bool of [write_text of [scratch + "/a.txt", "x"], "write_text"] +is_bool of [rename of [scratch + "/a.txt", scratch + "/b.txt"], "rename"] +is_bool of [remove_file of (scratch + "/b.txt"), "remove_file"] +is_bool of [rm of (scratch + "/nothing"), "rm (missing file)"] +exec_capture of ["rm", "-rf", scratch, scratch_file] +mf is math_flags of null +is_bool of [mf.overflow, "math_flags.overflow"] +is_bool of [mf.invalid, "math_flags.invalid"] +is_bool of [mf.underflow, "math_flags.underflow"] +sb is sandbox_run of [[1, [0, 0], [], []]] +is_bool of [sb.ok, "sandbox_run ok (refused descriptor)"] +same of [sb.ok, false, "sandbox_run ok is false for a refused descriptor"] +# JSON: encode emits true/false; decode gives the bools back; round trip. +same of [json_encode of [[true, false, 1 < 2]], "[true,false,true]", "json_encode of bools"] +same of [json_encode of {"t": true}, "{\"t\":true}", "json_encode of a bool field"] +decoded is json_decode of "[true, false, 1, 0]" +same of [decoded[0], true, "json_decode true"] +same of [decoded[1], false, "json_decode false"] +same of [decoded[2], 1, "json_decode 1 stays a number"] +same of [json_decode of (json_encode of {"a": [true, {"b": false}]}), {"a": [true, {"b": false}]}, "json round trip keeps bools"] +# Round 5 (#1637): `what` of a bool is the bool (it answered 0), on a bound +# name and on an expression; the history forms already answered bools. +r5_flag is 5 > 3 +r5_flag is 2 > 3 +r5_flag is 5 > 3 +same of [what is r5_flag, true, "what is a bool binding"] +same of [what is (2 > 3), false, "what is a bool expression"] +same of [prev of r5_flag, false, "prev of a bool binding"] +same of [what is r5_flag when 2, false, "what is a bool binding when 2"] +same of [when is r5_flag, 3, "when counts a bool binding's assignments"] +same of [who is (1 < 2), "bool", "who names a bool"] +# Round 5 (#1637, owner decision): the tensor mutators raise on any +# non-number cell or row in every strict mode (v0.44.0 skipped a null row). +raises of [(_) => sgd_update of [[[1, 2], null], [[1, 1], [1, 1]], 0.1], "sgd_update: expected a number, got null", "sgd_update of a null row"] +# A descriptor's param_count: a bool is refused, the [] placeholder still +# means 0 parameters (round 4 refused it and broke test_vm_run_bytecode). +raises of [(_) => vm_run_bytecode of [1, [0, 0, 0, 40], [7], [], true], "bool", "vm_run_bytecode bool param_count"] +same of [vm_run_bytecode of [1, [0, 0, 0, 40], [7], [], []], 7, "vm_run_bytecode [] param_count placeholder"] +# Round 5 (#1637): `report` has an opcode twin. The C compiler's +# OP_REPORT_NAME and vm_run_bytecode's GET_NAME("report") + CALL must agree +# on a bool (the bytecode path raised: report was not in k_bool_policy). +r5_rep is 5 > 3 +same of [vm_run_bytecode of [1, [25,0,0, 0,1,0, 39,1,0, 40], ["report", true]], report of r5_rep, "report of a bool: bytecode path == compiled path"] +# Round 6: the sandbox's blocked stub stays ungated -- a bool argument to a +# blocked builtin still reports the sandbox refusal, not a type error. +r6_sb is sandbox_run of [[1, [25,0,0, 0,1,0, 39,1,0, 40], ["read_text", true]], 1000] +same of [r6_sb["error"]["kind"], "sandbox", "a blocked builtin given a bool reports the sandbox refusal"] +# Round 5 (#1637): json_build writes a bool value as a JSON boolean and it +# decodes back to a bool. +same of [json_build of ["ok", true, "bad", 2 > 3], "{\"ok\": true, \"bad\": false}", "json_build of bools"] +same of [json_decode of (json_build of ["ok", true]), {"ok": true}, "json_build bool round trip"] +# lib predicates return a bool on every path, including the early-out paths. +load_file of "lib/set.eigs" +load_file of "lib/validate.eigs" +load_file of "lib/sort.eigs" +load_file of "lib/list.eigs" +load_file of "lib/json.eigs" +load_file of "lib/map.eigs" +s1 is set_from of [1, 2] +is_bool of [set_has of [s1, 1], "set_has (present)"] +is_bool of [set_has of [s1, 9], "set_has (absent)"] +is_bool of [is_subset of [s1, s1], "is_subset"] +is_bool of [set_equal of [s1, s1], "set_equal"] +is_bool of [is_number of "12", "validate.is_number"] +is_bool of [is_nonempty of "", "validate.is_nonempty (empty)"] +is_bool of [in_range of [5, 1, 3], "validate.in_range"] +is_bool of [is_email of "a@b.c", "validate.is_email"] +is_bool of [is_sorted of [1, 2, 3], "sort.is_sorted"] +is_bool of [is_sorted of [], "sort.is_sorted (empty)"] +is_bool of [any of [[1, 2], (x) => x > 1], "list.any"] +is_bool of [all of [[], (x) => x > 1], "list.all (empty)"] +is_bool of [json_has of ["{\"a\": 1}", "a"], "json.json_has"] + +# ---- observer (item 6): a bool binding is observed in its own band ---- +# entropy false 0.0 / true 1.0 (compute_entropy's VAL_BOOL case); a bool is +# not numeric, so the predicates read it on the entropy channel. +define observe_flag(mode) as: + local flag is false + local i is 0 + loop while i < 30: + if mode == "flip": + flag is (i % 2) == 0 + else: + flag is i > 100 + i is i + 1 + return [trajectory of flag, (observe of flag)[1], report of flag] +steady is observe_flag of "steady" +flip is observe_flag of "flip" +same of [steady[0].observed, true, "a bool binding is observed"] +same of [steady[0].numeric, false, "a bool binding is not on the numeric channel"] +same of [steady[1], 0, "observe of a false flag: entropy 0"] +same of [flip[1], 0, "observe of a flipping flag ending false: entropy 0"] +same of [steady[2], "converged", "a constant flag converges"] +same of [flip[2], "oscillating", "a flipping flag oscillates"] +held is true +held is 1 < 2 +same of [(observe of held)[1], 1, "observe of a true flag: entropy 1"] + +test_summary of null diff --git a/tests/test_bool_fuzz.sh b/tests/test_bool_fuzz.sh new file mode 100644 index 00000000..c768e999 --- /dev/null +++ b/tests/test_bool_fuzz.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# test_bool_fuzz.sh -- #1637: a bool is not a number, and not any other type +# a builtin did not declare. `true`, then `false`, goes into every argument +# slot of every builtin and extension name `--api --json` lists (slot kinds: +# tests/bool_fuzz_gen.eigs), and into the VM operand positions (index, slice +# bounds, range, for-in, arithmetic, list/string repetition), under +# EIGS_STRICT=1 and EIGS_STRICT=0. +# +# A call that RETURNS -- in either mode -- is a violation, unless its +# name|slot is on the reviewed any-value list tests/bool_fuzz_anyvalue.txt +# (print, type of, container elements, JSON, ...; every entry carries a +# reason and must be USED: a listed slot whose calls all raise is stale). A probe program that crashes, times out or does +# not reach its end marker is broken, and broken is a FAIL. A name the binary +# was built without (an extension that is off) is counted absent, never as a +# raise; every core builtin must be present. +# +# Prints `BOOL_FUZZ: examined=N/D ...` and exits 0 iff violations=0, +# broken=0, unused_anyvalue=0 and examined == present > 0. +# usage: test_bool_fuzz.sh [BINARY] +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +BIN="$(cd "$(dirname "${1:-$HERE/../src/eigenscript}")" && pwd)/$(basename "${1:-eigenscript}")" +ANY="${BOOL_FUZZ_ANYVALUE:-$HERE/bool_fuzz_anyvalue.txt}" +WORK=$(mktemp -d "${TMPDIR:-/tmp}/eigs_bool_fuzz.XXXXXX") || exit 2 +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/gen" "$WORK/run" +# A tiny mono 16-bit WAV for the audio rows (44-byte header + 32 samples). +printf 'RIFF\x64\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00\x44\xac\x00\x00\x88\x58\x01\x00\x02\x00\x10\x00data\x40\x00\x00\x00' > "$WORK/t.wav" +head -c 64 /dev/zero >> "$WORK/t.wav" + +# A bound per program: coreutils timeout, or gtimeout (macOS), or none. +if command -v timeout >/dev/null 2>&1; then TMO="timeout 20" +elif command -v gtimeout >/dev/null 2>&1; then TMO="gtimeout 20" +else TMO="env"; echo "BOOL_FUZZ: NOTE no timeout(1); programs run unbounded"; fi +# The address-space cap keeps a gfx probe from swamping a 4 GB box. A +# sanitizer build cannot run under it: ASan reserves ~15 TB of shadow +# address space at startup, so every probe aborted ("ReserveShadowMemoryRange +# failed ... ulimit -v") and each abort cost ~1.5 s -- the round-8 ASan shard +# overran its 55-minute limit on exactly this. Under a sanitizer build the +# cap is lifted; video and audio stay on SDL's dummy drivers. +export VCAP=1500000 +if grep -qa -e __asan_init -e __tsan_init -e __msan_init "$BIN" 2>/dev/null; then + VCAP=unlimited + echo " NOTE: sanitizer build: probes run without the ulimit -v cap" +fi + +"$BIN" --api --json > "$WORK/api.json" || { echo "BOOL_FUZZ: FAIL (--api --json)"; exit 1; } +GEN_OUT=$("$BIN" "$HERE/bool_fuzz_gen.eigs" "$WORK/api.json" "$HERE/strict_shape_cases.json" \ + "$WORK/run" "$WORK/t.wav" "$WORK/gen" 2>&1) +echo "$GEN_OUT" | grep -q '^GEN: ' || { echo "BOOL_FUZZ: FAIL (generator)"; echo "$GEN_OUT"; exit 1; } +DECLARED=$(echo "$GEN_OUT" | sed -n 's/^GEN: names=\([0-9]*\).*/\1/p') +"$BIN" --api | awk '$1 == "builtin" {print $2}' > "$WORK/core.txt" +CORE=$(grep -c . "$WORK/core.txt") + +# Run every program in both modes, headless, memory-capped, under a deadline. +: > "$WORK/marks" +: > "$WORK/broken" +for p in "$WORK"/gen/p*.eigs; do + id=$(basename "$p" .eigs) + for strict in 1 0; do + out=$(cd "$WORK/run" && env -u DISPLAY -u WAYLAND_DISPLAY SDL_VIDEODRIVER=dummy \ + SDL_AUDIODRIVER=dummy EIGS_STRICT=$strict \ + bash -c 'ulimit -v $VCAP; exec $0 "$1" "$2"' "$TMO" "$BIN" "$p" \ + /dev/null) + rc=$? + # Markers may follow a builtin's own unterminated output on a line + # (`write`, the screen_* escapes), so they are matched anywhere. + if ! grep -q '@END$' <<< "$out"; then + echo "$id|$strict|rc=$rc" >> "$WORK/broken" + continue + fi + awk -v P="$id|$strict|" '{ if (match($0, /@(P:[a-z]+|[0-9]+:(V:[0-9a-z]+|R:[a-z_]+))$/)) print P substr($0, RSTART + 1) }' <<< "$out" >> "$WORK/marks" + done +done + +# Join the manifest with the marks: one verdict per (call, mode). +awk -F'|' -v ANY="$ANY" -v BROKEN="$WORK/broken" -v CORE="$CORE" -v DECLARED="$DECLARED" \ + -v CORELIST="$WORK/core.txt" ' +BEGIN { + while ((getline l < CORELIST) > 0) core[l] = 1 + while ((getline l < ANY) > 0) { + if (l ~ /^#/ || l == "") continue + split(l, f, "|") + if (f[3] == "") { printf " FAIL: any-value entry without a reason: %s|%s\n", f[1], f[2]; bad_any++ } + anyv[f[1] "|" f[2]] = 1 + } + while ((getline l < BROKEN) > 0) { split(l, f, "|"); broken[f[1]] = 1; brokenl[++nb] = l } +} +FNR == NR { # marks: prog|strict|P:type or prog|strict|k:V / k:R:kind + if ($3 ~ /^P:/) { pres[$1 "|" $2] = substr($3, 3); next } + split($3, m, ":"); mark[$1 "|" $2 "|" m[1]] = m[2] (m[3] != "" ? ":" m[3] : ""); next +} +{ # manifest: prog|k|name|slot|value|control + prog = $1; k = $2; name = $3; slot = $4; val = $5; cref = $6 + if (prog == "skip") { skipped[name] = slot; next } + if (slot ~ /^ctl:/) { controls++; next } + calls++ + if (!(prog in seenprog)) { seenprog[prog] = name; nprog++ } + for (s = 1; s >= 0; s--) { + if (prog in broken) continue + if (name != "@vm") { + t = pres[prog "|" s] + if (t == "absent") { absent[name] = 1; continue } + if (t != "builtin" && t != "extension") { printf " FAIL: %s: presence marker %s\n", name, t; badpres++; continue } + } + examined_name[name] = 1 + v = mark[prog "|" s "|" k] + probes++ + if (v == "") { printf " FAIL: %s %s=%s (EIGS_STRICT=%d): no marker\n", name, slot, val, s; nomark++; continue } + if (v ~ /^V/) { + if ((name "|" slot) in anyv) { anyok++; usedany[name "|" slot] = 1; continue } + # a bool two or three brackets deep that left the result equal to + # its control (the same call with the number) was not read + if (slot ~ /[.]d[23]/ && cref != "-" && mark[prog "|" s "|" cref] == v) { unread++; continue } + printf " VIOLATION: %s slot %s given %s returned without raising (EIGS_STRICT=%d)\n", name, slot, val, s + viol++ + } else { + raised++; kinds[substr(v, 3)]++ + } + } +} +END { + # an entry for a name this build lacks (an extension that is off) is not + # stale -- it is unexercised here; an unused entry for a present name is + for (key in anyv) if (!(key in usedany)) { + nm = key; sub(/[|].*/, "", nm) + if ((nm in absent) && !(nm in examined_name)) { anyabsent++; continue } + printf " FAIL: unused any-value entry %s\n", key; bad_any++ + } + for (i = 1; i <= nb; i++) printf " FAIL: broken probe program %s (%s)\n", brokenl[i], seenprog[substr(brokenl[i], 1, index(brokenl[i], "|") - 1)] + nex = 0; for (n in examined_name) if (n != "@vm") nex++ + nab = 0; for (n in absent) if (!(n in examined_name)) nab++ + nsk = 0; for (n in skipped) { nsk++; printf " SKIP: %s (%s)\n", n, skipped[n] } + for (n in core) if (!(n in examined_name) && !(n in skipped)) { printf " FAIL: core builtin %s was not examined\n", n; badpres++ } + kstr = ""; for (kk in kinds) kstr = kstr kk "=" kinds[kk] " " + ok = (viol == 0 && nb == 0 && nomark == 0 && badpres == 0 && bad_any == 0 && \ + nex + nab + nsk == DECLARED && nex + nsk >= CORE && nex > 0 && ("@vm" in examined_name)) + printf "BOOL_FUZZ: raise kinds: %s\n", kstr + # the runner #988 rule wants a PASS:/FAIL: marker from a test_* child + printf " %s: a bool in every builtin slot and VM operand\n", ok ? "PASS" : "FAIL" + printf "BOOL_FUZZ: examined=%d/%d absent=%d skipped=%d core=%d programs=%d calls=%d controls=%d probes=%d raised=%d anyvalue=%d unread=%d violations=%d broken=%d unused_anyvalue=%d %s\n", \ + nex, DECLARED, nab, nsk, CORE, nprog, calls, controls, probes, raised, anyok, unread, viol + nomark + badpres, nb, bad_any, ok ? "PASS" : "FAIL" + exit ok ? 0 : 1 +}' "$WORK/marks" "$WORK/gen/manifest.txt" diff --git a/tests/test_bool_lib_predicates.sh b/tests/test_bool_lib_predicates.sh new file mode 100644 index 00000000..9c58a3e7 --- /dev/null +++ b/tests/test_bool_lib_predicates.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# test_bool_lib_predicates.sh -- #1637: every lib/ predicate returns a bool on +# every path. This is measured by running the predicates, not by reading the +# source. +# +# Population: the lib functions `eigenscript --api` lists whose name is +# predicate-shaped (is_*, has_*, can_*, *_has, *_empty, *_equal, +# *s_intersect, point_in_*, polygon_is_*, any, all, ...; PRED below). +# +# Each predicate is called on a fixed battery of argument tuples drawn from +# POOL: numbers, strings, lists, points, polygons, dicts, null, bools and a +# lambda. The extra cases in tests/bool_lib_predicates_cases.txt are added. +# Every call runs in its own try: +# - a call that RAISES is not an answer and is ignored; +# - a call that RETURNS must return a `bool`. +# +# Exits 0 iff all of these hold: +# - no predicate returned a non-bool; +# - examined == declared > 0; +# - every predicate answered at least once; +# - each predicate answered both true and false. The exceptions are the +# one_sided rows, and each needs a reason. +# +# The result type is printed on a line of its own (`@T:`), so a string +# answer containing spaces or newlines is still counted as a non-bool. +# usage: test_bool_lib_predicates.sh [BINARY] [--table] +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" +BIN="${1:-$ROOT/src/eigenscript}" +case "$BIN" in --table) BIN="$ROOT/src/eigenscript" ;; esac +case "$BIN" in /*) ;; *) BIN="$(pwd)/$BIN" ;; esac +TABLE=0; for a in "$@"; do [ "$a" = --table ] && TABLE=1; done +CASES="$HERE/bool_lib_predicates_cases.txt" +WORK=$(mktemp -d "${TMPDIR:-/tmp}/eigs_bool_libpred.XXXXXX") || exit 2 +trap 'rm -rf "$WORK"' EXIT +if command -v timeout >/dev/null 2>&1; then TMO="timeout 60" +elif command -v gtimeout >/dev/null 2>&1; then TMO="gtimeout 60" +else TMO="env"; fi +# The address-space cap keeps a gfx probe from swamping a 4 GB box. A +# sanitizer build cannot run under it: ASan reserves ~15 TB of shadow +# address space at startup, so every probe aborted ("ReserveShadowMemoryRange +# failed ... ulimit -v") and each abort cost ~1.5 s -- the round-8 ASan shard +# overran its 55-minute limit on exactly this. Under a sanitizer build the +# cap is lifted; video and audio stay on SDL's dummy drivers. +export VCAP=1500000 +if grep -qa -e __asan_init -e __tsan_init -e __msan_init "$BIN" 2>/dev/null; then + VCAP=unlimited + echo " NOTE: sanitizer build: probes run without the ulimit -v cap" +fi + +PRED='^((is_|has_|can_)[a-z0-9_]*|[a-z0-9_]*_has|[a-z0-9_]*_empty|[a-z0-9_]*_equal|[a-z0-9_]*s_intersect|point_in_[a-z0-9_]*|polygon_is_[a-z0-9_]*|any|all|collinear|on_segment|in_range|eq_near|get_flag|is_one_of|utf8_validate|sm_can_send|sm_is|json_has|map_has|set_has|is_subset|is_superset|set_equal|git_worktree_clean|check_openai)$' +"$BIN" --api > "$WORK/api.txt" || { echo "BOOL_LIB_PREDICATES: FAIL (--api)"; exit 1; } +# module|name|arity +awk -v PRED="$PRED" '$1 == "lib" { + sig = $0; sub(/^lib /, "", sig) + if (!match(sig, /^[a-z0-9_]+[.][a-z0-9_]+[(]/)) next + mn = substr(sig, 1, RLENGTH - 1); split(mn, p, ".") + params = substr(sig, RLENGTH + 1); sub(/[)].*/, "", params) + k = 0; if (params ~ /[^ ]/) k = split(params, junk, ",") + if (p[2] ~ PRED || mn == "functional.complement") print p[1] "|" p[2] "|" k +}' "$WORK/api.txt" > "$WORK/fns.txt" +DECLARED=$(grep -c . "$WORK/fns.txt") + +# The battery: tuple i of arity k takes POOL[(i + 3j) mod n] for j < k, plus +# the constant tuple POOL[i] repeated k times. +POOL_FILE="$WORK/pool.txt" +cat > "$POOL_FILE" <<'EOF' +0 +1 +-1 +2.5 +10 +"" +"abc" +"a@b.co" +"http://x.y/z" +"123" +[] +[1, 2, 3] +[3, 1, 2] +[0, 0] +[1, 1] +[2, 2] +[[0, 0], [4, 0], [0, 4]] +[[0, 0], [4, 0], [4, 4], [0, 4]] +{} +{"a": 1} +null +true +false +(x) => x > 1 +EOF + +emit_call() { # name arity "a;;b;;c" -> one try block + local name=$1 k=$2 args=$3 call + if [ "$k" -ge 2 ]; then call="$name of [$(printf '%s' "$args" | sed 's/;;/, /g')]" + elif [ "$k" -eq 1 ]; then call="$name of ($args)" + else call="$name of null"; fi + printf 'try:\n __r is %s\n print of ("@T:" + (type of __r))\n if (type of __r) == "bool":\n print of ("@B:" + (str of __r))\ncatch __e:\n print of "@raised"\n' "$call" +} + +examined=0; failures=0; unmeasured=0 +: > "$WORK/table.txt" +while IFS='|' read -r mod name k; do + examined=$((examined + 1)) + skipwhy=$(awk -F'|' -v n="$name" '$1 == "skip" && $2 == n {print $3; exit}' "$CASES") + if [ -n "$skipwhy" ]; then echo "$mod.$name: skipped ($skipwhy)" >> "$WORK/table.txt"; continue; fi + prog="$WORK/p.eigs" + printf 'load_file of "%s/lib/%s.eigs"\n' "$ROOT" "$mod" > "$prog" + awk -F'|' -v q="$mod.$name" '$1 == "case" && $2 == q && $3 != "" {print $3}' "$CASES" | + sed 's/\\n/\ +/g' >> "$prog" + if [ "$k" -eq 0 ]; then emit_call "$name" 0 "" >> "$prog" + else + awk -v k="$k" '{ pool[n++] = $0 } END { + for (i = 0; i < n; i++) { + t = ""; c = "" + for (j = 0; j < k; j++) { + t = t (j ? ";;" : "") pool[(i + j * 3) % n] + c = c (j ? ";;" : "") pool[i] + } + print t; print c + } }' "$POOL_FILE" > "$WORK/tuples.txt" + while IFS= read -r t; do emit_call "$name" "$k" "$t" >> "$prog"; done < "$WORK/tuples.txt" + fi + awk -F'|' -v q="$mod.$name" '$1 == "case" && $2 == q {print $4}' "$CASES" > "$WORK/extra.txt" + while IFS= read -r t; do emit_call "$name" "$k" "$t" >> "$prog"; done < "$WORK/extra.txt" + printf 'print of "@END"\n' >> "$prog" + out=$(cd "$WORK" && env -u DISPLAY -u WAYLAND_DISPLAY SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + EIGS_STRICT=1 bash -c 'ulimit -v $VCAP; exec $0 "$1" "$2"' "$TMO" "$BIN" "$prog" /dev/null) + if ! grep -q '^@END$' <<< "$out"; then + echo " FAIL: $mod.$name: probe program did not complete"; failures=$((failures + 1)); continue + fi + nt=$(grep -c '^@B:true$' <<< "$out"); nf=$(grep -c '^@B:false$' <<< "$out") + nr=$(grep -c '^@raised$' <<< "$out") + types=$(grep '^@T:' <<< "$out" | sort | uniq -c | awk '{printf "%s%s=%s", (NR > 1 ? "," : ""), substr($2, 4), $1}') + echo "$mod.$name: types={$types} true=$nt false=$nf raised=$nr" >> "$WORK/table.txt" + nonbool=$(grep '^@T:' <<< "$out" | grep -vc '^@T:bool$') + if [ "$nonbool" -gt 0 ]; then + echo " FAIL: $mod.$name: returned a non-bool ($types)"; failures=$((failures + 1)) + elif [ -z "$types" ]; then + echo " FAIL: $mod.$name: every battery call raised; nothing measured"; unmeasured=$((unmeasured + 1)) + elif [ "$nt" -eq 0 ] || [ "$nf" -eq 0 ]; then + if ! awk -F'|' -v q="$mod.$name" '$1 == "one_sided" && $2 == q && $3 != "" {f = 1} END {exit !f}' "$CASES"; then + [ "$nt" -eq 0 ] && only=false || only=true + echo " FAIL: $mod.$name: answered only $only; add a case for the other answer" + failures=$((failures + 1)) + fi + fi +done < "$WORK/fns.txt" +[ "$TABLE" = 1 ] && cat "$WORK/table.txt" +ok=0 +[ "$examined" -eq "$DECLARED" ] && [ "$DECLARED" -gt 0 ] && [ "$failures" -eq 0 ] && [ "$unmeasured" -eq 0 ] && ok=1 +echo "BOOL_LIB_PREDICATES: examined=$examined/$DECLARED nonbool=$failures unmeasured=$unmeasured $([ "$ok" = 1 ] && echo PASS || echo FAIL)" +# The runner's #988 rule wants a PASS:/FAIL: marker from a test_* child. +[ "$ok" = 1 ] && echo " PASS: every lib predicate answers with a bool" || echo " FAIL: lib predicates (see above)" +[ "$ok" = 1 ] diff --git a/tests/test_bool_meta_diff.sh b/tests/test_bool_meta_diff.sh new file mode 100644 index 00000000..660c89c1 --- /dev/null +++ b/tests/test_bool_meta_diff.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# test_bool_meta_diff.sh -- #1637: the self-hosted interpreter (lib/eigen.eigs +# `eigen_run`) agrees with the VM on every bool-producing form: comparisons, +# `not`, the literals, the predicate builtins and the observer predicates (bare +# and named). It must also agree on the raises (bool vs number, a bool index). +# Each snippet in tests/bool_meta_snippets.txt runs as its own VM program and +# through `eigen_run`; both print `[type of v, v]` or `raised `, and the +# two lines must be identical. Exit 0 iff every snippet agreed and ran, and +# snippets == declared > 0. +# usage: test_bool_meta_diff.sh [BINARY] +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" +BIN="${1:-$ROOT/src/eigenscript}" +case "$BIN" in /*) ;; *) BIN="$(pwd)/$BIN" ;; esac +SNIPS="$HERE/bool_meta_snippets.txt" +WORK=$(mktemp -d "${TMPDIR:-/tmp}/eigs_bool_meta.XXXXXX") || exit 2 +trap 'rm -rf "$WORK"' EXIT +if command -v timeout >/dev/null 2>&1; then TMO="timeout 30" +elif command -v gtimeout >/dev/null 2>&1; then TMO="gtimeout 30" +else TMO="env"; fi + +wrap() { # body lines on stdin -> a try block printing [type of __v, __v] + printf 'try:\n' + sed 's/^/ /' + printf ' print of [type of __v, __v]\ncatch __e:\n print of ("raised " + __e.kind)\n' +} +run() { (cd "$ROOT" && $TMO "$BIN" "$1" /dev/null); } + +declared=$(grep -v '^#' "$SNIPS" | grep -c .) +ran=0; bad=0 +while IFS='|' read -r setup expr; do + case "$setup" in \#*) continue ;; esac + [ -z "$setup$expr" ] && continue + code=$(printf '%s' "$setup" | sed 's/\\n/\ +/g') + [ -n "$code" ] && code="$code +" + code="$code$expr" + { [ -n "$setup" ] && printf '%s\n' "$code" | sed '$d'; printf '__v is (%s)\n' "$expr"; } | wrap > "$WORK/vm.eigs" + lit=$(printf '%s' "$code" | awk 'BEGIN { ORS = "" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); if (NR > 1) print "\\n"; print }') + { printf 'load_file of "lib/eigen.eigs"\n'; printf '__v is eigen_run of "%s"\n' "$lit" | wrap; } > "$WORK/meta.eigs" + ov=$(run "$WORK/vm.eigs"); rv=$? + om=$(run "$WORK/meta.eigs"); rm_=$? + ran=$((ran + 1)) + if [ "$rv" != 0 ] || [ "$rm_" != 0 ] || [ -z "$ov" ] || [ "$ov" != "$om" ]; then + echo " FAIL: '$expr' (setup '$setup'): VM='$ov' (rc $rv) eigen_run='$om' (rc $rm_)" + bad=$((bad + 1)) + fi +done < "$SNIPS" +ok=0 +[ "$ran" -eq "$declared" ] && [ "$declared" -gt 0 ] && [ "$bad" -eq 0 ] && ok=1 +echo "BOOL_META_DIFF: snippets=$ran/$declared disagree=$bad $([ "$ok" = 1 ] && echo PASS || echo FAIL)" +# The runner's #988 rule wants a PASS:/FAIL: marker from a test_* child. +[ "$ok" = 1 ] && echo " PASS: eigen_run agrees with the VM on every snippet" || echo " FAIL: eigen_run vs VM (see above)" +[ "$ok" = 1 ] diff --git a/tests/test_buf_resample.eigs b/tests/test_buf_resample.eigs index 01d0610f..cc431ff7 100644 --- a/tests/test_buf_resample.eigs +++ b/tests/test_buf_resample.eigs @@ -11,7 +11,7 @@ fails is 0 define check(name, ok) as: - if ok == 0: + if not ok: print of f"FAIL: {name}" fails is fails + 1 diff --git a/tests/test_buf_vectorized.eigs b/tests/test_buf_vectorized.eigs index 3f72a34b..249869ab 100644 --- a/tests/test_buf_vectorized.eigs +++ b/tests/test_buf_vectorized.eigs @@ -13,7 +13,7 @@ fails is 0 define check(name, ok) as: - if ok == 0: + if not ok: print of f"FAIL: {name}" fails is fails + 1 diff --git a/tests/test_buffer_nonfinite_read.out b/tests/test_buffer_nonfinite_read.out index 75a2bcff..28577cf2 100644 --- a/tests/test_buffer_nonfinite_read.out +++ b/tests/test_buffer_nonfinite_read.out @@ -1,8 +1,8 @@ -0 +false 1 2 -0 -0 +false +false 1e+308 1e+308 1e+308 @@ -10,7 +10,7 @@ 0 0 0 -1 +true 7 5 -1 +true diff --git a/tests/test_buffer_store_type.eigs b/tests/test_buffer_store_type.eigs index d844e8a1..78630a01 100644 --- a/tests/test_buffer_store_type.eigs +++ b/tests/test_buffer_store_type.eigs @@ -2,7 +2,7 @@ # numeric context. Before: the store was silently dropped (the old element # stayed) at rc 0, and the same value died one line later in arithmetic. # Every store shape -- the opcode path (b[i] is v), buf_set, buf_from_list -- -# and every non-number kind; booleans are numbers and stay accepted. +# and every non-number kind, bools included (#1637: a bool is not a number). load_file of "lib/test.eigs" define store_fails(v) as: @@ -25,12 +25,11 @@ assert_true of [(index_of of [r[0], "cannot store dict in a buffer"]) >= 0, "b[i r is store_fails of null assert_true of [(index_of of [r[0], "cannot store null in a buffer"]) >= 0, "b[i] is null raises"] +# #1637: a comparison is a bool, not a number, so a buffer refuses it too. +r is store_fails of (1 == 1) +assert_true of [(index_of of [r[0], "cannot store bool in a buffer"]) >= 0, "b[i] is a comparison raises (#1637)"] b is buffer of 3 -b[0] is (1 == 1) -b[1] is (1 == 2) b[2] is 2.5 -assert_eq of [b[0], 1, "a true comparison stores 1 (booleans are numbers)"] -assert_eq of [b[1], 0, "a false comparison stores 0"] assert_eq of [b[2], 2.5, "a number stores as itself"] # the hot-loop shape: the JIT's inline buffer write bails to its helper on a diff --git a/tests/test_builtin_contracts.eigs b/tests/test_builtin_contracts.eigs index 3c934990..3d79025f 100644 --- a/tests/test_builtin_contracts.eigs +++ b/tests/test_builtin_contracts.eigs @@ -19,14 +19,14 @@ assert_eq of [index_of of [[1, 2, 3], 2], -1, "index_of list haystack is a miss" assert_eq of [index_of of [123, 2], -1, "index_of number haystack is a miss"] assert_eq of [index_of of ["abc", 5], -1, "index_of non-string needle is a miss"] assert_eq of [index_of of ["abc", "c"], 2, "index_of still finds real needles"] -assert_eq of [contains of [[1, 2, 3], 2], 0, "contains list haystack is 0"] -assert_eq of [contains of ["abc", 5], 0, "contains non-string needle is 0"] -assert_eq of [contains of ["abc", "b"], 1, "contains still matches"] -assert_eq of [starts_with of [[1, 2, 3], 1], 0, "starts_with non-strings is 0"] -assert_eq of [starts_with of ["abc", "ab"], 1, "starts_with still matches"] -assert_eq of [ends_with of [999, 9], 0, "ends_with non-strings is 0"] -assert_eq of [ends_with of ["abc", "bc"], 1, "ends_with still matches"] -assert_eq of [ends_with of ["abc", ""], 1, "empty suffix still matches a real string"] +assert_eq of [contains of [[1, 2, 3], 2], false, "contains list haystack is 0"] +assert_eq of [contains of ["abc", 5], false, "contains non-string needle is 0"] +assert_eq of [contains of ["abc", "b"], true, "contains still matches"] +assert_eq of [starts_with of [[1, 2, 3], 1], false, "starts_with non-strings is 0"] +assert_eq of [starts_with of ["abc", "ab"], true, "starts_with still matches"] +assert_eq of [ends_with of [999, 9], false, "ends_with non-strings is 0"] +assert_eq of [ends_with of ["abc", "bc"], true, "ends_with still matches"] +assert_eq of [ends_with of ["abc", ""], true, "empty suffix still matches a real string"] # ---- #312: builtin accessors honor negative indices like [] ---- xs is [10, 20, 30] diff --git a/tests/test_builtin_errors.eigs b/tests/test_builtin_errors.eigs index fcd1f6c5..3c54938a 100644 --- a/tests/test_builtin_errors.eigs +++ b/tests/test_builtin_errors.eigs @@ -22,7 +22,7 @@ catch e: c1 is 1 m1 is e assert of [c1 == 1, "BE01 set_observer_thresholds wrong arity caught"] -assert of [(contains of [m1.message, "set_observer_thresholds requires"]) == 1, "BE01 message"] +assert of [(contains of [m1.message, "set_observer_thresholds requires"]), "BE01 message"] # ---- set_observer_thresholds: non-positive threshold ---- c2 is 0 @@ -33,7 +33,7 @@ catch e: c2 is 1 m2 is e assert of [c2 == 1, "BE02 non-positive threshold caught"] -assert of [(contains of [m2.message, "must be positive"]) == 1, "BE02 message"] +assert of [(contains of [m2.message, "must be positive"]), "BE02 message"] # ---- set_observer_thresholds: dh_zero >= dh_small ---- c3 is 0 @@ -44,7 +44,7 @@ catch e: c3 is 1 m3 is e assert of [c3 == 1, "BE03 dh_zero>=dh_small caught"] -assert of [(contains of [m3.message, "dh_zero must be less than dh_small"]) == 1, "BE03 message"] +assert of [(contains of [m3.message, "dh_zero must be less than dh_small"]), "BE03 message"] # ---- load_file: non-string path ---- c4 is 0 @@ -55,7 +55,7 @@ catch e: c4 is 1 m4 is e assert of [c4 == 1, "BE04 load_file non-string caught"] -assert of [(contains of [m4.message, "load_file requires a string"]) == 1, "BE04 message"] +assert of [(contains of [m4.message, "load_file requires a string"]), "BE04 message"] # ---- fill: wrong argument shape ---- c5 is 0 @@ -66,7 +66,7 @@ catch e: c5 is 1 m5 is e assert of [c5 == 1, "BE05 fill wrong shape caught"] -assert of [(contains of [m5.message, "fill requires"]) == 1, "BE05 message"] +assert of [(contains of [m5.message, "fill requires"]), "BE05 message"] # ---- send: not a channel ---- c6 is 0 @@ -77,7 +77,7 @@ catch e: c6 is 1 m6 is e assert of [c6 == 1, "BE06 send invalid channel caught"] -assert of [(contains of [m6.message, "invalid channel"]) == 1, "BE06 message"] +assert of [(contains of [m6.message, "invalid channel"]), "BE06 message"] # ---- try_recv: not a channel ---- c7 is 0 @@ -88,7 +88,7 @@ catch e: c7 is 1 m7 is e assert of [c7 == 1, "BE07 try_recv invalid channel caught"] -assert of [(contains of [m7.message, "invalid channel"]) == 1, "BE07 message"] +assert of [(contains of [m7.message, "invalid channel"]), "BE07 message"] # ---- recv_timeout: not a channel ---- c8 is 0 @@ -99,7 +99,7 @@ catch e: c8 is 1 m8 is e assert of [c8 == 1, "BE08 recv_timeout invalid channel caught"] -assert of [(contains of [m8.message, "invalid channel"]) == 1, "BE08 message"] +assert of [(contains of [m8.message, "invalid channel"]), "BE08 message"] # ---- dispatch: wrong arity ---- c9 is 0 @@ -110,7 +110,7 @@ catch e: c9 is 1 m9 is e assert of [c9 == 1, "BE09 dispatch wrong arity caught"] -assert of [(contains of [m9.message, "dispatch requires"]) == 1, "BE09 message"] +assert of [(contains of [m9.message, "dispatch requires"]), "BE09 message"] # ---- dispatch: table is not a list (variable arg to avoid list spread) ---- c10 is 0 @@ -122,7 +122,7 @@ catch e: c10 is 1 m10 is e assert of [c10 == 1, "BE10 dispatch table-not-list caught"] -assert of [(contains of [m10.message, "table must be a list"]) == 1, "BE10 message"] +assert of [(contains of [m10.message, "table must be a list"]), "BE10 message"] # ---- dispatch: slot is not a function ---- c11 is 0 @@ -135,7 +135,7 @@ catch e: c11 is 1 m11 is e assert of [c11 == 1, "BE11 dispatch slot-not-fn caught"] -assert of [(contains of [m11.message, "is not a function"]) == 1, "BE11 message"] +assert of [(contains of [m11.message, "is not a function"]), "BE11 message"] # ---- dispatch: key must be a number (builtin path; used to reinterpret ---- # ---- the value's union bits as a double — #353 parity fix) ---- @@ -148,7 +148,7 @@ catch e: c11b is 1 m11b is e assert of [c11b == 1, "BE11b dispatch non-number key caught"] -assert of [(contains of [m11b.message, "key must be a number"]) == 1, "BE11b message"] +assert of [(contains of [m11b.message, "key must be a number"]), "BE11b message"] # ---- dispatch: fractional key raises on the builtin path too (#353) ---- c11c is 0 @@ -160,7 +160,7 @@ catch e: c11c is 1 m11c is e assert of [c11c == 1, "BE11c dispatch fractional key caught"] -assert of [(contains of [m11c.message, "integer"]) == 1, "BE11c message"] +assert of [(contains of [m11c.message, "integer"]), "BE11c message"] # ---- sort: list of records raises instead of silently no-oping (#368) ---- c11d is 0 @@ -172,7 +172,7 @@ catch e: c11d is 1 m11d is e assert of [c11d == 1, "BE11d sort on records caught"] -assert of [(contains of [m11d.message, "use sort_by"]) == 1, "BE11d message"] +assert of [(contains of [m11d.message, "use sort_by"]), "BE11d message"] # ---- sort: mixed numbers and strings raises ---- c11e is 0 @@ -184,7 +184,7 @@ catch e: c11e is 1 m11e is e assert of [c11e == 1, "BE11e sort on mixed types caught"] -assert of [(contains of [m11e.message, "element 1 is"]) == 1, "BE11e message"] +assert of [(contains of [m11e.message, "element 1 is"]), "BE11e message"] # ---- sort: strings now actually sort (was a silent no-op, #368) ---- strs is ["cherry", "apple", "banana"] @@ -212,7 +212,7 @@ catch e: c_hex1 is 1 m_hex1 is e assert of [c_hex1 == 1, "hex negative raises"] -assert of [(contains of [m_hex1.message, "non-negative integer"]) == 1, "hex negative message"] +assert of [(contains of [m_hex1.message, "non-negative integer"]), "hex negative message"] c_hex2 is 0 try: hex of 1.5 @@ -237,8 +237,8 @@ catch e: c_chr1 is 1 m_chr1 is e assert of [c_chr1 == 1, "chr above 255 raises"] -assert of [(contains of [m_chr1.message, "1..255"]) == 1, "chr range message"] -assert of [(contains of [m_chr1.message, "utf8_encode"]) == 1, "chr message points at utf8_encode"] +assert of [(contains of [m_chr1.message, "1..255"]), "chr range message"] +assert of [(contains of [m_chr1.message, "utf8_encode"]), "chr message points at utf8_encode"] c_chr2 is 0 try: chr of (0 - 1) @@ -259,7 +259,7 @@ catch e: c_chr4 is 1 m_chr4 is e assert of [c_chr4 == 1, "chr of 0 raises (NUL unrepresentable)"] -assert of [(contains of [m_chr4.message, "NUL"]) == 1, "chr NUL message"] +assert of [(contains of [m_chr4.message, "NUL"]), "chr NUL message"] c_chr5 is 0 try: chr of "A" @@ -276,7 +276,7 @@ catch e: c12 is 1 m12 is e assert of [c12 == 1, "BE12 nearest_in_range wrong shape caught"] -assert of [(contains of [m12.message, "nearest_in_range requires"]) == 1, "BE12 message"] +assert of [(contains of [m12.message, "nearest_in_range requires"]), "BE12 message"] # ---- nearest_in_range_all: wrong argument shape ---- c13 is 0 @@ -287,6 +287,6 @@ catch e: c13 is 1 m13 is e assert of [c13 == 1, "BE13 nearest_in_range_all wrong shape caught"] -assert of [(contains of [m13.message, "nearest_in_range_all requires"]) == 1, "BE13 message"] +assert of [(contains of [m13.message, "nearest_in_range_all requires"]), "BE13 message"] print of "All builtin_errors tests passed" diff --git a/tests/test_call_semantics.eigs b/tests/test_call_semantics.eigs index 2b59f792..f32954b3 100644 --- a/tests/test_call_semantics.eigs +++ b/tests/test_call_semantics.eigs @@ -25,8 +25,8 @@ catch oa974_e: oa974_err is oa974_e assert of [oa974_same == 1, "#974 same-module over-arity raises"] assert of [oa974_err.kind == "value", "#974 over-arity error kind is value"] -assert of [(contains of [oa974_err.message, "passes 3"]) == 1, "#974 message reports passed count"] -assert of [(contains of [oa974_err.message, "takes 2"]) == 1, "#974 message reports callee arity"] +assert of [(contains of [oa974_err.message, "passes 3"]), "#974 message reports passed count"] +assert of [(contains of [oa974_err.message, "takes 2"]), "#974 message reports callee arity"] # #974 cross-module: W022 is same-file-only, so the runtime raise is the # only guard here (clamp is defined in lib/math.eigs and takes 3). @@ -62,13 +62,13 @@ catch oa989_e: oa989_msg is oa989_e.message assert of [oa989_hit == 1, "#989 sort_by key callback over-arity raises"] assert of [oa989_kind == "value", "#989 sort_by callback error kind is value"] -assert of [(contains of [oa989_msg, "passes 3"]) == 1, "#989 callback message reports passed count"] -assert of [(contains of [oa989_msg, "takes 2"]) == 1, "#989 callback message reports callee arity"] +assert of [(contains of [oa989_msg, "passes 3"]), "#989 callback message reports passed count"] +assert of [(contains of [oa989_msg, "takes 2"]), "#989 callback message reports callee arity"] # The key function's own error must survive: sort_by used to inspect the # returned value and report "must return a number", burying the raise that # actually happened under a symptom of it. -assert of [(contains of [oa989_msg, "must return a number"]) == 0, "#989 real error is not masked by sort_by's return-type check"] +assert of [(not (contains of [oa989_msg, "must return a number"])), "#989 real error is not masked by sort_by's return-type check"] # Carve-out preserved through the callback path: a 1-param key still # re-collects the whole element, and a matched-arity key still sorts. diff --git a/tests/test_chan_dict_xthread.eigs b/tests/test_chan_dict_xthread.eigs index f72698f4..2abc1e99 100644 --- a/tests/test_chan_dict_xthread.eigs +++ b/tests/test_chan_dict_xthread.eigs @@ -38,7 +38,7 @@ check of ["len", len of msg, 4] check of ["id", msg.id, 7] # interned-key lookup must still resolve check of ["total", msg.total, 42] check of ["name", msg.name, "abc"] # owned string value survives -check of ["has_key", has_key of [msg, "total"], 1] +check of ["has_key", has_key of [msg, "total"], true] check of ["nested", msg.inner.k, 9] # nested dict keys rehomed too if passed == checks: diff --git a/tests/test_channel_nb.eigs b/tests/test_channel_nb.eigs index ef41c8f1..3cb0c9d8 100644 --- a/tests/test_channel_nb.eigs +++ b/tests/test_channel_nb.eigs @@ -51,7 +51,7 @@ v is recv_timeout of [ch2, 20] elapsed is monotonic_ms of null - t0 check of ["6a", v, null] # Allow a generous floor — system load can stretch this. -check of ["6b", elapsed >= 15, 1] +check of ["6b", elapsed >= 15, true] # 7. Negative ms degenerates to try_recv. v is recv_timeout of [ch2, -1] @@ -66,7 +66,7 @@ t0 is monotonic_ms of null v is recv_timeout of [ch2, 10000] elapsed is monotonic_ms of null - t0 check of ["8a", v, 9] -check of ["8b", elapsed < 100, 1] +check of ["8b", elapsed < 100, true] # 9. Value arriving from another thread before the deadline. ch3 is channel of null @@ -78,7 +78,7 @@ t0 is monotonic_ms of null v is recv_timeout of [ch3, 2000] elapsed is monotonic_ms of null - t0 check of ["9a", v, 77] -check of ["9b", elapsed < 500, 1] +check of ["9b", elapsed < 500, true] # 10. Close while waiting unblocks recv_timeout (returns null, no buffered val). ch4 is channel of null @@ -90,7 +90,7 @@ t0 is monotonic_ms of null v is recv_timeout of [ch4, 2000] elapsed is monotonic_ms of null - t0 check of ["10a", v, null] -check of ["10b", elapsed < 500, 1] +check of ["10b", elapsed < 500, true] # ---- Error paths ---- @@ -128,7 +128,7 @@ t0 is monotonic_ms of null v is recv_timeout of [ch_big, 1e30] elapsed is monotonic_ms of null - t0 check of ["13a", v, 11] -check of ["13b", elapsed < 100, 1] +check of ["13b", elapsed < 100, true] # 14. Same with an even larger value that saturates the runtime's double # range — still must not overflow the long cast or the deadline. @@ -138,7 +138,7 @@ t0 is monotonic_ms of null v is recv_timeout of [ch_huge, 1e308] elapsed is monotonic_ms of null - t0 check of ["14a", v, 22] -check of ["14b", elapsed < 100, 1] +check of ["14b", elapsed < 100, true] # 15. Empty channel + huge ms must use the clamped ceiling. We bound # the elapsed time to prove the deadline didn't degenerate to "wait @@ -152,7 +152,7 @@ t0 is monotonic_ms of null v is recv_timeout of [ch_giant, 1e30] elapsed is monotonic_ms of null - t0 check of ["15a", v, null] -check of ["15b", elapsed < 500, 1] +check of ["15b", elapsed < 500, true] if passed == checks: print of "All tests passed" diff --git a/tests/test_chunk_verify_stack.eigs b/tests/test_chunk_verify_stack.eigs index 4a0d4b18..170f8f45 100644 --- a/tests/test_chunk_verify_stack.eigs +++ b/tests/test_chunk_verify_stack.eigs @@ -36,13 +36,13 @@ LOOP_ENV_END is 51 # [OP_ADD, OP_RETURN]: passes passes 1-3 (known opcode, no operands, ends in # RETURN) and pops two operands off an empty stack. add_repro is sandbox_run of [[ABI, [ADD, RETURN], []], 1000] -assert_eq of [add_repro["ok"], 0, "bare ADD is refused"] +assert_eq of [add_repro["ok"], false, "bare ADD is refused"] assert_eq of [add_repro["error"]["message"], "invalid chunk descriptor", "bare ADD refused by the verifier, not at runtime"] # One operand is still one short of two. half_fed is sandbox_run of [[ABI, [CONST,0,0, ADD, RETURN], [1]], 1000] -assert_eq of [half_fed["ok"], 0, "ADD with one operand is refused"] +assert_eq of [half_fed["ok"], false, "ADD with one operand is refused"] # ---- 1b. The "guarded" opcodes are refused too -------------------------- # DIV and MOD have no fast path — both operands come off vm_pop(), which @@ -56,9 +56,9 @@ assert_eq of [half_fed["ok"], 0, "ADD with one operand is refused"] DIV is 7 LIST is 42 div_under is sandbox_run of [[ABI, [DIV, RETURN], []], 1000] -assert_eq of [div_under["ok"], 0, "under-fed DIV (a guarded pop) is refused"] +assert_eq of [div_under["ok"], false, "under-fed DIV (a guarded pop) is refused"] list_over is sandbox_run of [[ABI, [CONST,0,0, LIST,9,0, RETURN], [1]], 1000] -assert_eq of [list_over["ok"], 0, "LIST claiming more than the stack holds is refused"] +assert_eq of [list_over["ok"], false, "LIST claiming more than the stack holds is refused"] div_ok is sandbox_run of [[ABI, [CONST,0,0, CONST,1,0, DIV, RETURN], [84, 2]], 1000] assert_eq of [div_ok["result"], 42, "a fed DIV still divides"] @@ -66,7 +66,7 @@ assert_eq of [div_ok["result"], 42, "a fed DIV still divides"] # The pass must reject underflow, not arithmetic. Fed correctly, the same # opcode runs — a verifier that rejected everything would pass section 1. fed is sandbox_run of [[ABI, [CONST,0,0, CONST,1,0, ADD, RETURN], [2, 3]], 1000] -assert_eq of [fed["ok"], 1, "a balanced ADD still runs"] +assert_eq of [fed["ok"], true, "a balanced ADD still runs"] assert_eq of [fed["result"], 5, "a balanced ADD still computes"] # Height bookkeeping across the stack shufflers. @@ -83,7 +83,7 @@ JUMP_IF_FALSE is 31 branch_ok is sandbox_run of [[ABI, [CONST,0,0, JUMP_IF_FALSE,6,0, CONST,1,0, JUMP,3,0, CONST,2,0, RETURN], [1, 7, 9]], 1000] -assert_eq of [branch_ok["ok"], 1, "a balanced if/else verifies"] +assert_eq of [branch_ok["ok"], true, "a balanced if/else verifies"] assert_eq of [branch_ok["result"], 7, "the taken arm is the one that ran"] # A join where the two edges DISAGREE on depth is refused. Here the taken edge @@ -93,7 +93,7 @@ assert_eq of [branch_ok["result"], 7, "the taken arm is the one that ran"] # balanced shape above. Compiler output already does; this is the contract an # external bytecode producer is held to. lopsided is sandbox_run of [[ABI, [CONST,0,0, JUMP_IF_FALSE,3,0, CONST,1,0, RETURN], [1, 7]], 1000] -assert_eq of [lopsided["ok"], 0, "a join at mismatched depth is refused"] +assert_eq of [lopsided["ok"], false, "a join at mismatched depth is refused"] # ---- 3. LOOP_ENV_END with no matching FRESH ------------------------------ # A distinct mechanism from the stack pass: the env chain, not the operand @@ -103,7 +103,7 @@ assert_eq of [lopsided["ok"], 0, "a join at mismatched depth is refused"] # settled statically (an error unwinding into a catch does not restore # frame->env), so the VM refuses it at the instruction. env_under is sandbox_run of [[ABI, [LOOP_ENV_END, LOOP_ENV_END, LOOP_ENV_END, RETURN], []], 1000] -assert_eq of [env_under["ok"], 0, "unmatched LOOP_ENV_END is refused"] +assert_eq of [env_under["ok"], false, "unmatched LOOP_ENV_END is refused"] assert_true of [(index_of of [env_under["error"]["message"], "loop-env underflow"]) >= 0, "unmatched LOOP_ENV_END names itself"] @@ -143,7 +143,7 @@ for op in range of 256: append of [code, RETURN] local r is sandbox_run of [[ABI, code, [1]], 200] sweep_runs is sweep_runs + 1 - if r["ok"] == 1: + if r["ok"]: sweep_ran is sweep_ran + 1 else: sweep_refused is sweep_refused + 1 diff --git a/tests/test_coercion.eigs b/tests/test_coercion.eigs index f1f262f4..a1f8f9e0 100644 --- a/tests/test_coercion.eigs +++ b/tests/test_coercion.eigs @@ -28,10 +28,10 @@ assert of [f"n={42}" == "n=42", "f-string mixes types"] assert of [f"v={1 / 2}" == "v=0.5", "f-string number round-trips"] # Ordering: same-type works -assert of [(2 < 5) == 1, "num < num"] -assert of [(5 > 2) == 1, "num > num"] -assert of [("a" < "b") == 1, "str < str"] -assert of [("b" >= "a") == 1, "str >= str"] +assert of [(2 < 5), "num < num"] +assert of [(5 > 2), "num > num"] +assert of [("a" < "b"), "str < str"] +assert of [("b" >= "a"), "str >= str"] # Ordering: mixed types raise (not a silent false) ord_caught is 0 @@ -49,9 +49,9 @@ catch e: assert of [ord2_caught == 1, "mixed ordering raises (other direction)"] # Equality: cross-type is safely not-equal, never an error -assert of [("3" == 3) == 0, "str vs num not equal"] -assert of [(3 == 3) == 1, "num == num"] -assert of [([1] == 1) == 0, "list vs num not equal"] -assert of [("3" != 3) == 1, "str != num true"] +assert of [(not ("3" == 3)), "str vs num not equal"] +assert of [(3 == 3), "num == num"] +assert of [(not ([1] == 1)), "list vs num not equal"] +assert of [("3" != 3), "str != num true"] print of "All tests passed" diff --git a/tests/test_complex.eigs b/tests/test_complex.eigs index d049b0dc..8d4c653e 100644 --- a/tests/test_complex.eigs +++ b/tests/test_complex.eigs @@ -99,11 +99,11 @@ assert_near of [r[1], 4, 1e-12, "from_polar im"] # ---- Comparison ---- print of "--- Comparison ---" -assert_eq of [complex.eq_near of [[3, 4], [3, 4], 1e-9], 1, "eq_near identical"] -assert_eq of [complex.eq_near of [[3, 4], [3.0000000001, 4], 1e-6], 1, "eq_near within tol"] -assert_eq of [complex.eq_near of [[3, 4], [3, 4.1], 1e-6], 0, "eq_near differs in im"] +assert_eq of [complex.eq_near of [[3, 4], [3, 4], 1e-9], true, "eq_near identical"] +assert_eq of [complex.eq_near of [[3, 4], [3.0000000001, 4], 1e-6], true, "eq_near within tol"] +assert_eq of [complex.eq_near of [[3, 4], [3, 4.1], 1e-6], false, "eq_near differs in im"] # Two axes, not one: 3+4i and 5+0i have the SAME modulus and are not equal. -assert_eq of [complex.eq_near of [[3, 4], [5, 0], 1e-6], 0, "eq_near is not a modulus comparison"] +assert_eq of [complex.eq_near of [[3, 4], [5, 0], 1e-6], false, "eq_near is not a modulus comparison"] # ---- Polynomials ---- print of "--- Polynomials ---" diff --git a/tests/test_concurrent.eigs b/tests/test_concurrent.eigs index 755e9649..f292c2b0 100644 --- a/tests/test_concurrent.eigs +++ b/tests/test_concurrent.eigs @@ -77,7 +77,7 @@ loop while done == 0: item is recv of ch2 if item != null: append of [collected, item] - if (channel_closed of ch2) == 1 and item == null: + if (channel_closed of ch2) and item == null: done is 1 thread_join of h_prod @@ -95,7 +95,7 @@ ch3 is channel of null c1 is channel_closed of ch3 close_channel of ch3 c2 is channel_closed of ch3 -if c1 == 0 and c2 == 1: +if (not c1) and c2: pass is pass + 1 print of "PASS: T5 close_channel/channel_closed" else: diff --git a/tests/test_contract.eigs b/tests/test_contract.eigs index c247aca9..da5e6664 100644 --- a/tests/test_contract.eigs +++ b/tests/test_contract.eigs @@ -30,8 +30,8 @@ define to_list(x) as: # returns a non-num after the seed print of "=== Contracts (lib/contract.eigs) ===" # ---- require / ensure: plain predicates ---- -check of ["require truthy passes", require of [1, "n/a"], 1] -check of ["ensure truthy passes", ensure of [5 > 2, "n/a"], 1] +check of ["require truthy passes", require of [1, "n/a"], true] +check of ["ensure truthy passes", ensure of [5 > 2, "n/a"], true] caught is 0 try: @@ -49,10 +49,10 @@ check of ["ensure falsy throws", caught, 1] # ---- expect_converging ---- r is expect_converging of [1.0, halve, 60, "halving must converge"] -check of ["expect_converging halve -> settled", r < 0.01, 1] +check of ["expect_converging halve -> settled", r < 0.01, true] r is expect_converging of [2.0, newton2, 60, "newton must converge"] -check of ["expect_converging newton -> sqrt(2)", (r > 1.41) and (r < 1.42), 1] +check of ["expect_converging newton -> sqrt(2)", (r > 1.41) and (r < 1.42), true] caught is 0 try: @@ -63,7 +63,7 @@ check of ["expect_converging on divergent throws", caught, 1] # ---- expect_monotone (value channel, #294) ---- r is expect_monotone of [64.0, halve, 60, "halving is monotone"] -check of ["expect_monotone halve passes", r < 1.0, 1] +check of ["expect_monotone halve passes", r < 1.0, true] caught is 0 try: diff --git a/tests/test_convergence_oracle.eigs b/tests/test_convergence_oracle.eigs index 152d4621..e3b77c68 100644 --- a/tests/test_convergence_oracle.eigs +++ b/tests/test_convergence_oracle.eigs @@ -194,7 +194,10 @@ positives is 0 for id in range of 27: i is id + 1 r is run_case of i - got is r[1] + # #1637: converged is a bool; TRUTH holds 0/1, so convert explicitly. + got is 0 + if r[1]: + got is 1 vgot is 0 if r[3] == "converged": vgot is 1 diff --git a/tests/test_corpus.eigs b/tests/test_corpus.eigs index 122451bf..784cc96d 100644 --- a/tests/test_corpus.eigs +++ b/tests/test_corpus.eigs @@ -79,7 +79,7 @@ check of ["histogram sorted descending", first_count >= last_count] write_text of ["/tmp/eigs_corpus_f.eigs", "x is 5\ny is f\"a{x}b\"\n"] rf is build_corpus of [["/tmp/eigs_corpus_f.eigs"], 8, "/tmp/eigs_corpus_f.bin", "/tmp/eigs_corpus_fv.json", "/tmp/eigs_corpus_fh.json"] fh_text is read_text of "/tmp/eigs_corpus_fh.json" -check of ["f-string corpus names carry no reserved name", (contains of [fh_text, "#"]) == 0] +check of ["f-string corpus names carry no reserved name", (not (contains of [fh_text, "#"]))] check of ["f-string corpus counts the conversion as str", contains of [fh_text, "\"str\""]] # --- Bad args return null --- diff --git a/tests/test_coverage_gaps.eigs b/tests/test_coverage_gaps.eigs index b0687631..21437d82 100644 --- a/tests/test_coverage_gaps.eigs +++ b/tests/test_coverage_gaps.eigs @@ -12,8 +12,8 @@ assert of [parts[2] == "c", "CG2 split element"] single is split of ["no-delim-here", ","] assert of [(len of single) == 1, "CG3 split no-delim"] -assert of [(starts_with of ["hello world", "hello"]) == 1, "CG4 starts_with hit"] -assert of [(starts_with of ["hello world", "world"]) == 0, "CG5 starts_with miss"] +assert of [(starts_with of ["hello world", "hello"]), "CG4 starts_with hit"] +assert of [(not (starts_with of ["hello world", "world"])), "CG5 starts_with miss"] rep is str_replace of ["foo bar foo", "foo", "baz"] assert of [rep == "baz bar baz", "CG6 str_replace all"] @@ -35,7 +35,7 @@ assert of [(len of hex) == 16, "CG10 random_hex length"] # /private/tmp, so getcwd may report either spelling — accept both. original is getcwd of null ok is chdir of "/tmp" -assert of [ok == 1, "CG11 chdir succeeds"] +assert of [ok, "CG11 chdir succeeds"] now is getcwd of null assert of [now == "/tmp" or now == "/private/tmp", "CG12 chdir moved us"] chdir of original @@ -111,14 +111,14 @@ assert of [(len of toks) > 0, "CG36 tokenize_with_names non-empty"] # --- stream_open/write/close: write 3 doubles to a temp file --- sfile is mktemp of null ok is stream_open of [sfile, 3] -assert of [ok == 1, "CG37 stream_open"] +assert of [ok, "CG37 stream_open"] w1 is stream_write of 1.5 w2 is stream_write of 2.5 w3 is stream_write of 3.5 -assert of [w1 == 1, "CG38 stream_write 1"] -assert of [w3 == 1, "CG39 stream_write 3"] +assert of [w1, "CG38 stream_write 1"] +assert of [w3, "CG39 stream_write 3"] closed is stream_close of null -assert of [closed == 1, "CG40 stream_close"] +assert of [closed, "CG40 stream_close"] rm of sfile # --- numerical_grad_rows + sgd_update_rows --- @@ -207,7 +207,7 @@ assert of [p2d[0][0] < 1.0, "CG60 sgd_update 2D moved"] t_orig is [[1.5, 2.5], [3.5, 4.5]] tfile is mktemp of null save_ok is tensor_save of [t_orig, tfile] -assert of [save_ok == 1, "CG61 tensor_save ok"] +assert of [save_ok, "CG61 tensor_save ok"] t_back is tensor_load of tfile assert of [t_back[0][0] == 1.5, "CG62 tensor_load round-trip 0,0"] assert of [t_back[1][1] == 4.5, "CG63 tensor_load round-trip 1,1"] @@ -284,8 +284,8 @@ assert of [big_dict["25"] == 50, "CG91 dict lookup after growth"] # --- dict_remove --- remove_test is {"keep": 1, "drop": 2, "also_keep": 3} dict_remove of [remove_test, "drop"] -assert of [(has_key of [remove_test, "drop"]) == 0, "CG92 dict_remove worked"] -assert of [(has_key of [remove_test, "keep"]) == 1, "CG93 dict_remove left others"] +assert of [(not (has_key of [remove_test, "drop"])), "CG92 dict_remove worked"] +assert of [(has_key of [remove_test, "keep"]), "CG93 dict_remove left others"] # --- arena_track_string + arena_alloc growth --- # When the arena is active, make_str calls arena_track_string. diff --git a/tests/test_coverage_v2.eigs b/tests/test_coverage_v2.eigs index 4569f030..278a5400 100644 --- a/tests/test_coverage_v2.eigs +++ b/tests/test_coverage_v2.eigs @@ -110,10 +110,10 @@ assert of [(len of jd_arr) == 3, "CV2-24 json_decode array length"] assert of [jd_arr[0] == 1, "CV2-25 json_decode array elem"] jd_bool_t is json_decode of "true" -assert of [jd_bool_t == 1, "CV2-26 json_decode true"] +assert of [jd_bool_t, "CV2-26 json_decode true"] jd_bool_f is json_decode of "false" -assert of [jd_bool_f == 0, "CV2-27 json_decode false"] +assert of [(not jd_bool_f), "CV2-27 json_decode false"] jd_null is json_decode of "null" assert of [(type of jd_null) == "none", "CV2-28 json_decode null"] @@ -136,18 +136,18 @@ assert of [(type of jd_esc5) == "str", "CV2-33 json_decode slash escape"] # --- json_build: num, null, list values --- jb1 is json_build of ["count", 42] -assert of [contains of [jb1, "\"count\": 42"] == 1, "CV2-34 json_build num value"] +assert of [contains of [jb1, "\"count\": 42"], "CV2-34 json_build num value"] jb2 is json_build of ["val", null] -assert of [contains of [jb2, "null"] == 1, "CV2-35 json_build null value"] +assert of [contains of [jb2, "null"], "CV2-35 json_build null value"] jb3 is json_build of ["big", 1234567890.123] -assert of [contains of [jb3, "1234567890"] == 1, "CV2-36 json_build large num"] +assert of [contains of [jb3, "1234567890"], "CV2-36 json_build large num"] # --- json_build with json_raw --- raw is json_raw of "[1,2,3]" jb4 is json_build of ["items", raw] -assert of [contains of [jb4, "[1,2,3]"] == 1, "CV2-37 json_build json_raw"] +assert of [contains of [jb4, "[1,2,3]"], "CV2-37 json_build json_raw"] # --- type of json_raw --- assert of [(type of raw) == "json_raw", "CV2-38 type of json_raw"] @@ -247,7 +247,7 @@ expect_error of [() => store_delete of null, "CV2-68 store_delete null arg raise # store_delete of a missing key is not an error: returns 0 sd_nk is store_delete of [tmp_store, "coll", 123] -assert of [sd_nk == 0, "CV2-69 store_delete numeric key"] +assert of [(not sd_nk), "CV2-69 store_delete numeric key"] expect_error of [() => store_query of null, "CV2-70 store_query null arg raises"] expect_error of [() => store_count of null, "CV2-71 store_count null arg raises"] @@ -274,14 +274,14 @@ assert of [cnt > 0, "CV2-79 store_count positive"] # --- Store: update --- upd is store_update of [tmp_store, "items", key, {"name": "updated", "value": 99}] -assert of [upd == 1, "CV2-80 store_update success"] +assert of [upd, "CV2-80 store_update success"] got2 is store_get of [tmp_store, "items", key] assert of [got2.name == "updated", "CV2-81 store_update verified"] # --- Store: delete --- del is store_delete of [tmp_store, "items", key] -assert of [del == 1, "CV2-82 store_delete success"] +assert of [del, "CV2-82 store_delete success"] got3 is store_get of [tmp_store, "items", key] assert of [(type of got3) == "none", "CV2-83 store_delete verified"] @@ -293,7 +293,7 @@ assert of [(type of cols) == "list", "CV2-84 store_collections type"] # --- Store: drop collection --- store_put of [tmp_store, "todrop", {"x": 1}] drp is store_drop of [tmp_store, "todrop"] -assert of [drp == 1, "CV2-85 store_drop success"] +assert of [drp, "CV2-85 store_drop success"] # --- Store: open with bad arg (raises) --- expect_error of [() => store_open of 42, "CV2-86 store_open bad arg raises"] @@ -328,7 +328,7 @@ expect_error of [() => json_decode of 42, "CV2-89 json_decode non-string raises" # --- json_encode with %.15g path (non-integer float) --- je_float is json_encode of 3.141592653589793 -assert of [contains of [je_float, "3.14159"] == 1, "CV2-90 json_encode float precision"] +assert of [contains of [je_float, "3.14159"], "CV2-90 json_encode float precision"] # --- Concurrency error paths (strict model: bad args raise; caught here) --- tj_caught is 0 @@ -445,7 +445,7 @@ assert of [1 == 1, "CV2-106 dot access on non-dict handled"] # BUILTINS.C: json_encode with float needing %.15g precision # ================================================================ je_pi is json_encode of 3.141592653589793 -assert of [contains of [je_pi, "3.14159"] == 1, "CV2-107 json_encode %.15g"] +assert of [contains of [je_pi, "3.14159"], "CV2-107 json_encode %.15g"] # ================================================================ # BUILTINS.C: type of dict and fn diff --git a/tests/test_data.eigs b/tests/test_data.eigs index 4c11c2af..00f4a83c 100644 --- a/tests/test_data.eigs +++ b/tests/test_data.eigs @@ -87,6 +87,10 @@ sorted_desc is data.df_sort_by of [df, "score", 0] assert_eq of [sorted_desc[0]["name"], "Alice", "df_sort_by desc first"] assert_eq of [sorted_desc[2]["name"], "Bob", "df_sort_by desc last"] +# #1637: the direction flag takes a bool +assert_eq of [(data.df_sort_by of [df, "age", true])[0]["name"], "Bob", "df_sort_by asc with true"] +assert_eq of [(data.df_sort_by of [df, "score", false])[0]["name"], "Alice", "df_sort_by desc with false"] + # df_map define double_score(x) as: return x * 2 @@ -104,11 +108,11 @@ assert_eq of [with_col[1]["name_len"], 3, "df_add_column Bob len"] # df_rename renamed is data.df_rename of [df, "name", "person"] assert_eq of [renamed[0]["person"], "Alice", "df_rename new key"] -assert_eq of [has_key of [renamed[0], "name"], 0, "df_rename old key gone"] +assert_eq of [has_key of [renamed[0], "name"], false, "df_rename old key gone"] # df_drop dropped is data.df_drop of [df, ["age"]] -assert_eq of [has_key of [dropped[0], "age"], 0, "df_drop removes column"] +assert_eq of [has_key of [dropped[0], "age"], false, "df_drop removes column"] assert_eq of [dropped[0]["name"], "Alice", "df_drop keeps other columns"] # df_sum, df_mean, df_min, df_max diff --git a/tests/test_db.eigs b/tests/test_db.eigs index 254e9819..1fa95d5e 100644 --- a/tests/test_db.eigs +++ b/tests/test_db.eigs @@ -15,9 +15,9 @@ # "no_database" path or the "connected"/"error" path. conn is db_connect of null has_status is (contains of [conn, "status"]) -assert of [has_status == 1, "DB01 db_connect returns JSON containing 'status'"] +assert of [has_status, "DB01 db_connect returns JSON containing 'status'"] -live is (contains of [conn, "connected"]) == 1 +live is (contains of [conn, "connected"]) # ---- #888: a query without a connection RAISES. It used to return "", # which is also what a successful query over an empty table returns. @@ -32,7 +32,7 @@ if not live: m02 is e.message k02 is e.kind assert of [c02 == 1, "DB02 db_query_value without a connection raises"] - assert of [(contains of [m02, "not connected"]) == 1, "DB02 message names the cause"] + assert of [(contains of [m02, "not connected"]), "DB02 message names the cause"] assert of [k02 == "io", "DB02 kind is io"] c04 is 0 @@ -43,7 +43,7 @@ if not live: c04 is 1 m04 is e.message assert of [c04 == 1, "DB04 db_query_json without a connection raises"] - assert of [(contains of [m04, "not connected"]) == 1, "DB04 message names the cause"] + assert of [(contains of [m04, "not connected"]), "DB04 message names the cause"] c08 is 0 try: @@ -87,7 +87,7 @@ if live: v_param is db_query_value of ["SELECT $1::text", "safe-param"] assert of [v_param == "safe-param", "DB06 db_query_value supports params"] j_param is db_query_json of ["SELECT $1::text AS value", "safe-json"] - assert of [(contains of [j_param, "safe-json"]) == 1, "DB07 db_query_json supports params"] + assert of [(contains of [j_param, "safe-json"]), "DB07 db_query_json supports params"] # ---- Live-DB table round-trip. Covers db_execute's COMMAND_OK path, # parameterized INSERT, multi-row db_query_json assembly, db_query_value @@ -122,7 +122,7 @@ if live: k15 is e.kind assert of [c15 == 1, "DB15 malformed SQL raises"] assert of [k15 == "io", "DB15 kind is io"] - assert of [(contains of [m15, "syntax error"]) == 1, "DB15 message carries libpq's text"] + assert of [(contains of [m15, "syntax error"]), "DB15 message carries libpq's text"] # ---- #356: param validation raises instead of silently proceeding ---- q17 is ["SELECT $1", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] @@ -134,7 +134,7 @@ if live: c16a is 1 m16a is e assert of [c16a == 1, "DB16 17-param query raises"] - assert of [(contains of [m16a.message, "too many parameters"]) == 1, "DB16 message names the cap"] + assert of [(contains of [m16a.message, "too many parameters"]), "DB16 message names the cap"] qbad is ["SELECT $1::text", [1, 2]] c17a is 0 @@ -145,7 +145,7 @@ if live: c17a is 1 m17a is e assert of [c17a == 1, "DB17 list param raises"] - assert of [(contains of [m17a.message, "not a string or number"]) == 1, "DB17 message names the type rule"] + assert of [(contains of [m17a.message, "not a string, number or bool"]), "DB17 message names the type rule"] # ---- #888: a failed query is distinguishable from an empty result ---- empty is db_query_json of "SELECT id FROM eigs_ci_smoke WHERE id = 999" @@ -159,7 +159,7 @@ if live: c19 is 1 m19 is e.message assert of [c19 == 1, "DB19 a missing table raises rather than returning []"] - assert of [(contains of [m19, "does not exist"]) == 1, "DB19 message names the missing relation"] + assert of [(contains of [m19, "does not exist"]), "DB19 message names the missing relation"] c20 is 0 try: @@ -174,8 +174,8 @@ if live: # 1. false is FALSY. It used to arrive as "f" — a non-empty string, and # every non-empty string is truthy, so `if row.flag:` was inverted. - assert of [d.yes == 1, "DB21 SQL true decodes to 1"] - assert of [d.no == 0, "DB21 SQL false decodes to 0"] + assert of [d.yes == true, "DB21 SQL true decodes to true (#1637)"] + assert of [d.no == false, "DB21 SQL false decodes to false (#1637)"] took is 0 if d.no: took is 1 @@ -191,8 +191,8 @@ if live: assert of [d.empty == "", "DB22 an empty string stays an empty string"] assert of [(type of d.empty) == "str", "DB22 an empty string is still a str"] # The claim in its plainest form: the two used to be one value. - assert of [(d.ntext == d.empty) == 0, "DB22 a NULL text column is NOT the empty string"] - assert of [(d.ntext == null) == 1, "DB22 a NULL column compares equal to null"] + assert of [(not (d.ntext == d.empty)), "DB22 a NULL text column is NOT the empty string"] + assert of [(d.ntext == null), "DB22 a NULL column compares equal to null"] # 3. Numbers are numbers — ordering and arithmetic, not text. assert of [(type of d.n) == "num", "DB23 an int column decodes to num"] @@ -201,7 +201,7 @@ if live: assert of [ord_rows[0].a < ord_rows[0].b, "DB23 numbers order numerically, not lexicographically"] # db_query_value applies the SAME mapping (one classifier, no drift). - assert of [(db_query_value of "SELECT false") == 0, "DB24 db_query_value types booleans"] + assert of [(db_query_value of "SELECT false") == false, "DB24 db_query_value types booleans"] assert of [(type of (db_query_value of "SELECT null::int")) == "none", "DB24 db_query_value returns null for SQL NULL"] assert of [(type of (db_query_value of "SELECT ''")) == "str", "DB24 db_query_value keeps '' a str"] assert of [(db_query_value of "SELECT 7::float8") == 7, "DB24 db_query_value types floats"] @@ -227,12 +227,28 @@ if live: c26 is 1 m26 is e.message assert of [c26 == 1, "DB26 a bigint past 2^53 raises instead of rounding"] - assert of [(contains of [m26, "big"]) == 1, "DB26 message names the column"] - assert of [(contains of [m26, "::text"]) == 1, "DB26 message names the fix"] + assert of [(contains of [m26, "big"]), "DB26 message names the column"] + assert of [(contains of [m26, "::text"]), "DB26 message names the fix"] # ...and the boundary itself is fine. ok53 is (json_decode of (db_query_json of "SELECT 9007199254740992::bigint AS big"))[0] assert of [ok53.big == 9007199254740992, "DB26 2^53 itself is representable"] + # ---- #1637: a bool parameter binds as an SQL boolean, so a bool read + # from a boolean column can be written back ---- + db_execute of "DROP TABLE IF EXISTS eigs_ci_bool" + db_execute of "CREATE TABLE eigs_ci_bool (id INT, ok BOOLEAN)" + db_execute of ["INSERT INTO eigs_ci_bool VALUES ($1, $2)", 1, true] + db_execute of ["INSERT INTO eigs_ci_bool VALUES ($1, $2)", 2, 1 > 2] + brows is json_decode of (db_query_json of "SELECT id, ok FROM eigs_ci_bool ORDER BY id") + assert of [brows[0].ok == true, "DB27 true param stored as SQL true"] + assert of [brows[1].ok == false, "DB27 false param stored as SQL false"] + db_execute of ["INSERT INTO eigs_ci_bool VALUES ($1, $2)", 3, brows[0].ok] + back is json_decode of (db_query_json of ["SELECT ok FROM eigs_ci_bool WHERE id = $1", 3]) + assert of [back[0].ok == true, "DB27 a bool read back from the db writes back"] + nmatch is db_query_value of ["SELECT COUNT(*) FROM eigs_ci_bool WHERE ok = $1", false] + assert of [(str of nmatch) == "1", "DB27 a bool parameter compares with a boolean column"] + db_execute of "DROP TABLE eigs_ci_bool" + db_execute of "DROP TABLE eigs_ci_smoke" else: print of "live-DB checks skipped (no connection)" diff --git a/tests/test_db_params.c b/tests/test_db_params.c new file mode 100644 index 00000000..301f7ebe --- /dev/null +++ b/tests/test_db_params.c @@ -0,0 +1,47 @@ +/* #1637: db parameter binding, without a PostgreSQL server. Includes + * src/ext_db.c so the static db_build_query is the function under test, and + * links the server-db variant's other objects. A bool parameter must bind as + * SQL boolean text with the boolean type OID; strings and numbers keep the + * server-inferred type (OID 0); an unsupported type still raises. */ +#include "../src/ext_db.c" +#include "eigs_embed.h" +#include +#include + +static int passed, failed; +static void check(int ok, const char *name) { + if (ok) passed++; else failed++; + printf("%s: %s\n", ok ? "PASS" : "FAIL", name); +} + +int main(void) { + EigsState *st = eigs_open(); + if (!st) return 2; + Value *arg = make_list(5); + list_append_owned(arg, make_str("update t set a = $1, b = $2, c = $3, d = $4")); + list_append(arg, make_bool(1)); + list_append_owned(arg, make_num(3)); + list_append_owned(arg, make_str("s")); + list_append(arg, make_bool(0)); + const char *sql = NULL; const char *params[DB_MAX_PARAMS]; + char numbuf[DB_MAX_PARAMS][64]; unsigned int types[DB_MAX_PARAMS]; + int n = -1; + int ok = db_build_query(arg, &sql, &n, params, numbuf, types); + check(ok && n == 4, "four parameters built"); + check(ok && strcmp(params[0], "true") == 0 && types[0] == 16, "true binds as SQL boolean 'true' (OID 16)"); + check(ok && strcmp(params[1], "3") == 0 && types[1] == 0, "a number keeps the inferred type"); + check(ok && strcmp(params[2], "s") == 0 && types[2] == 0, "a string keeps the inferred type"); + check(ok && strcmp(params[3], "false") == 0 && types[3] == 16, "false binds as SQL boolean 'false' (OID 16)"); + check(DB_PARAM_OID_BOOL == DB_OID_BOOL, "the parameter OID is the column classifier's boolean OID"); + val_decref(arg); + Value *bad = make_list(2); + list_append_owned(bad, make_str("select $1")); + list_append_owned(bad, make_dict(1)); + check(!db_build_query(bad, &sql, &n, params, numbuf, types) && eigs_has_error(), + "a dict parameter still raises"); + eigs_clear_error(); + val_decref(bad); + eigs_close(st); + printf("db params: %d passed, %d failed (7 declared)\n", passed, failed); + return failed || passed != 7; +} diff --git a/tests/test_dict.eigs b/tests/test_dict.eigs index a16ee3fd..1a2fd9a6 100644 --- a/tests/test_dict.eigs +++ b/tests/test_dict.eigs @@ -28,8 +28,8 @@ v_list is values of d assert_eq of [v_list[0], "eigen", "values first"] # has_key -assert_eq of [has_key of [d, "name"], 1, "has_key true"] -assert_eq of [has_key of [d, "missing"], 0, "has_key false"] +assert_eq of [has_key of [d, "name"], true, "has_key true"] +assert_eq of [has_key of [d, "missing"], false, "has_key false"] # dict_set (mutates) dict_set of [d, "lang", "C"] @@ -42,7 +42,7 @@ assert_eq of [d.version, 7, "dict_set overwrite"] # dict_remove dict_remove of [d, "lang"] -assert_eq of [has_key of [d, "lang"], 0, "dict_remove"] +assert_eq of [has_key of [d, "lang"], false, "dict_remove"] assert_eq of [len of d, 2, "dict_remove decreases len"] # Empty dict diff --git a/tests/test_doc_examples.py b/tests/test_doc_examples.py index 917db73d..44f0e503 100644 --- a/tests/test_doc_examples.py +++ b/tests/test_doc_examples.py @@ -326,7 +326,7 @@ def is_asan_build(): # file fences paired fragment nocheck value-comments "README.md": ( 8, 3, 5, 0, 0), "docs/llms.txt": ( 9, 6, 2, 1, 0), - "docs/SPEC.md": ( 83, 79, 1, 3, 0), + "docs/SPEC.md": ( 85, 81, 1, 3, 0), # #1637: +2 paired (bool raises, deep == short-circuit) "docs/COMPARISON.md": ( 19, 18, 1, 0, 0), "docs/CONCURRENCY.md": ( 7, 7, 0, 0, 0), "docs/STDLIB.md": ( 15, 7, 8, 0, 0), diff --git a/tests/test_embed_observer.c b/tests/test_embed_observer.c index 062aa671..d706510b 100644 --- a/tests/test_embed_observer.c +++ b/tests/test_embed_observer.c @@ -79,8 +79,9 @@ static void direct(void) { chunk_emit_u16(c, (uint16_t)name, 1); chunk_emit(c, OP_RETURN, 1); Value *r = vm_execute(c, env); - printf("assembled improving=%g\n", eigs_value_as_num(r)); - check(r && !eigs_has_error() && eigs_value_as_num(r) == 1, + printf("assembled improving=%d\n", eigs_value_as_bool(r)); + check(r && !eigs_has_error() && eigs_value_type(r) == EIGS_TYPE_BOOL && + eigs_value_as_bool(r) == 1, "assembled writes and predicate match native improving=1"); eigs_value_release(r); chunk_free(c); @@ -89,6 +90,7 @@ static void direct(void) { #ifndef EIGS_OBS_BASELINE_ONLY static void eval_num(const char *source, double want, const char *name); +static void eval_bool(const char *source, int want, const char *name); static void *arm_from_worker(void *arg) { EigsState *st = arg; if (!eigs_thread_attach(st)) return NULL; @@ -181,6 +183,13 @@ static void eval_num(const char *source, double want, const char *name) { eigs_value_as_num(r) == want, name); eigs_value_release(r); } +/* #1637: a predicate answers a bool. */ +static void eval_bool(const char *source, int want, const char *name) { + EigsValue *r = eigs_eval_string(source); + check(r && !eigs_has_error() && eigs_value_type(r) == EIGS_TYPE_BOOL && + eigs_value_as_bool(r) == want, name); + eigs_value_release(r); +} static void gap(const char *source, const char *name) { EigsValue *r = eigs_eval_string(source); const char *msg = eigs_last_error_message(); @@ -277,12 +286,12 @@ static void eval_contract(void) { EigsState *st = eigs_open(); eval_ok(series, "default: first unit records assignments"); check(g_obs_needed, "default: read-free unit is observed"); - eval_num("improving of x", 1, "default: later unit reads correct history"); + eval_bool("improving of x", 1, "default: later unit reads correct history"); eigs_close(st); st = eigs_open(); eigs_set_eval_observer_isolated(1); - eval_num("z is 8\nz is 4\nz is 2\nz is 1\nz is 0.5\nimproving of z", 1, + eval_bool("z is 8\nz is 4\nz is 2\nz is 1\nz is 0.5\nimproving of z", 1, "opt-in: observer-reading first unit is observed"); eval_ok(series, "opt-in: read-free unit executes"); printf("embed obs-gate: %s\n", g_obs_needed ? "observed" : "unobserved"); @@ -302,7 +311,7 @@ static void eval_contract(void) { " local y is 16\n y is 8\n y is 4\n" " y is 2\n y is 1\n return improving of y\n", "retained function: compile reader in first unit"); - eval_num("local_reader of 0", 1, + eval_bool("local_reader of 0", 1, "retained function: later read-free call site preserves recording"); check(g_obs_needed, "retained function: conservative observed verdict"); eigs_close(st); @@ -327,13 +336,13 @@ static void eval_contract(void) { eigs_set_eval_observer_isolated(1); eval_ok(series, "FORCE: first isolated unit records history"); check(g_obs_needed, "FORCE: read-free unit is observed"); - eval_num("improving of x", 1, "FORCE: cross-unit read has complete history"); + eval_bool("improving of x", 1, "FORCE: cross-unit read has complete history"); eigs_close(st); unsetenv("EIGS_OBS_FORCE"); st = eigs_open(); eval_ok(series, "fresh state: opt-in does not leak between states"); - eval_num("improving of x", 1, "fresh state: complete history still available"); + eval_bool("improving of x", 1, "fresh state: complete history still available"); eigs_close(st); } #endif diff --git a/tests/test_entropy_query_time.eigs b/tests/test_entropy_query_time.eigs index a6daed05..885f9c06 100644 --- a/tests/test_entropy_query_time.eigs +++ b/tests/test_entropy_query_time.eigs @@ -13,7 +13,7 @@ dict_set of [d, "k", 999999] h1 is where is d e is {"k": 999999} he is where is e -assert of [(h0 == h1) == 0, "QT01 dict_set moves where"] +assert of [(not (h0 == h1)), "QT01 dict_set moves where"] assert of [h1 == he, "QT02 identical contents => identical entropy"] l is [1.0, 1.0, 1.0] @@ -27,7 +27,7 @@ hv0 is where is v v[0] is 900.0 w is [900.0, 1.0, 1.0] assert of [(where is v) == (where is w), "QT04 indexed store is visible"] -assert of [((where is v) == hv0) == 0, "QT05 ...and moved from the snapshot"] +assert of [(not ((where is v) == hv0)), "QT05 ...and moved from the snapshot"] # ---- dH is the assignment trajectory: mutation does NOT move it ---- t is {"k": 1} @@ -55,7 +55,7 @@ o is {"k": 1} obs0 is observe of o dict_set of [o, "k", 999999] obs1 is observe of o -assert of [(obs0[1] == obs1[1]) == 0, "QT08 observe entropy element refreshes"] +assert of [(not (obs0[1] == obs1[1])), "QT08 observe entropy element refreshes"] assert of [obs0[2] == obs1[2], "QT09 observe dH element stays recorded"] # ---- trajectory snapshot carries query-time entropy ---- @@ -72,6 +72,6 @@ n is 5 loop while not converged: n is n * 0.5 assert of [(n < 0.001), "QT11 converged loop still terminates"] -assert of [converged == 1, "QT12 converged still reads true after the loop"] +assert of [converged, "QT12 converged still reads true after the loop"] print of "All tests passed" diff --git a/tests/test_equality.eigs b/tests/test_equality.eigs index 37f7fcec..3db473ab 100644 --- a/tests/test_equality.eigs +++ b/tests/test_equality.eigs @@ -3,22 +3,22 @@ # never equal; functions/builtins by identity. assert of [[1, 2, 3] == [1, 2, 3], "list structural eq"] -assert of [([1, 2] == [1, 2, 3]) == 0, "list different length"] +assert of [(not ([1, 2] == [1, 2, 3])), "list different length"] assert of [[[1], [2, 3]] == [[1], [2, 3]], "nested list eq"] -assert of [([[1], [2]] == [[1], [9]]) == 0, "nested list diff"] +assert of [(not ([[1], [2]] == [[1], [9]])), "nested list diff"] -assert of [({"a": 1, "b": 2} == {"b": 2, "a": 1}) == 1, "dict order-independent"] -assert of [({"a": 1} == {"a": 2}) == 0, "dict different value"] -assert of [({"a": 1} == {"a": 1, "b": 2}) == 0, "dict different size"] +assert of [({"a": 1, "b": 2} == {"b": 2, "a": 1}), "dict order-independent"] +assert of [(not ({"a": 1} == {"a": 2})), "dict different value"] +assert of [(not ({"a": 1} == {"a": 1, "b": 2})), "dict different size"] -assert of [(["x", "y"] == ["x", "y"]) == 1, "list of strings eq"] -assert of [(3 == 3.0) == 1, "number eq"] -assert of [("hi" == "hi") == 1, "string eq"] -assert of [([1] == 1) == 0, "mixed type never equal"] -assert of [("1" == 1) == 0, "string vs number never equal"] +assert of [(["x", "y"] == ["x", "y"]), "list of strings eq"] +assert of [(3 == 3.0), "number eq"] +assert of [("hi" == "hi"), "string eq"] +assert of [(not ([1] == 1)), "mixed type never equal"] +assert of [(not ("1" == 1)), "string vs number never equal"] -assert of [([1, 2] != [1, 3]) == 1, "list inequality true"] -assert of [([1, 2] != [1, 2]) == 0, "list inequality false"] +assert of [([1, 2] != [1, 3]), "list inequality true"] +assert of [(not ([1, 2] != [1, 2])), "list inequality false"] # match/case relies on the same structural equality m is "no" diff --git a/tests/test_error_extra.eigs b/tests/test_error_extra.eigs index 0f4c478e..2e4687e5 100644 --- a/tests/test_error_extra.eigs +++ b/tests/test_error_extra.eigs @@ -14,7 +14,7 @@ catch e: caught1 is 1 msg1 is e assert of [caught1 == 1, "EE01 try/catch caught undefined variable"] -assert of [(contains of [msg1.message, "undefined variable"]) == 1, "EE01 message mentions 'undefined variable'"] +assert of [(contains of [msg1.message, "undefined variable"]), "EE01 message mentions 'undefined variable'"] # ---- try/catch around an index-out-of-range ---- caught2 is 0 @@ -26,7 +26,7 @@ catch e: caught2 is 1 msg2 is e assert of [caught2 == 1, "EE02 try/catch caught out-of-range index"] -assert of [(contains of [msg2.message, "out of range"]) == 1, "EE02 message mentions 'out of range'"] +assert of [(contains of [msg2.message, "out of range"]), "EE02 message mentions 'out of range'"] # ---- try/catch around indexing a number ---- caught3 is 0 @@ -38,7 +38,7 @@ catch e: caught3 is 1 msg3 is e assert of [caught3 == 1, "EE03 try/catch caught index-on-num"] -assert of [(contains of [msg3.message, "cannot index"]) == 1, "EE03 message mentions 'cannot index'"] +assert of [(contains of [msg3.message, "cannot index"]), "EE03 message mentions 'cannot index'"] # ---- try/catch around calling a non-function ---- caught4 is 0 @@ -90,7 +90,7 @@ catch e: caught9 is 1 msg9 is e assert of [caught9 == 1, "EE09 throw captured by try/catch"] -assert of [(contains of [msg9, "boom-custom-EE09"]) == 1, "EE09 thrown message preserved"] +assert of [(contains of [msg9, "boom-custom-EE09"]), "EE09 thrown message preserved"] # ---- Nested try/catch: inner swallows, outer doesn't see it ---- outer_caught is 0 @@ -149,7 +149,7 @@ catch e: caught13 is 1 msg13 is e assert of [caught13 == 1, "EE13 caught non-integer list index (read)"] -assert of [(contains of [msg13.message, "index must be an integer"]) == 1, "EE13 message mentions integer index"] +assert of [(contains of [msg13.message, "index must be an integer"]), "EE13 message mentions integer index"] # ---- list index out of range (assignment) ---- caught14 is 0 @@ -161,7 +161,7 @@ catch e: caught14 is 1 msg14 is e assert of [caught14 == 1, "EE14 caught out-of-range list index (assignment)"] -assert of [(contains of [msg14.message, "out of range"]) == 1, "EE14 message mentions 'out of range'"] +assert of [(contains of [msg14.message, "out of range"]), "EE14 message mentions 'out of range'"] # ---- non-integer index (assignment) ---- caught15 is 0 @@ -173,7 +173,7 @@ catch e: caught15 is 1 msg15 is e assert of [caught15 == 1, "EE15 caught non-integer list index (assignment)"] -assert of [(contains of [msg15.message, "index must be an integer"]) == 1, "EE15 message mentions integer index"] +assert of [(contains of [msg15.message, "index must be an integer"]), "EE15 message mentions integer index"] # ---- set a field on a non-dict ---- caught16 is 0 @@ -185,7 +185,7 @@ catch e: caught16 is 1 msg16 is e assert of [caught16 == 1, "EE16 caught field-set on a number"] -assert of [(contains of [msg16.message, "cannot set field"]) == 1, "EE16 message mentions 'cannot set field'"] +assert of [(contains of [msg16.message, "cannot set field"]), "EE16 message mentions 'cannot set field'"] # ---- index a non-indexable for assignment ---- caught17 is 0 @@ -197,7 +197,7 @@ catch e: caught17 is 1 msg17 is e assert of [caught17 == 1, "EE17 caught index-assignment on a number"] -assert of [(contains of [msg17.message, "cannot index"]) == 1, "EE17 message mentions 'cannot index'"] +assert of [(contains of [msg17.message, "cannot index"]), "EE17 message mentions 'cannot index'"] # ---- dict indexed by a non-string key (read) ---- caught18 is 0 @@ -209,7 +209,7 @@ catch e: caught18 is 1 msg18 is e assert of [caught18 == 1, "EE18 caught dict indexed by number (read)"] -assert of [(contains of [msg18.message, "cannot index"]) == 1, "EE18 message mentions 'cannot index'"] +assert of [(contains of [msg18.message, "cannot index"]), "EE18 message mentions 'cannot index'"] # ---- dict indexed by a non-string key (assignment) ---- caught19 is 0 @@ -221,7 +221,7 @@ catch e: caught19 is 1 msg19 is e assert of [caught19 == 1, "EE19 caught dict indexed by number (assignment)"] -assert of [(contains of [msg19.message, "cannot index"]) == 1, "EE19 message mentions 'cannot index'"] +assert of [(contains of [msg19.message, "cannot index"]), "EE19 message mentions 'cannot index'"] # ---- '+' on incompatible types ---- caught20 is 0 @@ -232,7 +232,7 @@ catch e: caught20 is 1 msg20 is e assert of [caught20 == 1, "EE20 caught '+' on dict and num"] -assert of [(contains of [msg20.message, "cannot apply '+'"]) == 1, "EE20 message mentions cannot apply '+'"] +assert of [(contains of [msg20.message, "cannot apply '+'"]), "EE20 message mentions cannot apply '+'"] # ---- '+' on two lists NAMES THE FIX (#680: prescriptive, not just diagnostic) ---- caught20b is 0 @@ -243,8 +243,8 @@ catch e: caught20b is 1 msg20b is e assert of [caught20b == 1, "EE20b caught '+' on list and list"] -assert of [(contains of [msg20b.message, "append of"]) == 1, "EE20b list '+' message prescribes append"] -assert of [(contains of [msg20b.message, "concat of"]) == 1, "EE20b list '+' message prescribes concat"] +assert of [(contains of [msg20b.message, "append of"]), "EE20b list '+' message prescribes append"] +assert of [(contains of [msg20b.message, "concat of"]), "EE20b list '+' message prescribes concat"] # ---- '/' on incompatible types ---- caught21 is 0 @@ -255,7 +255,7 @@ catch e: caught21 is 1 msg21 is e assert of [caught21 == 1, "EE21 caught '/' on list and num"] -assert of [(contains of [msg21.message, "cannot apply '/'"]) == 1, "EE21 message mentions cannot apply '/'"] +assert of [(contains of [msg21.message, "cannot apply '/'"]), "EE21 message mentions cannot apply '/'"] # ---- '%' on incompatible types ---- caught22 is 0 @@ -266,7 +266,7 @@ catch e: caught22 is 1 msg22 is e assert of [caught22 == 1, "EE22 caught '%' on list and num"] -assert of [(contains of [msg22.message, "cannot apply"]) == 1, "EE22 message mentions cannot apply"] +assert of [(contains of [msg22.message, "cannot apply"]), "EE22 message mentions cannot apply"] # ---- buffer index out of range ---- caught23 is 0 @@ -278,7 +278,7 @@ catch e: caught23 is 1 msg23 is e assert of [caught23 == 1, "EE23 caught buffer index out of range"] -assert of [(contains of [msg23.message, "out of range"]) == 1, "EE23 message mentions 'out of range'"] +assert of [(contains of [msg23.message, "out of range"]), "EE23 message mentions 'out of range'"] # ---- buffer non-integer index ---- caught24 is 0 @@ -290,6 +290,6 @@ catch e: caught24 is 1 msg24 is e assert of [caught24 == 1, "EE24 caught non-integer buffer index"] -assert of [(contains of [msg24.message, "index must be an integer"]) == 1, "EE24 message mentions integer index"] +assert of [(contains of [msg24.message, "index must be an integer"]), "EE24 message mentions integer index"] print of "All error_extra tests passed" diff --git a/tests/test_error_line_fallback.c b/tests/test_error_line_fallback.c index 7f2d9980..6ce103cc 100644 --- a/tests/test_error_line_fallback.c +++ b/tests/test_error_line_fallback.c @@ -150,22 +150,22 @@ int main(void) { static const char *const NM = "errline_1434"; EigsSlot sl, out; g_has_error = 0; /* a pending error halts the callback before it runs */ - g_trace_current_line = 70; sl.d = 1.0; trace_assign(NM, sl); + g_trace_current_line = 70; SLOT_NUM_RAW(sl) = 1.0; trace_assign(NM, sl); g_trace_current_line = 80; r = builtin_sort_by(args_good); check(!g_has_error && r && r->type == VAL_LIST && r->data.list.count == 6001, "#1434 the history row's sort_by ran its callbacks"); if (r) val_decref(r); - sl.d = 2.0; trace_assign(NM, sl); + SLOT_NUM_RAW(sl) = 2.0; trace_assign(NM, sl); trace_line(95); /* a stop right after the store, for [0b]'s `--step` query */ - check(trace_query_at(0, NM, 75, &out) && out.d == 1.0 && - trace_query_at(0, NM, 85, &out) && out.d == 2.0, + check(trace_query_at(0, NM, 75, &out) && SLOT_NUM_RAW(out) == 1.0 && + trace_query_at(0, NM, 85, &out) && SLOT_NUM_RAW(out) == 2.0, "#1434 a store after the callbacks is filed under the call's stamp"); /* The line live history filed the second store under, for [0b] to compare * with the tape stepper's `t` row over the same run's EIGS_TRACE tape. */ int filed = 0; for (int ln = 1; ln <= 100 && !filed; ln++) - if (trace_query_at(0, NM, ln, &out) && out.d == 2.0) filed = ln; + if (trace_query_at(0, NM, ln, &out) && SLOT_NUM_RAW(out) == 2.0) filed = ln; printf("LIVE: %s=2 line %d\n", NM, filed); g_has_error = 0; diff --git a/tests/test_exe_path.py b/tests/test_exe_path.py index d2ece7b6..951e06cd 100644 --- a/tests/test_exe_path.py +++ b/tests/test_exe_path.py @@ -49,8 +49,8 @@ def main(): if path is not None: env["PATH"] = path expected = [ - f"before={BINARY}", "chdir=1", f"cwd={elsewhere}", - f"after={BINARY}", "meta=1", "vm=1", "restore=1", + f"before={BINARY}", "chdir=true", f"cwd={elsewhere}", + f"after={BINARY}", "meta=true", "vm=true", "restore=true", f"done={cwd}", ] try: diff --git a/tests/test_experiment.eigs b/tests/test_experiment.eigs index aca26413..7d5a0dfc 100644 --- a/tests/test_experiment.eigs +++ b/tests/test_experiment.eigs @@ -24,14 +24,14 @@ print of (" last status: " + tracked[12].status) print of "--- is_measurement_stable (stable) ---" stable_data is [5, 5, 5, 5, 5, 5, 5, 5, 5, 5] result is is_measurement_stable of [stable_data, 3] -assert_eq of [result, 1, "stable data is stable"] +assert_eq of [result, true, "stable data is stable"] print of (" stable: " + (str of result)) # ---- is_measurement_stable on noisy data ---- print of "--- is_measurement_stable (noisy) ---" noisy_data is [1, 10, 2, 9, 3, 8, 4, 7, 5, 6] result2 is is_measurement_stable of [noisy_data, 5] -assert_eq of [result2, 0, "noisy data not stable for 5"] +assert_eq of [result2, false, "noisy data not stable for 5"] print of (" stable: " + (str of result2)) # ---- detect_entropy_spikes finds outliers ---- diff --git a/tests/test_file_exists_fifo.sh b/tests/test_file_exists_fifo.sh index a492d654..4237746a 100755 --- a/tests/test_file_exists_fifo.sh +++ b/tests/test_file_exists_fifo.sh @@ -20,7 +20,7 @@ for i in $(seq 1 50); do done if [ -z "$rc" ]; then kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null; echo "FAIL: file_exists on a reader-less fifo BLOCKED (killed after 5s)"; exit 1; fi out=$(cat "$T/out") -want=$'1\n1\n1\n0\n0' +want=$'true\ntrue\ntrue\nfalse\nfalse' if [ "$rc" -eq 0 ] && [ "$out" = "$want" ]; then echo "PASS: file_exists answers fifo=1 file=1 dir=1 missing=0 without blocking; is_file(fifo)=0" else diff --git a/tests/test_file_io.eigs b/tests/test_file_io.eigs index aa64f2c1..a31bee3d 100644 --- a/tests/test_file_io.eigs +++ b/tests/test_file_io.eigs @@ -19,26 +19,26 @@ assert of [bad == "", "RT3 non-string arg should return empty string"] # ---- write_text ---- # WT1: write and read back ok is write_text of ["/tmp/eigen_test_wt1.txt", "hello eigen"] -assert of [ok == 1, "WT1 write should succeed"] +assert of [ok, "WT1 write should succeed"] readback is read_text of "/tmp/eigen_test_wt1.txt" assert of [readback == "hello eigen", "WT1 read back should match"] # WT2: write empty string ok2 is write_text of ["/tmp/eigen_test_wt2.txt", ""] -assert of [ok2 == 1, "WT2 write empty should succeed"] +assert of [ok2, "WT2 write empty should succeed"] readback2 is read_text of "/tmp/eigen_test_wt2.txt" assert of [readback2 == "", "WT2 read back empty should match"] # WT3: bad args return 0 -assert of [(write_text of 42) == 0, "WT3 non-list arg"] -assert of [(write_text of [42, "text"]) == 0, "WT3 non-string path"] -assert of [(write_text of ["/tmp/x", 42]) == 0, "WT3 non-string text"] +assert of [(not (write_text of 42)), "WT3 non-list arg"] +assert of [(not (write_text of [42, "text"])), "WT3 non-string path"] +assert of [(not (write_text of ["/tmp/x", 42])), "WT3 non-string text"] # ---- exec_capture ---- # EC1: basic command result is exec_capture of ["echo", "test123"] assert of [result[0] == 0, "EC1 echo exit code"] -assert of [(contains of [result[1], "test123"]) == 1, "EC1 echo stdout"] +assert of [(contains of [result[1], "test123"]), "EC1 echo stdout"] # EC2: failing command result2 is exec_capture of ["ls", "/nonexistent_eigen_xyz"] @@ -61,7 +61,7 @@ assert of [result5[1] == "", "EC5 cat produces no output"] # EC6: timeout form completes before deadline result6 is exec_capture of [["echo", "fast"], 5] assert of [result6[0] == 0, "EC6 timeout form exit code"] -assert of [(contains of [result6[1], "fast"]) == 1, "EC6 timeout form stdout"] +assert of [(contains of [result6[1], "fast"]), "EC6 timeout form stdout"] # EC7: timeout fires and returns -2 result7 is exec_capture of [["sleep", "10"], 1] diff --git a/tests/test_file_rename.eigs b/tests/test_file_rename.eigs index 5fd5d2ed..aa5e2d76 100644 --- a/tests/test_file_rename.eigs +++ b/tests/test_file_rename.eigs @@ -11,46 +11,46 @@ remove_file of A remove_file of B write_bytes of [A, [10, 20, 30], 0] -assert_eq of [file_exists of A, 1, "source exists before rename"] +assert_eq of [file_exists of A, true, "source exists before rename"] # rename moves the bytes and removes the source -assert_eq of [rename of [A, B], 1, "rename succeeds"] -assert_eq of [file_exists of A, 0, "source gone after rename"] -assert_eq of [file_exists of B, 1, "dest exists after rename"] +assert_eq of [rename of [A, B], true, "rename succeeds"] +assert_eq of [file_exists of A, false, "source gone after rename"] +assert_eq of [file_exists of B, true, "dest exists after rename"] assert_eq of [buf_get of [read_bytes_buf of B, 1], 20, "bytes preserved across rename"] # rename replaces an existing destination (atomic swap) write_bytes of [A, [99], 0] -assert_eq of [rename of [A, B], 1, "rename over existing dest"] +assert_eq of [rename of [A, B], true, "rename over existing dest"] assert_eq of [buf_len of (read_bytes_buf of B), 1, "dest replaced, not appended"] assert_eq of [buf_get of [read_bytes_buf of B, 0], 99, "dest holds new content"] # remove_file deletes -assert_eq of [remove_file of B, 1, "remove_file succeeds"] -assert_eq of [file_exists of B, 0, "file gone after remove_file"] +assert_eq of [remove_file of B, true, "remove_file succeeds"] +assert_eq of [file_exists of B, false, "file gone after remove_file"] # failures return 0, not a crash -assert_eq of [rename of ["/tmp/eigs_no_such_file_zzz", B], 0, "rename of missing -> 0"] -assert_eq of [remove_file of "/tmp/eigs_no_such_file_zzz", 0, "remove missing -> 0"] +assert_eq of [rename of ["/tmp/eigs_no_such_file_zzz", B], false, "rename of missing -> 0"] +assert_eq of [remove_file of "/tmp/eigs_no_such_file_zzz", false, "remove missing -> 0"] # is_dir (#576): directory detection without the file_exists of "path/." probe D is "/tmp/eigs_isdir_probe/nested" F is "/tmp/eigs_isdir_file.bin" mkdir of D write_bytes of [F, [1], 0] -assert_eq of [is_dir of "/tmp/eigs_isdir_probe", 1, "is_dir: directory -> 1"] -assert_eq of [is_dir of D, 1, "is_dir: nested directory -> 1"] -assert_eq of [is_dir of F, 0, "is_dir: plain file -> 0"] -assert_eq of [is_dir of "/tmp/eigs_no_such_dir_zzz", 0, "is_dir: missing path -> 0"] -assert_eq of [is_dir of 42, 0, "is_dir: non-string -> 0"] +assert_eq of [is_dir of "/tmp/eigs_isdir_probe", true, "is_dir: directory -> 1"] +assert_eq of [is_dir of D, true, "is_dir: nested directory -> 1"] +assert_eq of [is_dir of F, false, "is_dir: plain file -> 0"] +assert_eq of [is_dir of "/tmp/eigs_no_such_dir_zzz", false, "is_dir: missing path -> 0"] +assert_eq of [is_dir of 42, false, "is_dir: non-string -> 0"] # #1058: is_file is the S_ISREG probe -- the admission contract read_file_util # enforces. A device passes file_exists AND fails is_dir; only is_file says no. -assert_eq of [is_file of F, 1, "is_file: plain file -> 1"] -assert_eq of [is_file of "/tmp/eigs_isdir_probe", 0, "is_file: directory -> 0"] -assert_eq of [is_file of "/dev/null", 0, "is_file: device -> 0 (file_exists says 1)"] -assert_eq of [file_exists of "/dev/null", 1, "control: file_exists admits the device"] -assert_eq of [is_file of "/tmp/eigs_no_such_file_zzz", 0, "is_file: missing path -> 0"] -assert_eq of [is_file of 42, 0, "is_file: non-string -> 0"] +assert_eq of [is_file of F, true, "is_file: plain file -> 1"] +assert_eq of [is_file of "/tmp/eigs_isdir_probe", false, "is_file: directory -> 0"] +assert_eq of [is_file of "/dev/null", false, "is_file: device -> 0 (file_exists says 1)"] +assert_eq of [file_exists of "/dev/null", true, "control: file_exists admits the device"] +assert_eq of [is_file of "/tmp/eigs_no_such_file_zzz", false, "is_file: missing path -> 0"] +assert_eq of [is_file of 42, false, "is_file: non-string -> 0"] remove_file of F test_summary of null diff --git a/tests/test_fstrings.eigs b/tests/test_fstrings.eigs index f31645e6..2884f5fe 100644 --- a/tests/test_fstrings.eigs +++ b/tests/test_fstrings.eigs @@ -65,7 +65,7 @@ assert_eq of [f"{"#}" + "x"}", "#}x", "hash and brace inside a string"] assert_eq of [f"n#{w}#", "n#5#", "hash in literal text is text"] assert_eq of [f"{ {"k": w}["k"] }", "5", "genuine nested braces still balance"] # same-line comment swallows the closing brace and quote: loud, like `(x # c)` -assert_eq of [try_parse of "print of f\"{w # c}\"", 0, "same-line comment is not a silent value"] +assert_eq of [try_parse of "print of f\"{w # c}\"", false, "same-line comment is not a silent value"] # the two scanners must agree: a `#` or quote inside a NESTED f-string's # interpolation (review of #1324 — each worked on main and broke when only # one of the two scans learned comments) @@ -77,7 +77,7 @@ assert_eq of [f"{f"{1 # } assert_eq of [f"{f"n#{w}"}", "n#5", "hash in nested literal text is text"] # a same-line comment inside a NESTED interpolation is loud too (review of # #1325: it swallows the rest of the line, as at top level and in `(x # c)`) -assert_eq of [try_parse of "print of f\"{f\"{w #c}\"}\"\nprint of 1", 0, "same-line comment in a nested interpolation is loud"] +assert_eq of [try_parse of "print of f\"{f\"{w #c}\"}\"\nprint of 1", false, "same-line comment in a nested interpolation is loud"] # --- #1253: a brace-bearing string inside a NESTED f-string's interpolation. # The outer scanner used to treat the nested f"..." as a plain string, stop at @@ -91,7 +91,7 @@ assert_eq of [f"{f"\\{w}"}", "\\5", "escaped backslash before nested interpolati assert_eq of [f"{f"\{x\}"}", "{x}", "escaped braces in nested literal text"] assert_eq of [f"{f"{ {"k}": w}["k}"] }"}", "5", "dict with brace-bearing keys in nested f-string"] # malformed nesting still fails with a diagnostic -assert_eq of [try_parse of "print of f\"outer:{f\"inner:{\"a}b\"}\"}", 0, "unterminated nested f-string is loud"] +assert_eq of [try_parse of "print of f\"outer:{f\"inner:{\"a}b\"}\"}", false, "unterminated nested f-string is loud"] # --- #1251 / #1337: a physical newline inside a string literal, inside # f-string literal text, or inside an f-string interpolation is still a @@ -156,7 +156,7 @@ assert_eq of [eval of "define f() as:\n for i in [1,\n 2]:\n x is i\n assert_eq of [eval of "i is 0\nz is [i for i in\n [5]]\nwhat is i at 2", 5, "comprehension spanning lines: variable at its first line"] assert_eq of [eval of "define f(a, b is [1,\n 2]) as:\n return (what is b at 1)\nf of 5", [1, 2], "default spanning lines: parameter at its first line"] assert_eq of [eval of "e is 0\ntry:\n q is 1 / 0\ncatch e:\n y is 1\nwhat is e at 3", 0, "catch binding is not filed under the faulting line"] -assert_eq of [eval of "import log\nhas_key of [(what is log at 1), \"log_info\"]", 1, "import binding at the import line"] +assert_eq of [eval of "import log\nhas_key of [(what is log at 1), \"log_info\"]", true, "import binding at the import line"] # #1384: the #1064 binder restore after a function-scope `for` is compiler # bookkeeping, not assignment history. Normal exit and break therefore expose # the same temporal history as module scope's loop-env tier. @@ -257,7 +257,7 @@ for _fs_t in (tokenize_with_names of _fs_tsrc): append of [_fs_names, _fs_t[1]] assert_eq of [_fs_names, ["x", "y", "str", "x"], "#1322: tokenize_with_names spells the f-string conversion `str`"] _fs_round is _fs_detok of _fs_tsrc -assert_eq of [try_parse of _fs_round, 1, "#1322: a detokenized f-string parses"] -assert_eq of [contains of [_fs_round, "#"], 0, "#1322: a detokenized f-string carries no `#`"] +assert_eq of [try_parse of _fs_round, true, "#1322: a detokenized f-string parses"] +assert_eq of [contains of [_fs_round, "#"], false, "#1322: a detokenized f-string carries no `#`"] test_summary of null diff --git a/tests/test_gc_traversal.c b/tests/test_gc_traversal.c index 6b40f252..fbaa69ce 100644 --- a/tests/test_gc_traversal.c +++ b/tests/test_gc_traversal.c @@ -73,7 +73,7 @@ static void duplicate_and_leaf(void) { collect(4,2,4,0,0); CHECK(traversal_skips>before,"numeric-leaf mark traversal skipped"); CHECK(root->data.list.items[0]==root->data.list.items[1],"live duplicate aliases retained"); - CHECK(root->data.list.items[0]->data.list.items[63]->data.num==63,"live leaf contents retained"); + CHECK(VAL_NUM_RAW(root->data.list.items[0]->data.list.items[63])==63,"live leaf contents retained"); duplicate_child=NULL; val_decref(root); gc_collect_cycles(); ++cases; diff --git a/tests/test_geometry.eigs b/tests/test_geometry.eigs index ec3299c4..f92a9ff8 100644 --- a/tests/test_geometry.eigs +++ b/tests/test_geometry.eigs @@ -131,8 +131,8 @@ li2 is line_intersection of [[0,0], [1,0], [0,1], [1,1]] assert_eq of [li2, null, "parallel lines -> null"] # Segment intersection -assert_eq of [segments_intersect of [[0,0], [1,1], [0,1], [1,0]], 1, "X segments intersect"] -assert_eq of [segments_intersect of [[0,0], [1,0], [0,1], [1,1]], 0, "parallel segments don't"] +assert_eq of [segments_intersect of [[0,0], [1,1], [0,1], [1,0]], true, "X segments intersect"] +assert_eq of [segments_intersect of [[0,0], [1,0], [0,1], [1,1]], false, "parallel segments don't"] # ============================================================ # Geometric Predicates @@ -144,11 +144,11 @@ assert_eq of [orientation of [[0,0], [1,0], [0,1]], 1, "CCW orientation"] assert_eq of [orientation of [[0,0], [0,1], [1,0]], -1, "CW orientation"] assert_eq of [orientation of [[0,0], [1,1], [2,2]], 0, "collinear orientation"] -assert_eq of [collinear of [[0,0], [1,1], [2,2]], 1, "collinear points"] -assert_eq of [collinear of [[0,0], [1,0], [0,1]], 0, "non-collinear points"] +assert_eq of [collinear of [[0,0], [1,1], [2,2]], true, "collinear points"] +assert_eq of [collinear of [[0,0], [1,0], [0,1]], false, "non-collinear points"] -assert_eq of [on_segment of [[0,0], [1,1], [2,2]], 1, "on segment middle"] -assert_eq of [on_segment of [[0,0], [3,3], [2,2]], 0, "off segment"] +assert_eq of [on_segment of [[0,0], [1,1], [2,2]], true, "on segment middle"] +assert_eq of [on_segment of [[0,0], [3,3], [2,2]], false, "off segment"] # ============================================================ # Triangle Operations @@ -168,8 +168,8 @@ assert_near of [tc[0], 1, 0.001, "centroid x"] assert_near of [tc[1], 1, 0.001, "centroid y"] # Point in triangle -assert_eq of [point_in_triangle of [[1,1], [0,0], [4,0], [0,4]], 1, "point inside"] -assert_eq of [point_in_triangle of [[5,5], [0,0], [4,0], [0,4]], 0, "point outside"] +assert_eq of [point_in_triangle of [[1,1], [0,0], [4,0], [0,4]], true, "point inside"] +assert_eq of [point_in_triangle of [[5,5], [0,0], [4,0], [0,4]], false, "point outside"] # Circumcenter of right triangle (0,0), (4,0), (0,4): should be at (2,2) cc is triangle_circumcenter of [[0,0], [4,0], [0,4]] @@ -218,17 +218,17 @@ assert_near of [pc[0], 0.5, 0.001, "square centroid x"] assert_near of [pc[1], 0.5, 0.001, "square centroid y"] # Point in polygon -assert_eq of [point_in_polygon of [[0.5, 0.5], [[0,0], [1,0], [1,1], [0,1]]], 1, "point in square"] -assert_eq of [point_in_polygon of [[2, 2], [[0,0], [1,0], [1,1], [0,1]]], 0, "point outside square"] +assert_eq of [point_in_polygon of [[0.5, 0.5], [[0,0], [1,0], [1,1], [0,1]]], true, "point in square"] +assert_eq of [point_in_polygon of [[2, 2], [[0,0], [1,0], [1,1], [0,1]]], false, "point outside square"] # Convex check -assert_eq of [polygon_is_convex of [[0,0], [1,0], [1,1], [0,1]], 1, "square is convex"] +assert_eq of [polygon_is_convex of [[0,0], [1,0], [1,1], [0,1]], true, "square is convex"] # L-shape is not convex -assert_eq of [polygon_is_convex of [[0,0], [2,0], [2,1], [1,1], [1,2], [0,2]], 0, "L-shape not convex"] +assert_eq of [polygon_is_convex of [[0,0], [2,0], [2,1], [1,1], [1,2], [0,2]], false, "L-shape not convex"] # Winding order -assert_eq of [polygon_is_clockwise of [[0,0], [1,0], [1,1], [0,1]], 0, "CCW not clockwise"] -assert_eq of [polygon_is_clockwise of [[0,0], [0,1], [1,1], [1,0]], 1, "CW is clockwise"] +assert_eq of [polygon_is_clockwise of [[0,0], [1,0], [1,1], [0,1]], false, "CCW not clockwise"] +assert_eq of [polygon_is_clockwise of [[0,0], [0,1], [1,1], [1,0]], true, "CW is clockwise"] # ============================================================ # Convex Hull @@ -257,14 +257,14 @@ print of "--- Circle Operations ---" assert_near of [circle_area of 5, 78.54, 0.01, "circle area r=5"] assert_near of [circle_circumference of 1, 6.2832, 0.001, "circumference r=1"] -assert_eq of [point_in_circle of [1, 1, 0, 0, 2], 1, "point in circle"] -assert_eq of [point_in_circle of [3, 3, 0, 0, 2], 0, "point outside circle"] -assert_eq of [point_in_circle of [0, 0, 0, 0, 1], 1, "center in circle"] +assert_eq of [point_in_circle of [1, 1, 0, 0, 2], true, "point in circle"] +assert_eq of [point_in_circle of [3, 3, 0, 0, 2], false, "point outside circle"] +assert_eq of [point_in_circle of [0, 0, 0, 0, 1], true, "center in circle"] # Two circles intersecting -assert_eq of [circles_intersect of [0, 0, 2, 3, 0, 2], 1, "overlapping circles"] -assert_eq of [circles_intersect of [0, 0, 1, 5, 0, 1], 0, "distant circles"] -assert_eq of [circles_intersect of [0, 0, 1, 0, 0, 0.5], 0, "contained circle"] +assert_eq of [circles_intersect of [0, 0, 2, 3, 0, 2], true, "overlapping circles"] +assert_eq of [circles_intersect of [0, 0, 1, 5, 0, 1], false, "distant circles"] +assert_eq of [circles_intersect of [0, 0, 1, 0, 0, 0.5], false, "contained circle"] # Circle intersection points cip is circle_intersection_points of [0, 0, 1, 1, 0, 1] diff --git a/tests/test_gfx_argtypes.eigs b/tests/test_gfx_argtypes.eigs index fa7c59e7..600c0754 100644 --- a/tests/test_gfx_argtypes.eigs +++ b/tests/test_gfx_argtypes.eigs @@ -193,7 +193,7 @@ if env_get of "EIGS_STRICT" == "1": assert_eq of [strict_kind of ((_) => gfx_text of [0, 0, "H", 255, 255, 255, 2]), "none", "#1007 strict: a well-typed gfx_text scale does not raise"] print of "strict-pass: 1" else: - assert_eq of [gfx_open of ["800", "600", "t"], 0, "#1007: string w/h no longer opens a window"] + assert_eq of [gfx_open of ["800", "600", "t"], false, "#1007: string w/h no longer opens a window"] assert_eq of [audio_open of ["44100", "1"], 0, "#1007: string freq/channels no longer opens a device"] assert_eq of [audio_capture_open of ["44100", "1"], 0, "#1007: string freq/channels no longer opens a capture device"] # audio_stream_open never type-punned — its checks are inside the condition @@ -272,7 +272,7 @@ else: # missing one, and silent in both modes. Post-fix the call is refused # before the read, so the cleared colour survives. Needs a real renderer, # so it is gated and says when it did not run. - if sdl_open == 1: + if sdl_open: ignore is gfx_clear of [7, 8, 9] ignore is gfx_rect of [0, 0, 32, 32, "255", 0, 0] px is gfx_read of [4, 4] diff --git a/tests/test_gfx_text.eigs b/tests/test_gfx_text.eigs index 860fa4e8..d3a40bf1 100644 --- a/tests/test_gfx_text.eigs +++ b/tests/test_gfx_text.eigs @@ -60,7 +60,7 @@ else: # ---- Windowed render smoke (SDL dummy driver in the runner) ---- # gfx_text must not crash in either mode; skipped when no video driver. opened is gfx_open of [64, 48, "ttf-smoke"] -if opened == 1: +if opened: gfx_clear of [0, 0, 0] gfx_text of [2, 2, "Smoke", 255, 255, 255, 2] gfx_text of [2, 20, "", 255, 255, 255, 1] diff --git a/tests/test_harness.eigs b/tests/test_harness.eigs index 7fbe05a5..b3268fa8 100644 --- a/tests/test_harness.eigs +++ b/tests/test_harness.eigs @@ -29,7 +29,7 @@ try: harness.finish of [] catch err: threw is 1 - check of ["throw names the marker and tally", contains of [err, "INNER_BAD"], 1] + check of ["throw names the marker and tally", contains of [err, "INNER_BAD"], true] check of ["finish throws on failures", threw, 1] # start resets state: a fresh run is clean after the failing one diff --git a/tests/test_hexdump.eigs b/tests/test_hexdump.eigs index 3c2fa069..b9d4c8d9 100644 --- a/tests/test_hexdump.eigs +++ b/tests/test_hexdump.eigs @@ -26,7 +26,7 @@ d is hexdump of long lines is split of [d, "\n"] check of ["two rows", len of lines, 2] check of ["second row offset", char_at of [lines[1], 7], "0"] -check of ["second row starts at byte 16", contains of [lines[1], "2D 74 61 69 6C"], 1] +check of ["second row starts at byte 16", contains of [lines[1], "2D 74 61 69 6C"], true] # buffer input equals string input b is buffer of 6 diff --git a/tests/test_host_frame_line.eigs b/tests/test_host_frame_line.eigs index a2b8dbb3..7e2b3810 100644 --- a/tests/test_host_frame_line.eigs +++ b/tests/test_host_frame_line.eigs @@ -7,4 +7,4 @@ x is 1 y is 2 z is 3 r2 is sandbox_run of [desc, 100000] -print of (r1["ok"] + r2["ok"]) +print of [r1["ok"], r2["ok"]] diff --git a/tests/test_http.eigs b/tests/test_http.eigs index 5ac5d74b..805f6ee5 100644 --- a/tests/test_http.eigs +++ b/tests/test_http.eigs @@ -23,7 +23,7 @@ catch e: c3 is 1 m3 is e assert_true of [c3 == 1, "HTTP03 http_route non-list arg raises"] -assert_true of [(contains of [m3.message, "http_route requires"]) == 1, "HTTP03 message"] +assert_true of [(contains of [m3.message, "http_route requires"]), "HTTP03 message"] c4 is 0 try: diff --git a/tests/test_http_server.sh b/tests/test_http_server.sh index 555aa4ae..31117461 100755 --- a/tests/test_http_server.sh +++ b/tests/test_http_server.sh @@ -519,7 +519,7 @@ fi # ---- Shared-store: delete clears the entry ---- RESP=$(curl -s --max-time 2 "http://127.0.0.1:$PORT/sdel") -if [ "$RESP" = "0" ]; then +if [ "$RESP" = "false" ]; then ok "HS19 shared_delete removes the key" else fail "HS19 shared_delete" "got '$RESP'" @@ -562,10 +562,10 @@ fi # /sbig writes an 8 KiB string under a 4 KiB cap; shared_set must # reject the write and leave the store unchanged. RESP=$(curl -s --max-time 2 "http://127.0.0.1:$PORT/sbig") -if [ "$RESP" = "0" ]; then +if [ "$RESP" = "false" ]; then ok "HS22 shared_set rejects writes past EIGS_HTTP_SHARED_MAX_BYTES" else - fail "HS22 shared store bounds" "got '$RESP' (expected 0)" + fail "HS22 shared store bounds" "got '$RESP' (expected false)" fi # ---- Shared-store: atomic increment (single-lock RMW) ---- @@ -793,12 +793,12 @@ print of ("ARR:" + (str of (contains of [arr, "X-Shape: array"]))) EIGSCLI SHAPES=$("$EIGS" "$CLI" 2>/dev/null | tr -d '\r') rm -f "$CLI" -if echo "$SHAPES" | grep -q "^OBJ:1$"; then +if echo "$SHAPES" | grep -q "^OBJ:true$"; then ok "HS34 http_post sends headers given as a JSON object" else fail "HS34 http_post object headers" "header did not reach the wire ($(echo "$SHAPES" | tr '\n' ' '))" fi -if echo "$SHAPES" | grep -q "^ARR:1$"; then +if echo "$SHAPES" | grep -q "^ARR:true$"; then ok "HS34 http_post still sends headers given as a flat array" else fail "HS34 http_post array headers" "regressed ($(echo "$SHAPES" | tr '\n' ' '))" diff --git a/tests/test_import.eigs b/tests/test_import.eigs index 27690958..9cca3b7f 100644 --- a/tests/test_import.eigs +++ b/tests/test_import.eigs @@ -40,7 +40,7 @@ assert_eq of [abs of -10, 10, "global abs unchanged"] # keys shows module contents k is keys of math assert_true of [(len of k) > 0, "import has keys"] -assert_eq of [has_key of [math, "abs"], 1, "math has abs"] +assert_eq of [has_key of [math, "abs"], true, "math has abs"] # Import format import format @@ -60,8 +60,8 @@ rm of (_module_test_dir + "/tmp_user_module.eigs") # Names starting with _ are module-private: omitted from the dict. write_text of [(_module_test_dir + "/tmp_priv_module.eigs"), "_secret is 1\nvisible is 2\n"] import tmp_priv_module -assert_eq of [has_key of [tmp_priv_module, "visible"], 1, "public name exported"] -assert_eq of [has_key of [tmp_priv_module, "_secret"], 0, "_name stays private"] +assert_eq of [has_key of [tmp_priv_module, "visible"], true, "public name exported"] +assert_eq of [has_key of [tmp_priv_module, "_secret"], false, "_name stays private"] rm of (_module_test_dir + "/tmp_priv_module.eigs") # --- #821: a project file with a stdlib module's name wins (project-first). @@ -71,7 +71,7 @@ rm of (_module_test_dir + "/tmp_priv_module.eigs") # the shell level in run_all_tests.sh (it goes to stderr). write_text of [(_module_test_dir + "/physics.eigs"), "SHADOW_MARKER is 821\n"] import physics -assert_eq of [has_key of [physics, "SHADOW_MARKER"], 1, "project file shadows stdlib (#821)"] +assert_eq of [has_key of [physics, "SHADOW_MARKER"], true, "project file shadows stdlib (#821)"] assert_eq of [physics.SHADOW_MARKER, 821, "shadowing module's own binding read"] rm of (_module_test_dir + "/physics.eigs") diff --git a/tests/test_inflate.eigs b/tests/test_inflate.eigs index e86144ca..2ceaed2a 100644 --- a/tests/test_inflate.eigs +++ b/tests/test_inflate.eigs @@ -82,7 +82,7 @@ catch e: m13 is e check of ["D13 corrupt stream caught", c13, 1] check of ["D14 corrupt kind=value", m13.kind, "value"] -check of ["D15 corrupt message", contains of [m13.message, "invalid or truncated"], 1] +check of ["D15 corrupt message", contains of [m13.message, "invalid or truncated"], true] # ---- truncated stream: valid stream minus its last byte ---- c16 is 0 @@ -103,7 +103,7 @@ catch e3: m17 is e3 check of ["D17 string arg caught", c17, 1] check of ["D18 wrong-type kind", m17.kind, "type_mismatch"] -check of ["D19 wrong-type message", contains of [m17.message, "requires a list of byte values"], 1] +check of ["D19 wrong-type message", contains of [m17.message, "requires a list of byte values"], true] c20 is 0 try: @@ -145,7 +145,7 @@ sb_desc is [SB_ABI, [SB_GET_NAME,0,0, SB_CONST,1,0, SB_CALL,1,0, SB_RETURN], ["i # Refused under a budget far smaller than the decompressed result... sb_small is sandbox_run of [sb_desc, 1000000, 65536] -check of ["D23 sandboxed inflate charged against max_bytes", sb_small["ok"], 0] +check of ["D23 sandboxed inflate charged against max_bytes", sb_small["ok"], false] # ...and the codecs are untouched outside a budgeted sandbox. sb_round is inflate of sb_blob diff --git a/tests/test_json_hard.eigs b/tests/test_json_hard.eigs index 99d30761..834068f9 100644 --- a/tests/test_json_hard.eigs +++ b/tests/test_json_hard.eigs @@ -44,8 +44,8 @@ assert of [len of empty_a == 0, "JH18 empty array length 0"] # --- json_decode: special values --- bools is json_decode of "{\"t\": true, \"f\": false, \"n\": null}" -assert of [bools.t == 1, "JH19 true decodes to 1"] -assert of [bools.f == 0, "JH20 false decodes to 0"] +assert of [bools.t == true, "JH19 true decodes to the bool true (#1637)"] +assert of [bools.f == false, "JH20 false decodes to the bool false (#1637)"] assert of [type of bools.n == "none", "JH21 null decodes to none"] # --- Unicode escape \uXXXX --- diff --git a/tests/test_json_roundtrip.eigs b/tests/test_json_roundtrip.eigs index bed480d6..188d2a73 100644 --- a/tests/test_json_roundtrip.eigs +++ b/tests/test_json_roundtrip.eigs @@ -76,7 +76,7 @@ big_ints is [1234567890123456, 9007199254740992, 9007199254740991, 1000000000000000, 100000000000000000 / 100, 4000000000] for n in big_ints: enc is json_encode of n - assert of [(contains of [enc, "e+"]) == 0, "an exact integer never encodes in exponent form"] + assert of [(not (contains of [enc, "e+"])), "an exact integer never encodes in exponent form"] assert of [(json_decode of enc) == n, "an exact integer round-trips"] assert of [enc == (str of n), "integer encoding agrees with str of"] diff --git a/tests/test_list_contains.eigs b/tests/test_list_contains.eigs index cecc1a93..6939bea9 100644 --- a/tests/test_list_contains.eigs +++ b/tests/test_list_contains.eigs @@ -4,49 +4,49 @@ passed is 0 # T1: member gives 1 (#543's example) a is list_contains of [[10, 20], 20] checks += 1 -if a == 1: +if a: passed += 1 # T2: non-member gives 0 b is list_contains of [[10, 20], 99] checks += 1 -if b == 0: +if (not b): passed += 1 # T3: empty list gives 0 c is list_contains of [[], 1] checks += 1 -if c == 0: +if (not c): passed += 1 # T4: structural match on a nested list d is list_contains of [[[1, 2], [3, 4]], [3, 4]] checks += 1 -if d == 1: +if d: passed += 1 # T5: structural match on a dict element e is list_contains of [[{"a": 1}, {"b": 2}], {"b": 2}] checks += 1 -if e == 1: +if e: passed += 1 # T6: number vs string never equal f is list_contains of [[1], "1"] checks += 1 -if f == 0: +if (not f): passed += 1 # T7: string element hit g is list_contains of [["a", "b"], "b"] checks += 1 -if g == 1: +if g: passed += 1 # T8: near-miss structure (different length) gives 0 h is list_contains of [[[1, 2]], [1, 2, 3]] checks += 1 -if h == 0: +if (not h): passed += 1 if passed == checks: diff --git a/tests/test_loop_counter_sequential.c b/tests/test_loop_counter_sequential.c index 3dfce465..169c975a 100644 --- a/tests/test_loop_counter_sequential.c +++ b/tests/test_loop_counter_sequential.c @@ -42,8 +42,8 @@ static void step(Env *env, const char *label, int stall, EigsSlot value = env_get_hashed_slot(env, counter, 0, &found); int numeric = slot_is_num(value) || (slot_is_ptr(value) && slot_as_ptr(value)->type == VAL_NUM); - double actual = slot_is_num(value) ? value.d : - (numeric ? slot_as_ptr(value)->data.num : -1); + double actual = slot_is_num(value) ? SLOT_NUM_RAW(value) : + (numeric ? VAL_NUM_RAW(slot_as_ptr(value)) : -1); int assignments = env_get_assign_count(env, counter, 0); int ok = rc == 0 && !g_has_error && found && numeric && actual == (double)expected_iterations && diff --git a/tests/test_loop_halting.sh b/tests/test_loop_halting.sh index dfabceaf..376fd98d 100755 --- a/tests/test_loop_halting.sh +++ b/tests/test_loop_halting.sh @@ -44,7 +44,7 @@ loop while not converged: e is e * 0.5 print of f"{converged}" EOF -if [ "$("$EIGS" "$TMP/conv.eigs" 2>/dev/null | tail -1)" = "1" ]; then +if [ "$("$EIGS" "$TMP/conv.eigs" 2>/dev/null | tail -1)" = "true" ]; then echo "PASS: observer loop (loop while not converged) still halts" else echo "FAIL: observer loop did not halt as before" diff --git a/tests/test_math_underflow.eigs b/tests/test_math_underflow.eigs index 07401c15..4ad12e82 100644 --- a/tests/test_math_underflow.eigs +++ b/tests/test_math_underflow.eigs @@ -29,54 +29,54 @@ clear_math_flags of null a is 1e-300 * 1e-300 if not (a == 0): fail of "1e-300 * 1e-300 should be 0" -if not (uf of null == 1): +if not (uf of null): fail of "multiplication underflow was not flagged" clear_math_flags of null b is 1e-300 / 1e300 -if not (uf of null == 1): +if not (uf of null): fail of "division underflow was not flagged" # --- controls: these must NOT raise ------------------------------------------- clear_math_flags of null c is 5.0 - 5.0 -if not (uf of null == 0): +if uf of null: fail of "exact cancellation (5 - 5) was misreported as underflow" clear_math_flags of null d is 0.0 + 0.0 -if not (uf of null == 0): +if uf of null: fail of "adding zeros was misreported as underflow" clear_math_flags of null e is 3.0 * 0.0 -if not (uf of null == 0): +if uf of null: fail of "multiplying by a real zero was misreported as underflow" clear_math_flags of null f is 2.0 * 3.0 if not (f == 6): fail of "ordinary multiplication broke" -if not (uf of null == 0): +if uf of null: fail of "ordinary multiplication raised underflow" # --- the flag is sticky and clearable, like its siblings ---------------------- clear_math_flags of null g is 1e-300 * 1e-300 h is 2.0 * 3.0 -if not (uf of null == 1): +if not (uf of null): fail of "underflow flag should stay set until cleared" clear_math_flags of null -if not (uf of null == 0): +if uf of null: fail of "clear_math_flags did not clear underflow" # --- it is independent of the other two flags --------------------------------- clear_math_flags of null i is 1e-300 * 1e-300 fl is math_flags of null -if not (fl["overflow"] == 0): +if not ((not fl["overflow"])): fail of "underflow set the overflow flag" -if not (fl["invalid"] == 0): +if not ((not fl["invalid"])): fail of "underflow set the invalid flag" # #1079: a product in RETURN position of a leaf callee. `return x * y` is @@ -90,15 +90,15 @@ define uf_local(x, y) as: return p clear_math_flags of null r is uf_leaf of [1e-200, 1e-200] -if not ((math_flags of null)["underflow"] == 1): +if not ((math_flags of null)["underflow"]): fail of "return x * y in a leaf callee did not set underflow (#1079)" clear_math_flags of null r is uf_local of [1e-200, 1e-200] -if not ((math_flags of null)["underflow"] == 1): +if not ((math_flags of null)["underflow"]): fail of "local product in a callee did not set underflow" clear_math_flags of null r is uf_leaf of [2, 3] -if not ((math_flags of null)["underflow"] == 0): +if (math_flags of null)["underflow"]: fail of "a normal leaf product set underflow" if not (r == 6): fail of "leaf product value wrong" diff --git a/tests/test_meta_host_builtins.eigs b/tests/test_meta_host_builtins.eigs index 84ed6faf..3c387998 100644 --- a/tests/test_meta_host_builtins.eigs +++ b/tests/test_meta_host_builtins.eigs @@ -49,7 +49,7 @@ assert_eq of [_literal_result, 42, "captured scanner and numeric conversion"] assert_eq of [_list_result, 7, "captured parser append and meta append"] assert_eq of [_update_result, 3, "captured environment and dictionary updates"] assert_eq of [_function_result, 6, "captured parser and evaluator range/type"] -assert_eq of [_import_result, 1, "captured import path probes and file reader"] +assert_eq of [_import_result, true, "captured import path probes and file reader"] assert_near of [_entropy_result, 2, 0.000000000001, "entropy retains captured transitive log/divide"] test_summary of null diff --git a/tests/test_meta_parity.eigs b/tests/test_meta_parity.eigs index 6aca89d2..0ab26e92 100644 --- a/tests/test_meta_parity.eigs +++ b/tests/test_meta_parity.eigs @@ -141,8 +141,8 @@ define both_reject(label, src) as: eigen_run of src catch me: meta_msg is me - check of [f"meta rejects: {label}", contains of [meta_msg, "is a reserved observer form"], 1] - check of [f"meta names E005: {label}", contains of [meta_msg, "[E005]"], 1] + check of [f"meta rejects: {label}", contains of [meta_msg, "is a reserved observer form"], true] + check of [f"meta names E005: {label}", contains of [meta_msg, "[E005]"], true] # binding positions (the issue's two probes first) both_reject of ["report is 4 / print of report", "report is 4\nprint of report"] @@ -174,7 +174,7 @@ try: eigen_run of "report of nope_report_operand" catch ume: unb_meta is ume -check of ["meta: report of unbound raises", contains of [unb_meta, "undefined variable"], 1] +check of ["meta: report of unbound raises", contains of [unb_meta, "undefined variable"], true] # positive controls: a bound identifier operand still works on both sides. # Values are the documented divergence (the meta bridge classifies the VALUE @@ -187,7 +187,7 @@ check of ["meta: report_value of bound ident (bridge fallback)", eigen_run of "p check of ["meta: parenthesised identifier operand", eigen_run of "pv4 is 1\nreport of (pv4)", "equilibrium"] check of ["meta: report_value of ((x))", eigen_run of "pv5 is 1\nreport_value of ((pv5))", "equilibrium"] check of ["meta: `of` binds the identifier only (report of x + \"!\")", eigen_run of "pv6 is 1\nreport of pv6 + \"!\"", "equilibrium!"] -check of ["native: `of` binds the identifier only", ends_with of [eval of "pv7 is 1\nreport of pv7 + \"!\"", "!"], 1] +check of ["native: `of` binds the identifier only", ends_with of [eval of "pv7 is 1\nreport of pv7 + \"!\"", "!"], true] check of ["meta: host function operand is opaque", eigen_run of "report of print", "opaque"] check of ["meta: dict fields named report/report_value stay data keys", eigen_run of "rdk is {\"report\": 7, \"report_value\": 8}\nrdk.report is 9\nrdk.report + rdk.report_value", 17] check of ["native: dict fields named report/report_value stay data keys", eval of "rdn is {\"report\": 7, \"report_value\": 8}\nrdn.report is 9\nrdn.report + rdn.report_value", 17] @@ -265,15 +265,15 @@ define parity(label, src, want) as: check of [f"VM: {label}", vm_probe of src, want] parity of ["a public module binding is in the namespace", - "import log\nhas_key of [log, \"log_info\"]", ["ok", 1]] + "import log\nhas_key of [log, \"log_info\"]", ["ok", true]] parity of ["a `_` module binding reads as null through the namespace", "import log\nlog._log_min_level", ["ok", null]] parity of ["a `_` module binding is not a namespace key", - "import log\nhas_key of [log, \"_log_min_level\"]", ["ok", 0]] + "import log\nhas_key of [log, \"_log_min_level\"]", ["ok", false]] parity of ["a key written through the namespace reads back", "import map\nmap.probe1057 is 7\nmap.probe1057", ["ok", 7]] parity of ["a key written through the namespace becomes a namespace key", - "import map\nmap.probe1057 is 7\nhas_key of [map, \"probe1057\"]", ["ok", 1]] + "import map\nmap.probe1057 is 7\nhas_key of [map, \"probe1057\"]", ["ok", true]] parity of ["a `_` key written through the namespace reads back (stays private)", "import log\nlog._log_min_level is 3\nlog._log_min_level", ["ok", 3]] parity of ["an unresolvable module raises, it does not import empty", @@ -287,7 +287,7 @@ parity of ["an unresolvable module raises, it does not import empty", parity of ["rebinding the name drops the module view", "import log\nlog is {}\nlog.log_info", ["ok", null]] parity of ["rebinding the name drops the module keys", - "import log\nlog is {}\nhas_key of [log, \"log_info\"]", ["ok", 0]] + "import log\nlog is {}\nhas_key of [log, \"log_info\"]", ["ok", false]] # The cwd gate. Run the same import from a directory that has no `lib/`: both # evaluators must still answer the stdlib module beside the binary. Done in a @@ -304,8 +304,8 @@ _mp_meta_elsewhere is meta_probe of "import log\nhas_key of [log, \"log_info\"]" _mp_vm_elsewhere is vm_probe of "import log\nhas_key of [log, \"log_info\"]" chdir of _mp_cwd exec_capture of ["rm", "-rf", _mp_tmp_cwd] -check of ["meta: import resolves from a cwd with no lib/", _mp_meta_elsewhere, ["ok", 1]] -check of ["VM: import resolves from a cwd with no lib/", _mp_vm_elsewhere, ["ok", 1]] +check of ["meta: import resolves from a cwd with no lib/", _mp_meta_elsewhere, ["ok", true]] +check of ["VM: import resolves from a cwd with no lib/", _mp_vm_elsewhere, ["ok", true]] # GAP CANARY (a real gap, asserted as it behaves today). The live half of the # rule — a module FUNCTION mutating a module global and the importer seeing it @@ -383,15 +383,15 @@ define meta_err_msg(src) as: m is mm return m check of ["meta: line after a multi-line string", - contains of [meta_err_msg of "s is \"a\nb\"\nx is $", "at line 3"], 1] + contains of [meta_err_msg of "s is \"a\nb\"\nx is $", "at line 3"], true] check of ["meta: line after a multi-line f-string with an interpolation", - contains of [meta_err_msg of "s is f\"a\nb{1 +\n2}\"\nx is $", "at line 4"], 1] + contains of [meta_err_msg of "s is f\"a\nb{1 +\n2}\"\nx is $", "at line 4"], true] check of ["meta: backslash before a physical newline in a string advances the line", - contains of [meta_err_msg of "s is \"a\\\nb\"\nx is $", "at line 3"], 1] + contains of [meta_err_msg of "s is \"a\\\nb\"\nx is $", "at line 3"], true] check of ["meta: backslash before a physical newline in f-string text advances the line", - contains of [meta_err_msg of "s is f\"a\\\nb\"\nx is $", "at line 3"], 1] + contains of [meta_err_msg of "s is f\"a\\\nb\"\nx is $", "at line 3"], true] check of ["meta: escaped newline does not advance the line", - contains of [meta_err_msg of "s is \"a\\nb\"\nx is $", "at line 2"], 1] + contains of [meta_err_msg of "s is \"a\\nb\"\nx is $", "at line 2"], true] # --- #1254: parameters are comma-separated in define and in lambdas; a # missing comma is a parse error on both sides, not a silent second parameter --- diff --git a/tests/test_model_context_window.sh b/tests/test_model_context_window.sh index bfaddd0b..c8c6ae77 100755 --- a/tests/test_model_context_window.sh +++ b/tests/test_model_context_window.sh @@ -41,7 +41,7 @@ EXPECTED_ERR="[model-load] No live weights, using locked baseline: $MODEL" cat > "$HARNESS" <&1) WIDE_LOADED=$(echo "$WIDE_OUT" | grep -A1 '^WIDE_LOAD:$' | tail -1) WIDE_INFO=$(echo "$WIDE_OUT" | grep -A1 '^WIDE_INFO:$' | tail -1) - if [ "$WIDE_LOADED" = "1" ] && echo "$WIDE_INFO" | grep -q '"vocab_size": 118'; then + if [ "$WIDE_LOADED" = "true" ] && echo "$WIDE_INFO" | grep -q '"vocab_size": 118'; then ok "MRT03b wide vocab model loads" else fail "MRT03b wide vocab load" "loaded='$WIDE_LOADED' info='$WIDE_INFO'" @@ -174,7 +174,7 @@ OUT2=$("$EIGS" "$SCRIPT2" 2>&1) RELOADED=$(echo "$OUT2" | grep -A1 '^RELOAD:$' | tail -1) INFO2=$(echo "$OUT2" | grep -A1 '^INFO_AFTER:$' | tail -1) -if [ "$RELOADED" = "1" ]; then +if [ "$RELOADED" = "true" ]; then ok "MRT06 saved model reloads in fresh process" else fail "MRT06 reload" "reloaded='$RELOADED'" @@ -200,7 +200,7 @@ OUT2B=$("$EIGS" "$SCRIPT2B" 2>&1) RELOADED_BIN=$(echo "$OUT2B" | grep -A1 '^RELOAD_BIN:$' | tail -1) INFO2B=$(echo "$OUT2B" | grep -A1 '^INFO_BIN_AFTER:$' | tail -1) -if [ "$RELOADED_BIN" = "1" ]; then +if [ "$RELOADED_BIN" = "true" ]; then ok "MRT07b binary .eigen reloads in fresh process" else fail "MRT07b binary reload" "reloaded='$RELOADED_BIN'" diff --git a/tests/test_module_live_view.eigs b/tests/test_module_live_view.eigs index af4bbdfa..47f782b2 100644 --- a/tests/test_module_live_view.eigs +++ b/tests/test_module_live_view.eigs @@ -54,12 +54,12 @@ assert_eq of [fn of null, 3, "function value extracted from the namespace"] # ---- Control: enumeration still lists the bindings --------------------- k is keys of modlive_counter assert_true of [(len of k) >= 5, "keys lists the module's public bindings"] -assert_eq of [has_key of [modlive_counter, "ctr"], 1, "has_key sees ctr"] -assert_eq of [has_key of [modlive_counter, "peek"], 1, "has_key sees peek"] +assert_eq of [has_key of [modlive_counter, "ctr"], true, "has_key sees ctr"] +assert_eq of [has_key of [modlive_counter, "peek"], true, "has_key sees peek"] assert_eq of [len of modlive_counter, len of k, "len agrees with keys"] # ---- Control: `_`-private module bindings stay private ------------------ -assert_eq of [has_key of [modlive_counter, "_hidden"], 0, "_name stays private"] +assert_eq of [has_key of [modlive_counter, "_hidden"], false, "_name stays private"] assert_eq of [modlive_counter._hidden, null, "_name unreadable through namespace"] # ---- Control: whole-dict readers see the CURRENT value ----------------- @@ -75,7 +75,7 @@ assert_eq of [modlive_counter.ctr, 3, "re-import yields the same live state"] # ---- Control: a new key written through the namespace binds in the module modlive_plain.fresh is 7 assert_eq of [modlive_plain.fresh, 7, "new namespace key reads back"] -assert_eq of [has_key of [modlive_plain, "fresh"], 1, "new key enumerates"] +assert_eq of [has_key of [modlive_plain, "fresh"], true, "new key enumerates"] # ---- Control: nested imports stay live --------------------------------- import modlive_outer diff --git a/tests/test_named_params.eigs b/tests/test_named_params.eigs index 0e452c0f..acd17217 100644 --- a/tests/test_named_params.eigs +++ b/tests/test_named_params.eigs @@ -43,21 +43,21 @@ assert_eq of [outer_a, 100, "outer unchanged"] # #1254: parameters are comma-separated. A missing comma is a parse error in # both named functions and lambdas (it used to run as if written). nl is "\n" -assert_eq of [try_parse of ("define sub(a b) as:" + nl + " return a - b"), 0, "fn: missing comma"] -assert_eq of [try_parse of ("define sub(a is 7 b is 2) as:" + nl + " return a - b"), 0, "fn: missing comma between defaults"] -assert_eq of [try_parse of ("define sub(a, b c) as:" + nl + " return a"), 0, "fn: missing comma after a comma"] -assert_eq of [try_parse of ("define sub(a, b is 1 c is 2) as:" + nl + " return a"), 0, "fn: missing comma between defaults after a comma"] -assert_eq of [try_parse of "sub is (a b) => a - b", 0, "lambda: missing comma"] -assert_eq of [try_parse of "sub is (a, b c) => a", 0, "lambda: missing comma after a comma"] +assert_eq of [try_parse of ("define sub(a b) as:" + nl + " return a - b"), false, "fn: missing comma"] +assert_eq of [try_parse of ("define sub(a is 7 b is 2) as:" + nl + " return a - b"), false, "fn: missing comma between defaults"] +assert_eq of [try_parse of ("define sub(a, b c) as:" + nl + " return a"), false, "fn: missing comma after a comma"] +assert_eq of [try_parse of ("define sub(a, b is 1 c is 2) as:" + nl + " return a"), false, "fn: missing comma between defaults after a comma"] +assert_eq of [try_parse of "sub is (a b) => a - b", false, "lambda: missing comma"] +assert_eq of [try_parse of "sub is (a, b c) => a", false, "lambda: missing comma after a comma"] # controls: separated, trailing-comma and implicit-n forms stay valid -assert_eq of [try_parse of ("define sub(a, b) as:" + nl + " return a - b"), 1, "fn: commas"] -assert_eq of [try_parse of ("define sub(a is 7, b is 2) as:" + nl + " return a - b"), 1, "fn: defaults with commas"] -assert_eq of [try_parse of ("define sub(a, b,) as:" + nl + " return a - b"), 1, "fn: trailing comma"] -assert_eq of [try_parse of ("define sub() as:" + nl + " return n"), 1, "fn: empty signature"] -assert_eq of [try_parse of "sub is (a, b) => a - b", 1, "lambda: commas"] -assert_eq of [try_parse of "sub is (a, b,) => a - b", 1, "lambda: trailing comma"] -assert_eq of [try_parse of "sub is () => n", 1, "lambda: empty signature"] -assert_eq of [try_parse of ("define sub(a is 7, b) as:" + nl + " return a"), 0, "fn: required after default still rejected"] +assert_eq of [try_parse of ("define sub(a, b) as:" + nl + " return a - b"), true, "fn: commas"] +assert_eq of [try_parse of ("define sub(a is 7, b is 2) as:" + nl + " return a - b"), true, "fn: defaults with commas"] +assert_eq of [try_parse of ("define sub(a, b,) as:" + nl + " return a - b"), true, "fn: trailing comma"] +assert_eq of [try_parse of ("define sub() as:" + nl + " return n"), true, "fn: empty signature"] +assert_eq of [try_parse of "sub is (a, b) => a - b", true, "lambda: commas"] +assert_eq of [try_parse of "sub is (a, b,) => a - b", true, "lambda: trailing comma"] +assert_eq of [try_parse of "sub is () => n", true, "lambda: empty signature"] +assert_eq of [try_parse of ("define sub(a is 7, b) as:" + nl + " return a"), false, "fn: required after default still rejected"] define sep_sub(a is 7, b is 2) as: return a - b assert_eq of [sep_sub of [], 5, "defaults still apply"] diff --git a/tests/test_named_predicates.eigs b/tests/test_named_predicates.eigs index 3391a698..e5cc9862 100644 --- a/tests/test_named_predicates.eigs +++ b/tests/test_named_predicates.eigs @@ -12,8 +12,8 @@ loop while i < 40: x is x * 0.5 i is i + 1 y is 999.0 -assert_eq of [converged of x, 1, "converged of x reads x (settled)"] -assert_eq of [converged, 0, "bare converged reads the last-observed binding (y)"] +assert_eq of [converged of x, true, "converged of x reads x (settled)"] +assert_eq of [converged, false, "bare converged reads the last-observed binding (y)"] # 2. A named-predicate loop condition is rate-dependent and self-terminating — # no global-alias auto-stall hijack from the trailing counter `k`. @@ -28,10 +28,10 @@ assert of [(settle of 0.5) != (settle of 0.95), "settle is rate-dependent (named # 3. all six predicates accept the named form a is 1.0 -assert_eq of [type of (stable of a), "num", "stable of x"] -assert_eq of [type of (improving of a), "num", "improving of x"] -assert_eq of [type of (oscillating of a), "num", "oscillating of x"] -assert_eq of [type of (diverging of a), "num", "diverging of x"] -assert_eq of [type of (equilibrium of a), "num", "equilibrium of x"] +assert_eq of [type of (stable of a), "bool", "stable of x"] +assert_eq of [type of (improving of a), "bool", "improving of x"] +assert_eq of [type of (oscillating of a), "bool", "oscillating of x"] +assert_eq of [type of (diverging of a), "bool", "diverging of x"] +assert_eq of [type of (equilibrium of a), "bool", "equilibrium of x"] test_summary of null diff --git a/tests/test_native_fn.c b/tests/test_native_fn.c index 0aea5f74..fb81d837 100644 --- a/tests/test_native_fn.c +++ b/tests/test_native_fn.c @@ -15,7 +15,7 @@ static void check(int ok, const char *what) { if (!ok) fail = 1; } static Value *probe_add1(Value *a) { - double d = (a && a->type == VAL_NUM) ? a->data.num : 0.0; + double d = (a && a->type == VAL_NUM) ? VAL_NUM_RAW(a) : 0.0; return make_num(d + 1); } static Value *probe_plain(Value *a) { (void)a; return make_num(0); } diff --git a/tests/test_net.eigs b/tests/test_net.eigs index bb425501..dfccdce4 100644 --- a/tests/test_net.eigs +++ b/tests/test_net.eigs @@ -7,15 +7,15 @@ # ---- listen on an ephemeral port ---- lid is net_listen of 0 -assert of [(lid == null) == 0, "NET01 net_listen returns a handle"] +assert of [(not (lid == null)), "NET01 net_listen returns a handle"] port is net_port of lid assert of [port > 0, "NET02 net_port reports the kernel's ephemeral pick"] # ---- dial + accept ---- cid is net_dial of ["127.0.0.1", port] -assert of [(cid == null) == 0, "NET03 net_dial connects via the backlog"] +assert of [(not (cid == null)), "NET03 net_dial connects via the backlog"] sid is net_accept of [lid, 2000] -assert of [(sid == null) == 0, "NET04 net_accept returns a connection"] +assert of [(not (sid == null)), "NET04 net_accept returns a connection"] # ---- string send, byte-buffer recv, round-trip ---- n1 is net_send of [cid, "hello, tape"] diff --git a/tests/test_numeric_guard.eigs b/tests/test_numeric_guard.eigs index 81ed7b17..f13ca9b9 100644 --- a/tests/test_numeric_guard.eigs +++ b/tests/test_numeric_guard.eigs @@ -82,33 +82,33 @@ assert of [unobserved_inf == CAP, "NG19 unobserved reassignment literal caps"] clear_math_flags of null f0 is math_flags of null -assert of [f0.overflow == 0, "NG20 flags start clear"] -assert of [f0.invalid == 0, "NG20 invalid starts clear"] +assert of [(not f0.overflow), "NG20 flags start clear"] +assert of [(not f0.invalid), "NG20 invalid starts clear"] # Ordinary arithmetic never sets them. ordinary is (2.5 * 4.0) + (1.0 / 8.0) f1 is math_flags of null -assert of [f1.overflow == 0, "NG21 ordinary arithmetic sets no flag"] -assert of [f1.invalid == 0, "NG21 ordinary arithmetic sets no invalid flag"] +assert of [(not f1.overflow), "NG21 ordinary arithmetic sets no flag"] +assert of [(not f1.invalid), "NG21 ordinary arithmetic sets no invalid flag"] # Saturation sets overflow. clear_math_flags of null sat is 1e308 * 10 assert of [sat == CAP, "NG22 saturation still saturates (result unchanged)"] -assert of [(math_flags of null).overflow == 1, "NG22 saturation is now detectable"] +assert of [(math_flags of null).overflow, "NG22 saturation is now detectable"] # The associativity case from the issue: the RESULT is unchanged and still # implausible-looking-but-plausible, and it is now flagged. clear_math_flags of null reassoc is (1e300 * 1e300) / 1e300 assert of [reassoc == 100000000, "NG23 reassociated result is unchanged"] -assert of [(math_flags of null).overflow == 1, "NG23 but the contamination is visible"] +assert of [(math_flags of null).overflow, "NG23 but the contamination is visible"] # The other association order neither overflows nor flags. clear_math_flags of null other is 1e300 * (1e300 / 1e300) assert of [other == 1e300, "NG24 the other order is exact"] -assert of [(math_flags of null).overflow == 0, "NG24 and sets no flag"] +assert of [not (math_flags of null).overflow, "NG24 and sets no flag"] # Flags are STICKY across statements until cleared — that is what makes # clear/compute/check work the way it does on an FPU. @@ -117,9 +117,9 @@ sticky is 1e308 * 10 a is 1 b is 2 c is a + b -assert of [(math_flags of null).overflow == 1, "NG25 the flag survives later clean arithmetic"] +assert of [(math_flags of null).overflow, "NG25 the flag survives later clean arithmetic"] clear_math_flags of null -assert of [(math_flags of null).overflow == 0, "NG25 and clears on request"] +assert of [not (math_flags of null).overflow, "NG25 and clears on request"] # A NaN cannot be produced by ARITHMETIC here — there is no way to obtain an # Inf to combine (an over-range literal is capped before it is ever an @@ -128,7 +128,7 @@ assert of [(math_flags of null).overflow == 0, "NG25 and clears on request"] clear_math_flags of null capped_operand is 0.0 * 1e400 assert of [capped_operand == 0, "NG26 an over-range literal is capped before use"] -assert of [(math_flags of null).invalid == 0, "NG26 and produces no NaN to collapse"] +assert of [not (math_flags of null).invalid, "NG26 and produces no NaN to collapse"] # But a NaN DOES arrive from outside arithmetic — string conversion is the # in-language route, and it was the quietest case of all: `num of "nan"` is 0 @@ -138,23 +138,23 @@ assert of [(math_flags of null).invalid == 0, "NG26 and produces no NaN to colla clear_math_flags of null parsed_nan is num of "nan" assert of [parsed_nan == 0, "NG30 num of \"nan\" still collapses to 0"] -assert of [(math_flags of null).invalid == 1, "NG30 and is no longer confusable with a real 0"] +assert of [(math_flags of null).invalid, "NG30 and is no longer confusable with a real 0"] clear_math_flags of null parsed_inf is num of "inf" assert of [parsed_inf == CAP, "NG30 num of \"inf\" still saturates"] -assert of [(math_flags of null).overflow == 1, "NG30 and is no longer confusable with a real 1e308"] +assert of [(math_flags of null).overflow, "NG30 and is no longer confusable with a real 1e308"] # A plain unparseable string is 0 by the documented parse rule, not a clamp. clear_math_flags of null parsed_junk is num of "abc" assert of [parsed_junk == 0, "NG30 num of a non-numeric string is 0"] -assert of [(math_flags of null).invalid == 0, "NG30 which is a parse result, not a clamp"] +assert of [not (math_flags of null).invalid, "NG30 which is a parse result, not a clamp"] clear_math_flags of null parsed_ok is num of "42" assert of [parsed_ok == 42, "NG30 an ordinary parse is exact"] -assert of [(math_flags of null).invalid + (math_flags of null).overflow == 0, "NG30 and clean"] +assert of [not ((math_flags of null).invalid or (math_flags of null).overflow), "NG30 and clean"] # The domain clamps are substitutions, not the two clamps the contract named. # Each keeps its value and each now says so. @@ -165,32 +165,32 @@ lt is log of 1e-15 assert of [(abs of (lt - (0 - 34.538776394910684))) < 1e-9, "NG27a log of 1e-15 is exact, not floored"] l10 is log of 1e-10 assert of [(abs of (l10 - (0 - 23.025850929940457))) < 1e-9, "NG27b log of 1e-10 is exact"] -assert of [(math_flags of null).invalid == 0, "NG27c a tiny positive input does not flag invalid"] +assert of [not (math_flags of null).invalid, "NG27c a tiny positive input does not flag invalid"] l0 is log of 0 -assert of [(math_flags of null).invalid == 1, "NG27 log of 0 flags invalid"] +assert of [(math_flags of null).invalid, "NG27 log of 0 flags invalid"] clear_math_flags of null lok is log of 1 assert of [lok == 0, "NG27 log of 1 is 0"] -assert of [(math_flags of null).invalid == 0, "NG27 an in-domain log is clean"] +assert of [not (math_flags of null).invalid, "NG27 an in-domain log is clean"] clear_math_flags of null s_neg is sqrt of (0 - 1) assert of [s_neg == 0, "NG28 sqrt of a negative still returns 0"] -assert of [(math_flags of null).invalid == 1, "NG28 and is no longer confusable with sqrt of 0"] +assert of [(math_flags of null).invalid, "NG28 and is no longer confusable with sqrt of 0"] clear_math_flags of null s_ok is sqrt of 4 assert of [s_ok == 2, "NG28 an in-domain sqrt is exact"] -assert of [(math_flags of null).invalid == 0, "NG28 and clean"] +assert of [not (math_flags of null).invalid, "NG28 and clean"] clear_math_flags of null a5 is asin of 5 -assert of [(math_flags of null).invalid == 1, "NG29 asin of an out-of-domain argument flags"] +assert of [(math_flags of null).invalid, "NG29 asin of an out-of-domain argument flags"] clear_math_flags of null ah is asin of 0.5 -assert of [(math_flags of null).invalid == 0, "NG29 an in-domain asin is clean"] +assert of [not (math_flags of null).invalid, "NG29 an in-domain asin is clean"] clear_math_flags of null ac is acos of (0 - 9) -assert of [(math_flags of null).invalid == 1, "NG29 acos clamps and flags"] +assert of [(math_flags of null).invalid, "NG29 acos clamps and flags"] # #1417: tensor kernels may hold an intermediate IEEE infinity in their # unboxed result buffer. Every language-level read must expose the numeric @@ -223,17 +223,17 @@ nan_right[1] is 0 - 1e200 raw_nan_product is matmul of [nan_left, nan_right] clear_math_flags of null assert of [raw_nan_product[0] == 0, "NG32 indexed NaN read collapses to zero"] -assert of [(math_flags of null).invalid == 1, "NG32 indexed NaN read sets invalid"] +assert of [(math_flags of null).invalid, "NG32 indexed NaN read sets invalid"] clear_math_flags of null assert of [(buf_get of [raw_nan_product, 0]) == 0, "NG33 buf_get NaN read collapses to zero"] -assert of [(math_flags of null).invalid == 1, "NG33 buf_get NaN read sets invalid"] +assert of [(math_flags of null).invalid, "NG33 buf_get NaN read sets invalid"] clear_math_flags of null assert of [(get_at of [raw_nan_product, 0]) == 0, "NG34 get_at NaN read collapses to zero"] -assert of [(math_flags of null).invalid == 1, "NG34 get_at NaN read sets invalid"] +assert of [(math_flags of null).invalid, "NG34 get_at NaN read sets invalid"] clear_math_flags of null nan_gathered is gather of [raw_nan_product, [0]] assert of [nan_gathered[0] == 0, "NG35 gather NaN read collapses to zero"] -assert of [(math_flags of null).invalid == 1, "NG35 gather NaN read sets invalid"] +assert of [(math_flags of null).invalid, "NG35 gather NaN read sets invalid"] # #1417: structural comparisons and scalar reductions read normalized inputs. pair_left is buffer of [1, 1] @@ -246,27 +246,27 @@ finite_pair is buffer of 2 finite_pair[0] is CAP finite_pair[1] is 0 - CAP assert of [raw_pair == finite_pair, "NG36 structural equality normalizes infinities"] -assert of [(list_contains of [[raw_pair], finite_pair]) == 1, "NG36 list_contains shares normalized equality"] +assert of [(list_contains of [[raw_pair], finite_pair]), "NG36 list_contains shares normalized equality"] assert of [(list_index_of of [[raw_pair], finite_pair]) == 0, "NG36 list_index_of shares normalized equality"] assert of [(sum of raw_pair) == 0, "NG37 sum normalizes before cancellation"] assert of [(mean of raw_pair) == 0, "NG37 mean normalizes before cancellation"] clear_math_flags of null assert of [raw_nan_product == raw_nan_product, "NG38 same-buffer equality normalizes NaN"] -assert of [(math_flags of null).invalid == 1, "NG38 equality exposes invalid"] +assert of [(math_flags of null).invalid, "NG38 equality exposes invalid"] zero_one is buffer of 1 clear_math_flags of null assert of [raw_nan_product == zero_one, "NG39 distinct-buffer equality normalizes NaN"] -assert of [(math_flags of null).invalid == 1, "NG39 equality sets invalid"] +assert of [(math_flags of null).invalid, "NG39 equality sets invalid"] zero_pair is buffer of 2 clear_math_flags of null assert of [(dot of [raw_pair, zero_pair]) == 0, "NG40 dot normalizes before multiplying by zero"] -assert of [(math_flags of null).invalid == 0, "NG40 normalized dot does not create NaN"] +assert of [not (math_flags of null).invalid, "NG40 normalized dot does not create NaN"] clear_math_flags of null assert of [(buf_dot of [raw_pair, zero_pair, 0, 0, 2]) == 0, "NG41 buf_dot normalizes each operand"] -assert of [(math_flags of null).invalid == 0, "NG41 normalized buf_dot does not create NaN"] +assert of [not (math_flags of null).invalid, "NG41 normalized buf_dot does not create NaN"] clear_math_flags of null assert of [(buf_peak of [raw_nan_product, 0, 1]) == 0, "NG42 buf_peak reads NaN as zero"] -assert of [(math_flags of null).invalid == 1, "NG42 buf_peak does not ignore NaN"] +assert of [(math_flags of null).invalid, "NG42 buf_peak does not ignore NaN"] # Ordinary finite/empty controls preserve the pre-existing reduction policies. finite is buffer of 2 @@ -301,13 +301,13 @@ assert of [(mean of ([raw_pair])) == 0, "NG49 nested mean normalizes before canc assert of [(norm of ([raw_pair])) == (norm of raw_pair), "NG49 nested norm uses normalized inputs"] clear_math_flags of null assert of [(sum of ([raw_nan_product])) == 0, "NG50 nested NaN sum is zero"] -assert of [(math_flags of null).invalid == 1, "NG50 nested sum sets invalid"] +assert of [(math_flags of null).invalid, "NG50 nested sum sets invalid"] clear_math_flags of null assert of [(mean of ([raw_nan_product])) == 0, "NG50 nested NaN mean is zero"] -assert of [(math_flags of null).invalid == 1, "NG50 nested mean sets invalid"] +assert of [(math_flags of null).invalid, "NG50 nested mean sets invalid"] clear_math_flags of null assert of [(norm of ([raw_nan_product])) == 0, "NG50 nested NaN norm is zero"] -assert of [(math_flags of null).invalid == 1, "NG50 nested norm sets invalid"] +assert of [(math_flags of null).invalid, "NG50 nested norm sets invalid"] assert of [(sum of ([finite])) == 7, "NG51 nested finite sum"] assert of [(mean of ([finite])) == 3.5, "NG51 nested finite mean"] assert of [(norm of ([finite])) == 5, "NG51 nested finite norm"] @@ -321,13 +321,13 @@ index_dst[0] is 10 clear_math_flags of null scatter_add of [index_dst, raw_nan_product, 5] assert of [index_dst[0] == 15, "NG52 non-strict scatter NaN index selects zero"] -assert of [(math_flags of null).invalid == 1, "NG52 scatter index read sets invalid"] +assert of [(math_flags of null).invalid, "NG52 scatter index read sets invalid"] index_matrix is buffer of [1, 1] index_matrix[0] is 7 clear_math_flags of null index_selected is gather of [index_matrix, raw_nan_product] assert of [index_selected[0] == 7, "NG53 non-strict gather NaN index selects zero"] -assert of [(math_flags of null).invalid == 1, "NG53 gather index read sets invalid"] +assert of [(math_flags of null).invalid, "NG53 gather index read sets invalid"] # Numeric byte decoders normalize before converting, preserving finite byte # truncation/modulo behavior and the buffer's raw work-area representation. @@ -335,21 +335,21 @@ assert of [(str_from_bytes of (buf_from_list of [65.75, 321, -191])) == "AAA", " assert of [(f64_from_bytes of (buf_from_list of (f64_to_bytes of 12.5))) == 12.5, "NG54 finite f64 byte round trip"] clear_math_flags of null assert of [(str_from_bytes of raw_nan_product) == "", "NG55 non-strict NaN byte is NUL"] -assert of [(math_flags of null).invalid == 1, "NG55 string decoder observes invalid read"] +assert of [(math_flags of null).invalid, "NG55 string decoder observes invalid read"] clear_math_flags of null assert of [(f64_from_bytes of raw_nan_product) == 0, "NG56 non-strict NaN byte decodes zero"] -assert of [(math_flags of null).invalid == 1, "NG56 f64 decoder observes invalid read"] +assert of [(math_flags of null).invalid, "NG56 f64 decoder observes invalid read"] clear_math_flags of null assert of [(str_from_bytes of raw_pair) == "", "NG57 normalized infinity wraps to byte zero"] -assert of [(math_flags of null).overflow == 1, "NG57 byte decoder observes overflow read"] +assert of [(math_flags of null).overflow, "NG57 byte decoder observes overflow read"] clear_math_flags of null encoded_nan is buf_to_pcm16le of [raw_nan_product, 0, 1] assert of [encoded_nan == (buf_from_list of [0, 0]), "NG58 non-strict NaN PCM sample is silence"] -assert of [(math_flags of null).invalid == 1, "NG58 PCM encoder observes invalid read"] +assert of [(math_flags of null).invalid, "NG58 PCM encoder observes invalid read"] clear_math_flags of null encoded_infinity is buf_to_pcm16le of [raw_pair, 0, 2] assert of [encoded_infinity == (buf_from_list of [255, 127, 1, 128]), "NG59 normalized PCM infinities clamp to samples"] -assert of [(math_flags of null).overflow == 1, "NG59 PCM encoder observes overflow read"] +assert of [(math_flags of null).overflow, "NG59 PCM encoder observes overflow read"] # Mixed buffer/list results box each buffer element before arithmetic. # Shaped buffers keep their matrix rows when boxed into nested lists. @@ -365,7 +365,7 @@ assert of [(add of [[1, 2], finite]) == [4, 6], "NG60 finite mixed tensor right" assert of [(add of [empty, []]) == [], "NG60 empty mixed tensor"] clear_math_flags of null assert of [(add of [raw_nan_product, [2]]) == [[2]], "NG61 non-strict mixed NaN is zero"] -assert of [(math_flags of null).invalid == 1, "NG61 mixed materialization observes invalid"] +assert of [(math_flags of null).invalid, "NG61 mixed materialization observes invalid"] assert of [(add of [raw_pair, [0, 0]]) == [[CAP, 0 - CAP]], "NG62 mixed materialization clamps both infinity signs"] print of "All numeric-guard tests passed" diff --git a/tests/test_observer_coherence.eigs b/tests/test_observer_coherence.eigs index 0634c349..ad33d0e5 100644 --- a/tests/test_observer_coherence.eigs +++ b/tests/test_observer_coherence.eigs @@ -40,7 +40,7 @@ loop while i < 12: # but it no longer vetoes the settle verdict, which is about motion, not # information content. check of ["flat run AT 1.0 certifies converged (value route)", (report of y) == "converged"] -check of ["converged predicate fires at the horizon too", (converged of y) == 1] +check of ["converged predicate fires at the horizon too", (converged of y)] check of ["the horizon property lives in the measurement: H(1.0) is max", (where is y) == 1.0] # a genuine convergence to the home region still classifies converged diff --git a/tests/test_observer_interactions.eigs b/tests/test_observer_interactions.eigs index 54a563fa..29ed0b39 100644 --- a/tests/test_observer_interactions.eigs +++ b/tests/test_observer_interactions.eigs @@ -11,9 +11,9 @@ loop while not converged: x is x * 0.1 # Check mutual exclusion of predicates -assert of [converged == 1, "OI1 converged after descent"] -assert of [diverging == 0, "OI2 converged implies not diverging"] -assert of [improving == 0, "OI3 converged implies not improving (dH~0)"] +assert of [converged, "OI1 converged after descent"] +assert of [(not diverging), "OI2 converged implies not diverging"] +assert of [(not improving), "OI3 converged implies not improving (dH~0)"] # === OBSERVER STATE THROUGH FUNCTION CALLS === @@ -105,7 +105,7 @@ assert of [ob == null, "OI15 freed observed value cleared safely"] ob2 is buffer of 2 ob2 is null p is converged -assert of [(p == 0) or (p == 1), "OI16 predicate after observed free"] +assert of [(p == false) or (p == true), "OI16 predicate after observed free"] # === REGRESSION FOR #187 === # `report` switches improving/diverging at dh_small; the bare predicates @@ -144,13 +144,13 @@ stb_g is preds[2] rep_g is preds[3] # Mutual exclusion within {stable, improving, diverging}. -assert of [not (stb_g == 1 and imp_g == 1), "OI17 stable + improving must not collide in gray band (#187)"] -assert of [not (stb_g == 1 and div_g == 1), "OI18 stable + diverging must not collide in gray band (#187)"] +assert of [not (stb_g and imp_g), "OI17 stable + improving must not collide in gray band (#187)"] +assert of [not (stb_g and div_g), "OI18 stable + diverging must not collide in gray band (#187)"] # Predicates must agree with `report`. if rep_g == "stable": - assert of [imp_g == 0, "OI19 report=stable => improving=0 (#187)"] - assert of [div_g == 0, "OI20 report=stable => diverging=0 (#187)"] + assert of [not imp_g, "OI19 report=stable => improving=0 (#187)"] + assert of [not div_g, "OI20 report=stable => diverging=0 (#187)"] @@ -181,7 +181,7 @@ unobserved: # lint: allow W020 -- promotion of a block-local name is the point assert of [plain895 == 4.5, "OI-895 the plain read still works"] assert of [rep895 == "moving", "OI-895 report of a promoted name resolves"] assert of [repv895 == "moving", "OI-895 report_value of a promoted name resolves"] -assert of [traj895.observed == 1, "OI-895 trajectory of a promoted name resolves"] +assert of [traj895.observed, "OI-895 trajectory of a promoted name resolves"] assert of [obs895[0] == "moving", "OI-895 observe of a promoted name resolves"] # Same block, but the name escapes it, so nothing is promoted and the diff --git a/tests/test_observer_level_set.eigs b/tests/test_observer_level_set.eigs index 46e0c245..c10fd559 100644 --- a/tests/test_observer_level_set.eigs +++ b/tests/test_observer_level_set.eigs @@ -54,7 +54,7 @@ print of ("sign-flip: entropy=" + ent1 + " report=" + rep1 + " oscillating=" + if not (rep1 == "oscillating"): pass is 0 print of ("FAIL: `report` missed a sign-flip oscillator on the level set, got " + rep1) -if not (osc1 == 1): +if not (osc1 == true): pass is 0 print of "FAIL: `oscillating of x` did not fire on a sign-flip oscillator" @@ -67,7 +67,7 @@ print of ("reciprocal: entropy=" + ent2 + " report=" + rep2 + " oscillating=" + if not (rep2 == "oscillating"): pass is 0 print of ("FAIL: `report` missed a reciprocal oscillator on the level set, got " + rep2) -if not (osc2 == 1): +if not (osc2 == true): pass is 0 print of "FAIL: `oscillating of y` did not fire on a reciprocal oscillator" diff --git a/tests/test_observer_park.eigs b/tests/test_observer_park.eigs index 6140cce1..017bd485 100644 --- a/tests/test_observer_park.eigs +++ b/tests/test_observer_park.eigs @@ -43,7 +43,7 @@ define many() as: local b1 is many of null local b2 is many of null -if (a1 == 0) and (a2 == 0) and (a3 == 0) and (b1 == 1) and (b2 == 1): +if (not a1) and (not a2) and (not a3) and b1 and b2: print of "OBS_PARK_OK" else: print of "OBS_PARK_FAIL a=" diff --git a/tests/test_observer_park_storage.eigs b/tests/test_observer_park_storage.eigs index 7e7d893b..c6fb7ca7 100644 --- a/tests/test_observer_park_storage.eigs +++ b/tests/test_observer_park_storage.eigs @@ -20,8 +20,8 @@ define stale_history_probe() as: local stale_first is stale_history_probe of null local stale_second is stale_history_probe of null -assert_eq of [stale_first, 0, "stale-history first cached-path call"] -assert_eq of [stale_second, 0, "stale-history/override actual cached-call path"] +assert_eq of [stale_first, false, "stale-history first cached-path call"] +assert_eq of [stale_second, false, "stale-history/override actual cached-call path"] local probe_mode is 0 local probe_seed is 0 @@ -43,7 +43,14 @@ define probe(x) as: x is seed y is seed + 100 y is seed + 100 - return (converged of x) * 1000 + (converged of y) * 100 + (get_observer_window of "x") + # #1637: converged is a bool; encode it as a digit explicitly. + local cx is 0 + if converged of x: + cx is 1 + local cy is 0 + if converged of y: + cy is 1 + return cx * 1000 + cy * 100 + (get_observer_window of "x") # First fill a short overridden window. The next two calls are the named # stale-history/override assertion: each has fewer than ten observed x samples, so a fresh @@ -88,7 +95,10 @@ define unwind_probe(a) as: a is seed if should_throw: throw of "ordinary unwind" - return (converged of a) * 100 + (get_observer_window of "a") + local ca is 0 + if converged of a: + ca is 1 + return ca * 100 + (get_observer_window of "a") # Park a real normal call first. The next throwing call must take that cache; # after the throw destroys its env, the following normal call starts fresh. diff --git a/tests/test_observer_saturation.eigs b/tests/test_observer_saturation.eigs index ad8fe119..32cdaebd 100644 --- a/tests/test_observer_saturation.eigs +++ b/tests/test_observer_saturation.eigs @@ -78,15 +78,15 @@ if not (r[0] == "diverging"): fail of ("entropy channel: expected 'diverging', got " + r[0]) if not (r[1] == "diverging"): fail of ("value channel: expected 'diverging', got " + r[1]) -if not (r[2] == 0): +if not (r[2] == false): fail of "converged certified a saturated value" -if not (r[3] == 1): +if not (r[3] == true): fail of "diverging did not claim a saturated value" -if not (r[4] == 0): +if not (r[4] == false): fail of "stable certified a saturated value" -if not (r[5] == 0): +if not (r[5] == false): fail of "equilibrium certified a saturated value" -if not (r[6] == 0): +if not (r[6] == false): fail of "improving claimed a saturated value" # --- Case 2: the negative ceiling behaves identically. @@ -96,7 +96,7 @@ if not (n[2] == (0 - 1e308)): fail of "negative runaway did not reach -1e308" if not (n[0] == "diverging"): fail of ("negative ceiling: expected 'diverging', got " + n[0]) -if not (n[1] == 1): +if not (n[1] == true): fail of "diverging did not claim the negative ceiling" # --- Case 3: genuine convergence is untouched (the non-regression half). @@ -104,7 +104,7 @@ c is run_converge of null print of ("decay: report=" + c[0]) if not (c[0] == "converged"): fail of ("geometric decay lost its 'converged' verdict, got " + c[0]) -if not (c[1] == 1): +if not (c[1] == true): fail of "converged predicate lost a genuine convergence" # --- Case 4: the gate is tight to the ceiling and does not fire below it. @@ -124,7 +124,7 @@ if not (c[1] == 1): # measurement of what the predicate actually gets right. b is run_below_ceiling of null print of ("below-ceiling: report=" + b[0] + " (verdict not asserted — #861)") -if not (b[2] == 0): +if not (b[2] == false): fail of "the saturation gate fired below the ceiling — it must be tight to 1e308" # --- Case 5: a literal at the ceiling reads diverging, by design. @@ -132,7 +132,7 @@ l is run_literal_ceiling of null print of ("literal-ceiling: report=" + l[0]) if not (l[0] == "diverging"): fail of ("literal at the ceiling: expected 'diverging', got " + l[0]) -if not (l[1] == 0): +if not (l[1] == false): fail of "converged certified a literal at the ceiling" if pass == 1: diff --git a/tests/test_observer_slots.eigs b/tests/test_observer_slots.eigs index 3db72aa3..919c056a 100644 --- a/tests/test_observer_slots.eigs +++ b/tests/test_observer_slots.eigs @@ -19,7 +19,7 @@ i is 0 loop while i < 14: observer_slots.feed of [0, 5.0] i is i + 1 -check of ["wedged constant reads stable", observer_slots.is_stable of (0), 1] +check of ["wedged constant reads stable", observer_slots.is_stable of (0), true] # slot 1: growing magnitude -> NOT stable (an advancing signal keeps moving) v is 5.0 @@ -28,7 +28,7 @@ loop while i < 14: observer_slots.feed of [1, v] v is v * 2 i is i + 1 -check of ["growing signal is not stable", observer_slots.is_stable of (1), 0] +check of ["growing signal is not stable", observer_slots.is_stable of (1), false] # slot 3: entropy RISING (magnitude shrinking toward |x|=1 from above, # never crossing) -> diverging per the lattice @@ -41,10 +41,10 @@ loop while i < 14: # #861: v*0.6 shrinking toward ~0.32 is a CONTRACTING approach — the value # route reads improving, not diverging (the old expectation was the # entropy-rising artifact: H grows as |x| falls toward 1). -check of ["shrinking-toward-1 signal is not diverging", observer_slots.is_diverging of (3), 0] +check of ["shrinking-toward-1 signal is not diverging", observer_slots.is_diverging of (3), false] # independence: slot 0's verdict survived slot 1's activity -check of ["slot 0 verdict independent of slot 1", observer_slots.is_stable of (0), 1] +check of ["slot 0 verdict independent of slot 1", observer_slots.is_stable of (0), true] # feeding through a LIST (the shape callers keep) still lands on named slots sigs is [7.5, 7.5, 7.5] @@ -52,7 +52,7 @@ i is 0 loop while i < 14: observer_slots.feed of [2, sigs[i % 3]] i is i + 1 -check of ["list-sourced feed reads stable", observer_slots.is_stable of (2), 1] +check of ["list-sourced feed reads stable", observer_slots.is_stable of (2), true] caught is 0 try: @@ -72,7 +72,7 @@ loop while i < 14: observer_slots.feed of [4, dv] i is i + 1 check of ["verdict reads the slot regime (improving)", observer_slots.verdict of (4), "improving"] -check of ["verdict agrees with is_diverging (0)", observer_slots.is_diverging of (4), 0] +check of ["verdict agrees with is_diverging (0)", observer_slots.is_diverging of (4), false] # slot 5: wedged constant -> converged (#861: the value route certifies a # settled value at ANY magnitude — 9.0 was in the entropy dead zone, where # `converged` could never fire and "equilibrium" was the strongest label) @@ -81,7 +81,7 @@ loop while i < 14: observer_slots.feed of [5, 9.0] i is i + 1 check of ["verdict on a wedged constant is converged", observer_slots.verdict of (5), "converged"] -check of ["...and that same slot reads stable", observer_slots.is_stable of (5), 1] +check of ["...and that same slot reads stable", observer_slots.is_stable of (5), true] vcaught is 0 try: observer_slots.verdict of (8) diff --git a/tests/test_opaque_fn.eigs b/tests/test_opaque_fn.eigs index 6f55effc..bcb5c948 100644 --- a/tests/test_opaque_fn.eigs +++ b/tests/test_opaque_fn.eigs @@ -17,9 +17,9 @@ f is a f is b assert_true of [(report of f) == "opaque", "OP01 report of a fn binding is opaque"] assert_true of [(report_value of f) == "opaque", "OP02 report_value is opaque too"] -assert_true of [(equilibrium of f) == 0, "OP03 equilibrium predicate is false"] -assert_true of [(converged of f) == 0, "OP04 converged predicate is false"] -assert_true of [(stable of f) == 0, "OP05 stable predicate is false"] +assert_true of [(not (equilibrium of f)), "OP03 equilibrium predicate is false"] +assert_true of [(not (converged of f)), "OP04 converged predicate is false"] +assert_true of [(not (stable of f)), "OP05 stable predicate is false"] obs is observe of f assert_true of [obs[0] == "opaque", "OP06 observe band is opaque"] @@ -36,18 +36,18 @@ assert_true of [(report of held_fn) == "opaque", "OP09 extracted fn binding is o # ---- bare predicate reads the last-observed binding ---- f2 is a f2 is b -assert_true of [equilibrium == 0, "OP10 bare predicate on a fn binding is false"] +assert_true of [(not equilibrium), "OP10 bare predicate on a fn binding is false"] # ---- the check is ask-time: rebinding to a number answers again ---- f is 42 -assert_true of [((report of f) == "opaque") == 0, "OP11 number rebind is not opaque"] +assert_true of [(not ((report of f) == "opaque")), "OP11 number rebind is not opaque"] # ---- numeric bindings and containers are untouched ---- n is 1 n is 2 n is 3 -assert_true of [((report of n) == "opaque") == 0, "OP12 numeric binding classifies"] +assert_true of [(not ((report of n) == "opaque")), "OP12 numeric binding classifies"] d is {"h": a} -assert_true of [((report of d) == "opaque") == 0, "OP13 container holding a fn is not opaque"] +assert_true of [(not ((report of d) == "opaque")), "OP13 container holding a fn is not opaque"] test_summary of null diff --git a/tests/test_opcode_abi.c b/tests/test_opcode_abi.c index ae63961a..9eed0e09 100644 --- a/tests/test_opcode_abi.c +++ b/tests/test_opcode_abi.c @@ -30,6 +30,11 @@ ABI_ASSERT(OP_RETURN_NULL, 41); ABI_ASSERT(OP_DICT, 43); ABI_ASSERT(OP_LOOP_CAP_CHECK, 63); ABI_ASSERT(OP_LINE, 68); +/* #1637: the bool literals were appended after OP_SET_LOCAL_INTERNAL (94). + * External producers (ouroboros codegen) emit these numbers directly. */ +ABI_ASSERT(OP_SET_LOCAL_INTERNAL, 94); +ABI_ASSERT(OP_TRUE, 95); +ABI_ASSERT(OP_FALSE, 96); /* #704: the revision the ABI-stamped descriptors in the .eigs fixtures under * tests/ declare. Those diff --git a/tests/test_pcm_codec.eigs b/tests/test_pcm_codec.eigs index fdd149d5..6053e907 100644 --- a/tests/test_pcm_codec.eigs +++ b/tests/test_pcm_codec.eigs @@ -13,7 +13,7 @@ fails is 0 define check(name, ok) as: - if ok == 0: + if not ok: print of f"FAIL: {name}" fails is fails + 1 diff --git a/tests/test_predicate_matrix.eigs b/tests/test_predicate_matrix.eigs index 1997dba9..1013e65d 100644 --- a/tests/test_predicate_matrix.eigs +++ b/tests/test_predicate_matrix.eigs @@ -42,7 +42,7 @@ a is 19.0 a is 99.0 pa is [converged, stable, improving, diverging, oscillating, equilibrium, report of a] # #861: growth 2->99 is a runaway on the value route (raw same-sign non-vanishing); "improving" was the entropy channel reading its own descent -expect of ["improving", pa, 0, 0, 0, 1, 0, 0, "diverging"] +expect of ["improving", pa, false, false, false, true, false, false, "diverging"] # --- diverging only: entropy rising fast (decreasing |x| toward 1) --- b is 99.0 @@ -54,7 +54,7 @@ b is 3.0 b is 2.0 pb is [converged, stable, improving, diverging, oscillating, equilibrium, report of b] # #861: decay 99->2 contracts its steps toward a limit — improving; "diverging" was the entropy-rising artifact -expect of ["diverging", pb, 0, 0, 1, 0, 0, 0, "improving"] +expect of ["diverging", pb, false, false, true, false, false, false, "improving"] # --- oscillating only: small-amplitude sign flips, |dH|~0.0057 in the gray band # (dh_zero < |dH| < dh_small). Needs >=4 flips for windowed `oscillating` @@ -68,7 +68,7 @@ c is 70.0 c is 75.0 c is 70.0 pc is [converged, stable, improving, diverging, oscillating, equilibrium, report of c] -expect of ["oscillating", pc, 0, 0, 0, 0, 1, 0, "oscillating"] +expect of ["oscillating", pc, false, false, false, false, true, false, "oscillating"] # --- stable only (#205): FULL window of steady small drift at high entropy. # Each |dH|~0.006 is in the gray band (< dh_small, > dh_zero), same sign, no @@ -87,7 +87,7 @@ e is 48.0 e is 50.0 pe is [converged, stable, improving, diverging, oscillating, equilibrium, report of e] # #861: a steady drift is a linear runaway on the value route (the #422 additive class the entropy gray band hid) -expect of ["stable", pe, 0, 0, 0, 1, 0, 0, "diverging"] +expect of ["stable", pe, false, false, false, true, false, false, "diverging"] # --- equilibrium + stable (#209/#205): FULL window at rest, HIGH entropy. # Zero-mean low-variance -> equilibrium; quiet + high entropy + no flips -> @@ -107,7 +107,7 @@ g is 2.0 g is 2.0 pg is [converged, stable, improving, diverging, oscillating, equilibrium, report of g] # #861: quiescent lattice — a settled value certifies converged, which implies equilibrium AND stable -expect of ["equilibrium+stable", pg, 1, 1, 0, 0, 0, 1, "converged"] +expect of ["equilibrium+stable", pg, true, true, false, false, false, true, "converged"] # --- converged: low-entropy value, FULL window (>= N=10). converged is a # strict subset of equilibrium, so BOTH fire; report resolves to converged. --- @@ -124,7 +124,7 @@ k is 1000000.0 k is 1000000.0 pk is [converged, stable, improving, diverging, oscillating, equilibrium, report of k] # #861: converged implies stable now (all-under-deadband satisfies both) -expect of ["converged", pk, 1, 1, 0, 0, 0, 1, "converged"] +expect of ["converged", pk, true, true, false, false, false, true, "converged"] # --- EDGE: large-amplitude oscillation is `oscillating` only. Under the # pointwise rule it ALSO co-fired diverging (last dH > dh_small); windowed @@ -140,7 +140,7 @@ d is 2.0 d is 10.0 d is 2.0 pd is [converged, stable, improving, diverging, oscillating, equilibrium, report of d] -expect of ["osc-only (no longer co-fires diverging)", pd, 0, 0, 0, 0, 1, 0, "oscillating"] +expect of ["osc-only (no longer co-fires diverging)", pd, false, false, false, false, true, false, "oscillating"] # --- EDGE: converged at the entropy boundary. FULL window at rest with entropy # JUST BELOW h_low (|x|=100 -> H~0.080 < 0.1) -> converged (and equilibrium, @@ -159,7 +159,7 @@ h is 100.0 h is 100.0 ph is [converged, stable, improving, diverging, oscillating, equilibrium, report of h] # #861: same lattice at the h_low boundary value — the boundary is meaningless on the value route -expect of ["converged-at-boundary", ph, 1, 1, 0, 0, 0, 1, "converged"] +expect of ["converged-at-boundary", ph, true, true, false, false, false, true, "converged"] # --- THRESHOLD KNOB (#861 semantics): dh_zero is the settle deadband on # RELATIVE steps. A decay whose tail steps sit between the default @@ -174,11 +174,11 @@ define tail_decay as: k is k + 1 return converged of x kd is tail_decay of 0 -assert of [kd == 0, f"knob-default: tail steps (~0.0034..0.0096 rel) exceed dh_zero=0.001, converged expected 0 got {kd}"] +assert of [not kd, f"knob-default: tail steps (~0.0034..0.0096 rel) exceed dh_zero=0.001, converged expected 0 got {kd}"] print of " PASS: knob-default (tail decay not yet converged at 0.001)" set_observer_thresholds of [0.01, 0.05, 0.5] kr is tail_decay of 0 -assert of [kr == 1, f"knob-raised: same tail under dh_zero=0.01, converged expected 1 got {kr}"] +assert of [kr, f"knob-raised: same tail under dh_zero=0.01, converged expected 1 got {kr}"] print of " PASS: knob-raised dh_zero -> converged (caller-chosen tolerance)" # --- Perpetual-motion STRUCTURE is threshold-free (#422): a +/-5 swing is @@ -197,8 +197,8 @@ ts is 70.0 ts is 75.0 ts is 70.0 tsr is [oscillating, equilibrium] -assert of [tsr[0] == 1, f"structure-threshold-free: oscillating expected 1 got {tsr[0]}"] -assert of [tsr[1] == 0, f"structure-threshold-free: equilibrium expected 0 got {tsr[1]}"] +assert of [tsr[0], f"structure-threshold-free: oscillating expected 1 got {tsr[0]}"] +assert of [not tsr[1], f"structure-threshold-free: equilibrium expected 0 got {tsr[1]}"] print of " PASS: perpetual swing stays oscillating at raised thresholds (#422)" set_observer_thresholds of [0.001, 0.01, 0.1] @@ -223,13 +223,13 @@ pm20 is [converged of m, stable of m, improving of m, diverging of m, oscillatin # geometric decay is never certified merely for being small). The old # Δv/(1+|v|) certified it here because 8.8e-4 < dh_zero in ABSOLUTE terms — # the same decay stored in millis would have read improving. -expect of ["mid-decay toward zero (#1045)", pm20, 0, 0, 1, 0, 0, 0, "improving"] +expect of ["mid-decay toward zero (#1045)", pm20, false, false, true, false, false, false, "improving"] loop while mi < 30: m is m * 0.5 mi is mi + 1 pm is [converged of m, stable of m, improving of m, diverging of m, oscillating of m, equilibrium of m, report of m] # #861: the value route CERTIFIES this settle — a decay toward zero is exactly what converged should claim once a full window of steps sits under the floor (ten more halvings: steps 4e-7 .. 8e-10). The #735 no-band gray state remains constructible only on the entropy channel (non-numeric bindings) -expect of ["full-window residual drift (#735)", pm, 1, 1, 0, 0, 0, 1, "converged"] +expect of ["full-window residual drift (#735)", pm, true, true, false, false, false, true, "converged"] # --- The full-window agreement guarantee (PREDICATES.md "The report builtin"), # asserted directly instead of in prose. At a full window `report of x` must @@ -242,8 +242,13 @@ define agrees(tag, p) as: local names is ["oscillating", "diverging", "improving", "converged", "equilibrium", "stable"] local idx is [4, 3, 2, 0, 5, 1] # each band's index in p local r is p[6] - local ok is 0 - local n_true is p[0] + p[1] + p[2] + p[3] + p[4] + p[5] + local ok is false + local n_true is 0 + local j is 0 + loop while j < 6: + if p[j]: + n_true is n_true + 1 + j is j + 1 local i is 0 loop while i < 6: if names[i] == r: @@ -251,8 +256,8 @@ define agrees(tag, p) as: i is i + 1 if r == "moving": if n_true == 0: - ok is 1 - assert of [ok == 1, f"{tag}: report={r} is not backed by a true predicate (p={p})"] + ok is true + assert of [ok, f"{tag}: report={r} is not backed by a true predicate (p={p})"] print of f" PASS: full-window agreement {tag} (report={r})" agrees of ["stable", pe] diff --git a/tests/test_proc_stream.eigs b/tests/test_proc_stream.eigs index f9cbddee..4aff1bfb 100644 --- a/tests/test_proc_stream.eigs +++ b/tests/test_proc_stream.eigs @@ -12,9 +12,9 @@ define check(label, got, want) as: # 1. proc_spawn returns a 3-element [pid, in_fd, out_fd] handle for echo. h is proc_spawn of ["/bin/echo", "hello"] check of ["1a", len of h, 3] -check of ["1b", h[0] > 0, 1] -check of ["1c", h[1] >= 0, 1] -check of ["1d", h[2] >= 0, 1] +check of ["1b", h[0] > 0, true] +check of ["1c", h[1] >= 0, true] +check of ["1d", h[2] >= 0, true] # 2. proc_read_line yields the line without the trailing newline. line is proc_read_line of h[2] @@ -59,14 +59,14 @@ chunk is proc_read of [h[2], 3] check of ["6a", chunk, "abc"] rest is proc_read of [h[2], 16] # rest should be "def\n" or whatever remains (read may return fewer than max). -check of ["6b", rest != null, 1] +check of ["6b", rest != null, true] proc_close of h[2] code is proc_wait of h[0] check of ["6c", code, 0] # 7. Nonexistent program: spawn succeeds, child _exit(127) → exit code 127. h is proc_spawn of (["/no/such/binary/anywhere"]) -check of ["7a", h[0] > 0, 1] +check of ["7a", h[0] > 0, true] proc_close of h[1] proc_close of h[2] code is proc_wait of h[0] diff --git a/tests/test_read_bytes_cap.sh b/tests/test_read_bytes_cap.sh index a575396b..ed697d02 100755 --- a/tests/test_read_bytes_cap.sh +++ b/tests/test_read_bytes_cap.sh @@ -52,7 +52,7 @@ catch e: has_size is contains of [msg, \"12582912 bytes\"] has_cap is contains of [msg, \"10485760-byte cap\"] print of f\"kind={caught} size={has_size} cap={has_cap}\"" \ -"kind=io size=1 cap=1" +"kind=io size=true cap=true" # 2. Opt-in max_bytes reads the >10 MB file fully. run_case "opt-in [path, max_bytes] reads a >10MB file" \ @@ -108,7 +108,7 @@ print of caught" \ run_case "missing file still returns null" \ "b is read_bytes_buf of \"$TMPDIR/nope.bin\" print of f\"isnull={b == null}\"" \ -"isnull=1" +"isnull=true" # 7. Record/replay: the over-cap raise is re-derived from the tape with # the file DELETED — byte-identical output, no live fs dependence. diff --git a/tests/test_replay.sh b/tests/test_replay.sh index 92671791..ccc6799a 100755 --- a/tests/test_replay.sh +++ b/tests/test_replay.sh @@ -82,17 +82,18 @@ else fi # ---- Dict replay (handcrafted tape — no nondet builtin returns dicts) ---- -# trace_replay_take is lenient on name mismatch (warns, uses anyway), -# so we craft the N record under any nondet name and bind it. +# The record must be a kind its builtin can return (#1637: k_tape_kinds in +# src/trace.c). No core taped builtin returns a dict, so the dict rides inside +# `ls`'s list. cat > "$TMPDIR/p_dict.eigs" <<'EOF' -v is env_get of "EIGS_REPLAY_TEST_KEY_DOES_NOT_EXIST" -print of v +v is ls of "." +print of v[0] EOF cat > "$TMPDIR/dict.tape" </dev/null) @@ -105,7 +106,7 @@ fi # ---- Nested replay (list containing dict and buffer) ---- cat > "$TMPDIR/p_nested.eigs" <<'EOF' -v is env_get of "EIGS_REPLAY_TEST_KEY_NESTED" +v is ls of "." print of v[0] print of v[1]["k"] print of v[2][1] @@ -115,7 +116,7 @@ EOF cat > "$TMPDIR/nested.tape" </dev/null) @@ -250,7 +251,7 @@ EOF MKT_LIVE=$("$EIGS" "$TMPDIR/p_mktemp_live.eigs" 2>&1); MKT_LIVE_RC=$? MKT_PATH=$(printf '%s\n' "$MKT_LIVE" | sed -n '1p') MKT_STATE=$(printf '%s\n' "$MKT_LIVE" | sed -n '2,4p') -if [ "$MKT_LIVE_RC" -eq 0 ] && [ "$MKT_STATE" = $'1\n1\n0' ] \ +if [ "$MKT_LIVE_RC" -eq 0 ] && [ "$MKT_STATE" = $'true\ntrue\nfalse' ] \ && [ ! -e "$MKT_PATH" ]; then ok "mktemp ordinary lifecycle: create, print, and rm" else @@ -402,7 +403,7 @@ REC_ID=$(EIGS_TRACE="$TAPE_D" "$EIGS" "$TMPDIR/p_isdir.eigs" 2>&1) rmdir "$TMPDIR/probe_dir" REP_ID=$(EIGS_REPLAY="$TAPE_D" "$EIGS" "$TMPDIR/p_isdir.eigs" 2>&1) -if [ "$REC_ID" = "1" ] && [ "$REP_ID" = "1" ]; then +if [ "$REC_ID" = "true" ] && [ "$REP_ID" = "true" ]; then ok "is_dir replay: recorded answer wins after the directory is deleted" else fail "is_dir replay" "rec='$REC_ID' rep='$REP_ID'" @@ -419,7 +420,7 @@ REC_IF=$(EIGS_TRACE="$TAPE_F" "$EIGS" "$TMPDIR/p_isfile.eigs" 2>&1) rm -f "$TMPDIR/probe_file" REP_IF=$(EIGS_REPLAY="$TAPE_F" "$EIGS" "$TMPDIR/p_isfile.eigs" 2>&1) LIVE_IF=$("$EIGS" "$TMPDIR/p_isfile.eigs" 2>&1) -if [ "$REC_IF" = "1" ] && [ "$REP_IF" = "1" ] && [ "$LIVE_IF" = "0" ]; then +if [ "$REC_IF" = "true" ] && [ "$REP_IF" = "true" ] && [ "$LIVE_IF" = "false" ]; then ok "is_file replay: recorded answer wins after the file is deleted (live says 0)" else fail "is_file replay" "rec='$REC_IF' rep='$REP_IF' live='$LIVE_IF'" @@ -439,7 +440,7 @@ EOF FE_REC=$(EIGS_TRACE="$TMPDIR/fe.tape" "$EIGS" "$TMPDIR/p_fe.eigs" 2>&1) rm -f "$TMPDIR/fe585/probe" FE_REP=$(EIGS_REPLAY="$TMPDIR/fe.tape" "$EIGS" "$TMPDIR/p_fe.eigs" 2>&1) -if [ "$FE_REC" = "1" ] && [ "$FE_REP" = "1" ]; then +if [ "$FE_REC" = "true" ] && [ "$FE_REP" = "true" ]; then ok "file_exists replay: recorded 1 wins after the file is deleted (#585)" else fail "file_exists replay (#585)" "rec='$FE_REC' rep='$FE_REP'" @@ -467,7 +468,7 @@ EOF MK_REC=$(EIGS_TRACE="$TMPDIR/mk.tape" "$EIGS" "$TMPDIR/p_mk.eigs" 2>&1) rm -rf "$TMPDIR/mk585" MK_REP=$(EIGS_REPLAY="$TMPDIR/mk.tape" "$EIGS" "$TMPDIR/p_mk.eigs" 2>&1) -if [ "$MK_REC" = "1" ] && [ "$MK_REP" = "1" ] && [ ! -d "$TMPDIR/mk585/sub" ]; then +if [ "$MK_REC" = "true" ] && [ "$MK_REP" = "true" ] && [ ! -d "$TMPDIR/mk585/sub" ]; then ok "mkdir replay: serves recorded bit, does NOT re-create the directory (#585)" else fail "mkdir replay (#585)" "rec='$MK_REC' rep='$MK_REP' recreated=$([ -d "$TMPDIR/mk585/sub" ] && echo yes || echo no)" @@ -566,7 +567,7 @@ EOF REP_C=$(SDL_AUDIODRIVER=doesnotexist EIGS_REPLAY="$TAPE_C" "$EIGS" "$TMPDIR/p_cap.eigs" 2>&1) REC_LEN=$(echo "$REC_C" | sed -n '2p') - if [ "$REC_C" = "$REP_C" ] && [ "$(echo "$REC_C" | sed -n '1p')" = "1" ] \ + if [ "$REC_C" = "$REP_C" ] && [ "$(echo "$REC_C" | sed -n '1p')" = "true" ] \ && [ "${REC_LEN:-0}" -gt 0 ] && grep -q '^N [0-9][0-9]* audio_capture_read=b\[' "$TAPE_C"; then ok "capture replay: recorded mic session replays without a device (#579)" else diff --git a/tests/test_sandbox_allow.eigs b/tests/test_sandbox_allow.eigs index 01bb4fae..172f8d00 100644 --- a/tests/test_sandbox_allow.eigs +++ b/tests/test_sandbox_allow.eigs @@ -22,18 +22,18 @@ before is get_observer_thresholds of null desc_set is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["set_observer_thresholds", [0.5, 0.7, 0.9]]] r_set is sandbox_run of [desc_set, 1000] after is get_observer_thresholds of null -escape_blocked is (r_set["ok"] == 0) and (after[0] == before[0]) and (after[1] == before[1]) and (after[2] == before[2]) +escape_blocked is ((not r_set["ok"])) and (after[0] == before[0]) and (after[1] == before[1]) and (after[2] == before[2]) # (b) Other newly-denied dangerous builtins -> blocked (ok == 0). If `exit` were # NOT blocked it would terminate THIS process (uncatchable) — the test reaching # the end is itself proof exit is shadowed. -screen_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CALL,0,0, RETURN], ["screen_clear"]], 1000])["ok"] == 0 -exit_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["exit", 0]], 1000])["ok"] == 0 -seed_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["seed_random", 1]], 1000])["ok"] == 0 -close_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["close_channel", 0]], 1000])["ok"] == 0 +screen_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CALL,0,0, RETURN], ["screen_clear"]], 1000])["ok"] == false +exit_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["exit", 0]], 1000])["ok"] == false +seed_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["seed_random", 1]], 1000])["ok"] == false +close_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["close_channel", 0]], 1000])["ok"] == false # #601: read_bytes_buf grew a bounded opt-in cap ([path, max_bytes]) — the # sandbox posture must NOT widen with it: fs reads stay blocked, bounded or not. -rbb_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["read_bytes_buf", "/etc/hostname"]], 1000])["ok"] == 0 +rbb_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["read_bytes_buf", "/etc/hostname"]], 1000])["ok"] == false # Temporal history is attached to the host thread rather than the sealed # sandbox Env. Reject both the builtin route and direct opcodes, including an @@ -41,25 +41,25 @@ rbb_blk is (sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN] state_at_result is sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["state_at", 1]], 1000] # Pin the descriptor-level temporal rejection, rather than accepting the later # callable-result or blocked-builtin guards as equivalent reasons to fail. -state_at_blk is (state_at_result["ok"] == 0) and (state_at_result["error"]["kind"] == "value") and (state_at_result["error"]["message"] == "sandbox descriptors cannot access temporal history") +state_at_blk is ((not state_at_result["ok"])) and (state_at_result["error"]["kind"] == "value") and (state_at_result["error"]["message"] == "sandbox descriptors cannot access temporal history") temporal_fn is [[GET_NAME,0,0, INTERROGATE_NAMED,6,0,0,0, RETURN], ["secret"], [], 0, "hidden", []] -temporal_nested_blk is (sandbox_run of [[ABI, [RETURN_NULL], [], [temporal_fn], 0, "", []], 1000])["ok"] == 0 +temporal_nested_blk is (sandbox_run of [[ABI, [RETURN_NULL], [], [temporal_fn], 0, "", []], 1000])["ok"] == false # (c) ALLOWED pure compute still runs and returns the right value (no over-block). r_len is sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["len", "hello"]], 1000] -allow_ok is (r_len["ok"] == 1) and (r_len["result"] == 5) +allow_ok is (r_len["ok"]) and (r_len["result"] == 5) # (c2) #973: the autograd tape's pure-compute kernels are allowlisted — # matmul_at/matmul_bt/scatter_add compute on the arguments handed in (a # deep-copied constant here) and touch no host state. r_at is sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["matmul_at", [[[1.0, 2.0]], [[3.0, 4.0]]]]], 1000] -at_ok is (r_at["ok"] == 1) and (r_at["result"] == [[3.0, 4.0], [6.0, 8.0]]) +at_ok is (r_at["ok"]) and (r_at["result"] == [[3.0, 4.0], [6.0, 8.0]]) r_bt is sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["matmul_bt", [[[1.0, 2.0]], [[3.0, 4.0]]]]], 1000] -bt_ok is (r_bt["ok"] == 1) and (r_bt["result"] == [11.0]) +bt_ok is (r_bt["ok"]) and (r_bt["result"] == [11.0]) r_sc is sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["scatter_add", [[0.0, 0.0], [1], 5.0]]], 1000] # dst is a list here, not a buffer: the call is REACHABLE (a type error from the # builtin itself, not the sandbox's blocked-stub denial). -sc_ok is (r_sc["ok"] == 0) and (r_sc["error"]["kind"] == "type_mismatch") +sc_ok is ((not r_sc["ok"])) and (r_sc["error"]["kind"] == "type_mismatch") allow_973 is at_ok and bt_ok and sc_ok # ---- Containment of the SANDBOX ENV ITSELF, not just the name policy ---- @@ -86,7 +86,7 @@ no_writethrough is (type of sum) == "builtin" # denial, and the result must not be callable. ret_fn is [[NUM_ONE, RETURN, RETURN_NULL], [], [], 0, "f", []] r_fn is sandbox_run of [[ABI, [CLOSURE,0,0, RETURN], [], [ret_fn], 0, "", []], 1000] -no_fn_escape is (r_fn["ok"] == 0) and ((type of r_fn["result"]) != "fn") +no_fn_escape is ((not r_fn["ok"])) and ((type of r_fn["result"]) != "fn") # (f) IMPORT IS AN OPCODE, NOT A NAME — the allowlist never saw it. A sandboxed # OP_IMPORT read any .eigs on the box (the request is "lib/.eigs", so @@ -96,7 +96,7 @@ no_fn_escape is (r_fn["ok"] == 0) and ((type of r_fn["result"]) != "fn") # denial KIND: a non-existent path would return ok==0 for the wrong reason # (not-found, not denied) and pass even with the gate removed. r_imp is sandbox_run of [[ABI, [IMPORT,0,0, RETURN], ["math"], [], 0, "", []], 1000] -no_import is (r_imp["ok"] == 0) and (r_imp["error"]["kind"] == "sandbox") +no_import is ((not r_imp["ok"])) and (r_imp["error"]["kind"] == "sandbox") # (g) A RESULT TOO BIG TO SCAN IS NOT "A CALLABLE". The boundary scan is # node/depth budgeted and fails CLOSED, which is right — but it used to report @@ -106,11 +106,11 @@ no_import is (r_imp["ok"] == 0) and (r_imp["error"]["kind"] == "sandbox") # and the literal claim only when a callable is genuinely present. r_big is sandbox_run of [[ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["range", 200000]], 1000000, 268435456] big_msg is r_big["error"]["message"] -big_unscannable is (r_big["ok"] == 0) and (r_big["error"]["kind"] == "sandbox") and ((index_of of [big_msg, "too large to scan"]) >= 0) +big_unscannable is ((not r_big["ok"])) and (r_big["error"]["kind"] == "sandbox") and ((index_of of [big_msg, "too large to scan"]) >= 0) r_fn is sandbox_run of [[ABI, [GET_NAME,0,0, RETURN], ["len"]], 1000] fn_msg is r_fn["error"]["message"] -fn_named is (r_fn["ok"] == 0) and ((index_of of [fn_msg, "contains a callable"]) >= 0) +fn_named is ((not r_fn["ok"])) and ((index_of of [fn_msg, "contains a callable"]) >= 0) if escape_blocked and screen_blk and exit_blk and seed_blk and close_blk and rbb_blk and state_at_blk and temporal_nested_blk and allow_ok and allow_973 and no_writethrough and no_fn_escape and no_import and big_unscannable and fn_named: print of "SANDBOX_ALLOW_OK" diff --git a/tests/test_sandbox_backedge_cap.eigs b/tests/test_sandbox_backedge_cap.eigs index 4fded8b1..b7d18fc8 100644 --- a/tests/test_sandbox_backedge_cap.eigs +++ b/tests/test_sandbox_backedge_cap.eigs @@ -35,7 +35,7 @@ LOOP_CAP_CHECK is 63 # ---- 1. The reported reproducer ------------------------------------------- # [OP_JUMP_BACK 3, OP_RETURN] jumps to itself. Pre-fix: infinite loop. spin is sandbox_run of [[ABI, [JUMP_BACK,3,0, RETURN], []], 10] -assert_eq of [spin["ok"], 0, "bare back edge returns a cap error, not a hang"] +assert_eq of [spin["ok"], false, "bare back edge returns a cap error, not a hang"] assert_eq of [spin["error"]["kind"], "sandbox", "bare back edge names the sandbox budget"] @@ -43,7 +43,7 @@ assert_eq of [spin["error"]["kind"], "sandbox", # Same shape under a generous budget must trip too — the point is that SOME # bound exists where none did, not the particular max_iterations value. spin_big is sandbox_run of [[ABI, [JUMP_BACK,3,0, RETURN], []], 100000] -assert_eq of [spin_big["ok"], 0, "bare back edge trips under a large budget too"] +assert_eq of [spin_big["ok"], false, "bare back edge trips under a large budget too"] # ---- 3. A compiler-shaped loop still trips at the CAP CHECK ----------------- # [LOOP_CAP_CHECK +3, JUMP_BACK 6, RETURN]: one cap check AND one back edge @@ -57,10 +57,10 @@ assert_eq of [spin_big["ok"], 0, "bare back edge trips under a large budget too" # counter would count 20 and die at the back edge with the raise message # ("back-edge iterations") instead. capped is sandbox_run of [[ABI, [LOOP_CAP_CHECK,3,0, JUMP_BACK,6,0, RETURN], []], 10] -assert_eq of [capped["ok"], 0, +assert_eq of [capped["ok"], false, "compiler-shaped capped loop reports NOT ok (partial run)"] has_partial is index_of of [capped["error"]["message"], "partial run"] -assert_eq of [has_partial >= 0, 1, +assert_eq of [has_partial >= 0, true, "cap-check trip names the partial run, not the back-edge raise"] # ---- 3b. An untripped run under an armed budget still reports ok ------------ @@ -69,7 +69,7 @@ assert_eq of [has_partial >= 0, 1, CONST is 0 POP is 35 small is sandbox_run of [[ABI, [CONST,0,0, POP, RETURN], [7]], 100] -assert_eq of [small["ok"], 1, "a non-looping run under an armed budget stays ok"] +assert_eq of [small["ok"], true, "a non-looping run under an armed budget stays ok"] # ---- 4. The trip leaves the host untripped ---------------------------------- # The counter is armed only inside sandbox_run: after a tripped run, the @@ -86,7 +86,7 @@ assert_eq of [total, 4950, "host loops run normally after a tripped sandbox"] # function). A tripped budget must not leak into the next run: the same chunk # trips on its own ten back edges, not on an inherited count. again is sandbox_run of [[ABI, [JUMP_BACK,3,0, RETURN], []], 10] -assert_eq of [again["ok"], 0, "a second run trips on its own fresh budget"] +assert_eq of [again["ok"], false, "a second run trips on its own fresh budget"] # ---- 6. The back-edge budget is CUMULATIVE ACROSS CALL FRAMES (#948) -------- # The two counters are scoped differently, on purpose. The per-frame loop @@ -112,24 +112,24 @@ xf_two is [CLOSURE,0,0, SET_NAME_LOCAL,0,0, GET_NAME,0,0, CALL,0,0, POP, # One call: the frame's own cap check trips, the loop exits gracefully, and the # run is reported as a capped PARTIAL run. xf_r1 is sandbox_run of [[ABI, xf_one, ["f"], [xf_fn], 0, "m", []], 10] -assert_eq of [xf_r1["ok"], 0, "#948 one call: capped run is not ok"] +assert_eq of [xf_r1["ok"], false, "#948 one call: capped run is not ok"] xf_p1 is index_of of [xf_r1["error"]["message"], "partial run"] -assert_eq of [xf_p1 >= 0, 1, "#948 one call trips the per-frame CAP CHECK"] +assert_eq of [xf_p1 >= 0, true, "#948 one call trips the per-frame CAP CHECK"] # Two calls: the second frame's cap check starts fresh, but the back-edge # total does not — so the run dies at the back-edge RAISE instead. Same loop, # same per-call iteration count, different verdict. xf_r2 is sandbox_run of [[ABI, xf_two, ["f"], [xf_fn], 0, "m", []], 10] -assert_eq of [xf_r2["ok"], 0, "#948 two calls: still not ok"] +assert_eq of [xf_r2["ok"], false, "#948 two calls: still not ok"] xf_p2 is index_of of [xf_r2["error"]["message"], "back-edge iterations"] -assert_eq of [xf_p2 >= 0, 1, +assert_eq of [xf_p2 >= 0, true, "#948 two calls trip the CUMULATIVE back-edge budget, not the per-frame cap"] # Both negatives, so neither assertion above can pass merely because the two # messages happen to be the same string. -assert_eq of [(index_of of [xf_r1["error"]["message"], "back-edge iterations"]) < 0, 1, +assert_eq of [(index_of of [xf_r1["error"]["message"], "back-edge iterations"]) < 0, true, "#948 one call does NOT reach the back-edge budget"] -assert_eq of [(index_of of [xf_r2["error"]["message"], "partial run"]) < 0, 1, +assert_eq of [(index_of of [xf_r2["error"]["message"], "partial run"]) < 0, true, "#948 two calls do NOT report a graceful partial run"] test_summary of null diff --git a/tests/test_sandbox_budget.eigs b/tests/test_sandbox_budget.eigs index c3d3b6ff..b238ed62 100644 --- a/tests/test_sandbox_budget.eigs +++ b/tests/test_sandbox_budget.eigs @@ -44,10 +44,10 @@ for i in range of 8: append of [rd_over_fns, rd_child] rd_over_desc is [ABI, [CONST,0,0, RETURN], [0], rd_over_fns] sr_rd_over is sandbox_run of [rd_over_desc, 100000, 1000000] -assert_eq of [sr_rd_over["ok"], 0, +assert_eq of [sr_rd_over["ok"], false, "descriptor graph: nested chunks share ONE work allowance"] rd_over_msg is "" -if sr_rd_over["ok"] == 0: +if (not sr_rd_over["ok"]): rd_over_msg is sr_rd_over["error"]["message"] assert_eq of [rd_over_msg, "invalid chunk descriptor", "descriptor graph: exhausting the shared allowance fails closed"] @@ -107,7 +107,7 @@ rd_allow_desc is [ABI, sr_rd_allow is sandbox_run of [rd_allow_desc, 100000, 1000000] assert_eq of [(len of rd_alias), 1, "boundary: a rebound allowlist name cannot alias host state inward"] -assert_eq of [sr_rd_allow["ok"], 0, +assert_eq of [sr_rd_allow["ok"], false, "boundary: a rebound allowlist name is blocked, not copied"] # --- #965 (root redesign): the controls the redesign must not break --------- @@ -121,20 +121,20 @@ for i in range of 6: append of [rd_under_fns, rd_child] rd_under_desc is [ABI, [CONST,0,0, RETURN], [0], rd_under_fns] sr_rd_under is sandbox_run of [rd_under_desc, 100000, 1000000] -assert_eq of [sr_rd_under["ok"], 1, +assert_eq of [sr_rd_under["ok"], true, "descriptor graph: an in-budget nested graph still builds and runs"] rd_under_result is 999 -if sr_rd_under["ok"] == 1: +if sr_rd_under["ok"]: rd_under_result is sr_rd_under["result"] assert_eq of [rd_under_result, 0, "descriptor graph: the in-budget nested graph answers normally"] -assert_eq of [sr_rd_attach["ok"], 1, +assert_eq of [sr_rd_attach["ok"], true, "boundary: mutating the sandbox's OWN copy is still an ordinary run"] rd_cyc is [] append of [rd_cyc, rd_cyc] rd_cyc_desc is [ABI, [CONST,1,0, RETURN], [rd_cyc, 0]] sr_rd_cyc is sandbox_run of [rd_cyc_desc, 100000, 1000000] -assert_eq of [sr_rd_cyc["ok"], 0, +assert_eq of [sr_rd_cyc["ok"], false, "descriptor graph: a self-referential constant terminates and is refused"] list_truncate of [rd_cyc, 0] rd_data is [10, 20, 30] @@ -143,7 +143,7 @@ rd_get_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CONST,2,0, RD_LIST,2,0, ["get_at", rd_data, 1]] sr_rd_get is sandbox_run of [rd_get_desc, 100000, 1000000] rd_get_result is 999 -if sr_rd_get["ok"] == 1: +if sr_rd_get["ok"]: rd_get_result is sr_rd_get["result"] assert_eq of [rd_get_result, 20, "boundary: an isolated list constant still reads its own elements"] @@ -152,7 +152,7 @@ rd_bufget_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CONST,2,0, RD_LIST,2,0, ["buf_get", rd_buf, 0]] sr_rd_bufget is sandbox_run of [rd_bufget_desc, 100000, 1000000] rd_bufget_result is 999 -if sr_rd_bufget["ok"] == 1: +if sr_rd_bufget["ok"]: rd_bufget_result is sr_rd_bufget["result"] assert_eq of [rd_bufget_result, 3, "boundary: an isolated buffer constant still reads its own elements"] @@ -186,17 +186,17 @@ scan_code is [ADV2_TRY_BEGIN,15,0, GET_NAME,0,0, CONST,1,0, CALL,1,0, scan_fn is [[CONST,0,0, RETURN], ["x"]] scan_desc is [ABI, scan_code, ["str_lower", scan_big], [scan_fn]] sr_scan is sandbox_run of [scan_desc, 100000, 1000] -assert_eq of [sr_scan["ok"], 0, +assert_eq of [sr_scan["ok"], false, "adv scan: caught-refusal run returning a closure still refuses"] -assert_eq of [has_key of [sr_scan, "result"], 0, +assert_eq of [has_key of [sr_scan, "result"], false, "adv scan: refused run must not hand a callable across the boundary"] # Control: the SAME closure-returning chunk under a generous budget reaches the # scan on the ok path and is refused there with the result dropped - proves the # scan itself still works and the failure above is specific to the refusal path. sr_scan_ctrl is sandbox_run of [scan_desc, 100000, 100000] -assert_eq of [sr_scan_ctrl["ok"], 0, +assert_eq of [sr_scan_ctrl["ok"], false, "adv scan control: ok-path closure result is scan-refused"] -assert_eq of [has_key of [sr_scan_ctrl, "result"], 0, +assert_eq of [has_key of [sr_scan_ctrl, "result"], false, "adv scan control: scan refusal drops the callable result"] # --- #965 (fix5): a caught byte-budget refusal still refuses the run --------- @@ -226,11 +226,11 @@ caught_code is [TRY_BEGIN,11,0, GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, CONST,2,0, RETURN] caught_lower_desc is [ABI, caught_code, ["str_lower", big_caught, "caught-budget-survived"]] sr_caught_lower is sandbox_run of [caught_lower_desc, 100000, 1000] -assert_eq of [sr_caught_lower["ok"], 0, +assert_eq of [sr_caught_lower["ok"], false, "budget: caught make_str refusal cannot launder run to ok"] cl_kind is "" cl_msg is "" -if sr_caught_lower["ok"] == 0: +if (not sr_caught_lower["ok"]): cl_kind is sr_caught_lower["error"]["kind"] cl_msg is sr_caught_lower["error"]["message"] assert_eq of [cl_kind, "sandbox", "budget: caught make_str refusal names the sandbox"] @@ -240,11 +240,11 @@ assert_eq of [cl_msg, "sandbox memory budget exceeded (used 0 + 2001 > 1000 byte # --- #965 (fix5): same caught shape through the buffer allocator ------------- caught_buffer_desc is [ABI, caught_code, ["buffer", 500, "caught-budget-survived"]] sr_caught_buffer is sandbox_run of [caught_buffer_desc, 100000, 1000] -assert_eq of [sr_caught_buffer["ok"], 0, +assert_eq of [sr_caught_buffer["ok"], false, "budget: caught buffer refusal cannot launder run to ok"] cb_kind is "" cb_msg is "" -if sr_caught_buffer["ok"] == 0: +if (not sr_caught_buffer["ok"]): cb_kind is sr_caught_buffer["error"]["kind"] cb_msg is sr_caught_buffer["error"]["message"] assert_eq of [cb_kind, "sandbox", "budget: caught buffer refusal names the sandbox"] @@ -260,10 +260,10 @@ assert_eq of [cb_msg, "sandbox memory budget exceeded (used 0 + 4000 > 1000 byte # laundering one above. caught_strbuf_desc is [ABI, caught_code, ["json_encode", "x", "caught-strbuf-survived"]] sr_caught_strbuf is sandbox_run of [caught_strbuf_desc, 100000, 3] -assert_eq of [sr_caught_strbuf["ok"], 0, +assert_eq of [sr_caught_strbuf["ok"], false, "budget: caught strbuf refusal stays refused (control)"] cs_kind is "" -if sr_caught_strbuf["ok"] == 0: +if (not sr_caught_strbuf["ok"]): cs_kind is sr_caught_strbuf["error"]["kind"] assert_eq of [cs_kind, "sandbox", "budget: caught strbuf control names the sandbox"] @@ -286,11 +286,11 @@ ord_code is [LINE,7,0,0,0, CONST,4,0, RETURN] ord_desc is [ABI, ord_code, ["throw", "sentinel", "str_lower", big_caught, "ordering-survived"]] sr_ord is sandbox_run of [ord_desc, 100000, 3000] -assert_eq of [sr_ord["ok"], 0, "budget: caught user throw then budget refusal -> {ok:0}"] +assert_eq of [sr_ord["ok"], false, "budget: caught user throw then budget refusal -> {ok:0}"] ord_kind is "" ord_msg is "" ord_line is 0 -if sr_ord["ok"] == 0: +if (not sr_ord["ok"]): ord_kind is sr_ord["error"]["kind"] ord_msg is sr_ord["error"]["message"] ord_line is sr_ord["error"]["line"] @@ -322,33 +322,33 @@ nest_pair is vm_run_bytecode of [ABI, nest_outer_code, assert_eq of [nest_pair[1], "outer-continued", "nested: outer host flow continues after an inner refused run"] nest_inner_ok is nest_pair[0]["ok"] -assert_eq of [nest_inner_ok, 0, +assert_eq of [nest_inner_ok, false, "nested: inner caught refusal still refuses the inner run"] nest_inner_kind is "" -if nest_inner_ok == 0: +if not nest_inner_ok: nest_inner_kind is nest_pair[0]["error"]["kind"] assert_eq of [nest_inner_kind, "sandbox", "nested: inner refusal names the sandbox"] nest_after is sandbox_run of [nest_inner_desc, 100000, 1000] -assert_eq of [nest_after["ok"], 0, "nested: later tight run refuses on its own fresh budget"] +assert_eq of [nest_after["ok"], false, "nested: later tight run refuses on its own fresh budget"] na_msg is "" -if nest_after["ok"] == 0: +if (not nest_after["ok"]): na_msg is nest_after["error"]["message"] assert_eq of [na_msg, "sandbox memory budget exceeded (used 0 + 2001 > 1000 bytes)", "nested: later refusal reports its own diagnostic, not a restored one"] -assert_eq of [(sandbox_run of [nest_inner_desc, 100000, 100000])["ok"], 1, +assert_eq of [(sandbox_run of [nest_inner_desc, 100000, 100000])["ok"], true, "nested: later generous run is not poisoned by the refused runs"] # --- 1. A big allocation under a tiny budget is rejected (ok=0), not aborted --- ok_big is (sandbox_run of [zeros_desc of 1000000, 1000000, 1000])["ok"] -assert_eq of [ok_big, 0, "budget: zeros(1M) under 1000-byte budget -> {ok:0}"] +assert_eq of [ok_big, false, "budget: zeros(1M) under 1000-byte budget -> {ok:0}"] # --- 2. A small allocation under a generous budget runs (ok=1) --- ok_small is (sandbox_run of [zeros_desc of 10, 1000000, 1000000])["ok"] -assert_eq of [ok_small, 1, "budget: zeros(10) under 1MB budget -> {ok:1}"] +assert_eq of [ok_small, true, "budget: zeros(10) under 1MB budget -> {ok:1}"] # --- 3. fill (the aggregate-allocation vector) is charged too --- ok_fill is (sandbox_run of [fill_desc of 1000000, 1000000, 1000])["ok"] -assert_eq of [ok_fill, 0, "budget: fill(1M) under 1000-byte budget -> {ok:0}"] +assert_eq of [ok_fill, false, "budget: fill(1M) under 1000-byte budget -> {ok:0}"] # --- 4. The DEFAULT budget (no max_bytes arg) bounds an otherwise-uncapped bomb, # so it returns {ok:0} instead of x_oom/abort. @@ -366,14 +366,14 @@ big_alloc_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, CONST,2,0, RETURN], ["zeros", 10000000, 0]] sr_default is sandbox_run of [big_alloc_desc, 1000000] -assert_eq of [sr_default["ok"], 0, "budget: default budget bounds 4x zeros(10M) -> {ok:0}, no abort"] +assert_eq of [sr_default["ok"], false, "budget: default budget bounds 4x zeros(10M) -> {ok:0}, no abort"] assert_true of [(index_of of [sr_default["error"]["message"], "memory budget exceeded"]) >= 0, "budget: the default-budget refusal is the MEMORY diagnostic, not a boundary one"] # #1093: the old zeros(9M) bomb is still refused under the default budget, but # now at the result boundary (a 9M-element buffer exceeds the callable-scan # node budget). Pinned so the change of MECHANISM stays visible. sr_zeros9 is sandbox_run of [zeros_desc of 9000000, 1000000] -assert_eq of [sr_zeros9["ok"], 0, "budget: zeros(9M) under the default budget is still refused"] +assert_eq of [sr_zeros9["ok"], false, "budget: zeros(9M) under the default budget is still refused"] # --- 5. CUMULATIVE (F2): two fills that each fit but together exceed the budget. # fill(60k) result stays under the boundary result-scan budget @@ -387,9 +387,9 @@ assert_eq of [sr_zeros9["ok"], 0, "budget: zeros(9M) under the default budget is two_fills is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["fill", [60000, 0.0]]] ok_two_tight is (sandbox_run of [two_fills, 1000000, 600000])["ok"] -assert_eq of [ok_two_tight, 0, "budget: two fills under a one-fill budget -> {ok:0} (cumulative)"] +assert_eq of [ok_two_tight, false, "budget: two fills under a one-fill budget -> {ok:0} (cumulative)"] ok_two_loose is (sandbox_run of [two_fills, 1000000, 12000000])["ok"] -assert_eq of [ok_two_loose, 1, "budget: two fills under an ample budget -> {ok:1}"] +assert_eq of [ok_two_loose, true, "budget: two fills under an ample budget -> {ok:1}"] # --- 5b. The boundary result-scan budget is its own refusal: a WITHIN-byte- # budget run whose RESULT exceeds SANDBOX_RESULT_MAX_NODES nodes is @@ -398,11 +398,11 @@ assert_eq of [ok_two_loose, 1, "budget: two fills under an ample budget -> {ok:1 # this is the behavior that silently broke the old 600k case. --- big_fill is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["fill", [600000, 0.0]]] sr_bigres is sandbox_run of [big_fill, 1000000, 64000000] -assert_eq of [sr_bigres["ok"], 0, "boundary: >100k-node result refused at the scan"] +assert_eq of [sr_bigres["ok"], false, "boundary: >100k-node result refused at the scan"] # --- 6. The budget is per-run: a tight run doesn't poison a later generous run --- ok_after is (sandbox_run of [zeros_desc of 10, 1000000, 1000000])["ok"] -assert_eq of [ok_after, 1, "budget: resets per run (generous run after a tight one)"] +assert_eq of [ok_after, true, "budget: resets per run (generous run after a tight one)"] # --- 7. concat (the quadratic `result is concat of [result, more]` vector) is # charged on its output too. `la` (50 elems) is built outside the sandbox; @@ -410,9 +410,9 @@ assert_eq of [ok_after, 1, "budget: resets per run (generous run after a tight o la is range of 50 cdesc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["concat", [la, la]]] ok_concat_tight is (sandbox_run of [cdesc, 1000000, 100])["ok"] -assert_eq of [ok_concat_tight, 0, "budget: concat(100 slots) under 100-byte budget -> {ok:0}"] +assert_eq of [ok_concat_tight, false, "budget: concat(100 slots) under 100-byte budget -> {ok:0}"] ok_concat_loose is (sandbox_run of [cdesc, 1000000, 100000])["ok"] -assert_eq of [ok_concat_loose, 1, "budget: concat(100 slots) under 100KB budget -> {ok:1}"] +assert_eq of [ok_concat_loose, true, "budget: concat(100 slots) under 100KB budget -> {ok:1}"] # --- string concat is charged (the `s is s + s` doubling class) ------------- # #292 charged only the size-controlled builtins; ADD on two strings allocated @@ -425,12 +425,12 @@ for i in range of 200: big is big + "xxxxxxxx" cat_desc is [ABI, [CONST,0,0, CONST,1,0, ADD, RETURN], [big, big]] sr_cat is sandbox_run of [cat_desc, 100000, 1000] -assert_eq of [sr_cat["ok"], 0, "budget: oversized string concat -> {ok:0}, not abort"] +assert_eq of [sr_cat["ok"], false, "budget: oversized string concat -> {ok:0}, not abort"] cat_kind is sr_cat["error"]["kind"] assert_eq of [cat_kind, "sandbox", "budget: concat refusal names the sandbox"] # Control: the same concat under an ample budget succeeds. sr_cat_ok is sandbox_run of [cat_desc, 100000, 1000000] -assert_eq of [sr_cat_ok["ok"], 1, "budget: same concat within budget stays ok"] +assert_eq of [sr_cat_ok["ok"], true, "budget: same concat within budget stays ok"] # --- join is charged (the concat side door) ---------------------------------- # join is output-proportional (sum(parts) + sep*(count-1)) and was left @@ -439,18 +439,18 @@ assert_eq of [sr_cat_ok["ok"], 1, "budget: same concat within budget stays ok"] # the uncatchable x_oom abort through an armed 1000-byte budget. join_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["join", [[big, big], ""]]] sr_join is sandbox_run of [join_desc, 100000, 1000] -assert_eq of [sr_join["ok"], 0, "budget: oversized join -> {ok:0}, not bypass/abort"] +assert_eq of [sr_join["ok"], false, "budget: oversized join -> {ok:0}, not bypass/abort"] join_kind is sr_join["error"]["kind"] assert_eq of [join_kind, "sandbox", "budget: join refusal names the sandbox"] sr_join_ok is sandbox_run of [join_desc, 100000, 1000000] -assert_eq of [sr_join_ok["ok"], 1, "budget: same join within budget stays ok"] +assert_eq of [sr_join_ok["ok"], true, "budget: same join within budget stays ok"] # --- str_replace is charged (same class, catchably capped at 256MB) ---------- srep_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["str_replace", [big, "x", "xxxxxxxxxxxxxxxx"]]] sr_srep is sandbox_run of [srep_desc, 100000, 1000] -assert_eq of [sr_srep["ok"], 0, "budget: expanding str_replace -> {ok:0}"] +assert_eq of [sr_srep["ok"], false, "budget: expanding str_replace -> {ok:0}"] sr_srep_ok is sandbox_run of [srep_desc, 100000, 4000000] -assert_eq of [sr_srep_ok["ok"], 1, "budget: same str_replace within budget stays ok"] +assert_eq of [sr_srep_ok["ok"], true, "budget: same str_replace within budget stays ok"] # --- text_builder growth is charged at its chokepoint ------------------------ # Uncharged, a 50000-append loop reached the uncatchable x_oom abort from @@ -464,27 +464,27 @@ tb_code is [GET_NAME,0,0, CALL,0,0, SET_NAME_LOCAL,1,0, POP, GET_NAME,2,0, GET_NAME,1,0, CONST,3,0, CALL,2,0, RETURN] tb_desc is [ABI, tb_code, ["text_builder_new", "tb", "text_builder_extend", blist]] sr_tb is sandbox_run of [tb_desc, 100000, 1000] -assert_eq of [sr_tb["ok"], 0, "budget: text_builder growth -> {ok:0}, not abort"] +assert_eq of [sr_tb["ok"], false, "budget: text_builder growth -> {ok:0}, not abort"] tb_kind is sr_tb["error"]["kind"] assert_eq of [tb_kind, "sandbox", "budget: text_builder refusal names the sandbox"] small_list is ["a", "b", "c"] tb_small is [ABI, tb_code, ["text_builder_new", "tb", "text_builder_extend", small_list]] sr_tb_ok is sandbox_run of [tb_small, 100000, 1000000] -assert_eq of [sr_tb_ok["ok"], 1, "budget: small text_builder within budget stays ok"] +assert_eq of [sr_tb_ok["ok"], true, "budget: small text_builder within budget stays ok"] # --- split is charged (counted output: input bytes + per-part overhead) ------ split_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CONST,2,0, CALL,2,0, RETURN], ["split", big, "x"]] sr_split is sandbox_run of [split_desc, 100000, 1000] -assert_eq of [sr_split["ok"], 0, "budget: oversized split -> {ok:0}, not abort"] +assert_eq of [sr_split["ok"], false, "budget: oversized split -> {ok:0}, not abort"] sr_split_ok is sandbox_run of [split_desc, 100000, 4000000] -assert_eq of [sr_split_ok["ok"], 1, "budget: same split within budget stays ok"] +assert_eq of [sr_split_ok["ok"], true, "budget: same split within budget stays ok"] # --- strbuf consumers are charged at strbuf_reserve (json_encode probe) ------ je_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["json_encode", big]] sr_je is sandbox_run of [je_desc, 100000, 500] -assert_eq of [sr_je["ok"], 0, "budget: json_encode of a big value -> {ok:0}"] +assert_eq of [sr_je["ok"], false, "budget: json_encode of a big value -> {ok:0}"] sr_je_ok is sandbox_run of [je_desc, 100000, 1000000] -assert_eq of [sr_je_ok["ok"], 1, "budget: same json_encode within budget stays ok"] +assert_eq of [sr_je_ok["ok"], true, "budget: same json_encode within budget stays ok"] # --- the Value-tree/list class is charged (json_decode amplification) -------- # A "[0,0,...]" const amplifies ~40x into Value nodes + list slots; the tree @@ -496,11 +496,11 @@ for i in range of 500: jd_json is jd_json + "0]" jd_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, CONST,2,0, RETURN], ["json_decode", jd_json, 1]] sr_jd is sandbox_run of [jd_desc, 100000, 5000] -assert_eq of [sr_jd["ok"], 0, "budget: json_decode amplification -> {ok:0}, not bypass"] +assert_eq of [sr_jd["ok"], false, "budget: json_decode amplification -> {ok:0}, not bypass"] jd_kind is sr_jd["error"]["kind"] assert_eq of [jd_kind, "sandbox", "budget: json_decode refusal names the sandbox"] sr_jd_ok is sandbox_run of [jd_desc, 100000, 1000000] -assert_eq of [sr_jd_ok["ok"], 1, "budget: same decode within an ample budget stays ok"] +assert_eq of [sr_jd_ok["ok"], true, "budget: same decode within an ample budget stays ok"] # --- scan_ints: same class through the scanner family ------------------------ # (whitespace-separated — scan_ints rejects comma-joined runs as invalid) @@ -509,7 +509,7 @@ for i in range of 500: si_text is si_text + "0 0 0 0 0 0 0 0 " si_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, CONST,2,0, RETURN], ["scan_ints", si_text, 1]] sr_si is sandbox_run of [si_desc, 100000, 5000] -assert_eq of [sr_si["ok"], 0, "budget: scan_ints amplification -> {ok:0}"] +assert_eq of [sr_si["ok"], false, "budget: scan_ints amplification -> {ok:0}"] # --- small-container nesting cannot bypass the birth charge ------------------ # Charging only >8 pre-sizes and doublings left <=8-element containers free: @@ -522,9 +522,9 @@ for i in range of 300: nest_json is nest_json + "[0]]" nest_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, CONST,2,0, RETURN], ["json_decode", nest_json, 1]] sr_nest is sandbox_run of [nest_desc, 100000, 5000] -assert_eq of [sr_nest["ok"], 0, "budget: nested small-array amplification -> {ok:0}"] +assert_eq of [sr_nest["ok"], false, "budget: nested small-array amplification -> {ok:0}"] sr_nest_ok is sandbox_run of [nest_desc, 100000, 1000000] -assert_eq of [sr_nest_ok["ok"], 1, "budget: same nested decode within ample budget stays ok"] +assert_eq of [sr_nest_ok["ok"], true, "budget: same nested decode within ample budget stays ok"] # --- small JSON OBJECTS are charged too (dict half of the same hole) --------- obj_json is "[" @@ -533,7 +533,7 @@ for i in range of 300: obj_json is obj_json + "{\"a\":0}]" obj_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, CONST,2,0, RETURN], ["json_decode", obj_json, 1]] sr_obj is sandbox_run of [obj_desc, 100000, 5000] -assert_eq of [sr_obj["ok"], 0, "budget: small-object amplification -> {ok:0}"] +assert_eq of [sr_obj["ok"], false, "budget: small-object amplification -> {ok:0}"] # --- make_dict's BIRTH charge in isolation ----------------------------------- # The small-object rows above also grow the outer array, so list_append's @@ -547,7 +547,7 @@ for i in range of 7: iso_json is iso_json + "]" iso_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, CONST,2,0, RETURN], ["json_decode", iso_json, 1]] sr_iso is sandbox_run of [iso_desc, 100000, 800] -assert_eq of [sr_iso["ok"], 0, "budget: dict birth charge fires in isolation"] +assert_eq of [sr_iso["ok"], false, "budget: dict birth charge fires in isolation"] # --- the BUFFER class is charged (matmul outer-product amplification) -------- # "inputs charged" was a false waiver: an outer product amplifies two @@ -559,12 +559,12 @@ mm_code is [GET_NAME,0,0, CALL,2,0, POP, CONST,4,0, RETURN] mm_desc is [ABI, mm_code, ["matmul", "buffer", 500, 1, 1]] sr_mm is sandbox_run of [mm_desc, 100000, 1000000] -assert_eq of [sr_mm["ok"], 0, "budget: outer-product matmul -> {ok:0}, not bypass"] +assert_eq of [sr_mm["ok"], false, "budget: outer-product matmul -> {ok:0}, not bypass"] mm_kind is sr_mm["error"]["kind"] assert_eq of [mm_kind, "sandbox", "budget: buffer refusal names the sandbox"] mm_small is [ABI, mm_code, ["matmul", "buffer", 10, 1, 1]] sr_mm_ok is sandbox_run of [mm_small, 100000, 1000000] -assert_eq of [sr_mm_ok["ok"], 1, "budget: small matmul within budget stays ok"] +assert_eq of [sr_mm_ok["ok"], true, "budget: small matmul within budget stays ok"] # --- buf_from_list / reshape: the last two raw-xcalloc buffer producers ------ # 50 copies of an 800k buffer held 320MB under the default budget (ok:1) until @@ -575,9 +575,9 @@ for i in range of 2000: bfl_code is [GET_NAME,0,0, CONST,1,0, CALL,1,0, POP, CONST,2,0, RETURN] bfl_desc is [ABI, bfl_code, ["buf_from_list", bl_src, 1]] sr_bfl is sandbox_run of [bfl_desc, 100000, 8000] -assert_eq of [sr_bfl["ok"], 0, "budget: buf_from_list -> {ok:0}, not bypass"] +assert_eq of [sr_bfl["ok"], false, "budget: buf_from_list -> {ok:0}, not bypass"] sr_bfl_ok is sandbox_run of [bfl_desc, 100000, 4000000] -assert_eq of [sr_bfl_ok["ok"], 1, "budget: buf_from_list within budget stays ok"] +assert_eq of [sr_bfl_ok["ok"], true, "budget: buf_from_list within budget stays ok"] # --- #965: pure string transforms charge their payload bytes ----------------- # str_lower/str_upper/trim/substr/str_from_bytes built output via an uncharged @@ -603,25 +603,25 @@ loop965 is [NUM_ONE, JUMP_IF_FALSE,19,0, LOOP_CAP_CHECK,16,0, GET_NAME,2,0, RETURN] loop_desc is [ABI, loop965, ["str_lower", big965, "x"]] sr_loop is sandbox_run of [loop_desc, 100000, 30000] -assert_eq of [sr_loop["ok"], 0, "budget: looped str_lower of a reused input -> {ok:0}"] +assert_eq of [sr_loop["ok"], false, "budget: looped str_lower of a reused input -> {ok:0}"] # Error fields exist only when ok flips the expected way, so read them behind # a guard: a regression must surface as a failed ASSERTION (the guarded # default never matches), never as a crash that skips the remaining checks. loop_kind is "" loop_msg is "" -if sr_loop["ok"] == 0: +if (not sr_loop["ok"]): loop_kind is sr_loop["error"]["kind"] loop_msg is sr_loop["error"]["message"] assert_eq of [loop_kind, "sandbox", "budget: string-transform refusal names the sandbox"] loop_mem is index_of of [loop_msg, "memory budget"] -assert_eq of [loop_mem >= 0, 1, "budget: loop stops on the byte budget, not the iteration cap"] +assert_eq of [loop_mem >= 0, true, "budget: loop stops on the byte budget, not the iteration cap"] # Per-call: one str_lower over the same input is charged its payload. lower_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["str_lower", big965]] -assert_eq of [(sandbox_run of [lower_desc, 100000, 1000])["ok"], 0, "budget: str_lower payload -> {ok:0}"] +assert_eq of [(sandbox_run of [lower_desc, 100000, 1000])["ok"], false, "budget: str_lower payload -> {ok:0}"] sr_lower_ok is sandbox_run of [lower_desc, 100000, 100000] -assert_eq of [sr_lower_ok["ok"], 1, "budget: str_lower within budget stays ok"] +assert_eq of [sr_lower_ok["ok"], true, "budget: str_lower within budget stays ok"] lower_result is "" -if sr_lower_ok["ok"] == 1: +if sr_lower_ok["ok"]: lower_result is sr_lower_ok["result"] assert_eq of [lower_result, str_lower of big965, "budget: str_lower transforms correctly"] @@ -630,17 +630,17 @@ sfb_bytes is [] for i in range of 2000: append of [sfb_bytes, 65] sfb_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["str_from_bytes", sfb_bytes]] -assert_eq of [(sandbox_run of [sfb_desc, 100000, 1000])["ok"], 0, "budget: str_from_bytes payload -> {ok:0}"] -assert_eq of [(sandbox_run of [sfb_desc, 100000, 100000])["ok"], 1, "budget: str_from_bytes within budget stays ok"] +assert_eq of [(sandbox_run of [sfb_desc, 100000, 1000])["ok"], false, "budget: str_from_bytes payload -> {ok:0}"] +assert_eq of [(sandbox_run of [sfb_desc, 100000, 100000])["ok"], true, "budget: str_from_bytes within budget stays ok"] upper_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["str_upper", big965]] -assert_eq of [(sandbox_run of [upper_desc, 100000, 1000])["ok"], 0, "budget: str_upper payload -> {ok:0}"] -assert_eq of [(sandbox_run of [upper_desc, 100000, 100000])["ok"], 1, "budget: str_upper within budget stays ok"] +assert_eq of [(sandbox_run of [upper_desc, 100000, 1000])["ok"], false, "budget: str_upper payload -> {ok:0}"] +assert_eq of [(sandbox_run of [upper_desc, 100000, 100000])["ok"], true, "budget: str_upper within budget stays ok"] sub_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["substr", [big965, 0, 2000]]] -assert_eq of [(sandbox_run of [sub_desc, 100000, 1000])["ok"], 0, "budget: substr payload -> {ok:0}"] -assert_eq of [(sandbox_run of [sub_desc, 100000, 100000])["ok"], 1, "budget: substr within budget stays ok"] +assert_eq of [(sandbox_run of [sub_desc, 100000, 1000])["ok"], false, "budget: substr payload -> {ok:0}"] +assert_eq of [(sandbox_run of [sub_desc, 100000, 100000])["ok"], true, "budget: substr within budget stays ok"] trim_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["trim", big965]] -assert_eq of [(sandbox_run of [trim_desc, 100000, 1000])["ok"], 0, "budget: trim payload -> {ok:0}"] -assert_eq of [(sandbox_run of [trim_desc, 100000, 100000])["ok"], 1, "budget: trim within budget stays ok"] +assert_eq of [(sandbox_run of [trim_desc, 100000, 1000])["ok"], false, "budget: trim payload -> {ok:0}"] +assert_eq of [(sandbox_run of [trim_desc, 100000, 100000])["ok"], true, "budget: trim within budget stays ok"] # --- #965: a strbuf-charged producer is NOT charged twice -------------------- # json_encode of a 100000-char string grows its strbuf to 131072 (charged @@ -651,7 +651,7 @@ big_je is "" for i in range of 12500: big_je is big_je + "xxxxxxxx" je2_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["json_encode", big_je]] -assert_eq of [(sandbox_run of [je2_desc, 100000, 200000])["ok"], 1, "budget: json_encode charged once, not twice"] +assert_eq of [(sandbox_run of [je2_desc, 100000, 200000])["ok"], true, "budget: json_encode charged once, not twice"] # --- #965: a VAL_BUFFER result counts its element storage at the scan -------- # The boundary callable-scan counted a buffer as ONE node regardless of @@ -660,14 +660,14 @@ assert_eq of [(sandbox_run of [je2_desc, 100000, 200000])["ok"], 1, "budget: jso # element: 200000 > SANDBOX_RESULT_MAX_NODES (100000) refuses the result. buf_big_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["buffer", 200000]] sr_bufbig is sandbox_run of [buf_big_desc, 100000, 4000000] -assert_eq of [sr_bufbig["ok"], 0, "boundary: 200k-element buffer result counts its elements"] +assert_eq of [sr_bufbig["ok"], false, "boundary: 200k-element buffer result counts its elements"] bufbig_msg is "" -if sr_bufbig["ok"] == 0: +if (not sr_bufbig["ok"]): bufbig_msg is sr_bufbig["error"]["message"] bufbig_scan is index_of of [bufbig_msg, "too large to scan"] -assert_eq of [bufbig_scan >= 0, 1, "boundary: buffer refusal is the result scan, not the byte budget"] +assert_eq of [bufbig_scan >= 0, true, "boundary: buffer refusal is the result scan, not the byte budget"] buf_small_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["buffer", 1000]] -assert_eq of [(sandbox_run of [buf_small_desc, 100000, 4000000])["ok"], 1, "boundary: 1k-element buffer result still crosses"] +assert_eq of [(sandbox_run of [buf_small_desc, 100000, 4000000])["ok"], true, "boundary: 1k-element buffer result still crosses"] # --- #965 (fix1): SMALL strbuf-backed payloads are charged too ---------------- # strbuf_init's 64-byte initial capacity is never charged and strbuf_reserve @@ -682,22 +682,22 @@ je_loop is [NUM_ONE, JUMP_IF_FALSE,19,0, LOOP_CAP_CHECK,16,0, GET_NAME,2,0, RETURN] je_loop_desc is [ABI, je_loop, ["json_encode", "x", "r"]] sr_jeloop is sandbox_run of [je_loop_desc, 100000, 30000] -assert_eq of [sr_jeloop["ok"], 0, "budget: looped small json_encode -> {ok:0}"] +assert_eq of [sr_jeloop["ok"], false, "budget: looped small json_encode -> {ok:0}"] jeloop_kind is "" jeloop_msg is "" -if sr_jeloop["ok"] == 0: +if (not sr_jeloop["ok"]): jeloop_kind is sr_jeloop["error"]["kind"] jeloop_msg is sr_jeloop["error"]["message"] assert_eq of [jeloop_kind, "sandbox", "budget: small-strbuf refusal names the sandbox"] jeloop_mem is index_of of [jeloop_msg, "memory budget"] -assert_eq of [jeloop_mem >= 0, 1, "budget: small-strbuf loop stops on bytes, not the iteration cap"] +assert_eq of [jeloop_mem >= 0, true, "budget: small-strbuf loop stops on bytes, not the iteration cap"] # Per-call: one 3-byte json_encode result charges its 4 payload bytes. je_small is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["json_encode", "x"]] -assert_eq of [(sandbox_run of [je_small, 100000, 3])["ok"], 0, "budget: small json_encode payload -> {ok:0}"] +assert_eq of [(sandbox_run of [je_small, 100000, 3])["ok"], false, "budget: small json_encode payload -> {ok:0}"] sr_jes is sandbox_run of [je_small, 100000, 1000] -assert_eq of [sr_jes["ok"], 1, "budget: small json_encode within budget stays ok"] +assert_eq of [sr_jes["ok"], true, "budget: small json_encode within budget stays ok"] jes_result is "" -if sr_jes["ok"] == 1: +if sr_jes["ok"]: jes_result is sr_jes["result"] assert_eq of [jes_result, "\"x\"", "budget: small json_encode result is correct"] @@ -712,8 +712,8 @@ for i in range of 12: mid100 is mid100 + "xxxxxxxx" mid100 is mid100 + "xxxx" je_mid is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["json_encode", mid100]] -assert_eq of [(sandbox_run of [je_mid, 100000, 102])["ok"], 0, "budget: grown strbuf payload (103) refused at 102"] -assert_eq of [(sandbox_run of [je_mid, 100000, 103])["ok"], 1, "budget: grown strbuf charged exactly once (103 admitted)"] +assert_eq of [(sandbox_run of [je_mid, 100000, 102])["ok"], false, "budget: grown strbuf payload (103) refused at 102"] +assert_eq of [(sandbox_run of [je_mid, 100000, 103])["ok"], true, "budget: grown strbuf charged exactly once (103 admitted)"] # --- #965 (fix1): the original refusal diagnostic survives error construction -- # The error-result dict is built while its make_dict/make_str calls could @@ -728,15 +728,15 @@ for i in range of 75: cc_code is [CONST,0,0, CONST,0,0, ADD, POP, CONST,0,0, CONST,0,0, ADD, RETURN] cc_desc is [ABI, cc_code, [big_cc]] sr_cc is sandbox_run of [cc_desc, 100000, 1300] -assert_eq of [sr_cc["ok"], 0, "budget: second oversized concat -> {ok:0}"] +assert_eq of [sr_cc["ok"], false, "budget: second oversized concat -> {ok:0}"] cc_kind is "" cc_msg is "" -if sr_cc["ok"] == 0: +if (not sr_cc["ok"]): cc_kind is sr_cc["error"]["kind"] cc_msg is sr_cc["error"]["message"] assert_eq of [cc_kind, "sandbox", "budget: concat refusal names the sandbox"] cc_orig is index_of of [cc_msg, "used 1201 + 1201 > 1300"] -assert_eq of [cc_orig >= 0, 1, "budget: original refusal diagnostic survives error construction"] +assert_eq of [cc_orig >= 0, true, "budget: original refusal diagnostic survives error construction"] # --- #965 (fix1): the exact result-scan node boundary -------------------------- # The scan spends one node per VALUE: a container costs 1 + its elements, so a @@ -745,15 +745,15 @@ assert_eq of [cc_orig >= 0, 1, "budget: original refusal diagnostic survives err # 1 + 99998 = 99999 nodes crosses; 1 + 99999 = 100000 exhausts the budget and # fails closed (the scan's strict exhaustion rule, same as lists). buf_edge_lo is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["buffer", 99998]] -assert_eq of [(sandbox_run of [buf_edge_lo, 100000, 4000000])["ok"], 1, "boundary: 1+99998-node buffer result crosses"] +assert_eq of [(sandbox_run of [buf_edge_lo, 100000, 4000000])["ok"], true, "boundary: 1+99998-node buffer result crosses"] buf_edge_hi is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["buffer", 99999]] sr_edge is sandbox_run of [buf_edge_hi, 100000, 4000000] -assert_eq of [sr_edge["ok"], 0, "boundary: 1+99999-node buffer result refused at the exact limit"] +assert_eq of [sr_edge["ok"], false, "boundary: 1+99999-node buffer result refused at the exact limit"] edge_msg is "" -if sr_edge["ok"] == 0: +if (not sr_edge["ok"]): edge_msg is sr_edge["error"]["message"] edge_scan is index_of of [edge_msg, "too large to scan"] -assert_eq of [edge_scan >= 0, 1, "boundary: exact-limit refusal is the result scan"] +assert_eq of [edge_scan >= 0, true, "boundary: exact-limit refusal is the result scan"] # --- #965 (fix3): the first budget refusal wins across one producer --------- # json_decode is allowlisted and continues an array after a refused string. @@ -772,10 +772,10 @@ multi_json is "[\"" + first128 + "\",\"" + second64 + "\"]" multi_json_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["json_decode", multi_json]] sr_multi_json is sandbox_run of [multi_json_desc, 100000, 263] -assert_eq of [sr_multi_json["ok"], 0, +assert_eq of [sr_multi_json["ok"], false, "budget: later independent strbuf refusal keeps {ok:0}"] multi_json_msg is "" -if sr_multi_json["ok"] == 0: +if (not sr_multi_json["ok"]): multi_json_msg is sr_multi_json["error"]["message"] assert_eq of [multi_json_msg, "sandbox memory budget exceeded (used 208 + 128 > 263 bytes)", @@ -794,20 +794,20 @@ define descriptor_const_fn(x) as: builtin_const_desc is [ABI, [CONST,0,0, CONST,1,0, CALL,1,0, RETURN], [read_bytes_buf, [descriptor_fixture, 1]]] sr_builtin_const is sandbox_run of [builtin_const_desc, 100000, 1000000] -assert_eq of [sr_builtin_const["ok"], 0, +assert_eq of [sr_builtin_const["ok"], false, "sandbox descriptor: builtin constant is rejected at trust boundary"] builtin_const_error is "" -if sr_builtin_const["ok"] == 0: +if (not sr_builtin_const["ok"]): builtin_const_error is sr_builtin_const["error"]["message"] assert_eq of [builtin_const_error, "invalid chunk descriptor", "sandbox descriptor: builtin constant reports invalid descriptor"] fn_const_desc is [ABI, [CONST,0,0, CONST,1,0, CALL,1,0, RETURN], [descriptor_const_fn, 7]] sr_fn_const is sandbox_run of [fn_const_desc, 100000, 1000000] -assert_eq of [sr_fn_const["ok"], 0, +assert_eq of [sr_fn_const["ok"], false, "sandbox descriptor: function constant is rejected at trust boundary"] fn_const_error is "" -if sr_fn_const["ok"] == 0: +if (not sr_fn_const["ok"]): fn_const_error is sr_fn_const["error"]["message"] assert_eq of [fn_const_error, "invalid chunk descriptor", "sandbox descriptor: function constant reports invalid descriptor"] @@ -821,20 +821,20 @@ remove_file of descriptor_fixture # OP_CONST + an index, so the recursive contract is load-bearing. list_nest_desc is [ABI, [CONST,1,0, RETURN], [[7, [descriptor_const_fn]], 0]] sr_list_nest is sandbox_run of [list_nest_desc, 100000, 1000000] -assert_eq of [sr_list_nest["ok"], 0, +assert_eq of [sr_list_nest["ok"], false, "sandbox descriptor: callable nested in a list constant is rejected"] list_nest_msg is "" -if sr_list_nest["ok"] == 0: +if (not sr_list_nest["ok"]): list_nest_msg is sr_list_nest["error"]["message"] assert_eq of [list_nest_msg, "invalid chunk descriptor", "sandbox descriptor: nested list callable reports invalid descriptor"] dict_nest_desc is [ABI, [CONST,1,0, RETURN], [{"a": 1, "f": descriptor_const_fn}, 0]] sr_dict_nest is sandbox_run of [dict_nest_desc, 100000, 1000000] -assert_eq of [sr_dict_nest["ok"], 0, +assert_eq of [sr_dict_nest["ok"], false, "sandbox descriptor: callable nested in a dict value is rejected"] dict_nest_msg is "" -if sr_dict_nest["ok"] == 0: +if (not sr_dict_nest["ok"]): dict_nest_msg is sr_dict_nest["error"]["message"] assert_eq of [dict_nest_msg, "invalid chunk descriptor", "sandbox descriptor: nested dict callable reports invalid descriptor"] @@ -856,10 +856,10 @@ for i in range of 6: # without the descriptor pool ever having been bounded. dag_over_desc is [ABI, [CONST,1,0, RETURN], [dag_over, 0]] sr_dag_over is sandbox_run of [dag_over_desc, 100000, 1000000] -assert_eq of [sr_dag_over["ok"], 0, +assert_eq of [sr_dag_over["ok"], false, "sandbox descriptor: aliased data DAG past the node budget is refused"] dag_over_msg is "" -if sr_dag_over["ok"] == 0: +if (not sr_dag_over["ok"]): dag_over_msg is sr_dag_over["error"]["message"] assert_eq of [dag_over_msg, "invalid chunk descriptor", "sandbox descriptor: node-budget exhaustion fails closed"] @@ -872,7 +872,7 @@ for i in range of 4: dag_under is [dag_under, dag_under, dag_under, dag_under, dag_under, dag_under, dag_under, dag_under, dag_under, dag_under] dag_under_desc is [ABI, [CONST,1,0, RETURN], [dag_under, 0]] -assert_eq of [(sandbox_run of [dag_under_desc, 100000, 1000000])["ok"], 1, +assert_eq of [(sandbox_run of [dag_under_desc, 100000, 1000000])["ok"], true, "sandbox descriptor: aliased data DAG inside the node budget still verifies"] # The bound is spent across the WHOLE pool, not per constant: eight aliases of @@ -884,7 +884,7 @@ for i in range of 8: append of [dag_pool, 0] dag_pool_desc is [ABI, [CONST,8,0, RETURN], dag_pool] sr_dag_pool is sandbox_run of [dag_pool_desc, 100000, 1000000] -assert_eq of [sr_dag_pool["ok"], 0, +assert_eq of [sr_dag_pool["ok"], false, "sandbox descriptor: node budget is shared across the whole constant pool"] # --- #965 (fix2): raw VM slices charge each newly retained allocation ------- @@ -908,15 +908,15 @@ slice_loop_code is [LIST,0,0, SET_NAME_LOCAL,4,0, POP, slice_loop_desc is [ABI, slice_loop_code, ["append", slice_src, 0, 1000, "slice_acc"]] sr_slice_loop is sandbox_run of [slice_loop_desc, 7, 1000] -assert_eq of [sr_slice_loop["ok"], 0, "budget: retained string slices -> {ok:0}"] +assert_eq of [sr_slice_loop["ok"], false, "budget: retained string slices -> {ok:0}"] slice_loop_kind is "" slice_loop_msg is "" -if sr_slice_loop["ok"] == 0: +if (not sr_slice_loop["ok"]): slice_loop_kind is sr_slice_loop["error"]["kind"] slice_loop_msg is sr_slice_loop["error"]["message"] assert_eq of [slice_loop_kind, "sandbox", "budget: string-slice refusal names the sandbox"] slice_loop_mem is index_of of [slice_loop_msg, "memory budget"] -assert_eq of [slice_loop_mem >= 0, 1, "budget: retained string slices stop on bytes, not the iteration cap"] +assert_eq of [slice_loop_mem >= 0, true, "budget: retained string slices stop on bytes, not the iteration cap"] # A positive buffer slice is below the result-scan ceiling, so only its newly # allocated element storage should decide this tight budget. @@ -924,7 +924,7 @@ slice_target is buffer of 2000 buf_slice_desc is [ABI, [CONST,0,0, CONST,1,0, CONST,2,0, SLICE_GET, RETURN], [slice_target, 0, 1000]] sr_buf_slice is sandbox_run of [buf_slice_desc, 100000, 1000] -assert_eq of [sr_buf_slice["ok"], 0, "budget: positive buffer slice charges element storage"] +assert_eq of [sr_buf_slice["ok"], false, "budget: positive buffer slice charges element storage"] # Reserve refusal is the first diagnostic. strbuf_finish must transfer the # poisoned partial buffer without attempting a second charge that overwrites @@ -936,15 +936,15 @@ refuse_input is refuse_input + "xxxxxx" refuse_json_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["json_encode", refuse_input]] sr_refuse_json is sandbox_run of [refuse_json_desc, 100000, 127] -assert_eq of [sr_refuse_json["ok"], 0, "budget: refused strbuf -> {ok:0}"] +assert_eq of [sr_refuse_json["ok"], false, "budget: refused strbuf -> {ok:0}"] refuse_kind is "" refuse_msg is "" -if sr_refuse_json["ok"] == 0: +if (not sr_refuse_json["ok"]): refuse_kind is sr_refuse_json["error"]["kind"] refuse_msg is sr_refuse_json["error"]["message"] assert_eq of [refuse_kind, "sandbox", "budget: refused strbuf names the sandbox"] refuse_orig is index_of of [refuse_msg, "used 64 + 128 > 127"] -assert_eq of [refuse_orig >= 0, 1, "budget: first strbuf refusal diagnostic survives finish"] +assert_eq of [refuse_orig >= 0, true, "budget: first strbuf refusal diagnostic survives finish"] # Empty buffers have no element nodes. Pin the flat and one-level-nested # shapes at the same strict boundary as an equal-length list: 99,999 total @@ -955,14 +955,14 @@ for i in range of 99997: append of [empty_flat, 0] append of [empty_flat, empty_buf] empty_flat_desc is [ABI, [CONST,0,0, RETURN], [empty_flat]] -assert_eq of [(sandbox_run of [empty_flat_desc, 100000, 4000000])["ok"], 1, +assert_eq of [(sandbox_run of [empty_flat_desc, 100000, 4000000])["ok"], true, "boundary: empty buffer has weight 1 in a flat result"] empty_nested is [] for i in range of 99996: append of [empty_nested, 0] append of [empty_nested, [empty_buf]] empty_nested_desc is [ABI, [CONST,0,0, RETURN], [empty_nested]] -assert_eq of [(sandbox_run of [empty_nested_desc, 100000, 4000000])["ok"], 1, +assert_eq of [(sandbox_run of [empty_nested_desc, 100000, 4000000])["ok"], true, "boundary: nested empty buffer has weight 1 plus its container"] # --- #965 (fix1): changed strbuf consumers outside a sandbox are unchanged ----- diff --git a/tests/test_sandbox_error_silence.eigs b/tests/test_sandbox_error_silence.eigs index 413a2bd9..707dd674 100644 --- a/tests/test_sandbox_error_silence.eigs +++ b/tests/test_sandbox_error_silence.eigs @@ -9,7 +9,7 @@ RETURN is 40 desc is [1, [LINE,77,0,0,0, CONST,0,0, CONST,1,0, DIV, RETURN], [1, 0]] r is sandbox_run of [desc, 1000] -assert_eq of [r["ok"], 0, "sandbox error reports ok=0"] +assert_eq of [r["ok"], false, "sandbox error reports ok=0"] assert_eq of [r["error"]["kind"], "value", "sandbox error retains kind"] assert_eq of [r["error"]["message"], "division by zero", "sandbox error retains message"] assert_eq of [r["error"]["line"], 77, "sandbox error retains bytecode line"] diff --git a/tests/test_sandbox_intern_lifetime.c b/tests/test_sandbox_intern_lifetime.c index 7524d7b7..5de41e90 100644 --- a/tests/test_sandbox_intern_lifetime.c +++ b/tests/test_sandbox_intern_lifetime.c @@ -109,7 +109,7 @@ int main(void) { Value *out = sandbox_call(i); assert(out != NULL); Value *ok = dict_get(out, "ok"); - assert(ok && ok->type == VAL_NUM && ok->data.num == 1.0); + assert(ok && ok->type == VAL_BOOL && ok->data.boolean == 1); /* #1637 */ Value *result = dict_get(out, "result"); assert(result && result->type == VAL_DICT); if (i == 0) { @@ -134,7 +134,7 @@ int main(void) { free(diagnostic); } assert(escaped_value && escaped_value->type == VAL_NUM && - escaped_value->data.num == 1.0); + VAL_NUM_RAW(escaped_value) == 1.0); fprintf(stderr, "sandbox intern growth: baseline=%zu retained=%zu sandbox_only=%zu\n", baseline, sandbox_retained, sandbox_prefix_retained); @@ -161,8 +161,8 @@ int main(void) { Value *invalid_out = invalid_sandbox_call(); assert(invalid_out != NULL); Value *invalid_ok = dict_get(invalid_out, "ok"); - assert(invalid_ok && invalid_ok->type == VAL_NUM && - invalid_ok->data.num == 0.0); + assert(invalid_ok && invalid_ok->type == VAL_BOOL && + invalid_ok->data.boolean == 0); /* #1637: ok is a bool */ val_decref(invalid_out); assert(g_gc_val_count == 0); /* Candidate-only boundary collection must not scan the unrelated captured diff --git a/tests/test_sandbox_predicate_isolation.eigs b/tests/test_sandbox_predicate_isolation.eigs index d1bd5dd2..827f745e 100644 --- a/tests/test_sandbox_predicate_isolation.eigs +++ b/tests/test_sandbox_predicate_isolation.eigs @@ -24,7 +24,7 @@ for i in range of 12: r is report of secret for k in range of 6: res is sandbox_run of [[1, [59, k, 0, 40], []], 100000] - check of ["ramping host: bare predicate kind " + (str of k) + " reads 0 in the sandbox", res["result"], 0] + check of ["ramping host: bare predicate kind " + (str of k) + " reads 0 in the sandbox", res["result"], false] flat is 5.0 for i in range of 12: @@ -32,7 +32,7 @@ for i in range of 12: r is report of flat for k in range of 6: res is sandbox_run of [[1, [59, k, 0, 40], []], 100000] - check of ["flat host: bare predicate kind " + (str of k) + " reads 0 in the sandbox", res["result"], 0] + check of ["flat host: bare predicate kind " + (str of k) + " reads 0 in the sandbox", res["result"], false] # the host's tracker survives the run: its own view of `flat` is unchanged check of ["host report after the sandbox runs still names the host binding", report of flat, "converged"] diff --git a/tests/test_sandbox_work_budget.c b/tests/test_sandbox_work_budget.c index 46f5f0ae..8888f81a 100644 --- a/tests/test_sandbox_work_budget.c +++ b/tests/test_sandbox_work_budget.c @@ -21,7 +21,7 @@ static Value *ordinary_descriptor(void) { if (chunk && !g_has_error && !g_parse_errors && chunk->code_len > 0 && chunk->code_len <= 64 && !chunk->fn_count && !chunk->local_count && !chunk->param_count && chunk->const_count == 1 && - chunk->constants[0]->type == VAL_NUM && chunk->constants[0]->data.num == 42) { + chunk->constants[0]->type == VAL_NUM && VAL_NUM_RAW(chunk->constants[0]) == 42) { Value *code = make_list(chunk->code_len); Value *constants = make_list(1); for (int i = 0; i < chunk->code_len; i++) @@ -61,15 +61,15 @@ int main(void) { Value *out = builtin_sandbox_run(args); Value *ok = out ? dict_get(out, "ok") : NULL; Value *result = out ? dict_get(out, "result") : NULL; - REQUIRE(ok && ok->type == VAL_NUM && ok->data.num == 1 && - result && result->type == VAL_NUM && result->data.num == 42, + REQUIRE(ok && ok->type == VAL_BOOL && ok->data.boolean == 1 && + result && result->type == VAL_NUM && VAL_NUM_RAW(result) == 42, "ordinary sandbox result"); REQUIRE(!g_sandbox_active && g_sandbox_work_used == 13 && g_sandbox_work_max == 77, "sandbox restores caller accounting"); val_decref(out); val_decref(args); Value *host = eigs_eval_string("return 7\n"); - REQUIRE(host && host->type == VAL_NUM && host->data.num == 7, + REQUIRE(host && host->type == VAL_NUM && VAL_NUM_RAW(host) == 7, "host work between sandbox calls"); val_decref(host); REQUIRE(g_sandbox_work_used == 13 && g_sandbox_work_max == 77, @@ -84,13 +84,13 @@ int main(void) { g_sandbox_work_max = 10000; g_sandbox_work_used = 0; Value *ordinary = eigs_eval_string("return 42\n"); - REQUIRE(ordinary && ordinary->type == VAL_NUM && ordinary->data.num == 42, + REQUIRE(ordinary && ordinary->type == VAL_NUM && VAL_NUM_RAW(ordinary) == 42, "ordinary arithmetic result"); val_decref(ordinary); REQUIRE(g_sandbox_work_used > 0, "ordinary dispatch charges shared account"); uint64_t previous_work = g_sandbox_work_used; ordinary = eigs_eval_string("define work_identity(x) as:\n return x\nreturn work_identity of 42\n"); - REQUIRE(ordinary && ordinary->type == VAL_NUM && ordinary->data.num == 42, + REQUIRE(ordinary && ordinary->type == VAL_NUM && VAL_NUM_RAW(ordinary) == 42, "ordinary function result"); val_decref(ordinary); REQUIRE(g_sandbox_work_used > previous_work, "re-entry preserves accounting"); diff --git a/tests/test_scope_semantics.eigs b/tests/test_scope_semantics.eigs index 12910a55..0ae246ca 100644 --- a/tests/test_scope_semantics.eigs +++ b/tests/test_scope_semantics.eigs @@ -221,7 +221,7 @@ define catch_leak as: catch ev633: noop633 is 1 return ev633 == 100 -assert of [catch_leak of null == 0, "SS18 catch name leaks over an earlier plain assignment"] +assert of [(not catch_leak of null), "SS18 catch name leaks over an earlier plain assignment"] define catch_leak_dead_closure as: ev633 is 100 @@ -232,7 +232,7 @@ define catch_leak_dead_closure as: stale633 is ev633 == 100 dead633 is () => ev633 return stale633 -assert of [catch_leak_dead_closure of null == 0, "SS19 a never-called closure does not change an earlier catch read"] +assert of [not (catch_leak_dead_closure of null), "SS19 a never-called closure does not change an earlier catch read"] # SS20 (#642): the OTHER half of the double-binding defect #633's fix closed — # and the one SS16-SS19 cannot see. Those all read the name AFTER the diff --git a/tests/test_security_hardening.eigs b/tests/test_security_hardening.eigs index 7c2fae35..729a7a62 100644 --- a/tests/test_security_hardening.eigs +++ b/tests/test_security_hardening.eigs @@ -4,11 +4,11 @@ # input) must produce a parse error, not a stack-overflow crash. # --- secure_equals --- -assert of [(secure_equals of ["token123", "token123"]) == 1, "equal strings"] -assert of [(secure_equals of ["token123", "token124"]) == 0, "differing content"] -assert of [(secure_equals of ["abc", "abcd"]) == 0, "differing length"] -assert of [(secure_equals of ["", ""]) == 1, "both empty"] -assert of [(secure_equals of ["x", ""]) == 0, "one empty"] +assert of [(secure_equals of ["token123", "token123"]), "equal strings"] +assert of [(not (secure_equals of ["token123", "token124"])), "differing content"] +assert of [(not (secure_equals of ["abc", "abcd"])), "differing length"] +assert of [(secure_equals of ["", ""]), "both empty"] +assert of [(not (secure_equals of ["x", ""])), "one empty"] # --- parser depth guard via eval --- # Build valid but very deeply nested source: ((((...1...)))) — 2000 deep, diff --git a/tests/test_stdlib_fixes.eigs b/tests/test_stdlib_fixes.eigs index cfc2751f..86684a64 100644 --- a/tests/test_stdlib_fixes.eigs +++ b/tests/test_stdlib_fixes.eigs @@ -133,13 +133,13 @@ assert of [(drop of [[1, 2], 5]) == [], "SF47 drop exceeds length"] assert of [(drop of [[], 3]) == [], "SF48 drop empty list"] # --- list.any / list.all --- -assert of [(any of [[0, 0, 3, 0], (x) => x > 0]) == 1, "SF49 any matches mid"] -assert of [(any of [[5, 0, 0], (x) => x > 0]) == 1, "SF50 any short-circuits on first match"] -assert of [(any of [[0, 0, 0], (x) => x > 0]) == 0, "SF51 any none match"] -assert of [(any of [[], (x) => x > 0]) == 0, "SF52 any empty list is false"] -assert of [(all of [[1, 2, 3], (x) => x > 0]) == 1, "SF53 all match"] -assert of [(all of [[1, -1, 3], (x) => x > 0]) == 0, "SF54 all short-circuits on first fail"] -assert of [(all of [[], (x) => x > 0]) == 1, "SF55 all empty list is vacuously true"] +assert of [(any of [[0, 0, 3, 0], (x) => x > 0]), "SF49 any matches mid"] +assert of [(any of [[5, 0, 0], (x) => x > 0]), "SF50 any short-circuits on first match"] +assert of [(not (any of [[0, 0, 0], (x) => x > 0])), "SF51 any none match"] +assert of [(not (any of [[], (x) => x > 0])), "SF52 any empty list is false"] +assert of [(all of [[1, 2, 3], (x) => x > 0]), "SF53 all match"] +assert of [(not (all of [[1, -1, 3], (x) => x > 0])), "SF54 all short-circuits on first fail"] +assert of [(all of [[], (x) => x > 0]), "SF55 all empty list is vacuously true"] # --- list.find_index --- assert of [(find_index of [[10, 20, 30, 40], (x) => x > 25]) == 2, "SF56 find_index middle"] @@ -181,7 +181,7 @@ assert of [(count_of of [[], 5]) == 0, "SF79 count_of empty"] assert of [(count_of of [[7], 7]) == 1, "SF80 count_of single element"] fr is frequencies of [1, 2, 2, 3, 2, 1] assert of [fr["1"] == 2 and fr["2"] == 3 and fr["3"] == 1, "SF81 frequencies distribution"] -assert of [(has_key of [fr, "9"]) == 0, "SF82 frequencies omits absent values"] +assert of [(not (has_key of [fr, "9"])), "SF82 frequencies omits absent values"] assert of [(len of (keys of (frequencies of []))) == 0, "SF83 frequencies empty -> empty dict"] fr_mixed is frequencies of ["a", 1, "a", 1, 1] assert of [fr_mixed["a"] == 2 and fr_mixed["1"] == 3, "SF84 frequencies mixed types"] diff --git a/tests/test_stem_accuracy.eigs b/tests/test_stem_accuracy.eigs index 6fba95c9..61af0020 100644 --- a/tests/test_stem_accuracy.eigs +++ b/tests/test_stem_accuracy.eigs @@ -216,7 +216,7 @@ psq is punnett_square of ["Aa", "Aa"] approx of [psq["AA"], 0.25, "punnett AA (#639)"] approx of [psq["Aa"], 0.5, "punnett Aa heterozygote folds aA in (#639)"] approx of [psq["aa"], 0.25, "punnett aa (#639)"] -exact of [has_key of [psq, "aA"], 0, "punnett has no separate aA key (#639)"] +exact of [has_key of [psq, "aA"], false, "punnett has no separate aA key (#639)"] # #640 power_iteration: the dominant eigenvalue of diag(-5, 1) is -5, not |−5|. neg_pit is power_iteration of [[[0 - 5, 0], [0, 1]], 200] diff --git a/tests/test_store.eigs b/tests/test_store.eigs index 8ab7e377..ec098e9c 100644 --- a/tests/test_store.eigs +++ b/tests/test_store.eigs @@ -43,7 +43,7 @@ assert_eq of [r2.age, 31, "update works"] # Delete d is store_delete of [db, "users", k2] -assert_eq of [d, 1, "delete returns 1"] +assert_eq of [d, true, "delete returns 1"] c2 is store_count of [db, "users"] assert_eq of [c2, 1, "count after delete"] @@ -311,7 +311,7 @@ assert_eq of [u_kind, "limit", "#1006: an oversized update raises, catchably"] # reports 1" is not assertable from EigenScript and an assertion that looks # like it is true of HEAD too. The 0 that IS observable is the locate miss, # which the refactor rewrote, so pin that instead. -assert_eq of [store_update of [udb, "docs", "no-such-key", {"body": "x"}], 0, "#1006: updating an absent key reports 0 and raises nothing"] +assert_eq of [store_update of [udb, "docs", "no-such-key", {"body": "x"}], false, "#1006: updating an absent key reports 0 and raises nothing"] assert_eq of [store_count of [udb, "docs"], 3, "#1006: an absent-key update inserts nothing"] # The record and both neighbours are exactly as they were. @@ -352,7 +352,7 @@ assert of [contains of [u_msg, "store_update:"], "#1006: the error names store_u # record check above the locate would raise on an argument shape that used to # be quiet, with the strict flag off. Nothing else in the suite pins this — a # build with the check hoisted passes every other assertion in this file. -assert_eq of [store_update of [udb, "docs", "no-such-key", "not-a-dict"], 0, "#1006: an absent key with a non-dict record stays a silent 0"] +assert_eq of [store_update of [udb, "docs", "no-such-key", "not-a-dict"], false, "#1006: an absent key with a non-dict record stays a silent 0"] assert_eq of [(store_get of [udb, "docs", "doc1"]).body, "original", "#1006: non-dict update leaves the record"] store_close of udb @@ -361,7 +361,7 @@ assert_eq of [after_bytes, before_bytes, "#1006: rejected updates leave the file # The store is not wedged: a legitimate update still replaces the record. udb is store_open of "/tmp/eigs1006_update.db" -assert_eq of [store_update of [udb, "docs", "doc1", {"body": "replaced"}], 1, "#1006: a valid update still reports 1"] +assert_eq of [store_update of [udb, "docs", "doc1", {"body": "replaced"}], true, "#1006: a valid update still reports 1"] assert_eq of [(store_get of [udb, "docs", "doc1"]).body, "replaced", "#1006: a valid update still replaces"] assert_eq of [store_count of [udb, "docs"], 3, "#1006: a valid update does not duplicate the row"] store_close of udb diff --git a/tests/test_store_corruption.eigs b/tests/test_store_corruption.eigs index 4502f0c7..3e06392b 100644 --- a/tests/test_store_corruption.eigs +++ b/tests/test_store_corruption.eigs @@ -38,7 +38,7 @@ catch e: cm is 1 mm is e assert of [cm == 1, "SC01 corrupt magic rejected"] -assert of [(contains of [mm.message, "invalid database file"]) == 1, "SC01 message"] +assert of [(contains of [mm.message, "invalid database file"]), "SC01 message"] # ---- corrupt version (version != 1) ---- hv is base_hdr of 1 @@ -52,7 +52,7 @@ catch e: cv is 1 mv is e assert of [cv == 1, "SC02 corrupt version rejected"] -assert of [(contains of [mv.message, "invalid database file"]) == 1, "SC02 message"] +assert of [(contains of [mv.message, "invalid database file"]), "SC02 message"] # ---- corrupt page_size (4097 != 4096) ---- hp is base_hdr of 1 @@ -66,7 +66,7 @@ catch e: cp is 1 mp is e assert of [cp == 1, "SC03 corrupt page_size rejected"] -assert of [(contains of [mp.message, "invalid database file"]) == 1, "SC03 message"] +assert of [(contains of [mp.message, "invalid database file"]), "SC03 message"] # ---- not page-aligned (header + 10 stray bytes) ---- ha is base_hdr of 1 @@ -83,7 +83,7 @@ catch e: ca is 1 ma is e assert of [ca == 1, "SC04 non-page-aligned file rejected"] -assert of [(contains of [ma.message, "invalid database file"]) == 1, "SC04 message"] +assert of [(contains of [ma.message, "invalid database file"]), "SC04 message"] # ---- page_count mismatch (header claims 5, file has 1 page) ---- hpc is base_hdr of 5 @@ -100,7 +100,7 @@ catch e: cpc is 1 mpc is e assert of [cpc == 1, "SC05 page_count mismatch rejected"] -assert of [(contains of [mpc.message, "invalid database file"]) == 1, "SC05 message"] +assert of [(contains of [mpc.message, "invalid database file"]), "SC05 message"] # ---- store_put: record larger than a page ---- db is store_open of "/tmp/eigs_corrupt_put.db" @@ -115,7 +115,7 @@ catch e: cput is 1 mput is e assert of [cput == 1, "SC06 oversized record rejected"] -assert of [(contains of [mput.message, "exceeds page size"]) == 1, "SC06 message"] +assert of [(contains of [mput.message, "exceeds page size"]), "SC06 message"] store_close of db print of "All store_corruption tests passed" diff --git a/tests/test_stream_io.eigs b/tests/test_stream_io.eigs index f40bcbd2..48faacd6 100644 --- a/tests/test_stream_io.eigs +++ b/tests/test_stream_io.eigs @@ -5,16 +5,16 @@ path is mktemp of null # Open a stream for 4 values ok is stream_open of [path, 4] -assert_eq of [ok, 1, "stream_open returns 1 on success"] +assert_eq of [ok, true, "stream_open returns 1 on success"] # Write four doubles -assert_eq of [stream_write of 1.5, 1, "stream_write returns 1"] -assert_eq of [stream_write of -2.25, 1, "stream_write returns 1"] -assert_eq of [stream_write of 3.125, 1, "stream_write returns 1"] -assert_eq of [stream_write of 0, 1, "stream_write returns 1"] +assert_eq of [stream_write of 1.5, true, "stream_write returns 1"] +assert_eq of [stream_write of -2.25, true, "stream_write returns 1"] +assert_eq of [stream_write of 3.125, true, "stream_write returns 1"] +assert_eq of [stream_write of 0, true, "stream_write returns 1"] # Close -assert_eq of [stream_close of null, 1, "stream_close returns 1 on success"] +assert_eq of [stream_close of null, true, "stream_close returns 1 on success"] # Roundtrip via tensor_load (stream format is the no-observer 1-D tensor format) t is tensor_load of path @@ -25,9 +25,9 @@ assert_eq of [get_at of [t, 2], 3.125, "stream value 2"] assert_eq of [get_at of [t, 3], 0, "stream value 3"] # Rejection cases -assert_eq of [stream_write of 1.0, 0, "stream_write fails after close"] -assert_eq of [stream_close of null, 0, "stream_close on closed stream returns 0"] -assert_eq of [stream_open of ["nonsense", "not-a-number"], 0, "stream_open rejects bad args"] +assert_eq of [stream_write of 1.0, false, "stream_write fails after close"] +assert_eq of [stream_close of null, false, "stream_close on closed stream returns 0"] +assert_eq of [stream_open of ["nonsense", "not-a-number"], false, "stream_open rejects bad args"] rm of path test_summary of null diff --git a/tests/test_strict_math.sh b/tests/test_strict_math.sh index 781cf4c8..bc0062e1 100755 --- a/tests/test_strict_math.sh +++ b/tests/test_strict_math.sh @@ -149,8 +149,8 @@ run "SM17 strict leaves valid args alone" 1 0 "1" 'prin # The exclusions matter as much as the conversions: these 0s are DOCUMENTED # RETURN VALUES, not fail-soft guards, so strict must NOT make them raise. # A sed over `return make_num(0)` would have broken both. -run "SM18 strict: try_parse(bad) still answers 0" 1 0 "0" 'print of (try_parse of "!!!")' -run "SM19 strict: unknown task id still not alive" 1 0 "0" 'print of (task_alive of 99999)' +run "SM18 strict: try_parse(bad) still answers false" 1 0 "false" 'print of (try_parse of "!!!")' +run "SM19 strict: unknown task id still not alive" 1 0 "false" 'print of (task_alive of 99999)' # --- #971 Phase B: the ELEMENT-type guards ----------------------------------- # Phase A converted the outer ARITY guards and left the inner element-type @@ -187,7 +187,7 @@ run "SM29 strict str_replace coercion raises" 1 1 "str_replace: expected a stri # More exclusion pins. These grew with the conversion set on purpose: without # them, converting EVERYTHING would score a perfect "raises under strict" and # the reform would have no failure mode at all. -run "SM30 strict: ends_with, suffix too long, still 0" 1 0 "0" 'print of (ends_with of ["ab", "abc"])' +run "SM30 strict: ends_with, suffix too long, still false" 1 0 "false" 'print of (ends_with of ["ab", "abc"])' run "SM31 strict: char_at past the end still empty" 1 0 "[]" \ 'local r is char_at of ["ab", 9] print of f"[{r}]"' @@ -195,8 +195,8 @@ run "SM32 strict: join of an empty list still empty" 1 0 "[]" \ 'local r is join of [[], ","] print of f"[{r}]"' run "SM33 strict: num still COERCES a list to 0" 1 0 "0" 'print of (num of ([1, 2]))' -run "SM34 strict: list_contains finding nothing is 0" 1 0 "0" 'print of (list_contains of [[1, 2], 9])' -run "SM35 strict: JSON false still decodes to 0" 1 0 "0" \ +run "SM34 strict: list_contains finding nothing is false" 1 0 "false" 'print of (list_contains of [[1, 2], 9])' +run "SM35 strict: JSON false decodes to false (#1637)" 1 0 "false" \ 'print of (json_path of ["{\"a\": false}", "a"])' # --- #971 Phase C: JSON parse failure in json_path ---------------------------- @@ -223,7 +223,7 @@ run "SM42 strict json_path raise is catchable as value" 1 0 "caught value" \ x is json_path of ["{bad", "a"] catch e: print of f"caught {e.kind}"' -run "SM43 strict: JSON false is still 0" 1 0 "0" 'print of (json_path of ["{\"a\": false}", "a"])' +run "SM43 strict: JSON false is still false" 1 0 "false" 'print of (json_path of ["{\"a\": false}", "a"])' run "SM44 strict: absent key is still empty" 1 0 "[]" 'print of f"[{json_path of ["{\"a\": 1}", "b"]}]"' run "SM45 strict: JSON null still renders empty" 1 0 "[]" 'print of f"[{json_path of ["{\"a\": null}", "a"]}]"' run "SM46 strict: valid nested path still resolves" 1 0 "x" 'print of (json_path of ["{\"a\": [1, {\"b\": \"x\"}]}", "a.1.b"])' @@ -237,16 +237,16 @@ run "SM46 strict: valid nested path still resolves" 1 0 "x" 'print of (json # elementwise `divide` by zero (pre-collapsed to 0 where `/` raises). EIGS_STRICT=0 # collapses to 0 + math_flags.invalid exactly as before (SM47-SM49 pin it). run "SM47 opt-out pow(-8, 0.5) still 0" 0 0 "0" 'print of (pow of [0 - 8, 0.5])' -run "SM48 opt-out num(\"nan\") still 0 + invalid" 0 0 "0 1" \ +run "SM48 opt-out num(\"nan\") still 0 + invalid" 0 0 "0 true" \ 'local v is num of "nan" print of f"{v} {(math_flags of null).invalid}"' -run "SM49a opt-out matmul(inf-inf) LIST path still collapses to 0 + invalid" 0 0 "[0] 1" \ +run "SM49a opt-out matmul(inf-inf) LIST path still collapses to 0 + invalid" 0 0 "[0] true" \ 'local r is matmul of [[[1e200, 1e200]], [[1e200], [0 - 1e200]]] print of f"{r} {(math_flags of null).invalid}"' # #1417 applies the same scalar-read rule to buffer and list results. The # kernel may retain a raw NaN internally, but exposing it collapses it to 0 and # sets invalid just as make_num does on the boxed path. -run "SM49b opt-out matmul(inf-inf) BUFFER read collapses to 0 + invalid" 0 0 "0 1" \ +run "SM49b opt-out matmul(inf-inf) BUFFER read collapses to 0 + invalid" 0 0 "0 true" \ 'local m1 is buffer of [1, 2] m1[0] is 1e200 m1[1] is 1e200 @@ -257,7 +257,7 @@ local r is matmul of [m1, m2] print of f"{r[0]} {(math_flags of null).invalid}"' # #1131: canonicalization must visit every NaN and preserve intervening # finite results. On ARM the kernel's invalid-operation NaNs are positive. -run "SM49c opt-out matmul buffer guards each NaN and preserves finite neighbor" 0 0 "0 2e+200 0 1" \ +run "SM49c opt-out matmul buffer guards each NaN and preserves finite neighbor" 0 0 "0 2e+200 0 true" \ 'local a is buffer of [1, 2] a[0] is 1e200 a[1] is 1e200 @@ -353,8 +353,8 @@ run_jitoff "SM61c opt-out pow -> 0 with the JIT off" 0 0 "0" \ # The documented sentinel for a valid-but-absent input is pinned in BOTH modes; # only a wrong-typed argument raises. run "SM62 strict: index_of miss is still -1" 1 0 "-1" 'print of (index_of of ["abc", "z"])' -run "SM63 strict: file_exists of an absent path is 0" 1 0 "0" 'print of (file_exists of "/nonexistent/eigs_971_probe")' -run "SM64 strict: is_dir of an absent path is 0" 1 0 "0" 'print of (is_dir of "/nonexistent/eigs_971_probe")' +run "SM63 strict: file_exists of an absent path is false" 1 0 "false" 'print of (file_exists of "/nonexistent/eigs_971_probe")' +run "SM64 strict: is_dir of an absent path is false" 1 0 "false" 'print of (is_dir of "/nonexistent/eigs_971_probe")' run "SM65 strict: read_text of an absent path is empty" 1 0 "[]" 'print of f"[{read_text of "/nonexistent/eigs_971_probe"}]"' run "SM66 strict index_of(num, str) raises" 1 1 "index_of: expected" 'print of (index_of of [42, "x"])' run "SM67 strict file_exists(num) raises" 1 1 "file_exists: expected" 'print of (file_exists of 42)' @@ -368,7 +368,7 @@ run "SM70 strict split with a non-string delimiter raises" 1 1 "split: expected run "SM71 strict scan_ints(dict) raises" 1 1 "scan_ints: expected" 'print of (scan_ints of ({"k": 1}))' run "SM72 strict buffer(str) raises" 1 1 "buffer: expected" 'print of (buffer of "x")' run "SM73 strict channel_closed(num) raises" 1 1 "channel_closed: expected" 'print of (channel_closed of 42)' -run "SM74 strict: unknown channel is still closed (1)" 1 0 "1" 'print of (channel_closed of ({"_channel_id": 99999}))' +run "SM74 strict: unknown channel is still closed (true)" 1 0 "true" 'print of (channel_closed of ({"_channel_id": 99999}))' run "SM75 strict f64_to_bytes(str) raises" 1 1 "f64_to_bytes: expected" 'print of (f64_to_bytes of "x")' run "SM76 strict random_int(bad bounds) raises" 1 1 "random_int: expected" 'print of (random_int of ["a", 3])' run "SM77 strict json_build(dict) raises" 1 1 "json_build: expected" 'print of (json_build of ({"a": 1}))' @@ -396,10 +396,10 @@ run "SM88 opt-out scan_int_tokens(num) is still []" 0 0 "[]" 'print of f"[{scan_ # this witness stays finite, so a NaN/overflow special case cannot satisfy it. # Pin all three shared kernels, each on both storage roads and in both modes. for strict_mode in 0 1; do - run "SM89 mode=$strict_mode matmul list rounds product before sum" "$strict_mode" 0 "zero:1:end" \ + run "SM89 mode=$strict_mode matmul list rounds product before sum" "$strict_mode" 0 "zero:true:end" \ 'local r is matmul of [[[-1, 1.0000000074505806]], [[1], [0.9999999925494194]]] print of f"zero:{r[0] == 0}:end"' - run "SM90 mode=$strict_mode matmul buffer rounds product before sum" "$strict_mode" 0 "zero:1:end" \ + run "SM90 mode=$strict_mode matmul buffer rounds product before sum" "$strict_mode" 0 "zero:true:end" \ 'local a is buffer of [1, 2] a[0] is -1 a[1] is 1.0000000074505806 @@ -408,10 +408,10 @@ b[0] is 1 b[1] is 0.9999999925494194 local r is matmul of [a, b] print of f"zero:{r[0] == 0}:end"' - run "SM91 mode=$strict_mode matmul_at list rounds product before sum" "$strict_mode" 0 "zero:1:end" \ + run "SM91 mode=$strict_mode matmul_at list rounds product before sum" "$strict_mode" 0 "zero:true:end" \ 'local r is matmul_at of [[[-1], [1.0000000074505806]], [[1], [0.9999999925494194]]] print of f"zero:{r[0][0] == 0}:end"' - run "SM92 mode=$strict_mode matmul_at buffer rounds product before sum" "$strict_mode" 0 "zero:1:end" \ + run "SM92 mode=$strict_mode matmul_at buffer rounds product before sum" "$strict_mode" 0 "zero:true:end" \ 'local a is buffer of [2, 1] a[0] is -1 a[1] is 1.0000000074505806 @@ -420,10 +420,10 @@ b[0] is 1 b[1] is 0.9999999925494194 local r is matmul_at of [a, b] print of f"zero:{r[0] == 0}:end"' - run "SM93 mode=$strict_mode matmul_bt list rounds product before sum" "$strict_mode" 0 "zero:1:end" \ + run "SM93 mode=$strict_mode matmul_bt list rounds product before sum" "$strict_mode" 0 "zero:true:end" \ 'local r is matmul_bt of [[[-1, 1.0000000074505806]], [[1, 0.9999999925494194]]] print of f"zero:{r[0] == 0}:end"' - run "SM94 mode=$strict_mode matmul_bt buffer rounds product before sum" "$strict_mode" 0 "zero:1:end" \ + run "SM94 mode=$strict_mode matmul_bt buffer rounds product before sum" "$strict_mode" 0 "zero:true:end" \ 'local a is buffer of [1, 2] a[0] is -1 a[1] is 1.0000000074505806 diff --git a/tests/test_string_math.eigs b/tests/test_string_math.eigs index aacd375f..24925a06 100644 --- a/tests/test_string_math.eigs +++ b/tests/test_string_math.eigs @@ -15,9 +15,9 @@ assert of [(char_at of ["hello", 5]) == "", "CA3 out of range"] assert of [(char_at of ["hello", -1]) == "o", "CA4 negative index counts from the end (#312)"] # ---- ends_with ---- -assert of [(ends_with of ["hello.eigs", ".eigs"]) == 1, "EW1 match"] -assert of [(ends_with of ["hello.eigs", ".py"]) == 0, "EW2 no match"] -assert of [(ends_with of ["hi", "hello"]) == 0, "EW3 suffix longer"] +assert of [(ends_with of ["hello.eigs", ".eigs"]), "EW1 match"] +assert of [(not (ends_with of ["hello.eigs", ".py"])), "EW2 no match"] +assert of [(not (ends_with of ["hi", "hello"])), "EW3 suffix longer"] # ---- substr ---- assert of [(substr of ["hello world", 6, 5]) == "world", "SS1 basic"] diff --git a/tests/test_system.eigs b/tests/test_system.eigs index 6340e615..89e8793b 100644 --- a/tests/test_system.eigs +++ b/tests/test_system.eigs @@ -52,8 +52,8 @@ assert of [tmpdir_result[0] == 0, "MK0 mktemp directory"] tmpdir is trim of tmpdir_result[1] test_dir is tmpdir + "/system" ok is mkdir of test_dir -assert of [ok == 1, "MK1 mkdir"] -assert of [(file_exists of test_dir) == 1, "MK2 dir exists"] +assert of [ok, "MK1 mkdir"] +assert of [(file_exists of test_dir), "MK2 dir exists"] # ---- ls ---- write_text of [test_dir + "/e.txt", "e"] @@ -72,11 +72,11 @@ assert of [(len of cwd) > 0, "GC1 getcwd non-empty"] # ---- mktemp ---- tmp is mktemp of null assert of [(len of tmp) > 0, "MT1 mktemp non-empty"] -assert of [(file_exists of tmp) == 1, "MT2 temp file exists"] +assert of [(file_exists of tmp), "MT2 temp file exists"] # ---- rm ---- rm of tmp -assert of [(file_exists of tmp) == 0, "RM1 rm removes file"] +assert of [(not (file_exists of tmp)), "RM1 rm removes file"] # ---- cleanup ---- rm of (test_dir + "/a.txt") diff --git a/tests/test_tape_observer_config.sh b/tests/test_tape_observer_config.sh index 3f9e7555..d6a4c69d 100755 --- a/tests/test_tape_observer_config.sh +++ b/tests/test_tape_observer_config.sh @@ -228,29 +228,29 @@ RC=$? || fail "multiplicative observer replay work is refused with exit 3" \ "rc=$RC $(head -1 "$TMPDIR/work-limit.err")" -# ---- 7. #411 version/compat path. The `O` records are an ENCODING change, -# so TRACE_FORMAT_VERSION is now 5 for correspondence declarations. -# Version-and-reject, never migrate: a v4 tape has stream IDs but no -# host/causal associations and is refused by both the stepper and replay. -head -1 "$TMPDIR/thr.tape" | grep -q '^V 5 ' \ - && ok "tapes written by this build stamp format v5" \ - || fail "tapes written by this build stamp format v5" \ +# ---- 7. #411 version/compat path. A value-encoding change bumps +# TRACE_FORMAT_VERSION: v6 (#1637) writes true/false as bool values where a v5 +# tape held a predicate's answer as 1/0. Version-and-reject, never migrate: a +# v5 tape is refused by both the stepper and replay, never read as numbers. +head -1 "$TMPDIR/thr.tape" | grep -q '^V 6 ' \ + && ok "tapes written by this build stamp format v6" \ + || fail "tapes written by this build stamp format v6" \ "$(head -1 "$TMPDIR/thr.tape")" -V2="$TMPDIR/v4.tape" -sed "1s/^V 5 /V 4 /" "$TMPDIR/thr.tape" > "$V2" +V2="$TMPDIR/v5.tape" +sed "1s/^V 6 /V 5 /" "$TMPDIR/thr.tape" > "$V2" echo q | "$EIGS" --step "$V2" "$TMPDIR/thr.eigs" >/dev/null 2>"$TMPDIR/v2.err" RC=$? -[ "$RC" -eq 3 ] && grep -q "tape format v4" "$TMPDIR/v2.err" \ - && ok "a v4 (pre-association) tape is refused by --step with exit 3" \ - || fail "a v4 (pre-association) tape is refused by --step with exit 3" \ +[ "$RC" -eq 3 ] && grep -q "tape format v5" "$TMPDIR/v2.err" \ + && ok "a v5 (pre-bool) tape is refused by --step with exit 3" \ + || fail "a v5 (pre-bool) tape is refused by --step with exit 3" \ "rc=$RC $(head -1 "$TMPDIR/v2.err")" EIGS_REPLAY="$V2" "$EIGS" "$TMPDIR/thr.eigs" >/dev/null 2>"$TMPDIR/v2r.err" RC=$? -[ "$RC" -eq 3 ] && grep -q "format v4" "$TMPDIR/v2r.err" \ - && ok "a v4 (pre-association) tape is refused by EIGS_REPLAY with exit 3" \ - || fail "a v4 (pre-association) tape is refused by EIGS_REPLAY with exit 3" \ +[ "$RC" -eq 3 ] && grep -q "format v5" "$TMPDIR/v2r.err" \ + && ok "a v5 (pre-bool) tape is refused by EIGS_REPLAY with exit 3" \ + || fail "a v5 (pre-bool) tape is refused by EIGS_REPLAY with exit 3" \ "rc=$RC $(head -1 "$TMPDIR/v2r.err")" # The two checks above rewrite this build's own header, which proves the diff --git a/tests/test_task_sched_trace.eigs b/tests/test_task_sched_trace.eigs index e10d1640..454cfbd9 100644 --- a/tests/test_task_sched_trace.eigs +++ b/tests/test_task_sched_trace.eigs @@ -91,7 +91,7 @@ define receiver() as: r is task_spawn of receiver dict_set of [names, str of r, "r"] task_yield of null -assert_eq of [task_send of [r, "hi"], 1, "recv-wake: message delivered"] +assert_eq of [task_send of [r, "hi"], true, "recv-wake: message delivered"] assert_eq of [task_join of r, "hi", "recv-wake: receiver returns the message"] same_trace of [fmt_trace of [task_sched_trace of null, names, base], ["0:r:spawn", "0:main:yield", "0:r:recv-wake", "0:main:join-release"], @@ -194,7 +194,7 @@ SB_CALL is 39 SB_RETURN is 40 sb is sandbox_run of [[SB_ABI, [SB_GET_NAME, 0, 0, SB_CONST, 1, 0, SB_CALL, 1, 0, SB_RETURN], ["task_sched_trace", 1]], 1000] -assert_eq of [sb["ok"], 0, "sandbox: task_sched_trace is denied inside sandbox_run"] +assert_eq of [sb["ok"], false, "sandbox: task_sched_trace is denied inside sandbox_run"] lo3 is task_spawn of lone assert_eq of [task_join of lo3, 3, "sandbox: the host keeps running normally"] assert_eq of [len of (task_sched_trace of null), 0, diff --git a/tests/test_tasks.eigs b/tests/test_tasks.eigs index 5908aad9..b79c66f3 100644 --- a/tests/test_tasks.eigs +++ b/tests/test_tasks.eigs @@ -32,17 +32,17 @@ define worker() as: return 42 tid is task_spawn of worker assert_eq of [type of tid, "num", "task_spawn returns a numeric id"] -assert_eq of [task_alive of tid, 1, "a freshly spawned task is alive"] +assert_eq of [task_alive of tid, true, "a freshly spawned task is alive"] # --- unknown id is not alive (no crash, no false positive) --- -assert_eq of [task_alive of 99999, 0, "unknown task id is not alive"] -assert_eq of [task_alive of 0, 0, "id 0 (reserved/invalid) is not alive"] +assert_eq of [task_alive of 99999, false, "unknown task id is not alive"] +assert_eq of [task_alive of 0, false, "id 0 (reserved/invalid) is not alive"] # --- args form: [fn, arg1, ...] spawns and stays alive --- define adder(a, b) as: return a + b tid2 is task_spawn of [adder, 3, 4] -assert_eq of [task_alive of tid2, 1, "spawn with args is alive"] +assert_eq of [task_alive of tid2, true, "spawn with args is alive"] # --- distinct ids per spawn --- assert_true of [tid2 != tid, "each spawn gets a distinct id"] @@ -65,7 +65,7 @@ define lister(xs) as: shared is [1, 2, 3] tid3 is task_spawn of [lister, shared] shared is append of [shared, 4] # mutate caller's list post-spawn -assert_eq of [task_alive of tid3, 1, "spawn with a compound arg is alive"] +assert_eq of [task_alive of tid3, true, "spawn with a compound arg is alive"] assert_eq of [len of shared, 4, "caller's list still mutable post-spawn"] # the task saw the deep copy (len 3), not the caller's later-mutated list (len 4) assert_eq of [task_join of tid3, 3, "task got the deep-copied arg, not the mutation"] @@ -101,7 +101,7 @@ assert_eq of [r1, 42, "task_join returns the worker result"] # --- joining an already-finished task returns its result immediately --- r1b is task_join of d1 assert_eq of [r1b, 42, "re-join of a finished task returns the result"] -assert_eq of [task_alive of d1, 0, "a finished task is not alive"] +assert_eq of [task_alive of d1, false, "a finished task is not alive"] # --- an uncaught error in a task is rethrown at the join, as its #406 kind --- define crasher() as: @@ -215,8 +215,8 @@ define fast() as: return 1 f1 is task_spawn of fast jr is task_join of f1 # f1 finishes -assert_eq of [task_send of [f1, 99], 0, "send to a finished task drops (returns 0)"] -assert_eq of [task_send of [88888, 1], 0, "send to an unknown task drops (returns 0)"] +assert_eq of [task_send of [f1, 99], false, "send to a finished task drops (returns 0)"] +assert_eq of [task_send of [88888, 1], false, "send to an unknown task drops (returns 0)"] # --- messages cross the boundary deep-copied (share-nothing) --- echo_id is 0 @@ -235,8 +235,8 @@ define victim() as: return 1 vk is task_spawn of victim task_yield of null -assert_eq of [task_kill of vk, 1, "task_kill of a live task returns 1"] -assert_eq of [task_alive of vk, 0, "a killed task is not alive"] +assert_eq of [task_kill of vk, true, "task_kill of a live task returns 1"] +assert_eq of [task_alive of vk, false, "a killed task is not alive"] kthrew is 0 try: kr is task_join of vk @@ -474,7 +474,7 @@ _det_t2 is task_spawn of _det_w task_yield of null assert_eq of [task_detach of _det_t2, 1, "detach of a finished task reaps it"] assert_eq of [task_join of _det_t2, null, "joining a reaped id is null"] -assert_eq of [task_alive of _det_t2, 0, "a reaped id is not alive"] +assert_eq of [task_alive of _det_t2, false, "a reaped id is not alive"] # task_kill of a detached live task reaps too: 300 spawn/detach/kill cycles. define _det_block() as: diff --git a/tests/test_temporal.eigs b/tests/test_temporal.eigs index a1862cc1..2db10253 100644 --- a/tests/test_temporal.eigs +++ b/tests/test_temporal.eigs @@ -79,7 +79,7 @@ loop while li < 3: lv is lv + 1 li is li + 1 st is state_at of 999 -check of ["state_at hides __loop_exit__", (has_key of [st, "__loop_exit__"]) == 0] +check of ["state_at hides __loop_exit__", (not (has_key of [st, "__loop_exit__"]))] check of ["state_at keeps user bindings", st["lv"] == 4] if fail_count == 0: diff --git a/tests/test_temporal_producers.eigs b/tests/test_temporal_producers.eigs index 0d36ced9..4c68b50c 100644 --- a/tests/test_temporal_producers.eigs +++ b/tests/test_temporal_producers.eigs @@ -86,7 +86,7 @@ sandboxcode is [LINE,10,0,0,0, CONST,0,0, SET_NAME_LOCAL,1,0, POP, CONST,2,0, RETURN] sandboxed is sandbox_run of [[ABI, sandboxcode, [33, "tmp830sandbox", null]], 1000, 1000000] -assert_eq of [sandboxed["ok"], 1, "sandbox history: assignment run succeeds"] +assert_eq of [sandboxed["ok"], true, "sandbox history: assignment run succeeds"] sandboxread is [CONST,0,0, INTERROGATE_NAMED_AT,0,0,1,0, RETURN] escaped is vm_run_bytecode of [ABI, sandboxread, [99, "tmp830sandbox"]] assert_eq of [escaped, null, "sandbox history: unqueried value is not retained past run"] diff --git a/tests/test_tensor_buffer_inputs.eigs b/tests/test_tensor_buffer_inputs.eigs index d3cba94d..b60aabc4 100644 --- a/tests/test_tensor_buffer_inputs.eigs +++ b/tests/test_tensor_buffer_inputs.eigs @@ -149,7 +149,7 @@ assert_eq of [(str of (shape of rs)), "[2, 2]", "reshape: shape applied"] # ============ tensor_save / tensor_load ============ tpath is "/tmp/eigs_1093_tensor_buffer.bin" -assert_eq of [(tensor_save of [ba, tpath]), 1, "tensor_save accepts a buffer"] +assert_eq of [(tensor_save of [ba, tpath]), true, "tensor_save accepts a buffer"] reloaded is tensor_load of tpath assert_eq of [(flat_str of reloaded), (flat_str of la), "tensor_save/load buffer round trip"] remove_file of tpath diff --git a/tests/test_tensor_io_limit.sh b/tests/test_tensor_io_limit.sh index 41689d56..bd9a5e72 100755 --- a/tests/test_tensor_io_limit.sh +++ b/tests/test_tensor_io_limit.sh @@ -21,13 +21,13 @@ load_file of "lib/test.eigs" # The old reader's per-dimension 1,000,000 cap rejected this file with null. regression is zeros of 1000001 -assert_eq of [tensor_save of [regression, "$tmp/regression.tensor"], 1, "save old-over-cap tensor"] +assert_eq of [tensor_save of [regression, "$tmp/regression.tensor"], true, "save old-over-cap tensor"] regression_back is tensor_load of "$tmp/regression.tensor" assert_eq of [len of regression_back, 1000001, "load dimension above old cap"] # Pin writer/reader symmetry at the shared construction/file limit. at_limit is zeros of 10000000 -assert_eq of [tensor_save of [at_limit, "$tmp/limit.tensor"], 1, "save tensor at limit"] +assert_eq of [tensor_save of [at_limit, "$tmp/limit.tensor"], true, "save tensor at limit"] limit_back is tensor_load of "$tmp/limit.tensor" assert_eq of [len of limit_back, 10000000, "load tensor at limit"] assert_eq of [limit_back[9999999], 0, "last element survives limit round trip"] @@ -42,12 +42,12 @@ assert of [contains of [caught.message, "$tmp/over.tensor"], "over-cap error nam assert of [contains of [caught.message, "columns=10000001"], "over-cap error names dimension"] assert of [contains of [caught.message, "10000000-element cap"], "over-cap error names cap"] -writer_caught is 0 +writer_caught is false try: stream_open of ["$tmp/writer.tensor", 10000001] catch e: writer_caught is (e.kind == "limit") -assert_eq of [writer_caught, 1, "stream writer refuses above reader cap"] +assert_eq of [writer_caught, true, "stream writer refuses above reader cap"] test_summary of null EOF @@ -96,7 +96,7 @@ fi # Replace the absent file with an ordinary complete tensor for a second replay. cat >"$tmp/small.eigs" < #include #include +#include static int passed, failed, total; static void check(int ok, const char *name) { @@ -28,23 +29,56 @@ static EigsValue *advance_callback(EigsValue *arg) { } static const char file_tape[] = - "V 5 " EIGENSCRIPT_VERSION "\n" + "V 6 " EIGENSCRIPT_VERSION "\n" "B 0 1 1 0 root -\nB 1 2 2 0 host 70656572\n" "N 1 peer=12\nN 0 root=11\nN 0 root=13\n" - "V 5 " EIGENSCRIPT_VERSION "\n" + "V 6 " EIGENSCRIPT_VERSION "\n" "B 0 1 1 0 root -\nB 1 2 2 0 host 70656572\n" "N 0 root=21\nN 1 peer=22\n"; static const char memory_tape[] = - "V 5 " EIGENSCRIPT_VERSION "\n" + "V 6 " EIGENSCRIPT_VERSION "\n" "B 0 101 11 0 root -\nB 1 102 12 0 host 70656572\n" "N 1 peer=102\nN 0 root=101\nN 0 root=103\n" - "V 5 " EIGENSCRIPT_VERSION "\n" + "V 6 " EIGENSCRIPT_VERSION "\n" "B 0 101 11 0 root -\nN 0 root=201\n"; static const char replacement[] = - "V 5 " EIGENSCRIPT_VERSION "\n" + "V 6 " EIGENSCRIPT_VERSION "\n" "B 0 201 21 0 root -\nB 1 202 22 0 host 70656572\n" "N 1 peer=302\nN 0 root=301\n"; -static const char refused[] = "V 4 " EIGENSCRIPT_VERSION "\n"; +static const char refused[] = "V 5 " EIGENSCRIPT_VERSION "\n"; /* #1637: v5 predates bool values */ + +/* #1637: a host-recorded name's record must be a kind it declared. A child + * installs `tape` and takes `name`; the refusal is _exit(3) with `want` on + * stderr, so it runs in a forked child. */ +static const char host_kind_tape[] = + "V 6 " EIGENSCRIPT_VERSION "\n" + "B 0 401 41 0 root -\nN 0 root=true\n"; +static const char host_undeclared_tape[] = + "V 6 " EIGENSCRIPT_VERSION "\n" + "B 0 501 51 0 root -\nN 0 ghost=7\n"; +static int child_refuses(const char *tape, size_t len, const char *name, const char *want) { + int fds[2]; + if (pipe(fds) != 0) return 0; + fflush(stdout); fflush(stderr); + pid_t pid = fork(); + if (pid < 0) { close(fds[0]); close(fds[1]); return 0; } + if (pid == 0) { + dup2(fds[1], 2); close(fds[0]); close(fds[1]); + if (!eigs_set_replay_tape(tape, len, 1)) _exit(4); + EigsValue *v = NULL; + eigs_replay_take(name, &v); + _exit(0); /* served: the refusal did not happen */ + } + close(fds[1]); + char buf[1024]; size_t got = 0; ssize_t r; + while (got < sizeof buf - 1 && (r = read(fds[0], buf + got, sizeof buf - 1 - got)) > 0) + got += (size_t)r; + buf[got] = 0; + close(fds[0]); + int status = 0; + if (waitpid(pid, &status, 0) != pid) return 0; + return WIFEXITED(status) && WEXITSTATUS(status) == 3 && strstr(buf, want) != NULL; +} static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cv = PTHREAD_COND_INITIALIZER; @@ -110,6 +144,10 @@ static double finish_worker(Value *handle) { int main(void) { EigsState *root = eigs_open(); if (!root) return 2; + /* #1637: a host-recorded name declares its return kinds before replay. */ + const char *const host_names[] = {"root", "peer", "sample", "file_alias"}; + for (size_t i = 0; i < sizeof host_names / sizeof host_names[0]; i++) + eigs_trace_declare_kind(host_names[i], EIGS_KIND(EIGS_TYPE_NUM)); check(!eigs_replay_advance_session(), "advance without a source refuses"); char path[] = "/tmp/eigs_trace_context_XXXXXX"; int fd = mkstemp(path); @@ -188,10 +226,21 @@ int main(void) { check(!eigs_replay_advance_session(), "child journal ends after its declared sessions"); check(eigs_set_replay_tape(NULL, 0, 0), "memory clear disables replay when no file remains"); check(!g_replay_enabled, "final memory source is fully released"); + check(child_refuses(host_kind_tape, sizeof(host_kind_tape)-1, "root", + "root returns a num, the tape holds a bool; refusing to replay"), + "a declared host name refuses a record of another kind (exit 3)"); + check(child_refuses(host_undeclared_tape, sizeof(host_undeclared_tape)-1, "ghost", + "ghost has no declared return kind"), + "an undeclared host name is refused at replay (exit 3)"); + check(!eigs_trace_declare_kind("random", EIGS_KIND(EIGS_TYPE_BOOL)) && + !eigs_trace_declare_kind("", EIGS_KIND(EIGS_TYPE_NUM)) && + !eigs_trace_declare_kind("fnval", EIGS_KIND(EIGS_TYPE_FN)) && + !eigs_trace_declare_kind("nokind", 0), + "a runtime name, an empty name, a FN kind and no kind are refused"); if (!eigs_thread_switch(peer)) return 2; eigs_thread_detach(); eigs_state_destroy(peer); if (!eigs_thread_switch(root)) return 2; eigs_close(root); - printf("trace context: %d passed, %d failed (41 declared)\n", passed, failed); - return failed || total != 41; + printf("trace context: %d passed, %d failed (44 declared)\n", passed, failed); + return failed || total != 44; } diff --git a/tests/test_trace_context.sh b/tests/test_trace_context.sh index 90d16890..b32a734b 100644 --- a/tests/test_trace_context.sh +++ b/tests/test_trace_context.sh @@ -37,10 +37,10 @@ if [[ "$rc" -ne 0 || "$classification" -ne 2 ]]; then echo "FAIL: trace context child rc=$rc sanitizer=$classification" exit 1 fi -summaries=$(grep -Fxc 'trace context: 41 passed, 0 failed (41 declared)' "$out" || true) +summaries=$(grep -Fxc 'trace context: 44 passed, 0 failed (44 declared)' "$out" || true) [[ "$summaries" -eq 1 ]] || exit 1 passes=$(grep -c '^PASS:' "$out" || true) -[[ "$passes" -eq 41 ]] || exit 1 +[[ "$passes" -eq 44 ]] || exit 1 if grep -q '^FAIL:' "$out"; then exit 1 fi diff --git a/tests/test_trace_correspondence.c b/tests/test_trace_correspondence.c index 0052de03..74c0a5f9 100644 --- a/tests/test_trace_correspondence.c +++ b/tests/test_trace_correspondence.c @@ -200,6 +200,7 @@ int main(void) { EigsState *root = eigs_open(); check(root != NULL, "root state opens"); if (!root) return 2; + eigs_trace_declare_kind("ordinary_sensor", EIGS_KIND(EIGS_TYPE_NUM)); /* #1637 */ check(!eigs_trace_bind_stream(""), "empty key refused before any event"); eigs_set_trace_sink(sink, NULL); check(!eigs_trace_bind_stream("late-root"), "key after opener declaration refused"); diff --git a/tests/test_trace_history_boundary.c b/tests/test_trace_history_boundary.c index 59c94758..62a154fe 100644 --- a/tests/test_trace_history_boundary.c +++ b/tests/test_trace_history_boundary.c @@ -30,7 +30,7 @@ static void check(int ok, const char *name) { if (ok) passed++; else failed++; } static int number(EigsSlot value, double want) { - return slot_is_num(value) && value.d == want; + return slot_is_num(value) && SLOT_NUM_RAW(value) == want; } static int host_values(const PrevEntry *e) { return e->has_current && number(e->current, 22) && diff --git a/tests/test_trace_mt.sh b/tests/test_trace_mt.sh index 876b3dda..2607288b 100755 --- a/tests/test_trace_mt.sh +++ b/tests/test_trace_mt.sh @@ -326,7 +326,7 @@ for bad in 'N random=0.5' 'N -1 random=0.5' 'N 18446744073709551616 random=0.5' { printf '%s\n' "$vline" 'B 0 1 1 0 root -'; printf '%s\n' "$bad"; } > "$TMPDIR/bad-id.tape" EIGS_REPLAY="$TMPDIR/bad-id.tape" "$EIGS" "$TMPDIR/take.eigs" >/dev/null 2>"$TMPDIR/bad-id.err" brc=$? - if [ "$brc" -eq 3 ] && grep -q 'malformed v5 stream id' "$TMPDIR/bad-id.err"; then + if [ "$brc" -eq 3 ] && grep -q 'malformed v6 stream id' "$TMPDIR/bad-id.err"; then ok "parser rejects malformed stream tag: $bad" else fail "parser rejects malformed stream tag: $bad" "rc=$brc" diff --git a/tests/test_tsan.sh b/tests/test_tsan.sh index c5f1dd76..2dbef06a 100755 --- a/tests/test_tsan.sh +++ b/tests/test_tsan.sh @@ -194,7 +194,7 @@ if [ -f "$RP_EIGS" ]; then EIGS_TRACE="$RP_HDR" timeout "$TSAN_RUN_TIMEOUT" setarch -R "$EIGS" \ "$TESTS_DIR/../build/tsan_one.eigs" >/dev/null 2>&1 || header_rc=$? vline=$(head -1 "$RP_HDR" 2>/dev/null || true) - if [ "$header_rc" -ne 0 ] || ! printf '%s\n' "$vline" | grep -q '^V 5 '; then + if [ "$header_rc" -ne 0 ] || ! printf '%s\n' "$vline" | grep -q '^V 6 '; then echo " FAIL: replay-workers header setup rc=$header_rc" FAIL=$((FAIL + 1)) else diff --git a/tests/test_ui.eigs b/tests/test_ui.eigs index 8ff71cc7..39a3eb2f 100644 --- a/tests/test_ui.eigs +++ b/tests/test_ui.eigs @@ -168,9 +168,9 @@ assert_eq of [_widget_registry["label"].focusable, 0, "label not focusable"] assert_eq of [_widget_registry["separator"].focusable, 0, "separator not focusable"] # _is_container uses registry -assert_eq of [_is_container of "panel", 1, "_is_container panel"] -assert_eq of [_is_container of "vbox", 1, "_is_container vbox"] -assert_eq of [_is_container of "button", 0, "_is_container button"] +assert_eq of [_is_container of "panel", true, "_is_container panel"] +assert_eq of [_is_container of "vbox", true, "_is_container vbox"] +assert_eq of [_is_container of "button", false, "_is_container button"] # ============================================================ # 3. Layout tests @@ -645,24 +645,24 @@ define hk_cb() as: register_hotkey of ["ctrl+s", hk_cb] consumed is dispatch of [kroot, {"type": "keydown", "key": "s", "ctrl": 1, "shift": 0, "alt": 0}] assert_eq of [hk_fired[0], 1, "hotkey fires through the public dispatch door"] -assert_eq of [consumed, 1, "dispatch reports the hotkey as consumed"] +assert_eq of [consumed, true, "dispatch reports the hotkey as consumed"] # A key the toolkit does not claim is reported free for the app. Focus # navigation reports its own consumption, so an unbound letter is free # even while a widget holds focus. free is dispatch of [kroot, {"type": "keydown", "key": "j", "ctrl": 0, "shift": 0, "alt": 0}] -assert_eq of [free, 0, "unclaimed key reported unconsumed"] +assert_eq of [free, false, "unclaimed key reported unconsumed"] # handle_key is the same door app_loop uses — priority is pinned here # rather than duplicated inside the loop. hk2 is handle_key of [kroot, {"type": "keydown", "key": "s", "ctrl": 1, "shift": 0, "alt": 0}] -assert_eq of [hk2, 1, "handle_key consumes a registered hotkey"] +assert_eq of [hk2, true, "handle_key consumes a registered hotkey"] assert_eq of [hk_fired[0], 2, "handle_key fired the hotkey callback"] unregister_hotkey of "ctrl+s" # keyup is not a toolkit key event — it stays free for the app ku is dispatch of [kroot, {"type": "keyup", "key": "s", "ctrl": 1, "shift": 0, "alt": 0}] -assert_eq of [ku, 0, "keyup reported unconsumed"] +assert_eq of [ku, false, "keyup reported unconsumed"] # Escape pops the modal stack through dispatch mdlg is dialog of ["mdlg", 200, 100, "T", "M", ["OK"], null] @@ -670,7 +670,7 @@ push_modal of [mdlg, 800, 600] assert_eq of [len of _ui.modal_stack, 1, "modal pushed"] esc is dispatch of [kroot, {"type": "keydown", "key": "escape", "ctrl": 0, "shift": 0, "alt": 0}] assert_eq of [len of _ui.modal_stack, 0, "escape pops modal through dispatch"] -assert_eq of [esc, 1, "escape-pops-modal reported consumed"] +assert_eq of [esc, true, "escape-pops-modal reported consumed"] # Focused text input takes keys ahead of focus navigation ti is text_input of ["ti", 0, 0, 100, "", null] @@ -1749,7 +1749,7 @@ old_theme is {} for k in ["bg", "text_color", "text_dim", "accent", "font_scale", "char_h", "char_w", "focus_ring", "radius_sm", "disabled_overlay"]: old_theme[k] is dt[k] set_theme of old_theme -assert_eq of [_record_render of [mk_ch, 0, 0] > 0, 1, "#820 a theme dict without the plot keys falls back instead of crashing"] +assert_eq of [_record_render of [mk_ch, 0, 0] > 0, true, "#820 a theme dict without the plot keys falls back instead of crashing"] set_theme of saved_theme # ---- The two in-repo consumers of the old y-vs-index chart still work ---- @@ -1760,7 +1760,7 @@ for i in range of 5: lb is chart_bounds of legacy assert_eq of [lb.x0, 0, "a bare y-list series still spans index 0..n-1"] assert_eq of [lb.x1, 4, "a bare y-list series upper index bound"] -assert_eq of [_record_render of [legacy, 0, 0] > 0, 1, "a bare y-list series still renders"] +assert_eq of [_record_render of [legacy, 0, 0] > 0, true, "a bare y-list series still renders"] assert_eq of [_outside of [0, 0, 200, 100], 0, "a bare y-list series stays inside its rect too"] _gfx_log is [] @@ -2132,7 +2132,7 @@ old_theme2 is {} for k in ["bg", "text_color", "text_dim", "accent", "font_scale", "char_h", "char_w", "focus_ring", "radius_sm", "disabled_overlay"]: old_theme2[k] is dt2[k] set_theme of old_theme2 -assert_eq of [_record_render_tl of [tl_theme, 0, 0] > 0, 1, "#842 a theme dict without the tl keys falls back instead of crashing"] +assert_eq of [_record_render_tl of [tl_theme, 0, 0] > 0, true, "#842 a theme dict without the tl keys falls back instead of crashing"] set_theme of saved_theme # ---- Consumer contract 1: a liferaft-shaped record list maps in ---- @@ -3132,12 +3132,12 @@ assert_ui_result1 of [_ease_in, 0.5, 0.25, "ease-in half"] assert_ui_result1 of [_ease_out, 0.5, 0.75, "ease-out half"] assert_ui_result1 of [_ease_in_out, 0.25, 0.125, "ease-in-out first half"] assert_ui_result1 of [_ease_in_out, 0.75, 0.875, "ease-in-out second half"] -assert_eq of [_point_in_rect of [11, 21, 10, 20, 4, 5], 1, "point inside rectangle"] -assert_eq of [_point_in_rect of [14, 21, 10, 20, 4, 5], 0, "right rectangle edge excluded"] +assert_eq of [_point_in_rect of [11, 21, 10, 20, 4, 5], true, "point inside rectangle"] +assert_eq of [_point_in_rect of [14, 21, 10, 20, 4, 5], false, "right rectangle edge excluded"] assert_eq of [text_width of ["ab", 2], 24, "bitmap text advance"] assert_ui_result1 of [text_height, 2, 14, "bitmap text height"] -assert_eq of [_combo_matches of ["ctrl+shift+x", 1, 1, 0, "x"], 1, "hotkey exact modifiers"] -assert_eq of [_combo_matches of ["ctrl+shift+x", 1, 0, 0, "x"], 0, "hotkey missing shift"] +assert_eq of [_combo_matches of ["ctrl+shift+x", 1, 1, 0, "x"], true, "hotkey exact modifiers"] +assert_eq of [_combo_matches of ["ctrl+shift+x", 1, 0, 0, "x"], false, "hotkey missing shift"] # Basic constructors: meaningful geometry and content, not bound-name presence. assert_ui_fields of [label of ["probe-label", 2, 3, "ab"], {"type": "label", "text": "ab", "x": 2, "y": 3, "auto_size": 1}, "label constructor"] @@ -3413,7 +3413,7 @@ assert_eq of [coverage_fd.dir_label.text, coverage_fd_dir, "refresh updates disp assert_eq of [coverage_fd.listing.items[0], "..", "refresh prefixes parent entry"] assert_eq of [coverage_fd.listing.selected, -1, "refresh resets selection"] assert_eq of [coverage_fd.listing.scroll_y, 0, "refresh resets scroll"] -assert_eq of [_fd_is_dir of [coverage_fd_dir, "."], 1, "file dialog directory predicate reaches real cwd"] +assert_eq of [_fd_is_dir of [coverage_fd_dir, "."], true, "file dialog directory predicate reaches real cwd"] _gfx_rec is coverage_dialog_saved.record _ui.drag_widget is coverage_dialog_saved.drag @@ -3445,7 +3445,7 @@ b1263._ax is 10 b1263._ay is 20 e1263 is {"type": "mousedown", "x": 60, "y": 45, "button": 1} assert_ui_transition5 of [_mousedown_slider, s1263, s1263, 60, 45, e1263, {"dragging": 1, "value": 50}, "slider press starts positioned drag"] -assert_eq of [_ui.drag_widget == s1263, 1, "slider press owns capture"] +assert_eq of [_ui.drag_widget == s1263, true, "slider press owns capture"] assert_ui_transition5 of [_mousedown_vslider, v1263, v1263, 60, 45, e1263, {"dragging": 1, "value": 75}, "vertical press reverses axis"] assert_ui_transition5 of [_mousedown_knob, k1263, k1263, 60, 45, e1263, {"dragging": 1, "drag_start_y": 45, "drag_start_val": 25}, "knob press snapshots baseline"] assert_ui_transition5 of [_mousedown_scrollbar, b1263, b1263, 60, 45, e1263, {"dragging": 1, "value": 0.5}, "scrollbar press centers thumb"] @@ -3577,9 +3577,9 @@ coverage_dock_panel.collapsed is 0 _layout_dock of coverage_dock _gfx_rec is 1 assert_ui_render of [_render_dock, coverage_dock, "rrect", [10, 20, 400, 1, 0, (_theme_color of ["dock_border", (_theme_color of ["panel_border", [60, 60, 80]])])[0], (_theme_color of ["dock_border", (_theme_color of ["panel_border", [60, 60, 80]])])[1], (_theme_color of ["dock_border", (_theme_color of ["panel_border", [60, 60, 80]])])[2]], "dock top border geometry"] -assert_eq of [_dock_in of [[10, 20, 200, 200], 11, 21], 1, "dock rectangle contains point"] -assert_eq of [_dock_in of [null, 11, 21], 0, "absent region rejects point"] -assert_eq of [_dock_in_handle of [[210, 20, 5, 200], 208, 21, 0], 1, "vertical handle includes three-pixel slop"] +assert_eq of [_dock_in of [[10, 20, 200, 200], 11, 21], true, "dock rectangle contains point"] +assert_eq of [_dock_in of [null, 11, 21], false, "absent region rejects point"] +assert_eq of [_dock_in_handle of [[210, 20, 5, 200], 208, 21, 0], true, "vertical handle includes three-pixel slop"] assert_eq of [(_hit_test_dock of [coverage_dock, 10, 20, 211, 21]).id, "cov-dock", "handle hit belongs to dock"] assert_ui_transition5 of [_mousedown_dock, coverage_dock, coverage_dock, 211, 21, {"type": "mousedown"}, {"_drag_region": "west"}, "west handle starts drag"] assert_eq of [_ui.drag_widget.id, "cov-dock", "dock holds drag ownership"] @@ -3824,12 +3824,12 @@ set_theme of coverage_core_theme coverage_opts is {"container": 1, "focusable": 1} assert_true of [(_register_widget of ["coverage-custom", coverage_opts]) == null and _widget_registry["coverage-custom"].container == 1, "registry stores custom options"] -assert_eq of [_is_container of "coverage-custom", 1, "registered custom container"] -assert_eq of [_is_container of "coverage-absent", 0, "unregistered type is not container"] +assert_eq of [_is_container of "coverage-custom", true, "registered custom container"] +assert_eq of [_is_container of "coverage-absent", false, "unregistered type is not container"] coverage_focus_button is button of ["coverage-focus", 0, 0, 20, 10, "F", null] -assert_ui_result1 of [_is_focusable, coverage_focus_button, 1, "enabled visible button focusable"] +assert_ui_result1 of [_is_focusable, coverage_focus_button, true, "enabled visible button focusable"] coverage_focus_button.enabled is 0 -assert_ui_result1 of [_is_focusable, coverage_focus_button, 0, "disabled button not focusable"] +assert_ui_result1 of [_is_focusable, coverage_focus_button, false, "disabled button not focusable"] coverage_focus_button.enabled is 1 dict_remove of [_widget_registry, "coverage-custom"] @@ -3904,7 +3904,7 @@ _ui.focus_list is [] assert_true of [(_build_focus_list of coverage_focus_root) == null and (len of _ui.focus_list) == 2 and _ui.focus_list[1].id == "coverage-focus-toggle", "focus walk includes ordered visible controls"] assert_true of [(_sync_focus_to of coverage_focus_button) == null and _ui.focus_index == 0 and _ui.focused_widget.id == "coverage-focus", "focus sync locates actual widget"] assert_true of [(_focus_next of [coverage_focus_root, 0]) == null and _ui.focus_index == 1 and _ui.focused_widget.id == "coverage-focus-toggle", "focus next follows tree order"] -assert_true of [(_handle_focus_key of [coverage_focus_root, {"key": "space", "shift": 0}]) == 1 and coverage_focus_toggle.value == 1, "focus activation changes toggle"] +assert_true of [(_handle_focus_key of [coverage_focus_root, {"key": "space", "shift": 0}]) and coverage_focus_toggle.value == 1, "focus activation changes toggle"] _ui.focus_list is coverage_focus_saved.list _ui.focus_index is coverage_focus_saved.index _ui.focused_widget is coverage_focus_saved.widget @@ -3982,9 +3982,9 @@ coverage_main_dialog is dialog of ["coverage-main-dialog", 80, 40, "D", "M", ["O coverage_main_dialog.visible is 1 assert_eq of [(_find_visible_dialog of coverage_main_dialog).id, "coverage-main-dialog", "visible dialog discovery"] assert_true of [(register_hotkey of ["ctrl+q", coverage_main_hotkey]) == null and _ui.hotkeys[0].combo == "ctrl+q", "hotkey registration records requested combo"] -assert_true of [(_match_hotkey of {"type": "keydown", "key": "q", "ctrl": 1, "shift": 0, "alt": 0}) == 1 and coverage_main_log.hotkeys == 1, "matching hotkey invokes callback"] -assert_true of [(handle_key of [coverage_main_root, {"type": "keydown", "key": "q", "ctrl": 1, "shift": 0, "alt": 0}]) == 1 and coverage_main_log.hotkeys == 2, "public key handler preserves hotkey priority"] -assert_true of [(dispatch of [coverage_main_root, {"type": "keydown", "key": "q", "ctrl": 1, "shift": 0, "alt": 0}]) == 1 and coverage_main_log.hotkeys == 3, "public dispatch routes keyboard callback"] +assert_true of [(_match_hotkey of {"type": "keydown", "key": "q", "ctrl": 1, "shift": 0, "alt": 0}) and coverage_main_log.hotkeys == 1, "matching hotkey invokes callback"] +assert_true of [(handle_key of [coverage_main_root, {"type": "keydown", "key": "q", "ctrl": 1, "shift": 0, "alt": 0}]) and coverage_main_log.hotkeys == 2, "public key handler preserves hotkey priority"] +assert_true of [(dispatch of [coverage_main_root, {"type": "keydown", "key": "q", "ctrl": 1, "shift": 0, "alt": 0}]) and coverage_main_log.hotkeys == 3, "public dispatch routes keyboard callback"] assert_true of [(unregister_hotkey of "ctrl+q") == null and (len of _ui.hotkeys) == 0, "unregister removes exact combo"] assert_true of [(push_modal of [coverage_main_dialog, 200, 100]) == null and _ui.modal_stack[0].id == "coverage-main-dialog" and coverage_main_dialog.x == 60 and coverage_main_dialog.y == 30, "modal push centers and mounts dialog"] assert_true of [(pop_modal of null).id == "coverage-main-dialog" and coverage_main_dialog.visible == 0 and (len of _ui.modal_stack) == 0, "modal pop hides and returns same dialog"] @@ -3994,7 +3994,7 @@ coverage_main_menu is menu of ["coverage-main-menu", ["A"], null] assert_true of [(show_menu of [coverage_main_menu, 7, 9]) == null and coverage_main_menu.x == 7 and coverage_main_menu.y == 9 and coverage_main_menu.visible == 1, "show menu stamps requested origin"] coverage_main_popup is dropdown of ["coverage-main-popup", 0, 0, 100, ["A"], 0, null] coverage_main_popup.open is 1 -assert_eq of [_has_open_popup of coverage_main_popup, 1, "open dropdown reports overlay state"] +assert_eq of [_has_open_popup of coverage_main_popup, true, "open dropdown reports overlay state"] assert_true of [(claim_drag of coverage_main_canvas) == null and _ui.drag_widget.id == "coverage-main-canvas", "claim drag records exact owner"] assert_true of [(release_drag of null) == null and _ui.drag_widget == null, "release drag clears owner"] assert_true of [(request_quit of null) == null and _ui.quit_requested == 1, "quit request latches UI flag"] @@ -4026,4 +4026,27 @@ coverage_button_start is len of _gfx_log assert_true of [(_draw_hover_shade of [coverage_render_button, 10, 20, 40, 24, 3]) == null and (str of _gfx_log[coverage_button_start]) == (str of ["rrect", [10, 20, 40, 24, 3, _theme.hover_shade[0], _theme.hover_shade[1], _theme.hover_shade[2], _theme.hover_shade[3]]]), "hover shade retains alpha and geometry"] _gfx_rec is coverage_button_record +# #1637: gfx_poll delivers shift/ctrl/alt as bools. Hotkeys, text-input +# shortcuts and an on_key answer of false must work with real bools. +bh_hits is [0] +define bh_on_save(_) as: + bh_hits[0] is bh_hits[0] + 1 +register_hotkey of ["ctrl+s", bh_on_save] +bh_root is panel of ["bh_root", 0, 0, 200, 100] +bh_inp is text_input of ["bh_t", 0, 0, 100, "ab", null] +add_child of [bh_root, bh_inp] +handle_key of [bh_root, {"type": "keydown", "key": "s", "ctrl": true, "shift": false, "alt": false}] +assert_eq of [bh_hits[0], 1, "a ctrl+s hotkey fires on a bool ctrl modifier"] +_handle_text_key of [bh_inp, {"type": "keydown", "key": "x", "ctrl": false, "shift": true, "alt": false}] +assert_eq of [bh_inp.text, "abX", "shift as a bool upper-cases typed text"] +assert_eq of [_ev_flag of true, true, "a bool modifier reads as itself"] +assert_eq of [_ev_flag of 1, true, "the older 1 modifier reads as true"] +assert_eq of [_ev_flag of 0, false, "the older 0 modifier reads as false"] +assert_eq of [_ev_flag of null, false, "an absent modifier reads as false"] +assert_true of [_ev_stop of false, "an on_key answer of false stops the loop"] +assert_true of [_ev_stop of 0, "an on_key answer of 0 still stops the loop"] +assert_true of [not (_ev_stop of true), "an on_key answer of true keeps running"] +assert_true of [not (_ev_stop of null), "an on_key answer of null keeps running"] +unregister_hotkey of "ctrl+s" + test_summary of null diff --git a/tests/test_ui_containment_gfx.eigs b/tests/test_ui_containment_gfx.eigs index ae9b92eb..645c92af 100644 --- a/tests/test_ui_containment_gfx.eigs +++ b/tests/test_ui_containment_gfx.eigs @@ -17,7 +17,7 @@ load_file of "lib/test.eigs" opened is gfx_open of [400, 300, "containment-oracle-823"] -if opened == 0: +if (not opened): print of "SKIP: gfx_open failed (no SDL2)" exit of 0 diff --git a/tests/test_unobserved.eigs b/tests/test_unobserved.eigs index 0cf02557..7b51a3a7 100644 --- a/tests/test_unobserved.eigs +++ b/tests/test_unobserved.eigs @@ -86,9 +86,9 @@ catch e: msg is e.message assert of [caught == 1, "UO-871 a predicate in a callee under unobserved raises"] assert of [kind == "value", "UO-871 kind is value"] -assert of [(contains of [msg, "converged"]) == 1, "UO-871 message names the predicate"] -assert of [(contains of [msg, "unobserved"]) == 1, "UO-871 message names the block"] -assert of [(contains of [msg, "dynamic"]) == 1, "UO-871 message explains the transitive scope"] +assert of [(contains of [msg, "converged"]), "UO-871 message names the predicate"] +assert of [(contains of [msg, "unobserved"]), "UO-871 message names the block"] +assert of [(contains of [msg, "dynamic"]), "UO-871 message explains the transitive scope"] # The message names the predicate that was actually asked, not a fixed word. caught2 is 0 @@ -103,7 +103,7 @@ catch e: caught2 is 1 msg2 is e.message assert of [caught2 == 1, "UO-871 a direct named predicate raises too"] -assert of [(contains of [msg2, "diverging"]) == 1, "UO-871 message names THAT predicate"] +assert of [(contains of [msg2, "diverging"]), "UO-871 message names THAT predicate"] # A bare predicate (no operand) is the third opcode and raises as well. caught3 is 0 diff --git a/tests/test_unobserved_neutral.eigs b/tests/test_unobserved_neutral.eigs index 0ad60fd5..9144c8aa 100644 --- a/tests/test_unobserved_neutral.eigs +++ b/tests/test_unobserved_neutral.eigs @@ -151,7 +151,7 @@ assert_eq of [str of th_u.rel, str of th_o.rel, "J hot loop: rel window identica assert_eq of [str of th_u.raw, str of th_o.raw, "J hot loop: raw window identical"] assert_eq of [th_u.last_value, th_o.last_value, "J hot loop: last value identical"] assert_eq of [classify of th_u, classify of th_o, "J hot loop: snapshot classifies identically"] -assert_eq of [th_u.observed, 1, "J the sampled binding counts as observed"] +assert_eq of [th_u.observed, true, "J the sampled binding counts as observed"] assert_eq of [len of th_o.dh, 10, "J control: the observed dH window is full"] assert_eq of [len of th_u.dh, 0, "J the ENTROPY channel is still elided: no dH window"] assert_eq of [th_u.dH, 0, "J the ENTROPY channel is still elided: dH never computed"] @@ -210,6 +210,6 @@ define alias_probe() as: scratch is 50.0 return converged # lint: allow W016 -- the bare alias is exactly what is under test -assert_eq of [alias_probe of 0, 1, "A bare predicate still reads the last OBSERVED binding after the block"] +assert_eq of [alias_probe of 0, true, "A bare predicate still reads the last OBSERVED binding after the block"] test_summary of null diff --git a/tests/test_utf8.eigs b/tests/test_utf8.eigs index ba570b3b..dbcdc02d 100644 --- a/tests/test_utf8.eigs +++ b/tests/test_utf8.eigs @@ -26,12 +26,12 @@ check of ["char_at returns the whole character", utf8_char_at of [s, 2], "€"] check of ["char_at 4-byte", utf8_char_at of [s, 4], "𐍈"] # validation — invalid sequences built by slicing a real multi-byte char -check of ["valid ascii", utf8_validate of "hello", 1] -check of ["valid mixed", utf8_validate of s, 1] -check of ["empty is valid", utf8_validate of "", 1] -check of ["lone lead byte (truncated)", utf8_validate of (substr of ["€", 0, 1]), 0] -check of ["lone continuation byte", utf8_validate of (substr of ["€", 1, 1]), 0] -check of ["truncated 2-of-3", utf8_validate of (substr of ["€", 0, 2]), 0] +check of ["valid ascii", utf8_validate of "hello", true] +check of ["valid mixed", utf8_validate of s, true] +check of ["empty is valid", utf8_validate of "", true] +check of ["lone lead byte (truncated)", utf8_validate of (substr of ["€", 0, 1]), false] +check of ["lone continuation byte", utf8_validate of (substr of ["€", 1, 1]), false] +check of ["truncated 2-of-3", utf8_validate of (substr of ["€", 0, 2]), false] # encoding — the same published vectors, driven backwards (#435) check of ["encode ascii", utf8_encode of 65, "A"] @@ -51,8 +51,8 @@ check of ["past the ceiling is empty", utf8_encode of 1114112, ""] check of ["negative is empty", utf8_encode of (0 - 1), ""] check of ["NUL is empty (strings can't hold it)", utf8_encode of 0, ""] # every encoded boundary is structurally valid and round-trips -check of ["0x80 validates", utf8_validate of (utf8_encode of 128), 1] -check of ["0x10FFFF validates", utf8_validate of (utf8_encode of 1114111), 1] +check of ["0x80 validates", utf8_validate of (utf8_encode of 128), true] +check of ["0x10FFFF validates", utf8_validate of (utf8_encode of 1114111), true] check of ["0x10FFFF round-trips", utf8_at of [(utf8_encode of 1114111), 0], 1114111] # list form: utf8_from_codepoints inverts utf8_codepoints check of ["from_codepoints rebuilds the string", utf8_from_codepoints of (utf8_codepoints of s), s] diff --git a/tests/test_validate.eigs b/tests/test_validate.eigs index c3a23c77..16b1e16a 100644 --- a/tests/test_validate.eigs +++ b/tests/test_validate.eigs @@ -34,7 +34,7 @@ define _pre1235_is_number(value) as: if has_dot == 1: return 0 has_dot is 1 - elif (contains of ["0123456789", c]) == 0: + elif (not (contains of ["0123456789", c])): return 0 return 1 @@ -46,45 +46,49 @@ define _pre1235_is_integer(value) as: check of ["pre-fix scan accepts '.'", _pre1235_is_number of ".", 1] check of ["pre-fix scan accepts '-.'", _pre1235_is_number of "-.", 1] -check of ["pre-fix is_integer accepts '.' as zero", _pre1235_is_integer of ".", 1] -check of ["pre-fix is_integer accepts '-.' as zero", _pre1235_is_integer of "-.", 1] +check of ["pre-fix is_integer accepts '.' as zero", _pre1235_is_integer of ".", true] +check of ["pre-fix is_integer accepts '-.' as zero", _pre1235_is_integer of "-.", true] -check of ["is_number rejects '.'", validate.is_number of ".", 0] -check of ["is_number rejects '-.'", validate.is_number of "-.", 0] -check of ["is_integer rejects '.'", validate.is_integer of ".", 0] -check of ["is_integer rejects '-.'", validate.is_integer of "-.", 0] +check of ["is_number rejects '.'", validate.is_number of ".", false] +check of ["is_number rejects '-.'", validate.is_number of "-.", false] +check of ["is_integer rejects '.'", validate.is_integer of ".", false] +check of ["is_integer rejects '-.'", validate.is_integer of "-.", false] -check of ["is_number accepts '0'", validate.is_number of "0", 1] -check of ["is_number accepts '-1'", validate.is_number of "-1", 1] -check of ["is_number accepts '.5'", validate.is_number of ".5", 1] -check of ["is_number accepts '1.'", validate.is_number of "1.", 1] -check of ["is_integer accepts '0'", validate.is_integer of "0", 1] -check of ["is_integer accepts '-1'", validate.is_integer of "-1", 1] -check of ["is_integer accepts '1.'", validate.is_integer of "1.", 1] -check of ["is_integer rejects '.5'", validate.is_integer of ".5", 0] +check of ["is_number accepts '0'", validate.is_number of "0", true] +check of ["is_number accepts '-1'", validate.is_number of "-1", true] +check of ["is_number accepts '.5'", validate.is_number of ".5", true] +check of ["is_number accepts '1.'", validate.is_number of "1.", true] +check of ["is_integer accepts '0'", validate.is_integer of "0", true] +check of ["is_integer accepts '-1'", validate.is_integer of "-1", true] +check of ["is_integer accepts '1.'", validate.is_integer of "1.", true] +check of ["is_integer rejects '.5'", validate.is_integer of ".5", false] -check of ["is_number rejects empty", validate.is_number of "", 0] -check of ["is_number rejects '-'", validate.is_number of "-", 0] -check of ["is_number rejects two dots", validate.is_number of "1.2.3", 0] -check of ["is_number rejects letters", validate.is_number of "abc", 0] -check of ["is_number rejects an exponent", validate.is_number of "1e2", 0] -check of ["is_number rejects whitespace", validate.is_number of " 1", 0] -check of ["is_number rejects hex", validate.is_number of "0x10", 0] +check of ["is_number rejects empty", validate.is_number of "", false] +check of ["is_number rejects '-'", validate.is_number of "-", false] +check of ["is_number rejects two dots", validate.is_number of "1.2.3", false] +check of ["is_number rejects letters", validate.is_number of "abc", false] +check of ["is_number rejects an exponent", validate.is_number of "1e2", false] +check of ["is_number rejects whitespace", validate.is_number of " 1", false] +check of ["is_number rejects hex", validate.is_number of "0x10", false] -check of ["is_number accepts a number", validate.is_number of 3.5, 1] -check of ["is_number accepts a negative number", validate.is_number of (0 - 2), 1] -check of ["is_integer accepts an integer", validate.is_integer of 4, 1] -check of ["is_integer accepts a negative integer", validate.is_integer of (0 - 3), 1] -check of ["is_integer rejects a fraction", validate.is_integer of 3.5, 0] -check of ["is_number accepts '.0'", validate.is_number of ".0", 1] -check of ["is_integer accepts '.0'", validate.is_integer of ".0", 1] -check of ["is_number accepts '-.5'", validate.is_number of "-.5", 1] -check of ["is_integer rejects '-.5'", validate.is_integer of "-.5", 0] +check of ["is_number accepts a number", validate.is_number of 3.5, true] +check of ["is_number accepts a negative number", validate.is_number of (0 - 2), true] +check of ["is_integer accepts an integer", validate.is_integer of 4, true] +check of ["is_integer accepts a negative integer", validate.is_integer of (0 - 3), true] +check of ["is_integer rejects a fraction", validate.is_integer of 3.5, false] +check of ["is_number accepts '.0'", validate.is_number of ".0", true] +check of ["is_integer accepts '.0'", validate.is_integer of ".0", true] +check of ["is_number accepts '-.5'", validate.is_number of "-.5", true] +check of ["is_integer rejects '-.5'", validate.is_integer of "-.5", false] -check of ["is_integer rejects empty", validate.is_integer of "", 0] -check of ["is_integer rejects '-'", validate.is_integer of "-", 0] -check of ["is_integer rejects letters", validate.is_integer of "abc", 0] -check of ["is_integer rejects two dots", validate.is_integer of "1.2.3", 0] +check of ["is_integer rejects empty", validate.is_integer of "", false] +check of ["is_integer rejects '-'", validate.is_integer of "-", false] +check of ["is_integer rejects letters", validate.is_integer of "abc", false] +check of ["is_integer rejects two dots", validate.is_integer of "1.2.3", false] + +# #1637: validate_all consumes predicate answers (bools); `== 0` raised. +check of ["validate_all reports a failed check", validate.validate_all of [[["", validate.is_nonempty, "name"], ["x", validate.is_nonempty, "ok"]]], ["name"]] +check of ["validate_all with every check passing", validate.validate_all of [[["a@b.co", validate.is_email, "email"]]], []] if fails == 0: print of "VALIDATE_ALL_PASS" diff --git a/tests/test_vm_run_bytecode.eigs b/tests/test_vm_run_bytecode.eigs index 14f7d401..7701dd7e 100644 --- a/tests/test_vm_run_bytecode.eigs +++ b/tests/test_vm_run_bytecode.eigs @@ -53,8 +53,8 @@ assert_eq of [vm_run_bytecode of [ABI, [CONST,0,0, CONST,1,0, ADD, RETURN], [2, # string concat "a" + "b" -> "ab" assert_eq of [vm_run_bytecode of [ABI, [CONST,0,0, CONST,1,0, ADD, RETURN], ["a", "b"]], "ab", "string concat via ADD"] -# comparison 10 > 3 -> 1 -assert_eq of [vm_run_bytecode of [ABI, [CONST,0,0, CONST,1,0, GT, RETURN], [10, 3]], 1, "comparison GT"] +# comparison 10 > 3 -> true (#1637: a comparison yields a bool) +assert_eq of [vm_run_bytecode of [ABI, [CONST,0,0, CONST,1,0, GT, RETURN], [10, 3]], true, "comparison GT"] # builtin call: len of "hello" -> 5 (GET_NAME resolves "len" in the global env) assert_eq of [vm_run_bytecode of [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["len", "hello"]], 5, "builtin call via GET_NAME+CALL"] @@ -116,7 +116,7 @@ record_history of 0 # [descriptor, max_iter] where descriptor is [ABI, code, constants]. safe_desc is [ABI, [CONST,0,0, CONST,1,0, ADD, RETURN], [2, 3]] sr_safe is sandbox_run of [safe_desc, 100000] -assert_eq of [sr_safe["ok"], 1, "sandbox: safe code ok"] +assert_eq of [sr_safe["ok"], true, "sandbox: safe code ok"] assert_eq of [sr_safe["result"], 5, "sandbox: safe result"] # sandbox_run: a dangerous builtin (write_text) is shadowed -> blocked, ok=0, @@ -124,8 +124,8 @@ assert_eq of [sr_safe["result"], 5, "sandbox: safe result"] remove_file of "/tmp/eigs_sandbox_breach" block_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["write_text", "/tmp/eigs_sandbox_breach"]] sr_block is sandbox_run of [block_desc, 100000] -assert_eq of [sr_block["ok"], 0, "sandbox: dangerous builtin blocked"] -assert_eq of [file_exists of "/tmp/eigs_sandbox_breach", 0, "sandbox: nothing written"] +assert_eq of [sr_block["ok"], false, "sandbox: dangerous builtin blocked"] +assert_eq of [file_exists of "/tmp/eigs_sandbox_breach", false, "sandbox: nothing written"] # sandbox_run: BLOCKING builtins are denied too. The g_sandbox_loop_max bound # counts loop back-edges, not time inside one builtin, so a single blocking call @@ -134,13 +134,13 @@ assert_eq of [file_exists of "/tmp/eigs_sandbox_breach", 0, "sandbox: nothing wr # is the backstop): usleep would sleep 30s, raw_key would read() stdin, recv # would wait on a channel condvar forever. usleep_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["usleep", 30000000]] -assert_eq of [(sandbox_run of [usleep_desc, 100000])["ok"], 0, "sandbox: usleep blocked (no 30s hang)"] +assert_eq of [(sandbox_run of [usleep_desc, 100000])["ok"], false, "sandbox: usleep blocked (no 30s hang)"] rawkey_desc is [ABI, [GET_NAME,0,0, CALL,0,0, RETURN], ["raw_key"]] -assert_eq of [(sandbox_run of [rawkey_desc, 100000])["ok"], 0, "sandbox: raw_key blocked (no stdin read)"] +assert_eq of [(sandbox_run of [rawkey_desc, 100000])["ok"], false, "sandbox: raw_key blocked (no stdin read)"] recv_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["recv", 0]] -assert_eq of [(sandbox_run of [recv_desc, 100000])["ok"], 0, "sandbox: recv blocked (no channel-wait hang)"] +assert_eq of [(sandbox_run of [recv_desc, 100000])["ok"], false, "sandbox: recv blocked (no channel-wait hang)"] send_desc is [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["send", 0]] -assert_eq of [(sandbox_run of [send_desc, 100000])["ok"], 0, "sandbox: send blocked"] +assert_eq of [(sandbox_run of [send_desc, 100000])["ok"], false, "sandbox: send blocked"] # sandbox_run: a runaway loop is bounded by the iteration cap — it RETURNS # (no hang) but reports ok:0: a cap-truncated run produced partial results, @@ -149,9 +149,9 @@ assert_eq of [(sandbox_run of [send_desc, 100000])["ok"], 0, "sandbox: send bloc # NUM_ONE; JUMP_IF_FALSE +6; LOOP_CAP_CHECK +3; JUMP_BACK 10; RETURN_NULL loopcode is [3, 31,6,0, 63,3,0, 30,10,0, 41] sr_loop is sandbox_run of [[ABI, loopcode, []], 1000] -assert_eq of [sr_loop["ok"], 0, "sandbox: runaway loop bounded (returns, reports capped)"] +assert_eq of [sr_loop["ok"], false, "sandbox: runaway loop bounded (returns, reports capped)"] sr_partial is index_of of [sr_loop["error"]["message"], "partial run"] -assert_eq of [sr_partial >= 0, 1, "sandbox: capped loop names the partial run"] +assert_eq of [sr_partial >= 0, true, "sandbox: capped loop names the partial run"] # --- bytecode verifier: out-of-range operands are rejected, not run --- # Untrusted/hand-assembled descriptors must never reach the VM with an @@ -170,7 +170,7 @@ assert_bad_descriptor of [[ABI, [CONST], [42]], "truncated operand", "verify: tr assert_bad_descriptor of [[ABI, [254, RETURN], []], "unknown opcode 254", "verify: unknown opcode raises with its reason"] # same, through the sandbox: a bad descriptor returns ok=0, no crash sr_bad is sandbox_run of [[ABI, [CONST,255,255, RETURN], [42]], 1000] -assert_eq of [sr_bad["ok"], 0, "verify: sandbox rejects OOB const (ok=0)"] +assert_eq of [sr_bad["ok"], false, "verify: sandbox rejects OOB const (ok=0)"] # a name-consuming opcode (GET_NAME) whose constant index points at a NUMERIC # constant — the interned-name pointer is NULL for non-strings, so the VM @@ -275,14 +275,14 @@ assert_eq of [abi_notlist, 1, "#704: non-list descriptor raises (was a silent nu # message distinct from a malformed chunk's, so a grading ladder can tell # "your producer is stale" from "your bytecode is wrong" without re-running. sr_stale is sandbox_run of [[ABI + 1, [CONST,0,0, CONST,1,0, ADD, RETURN], [2, 3]], 1000] -assert_eq of [sr_stale["ok"], 0, "#704: sandbox_run rejects a stale-revision chunk"] +assert_eq of [sr_stale["ok"], false, "#704: sandbox_run rejects a stale-revision chunk"] assert_eq of [sr_stale["error"]["kind"], "value", "#704: sandbox_run reports kind=value"] assert of [sr_stale["error"]["message"] != "invalid chunk descriptor", "#704: stale revision is distinguishable from a malformed chunk"] assert of [contains of [sr_stale["error"]["message"], "ABI revision"], "#704: the message names the ABI revision"] # and the current revision still runs through the sandbox unchanged. sr_ok is sandbox_run of [[ABI, [CONST,0,0, CONST,1,0, ADD, RETURN], [2, 3]], 1000] -assert_eq of [sr_ok["ok"], 1, "#704: current-revision chunk still runs in the sandbox"] +assert_eq of [sr_ok["ok"], true, "#704: current-revision chunk still runs in the sandbox"] assert_eq of [sr_ok["result"], 5, "#704: current-revision sandbox result"] # ---- #721: chunk_verify is the boundary, so it must reject a chunk that can @@ -302,7 +302,7 @@ assert_eq of [vm_run_bytecode of [ABI, [CONST,0,0, RETURN], [7]], 7, "#721: the assert_eq of [vm_run_bytecode of [ABI, [RETURN_NULL], []], null, "#721: RETURN_NULL terminates too"] sr_fall is sandbox_run of [[ABI, [1], []], 1000] -assert_eq of [sr_fall["ok"], 0, "#721: sandbox_run refuses an unterminated chunk"] +assert_eq of [sr_fall["ok"], false, "#721: sandbox_run refuses an unterminated chunk"] # ---- #737: the verifier's operand-layout table had DRIFTED. OP_TRAJECTORY_SLOT # (91) was missing from op_verify_operands while its NAME sibling was present, diff --git a/tests/test_wait_until.eigs b/tests/test_wait_until.eigs index 69f433f0..988adf18 100644 --- a/tests/test_wait_until.eigs +++ b/tests/test_wait_until.eigs @@ -21,7 +21,7 @@ define nap() as: naps is naps + 1 return 1 -check of ["succeeds when pred turns true", wait_until of [ready_on_third, 10, nap], 1] +check of ["succeeds when pred turns true", wait_until of [ready_on_third, 10, nap], true] check of ["stopped at first success", calls, 3] check of ["slept only between attempts", naps, 2] @@ -30,20 +30,20 @@ naps is 0 define never() as: calls is calls + 1 return 0 -check of ["times out at the bound", wait_until of [never, 5, nap], 0] +check of ["times out at the bound", wait_until of [never, 5, nap], false] check of ["exactly tries attempts", calls, 5] # #1237: sleep_fn runs BETWEEN attempts, never after the last failed one. check of ["timeout slept tries-1 times", naps, 4] calls is 0 naps is 0 -check of ["single failed attempt times out", wait_until of [never, 1, nap], 0] +check of ["single failed attempt times out", wait_until of [never, 1, nap], false] check of ["single attempt called pred once", calls, 1] check of ["single failed attempt never sleeps", naps, 0] calls is 0 naps is 0 -check of ["zero tries times out", wait_until of [never, 0, nap], 0] +check of ["zero tries times out", wait_until of [never, 0, nap], false] check of ["zero tries never calls pred", calls, 0] check of ["zero tries never sleeps", naps, 0] @@ -52,18 +52,18 @@ naps is 0 define ready_now() as: calls is calls + 1 return 1 -check of ["immediate success", wait_until of [ready_now, 5, nap], 1] +check of ["immediate success", wait_until of [ready_now, 5, nap], true] check of ["immediate success called pred once", calls, 1] check of ["immediate success never sleeps", naps, 0] calls is 0 naps is 0 -check of ["success on the last attempt", wait_until of [ready_on_third, 3, nap], 1] +check of ["success on the last attempt", wait_until of [ready_on_third, 3, nap], true] check of ["last-attempt success called pred 3 times", calls, 3] check of ["last-attempt success slept between attempts", naps, 2] calls is 0 -check of ["null sleep_fn busy-retries", wait_until of [never, 3, null], 0] +check of ["null sleep_fn busy-retries", wait_until of [never, 3, null], false] check of ["busy retry attempted 3 times", calls, 3] if fails == 0: diff --git a/tests/test_windowed_diverging.eigs b/tests/test_windowed_diverging.eigs index 0d9c779f..9f39e289 100644 --- a/tests/test_windowed_diverging.eigs +++ b/tests/test_windowed_diverging.eigs @@ -32,7 +32,7 @@ a is 5.0 a is 3.0 d1 is diverging # #861 re-pin: value route: 499->3 with steps contracting 400->2 is a genuine approach -> improving; the ENTROPY ascent reads "diverging" only on the entropy channel (pinned via classify below) -want of ["WD1 monotone clean ascent fires", d1, 0] +want of ["WD1 monotone clean ascent fires", d1, false] # WD2: noisy ascent with down-ticks still fires — net up AND majority (4/6) # genuine ascents. Proportional vote tolerates the bounces. @@ -45,7 +45,7 @@ b is 5.0 b is 3.0 d2 is diverging # #861 re-pin: value route: bouncing -> oscillating -want of ["WD2 noisy ascent, net+majority, fires", d2, 0] +want of ["WD2 noisy ascent, net+majority, fires", d2, false] # WD3: net entropy FELL despite several ascent steps -> does NOT fire. # Magnitude mirror of the anti-lie guard: most steps tick up but the value @@ -57,7 +57,7 @@ c is 5.0 c is 4.0 c is 3.0 d3 is diverging -want of ["WD3 net-down despite ascents does not fire", d3, 0] +want of ["WD3 net-down despite ascents does not fire", d3, false] # WD4: steady ascent INSIDE the gray band (dh_zero <= |dH| < dh_small) does # NOT fire — it reads `stable`. #187 mutual-exclusivity guard. @@ -69,7 +69,7 @@ e is 52.0 e is 50.0 d4 is diverging # #861 re-pin: value route AGREES here for the right reason: -2/step is a linear runaway toward -inf (raw non-vanishing same-sign), where the entropy channel called it gray-band "stable" -want of ["WD4 gray-band steady ascent does not fire", d4, 1] +want of ["WD4 gray-band steady ascent does not fire", d4, true] # WD5: net UP but only a MINORITY (2/5) of steps are genuine ascents -> does # NOT fire. Proves the proportional vote gates independently of sum>0. @@ -80,7 +80,7 @@ f is 60.0 f is 200.0 f is 2.0 d5 is diverging -want of ["WD5 net-up but sub-majority does not fire", d5, 0] +want of ["WD5 net-up but sub-majority does not fire", d5, false] # WD6: flat sequence (all dH ~ 0) does not fire g is 5.0 @@ -89,7 +89,7 @@ g is 5.0 g is 5.0 g is 5.0 d6 is diverging -want of ["WD6 flat does not fire", d6, 0] +want of ["WD6 flat does not fire", d6, false] # WD7: slow descent (falling entropy, sum < 0) does not fire h is 25.0 @@ -97,14 +97,14 @@ h is 40.0 h is 60.0 h is 99.0 d7 is diverging -want of ["WD7 slow descent does not fire", d7, 0] +want of ["WD7 slow descent does not fire", d7, false] # WD8: fewer than 3 samples (count == 2) does not fire (partial-window rule) k is 9999.0 k is 50.0 k is 3.0 d8 is diverging -want of ["WD8 count < 3 does not fire", d8, 0] +want of ["WD8 count < 3 does not fire", d8, false] # #861: entropy-window mechanics — WD1 is the canonical entropy-ascent (value diff --git a/tests/test_windowed_equilibrium.eigs b/tests/test_windowed_equilibrium.eigs index 6988a70b..662fbea7 100644 --- a/tests/test_windowed_equilibrium.eigs +++ b/tests/test_windowed_equilibrium.eigs @@ -30,7 +30,7 @@ a is 2.0 a is 2.0 a is 2.0 a is 2.0 -want of ["WE1 quiet high-entropy fires", equilibrium, 1] +want of ["WE1 quiet high-entropy fires", equilibrium, true] # WE2: full-window quiet at LOW entropy -> equilibrium (converged is a subset) b is 1000000.0 @@ -44,7 +44,7 @@ b is 1000000.0 b is 1000000.0 b is 1000000.0 b is 1000000.0 -want of ["WE2 quiet low-entropy fires (converged subset)", equilibrium, 1] +want of ["WE2 quiet low-entropy fires (converged subset)", equilibrium, true] # WE3: partial window (count == 9 < N) -> does NOT fire (full-window rule) c is 2.0 @@ -57,7 +57,7 @@ c is 2.0 c is 2.0 c is 2.0 c is 2.0 -want of ["WE3 partial window does not fire", equilibrium, 0] +want of ["WE3 partial window does not fire", equilibrium, false] # WE4: steady drift, |mean| > dh_zero -> does NOT fire (not at rest on average) d is 30.0 @@ -71,7 +71,7 @@ d is 44.0 d is 46.0 d is 48.0 d is 50.0 -want of ["WE4 steady drift does not fire (mean)", equilibrium, 0] +want of ["WE4 steady drift does not fire (mean)", equilibrium, false] # WE5: large-amplitude oscillation -> zero-mean but variance too high -> NO e is 2.0 @@ -85,7 +85,7 @@ e is 10.0 e is 2.0 e is 10.0 e is 2.0 -want of ["WE5 large-amp oscillation does not fire (variance)", equilibrium, 0] +want of ["WE5 large-amp oscillation does not fire (variance)", equilibrium, false] # WE6: sub-dh_zero cancelling motion (|dH|~1.5e-5) -> zero-mean, low variance -> YES f is 800.0 @@ -100,7 +100,7 @@ f is 800.0 f is 801.0 f is 800.0 # #861 re-pin: value route: 800<->801 is a real +/-1 perpetual oscillation (#422) — an active-motion band, exclusive of equilibrium; the entropy channel saw dH~1.5e-5 and called it rest -want of ["WE6 sub-noise cancellation fires", equilibrium, 0] +want of ["WE6 sub-noise cancellation fires", equilibrium, false] # WE7: gray-band oscillation (|dH|~0.0057) -> zero-mean but variance over # dh_zero^2 -> does NOT fire (variance gate, distinct from large amplitude) @@ -115,7 +115,7 @@ g is 75.0 g is 70.0 g is 75.0 g is 70.0 -want of ["WE7 gray-band oscillation does not fire (variance)", equilibrium, 0] +want of ["WE7 gray-band oscillation does not fire (variance)", equilibrium, false] # WE8: quiet at moderate entropy (|x|=42) -> equilibrium (entropy-independent) h is 42.0 @@ -129,6 +129,6 @@ h is 42.0 h is 42.0 h is 42.0 h is 42.0 -want of ["WE8 quiet moderate-entropy fires", equilibrium, 1] +want of ["WE8 quiet moderate-entropy fires", equilibrium, true] print of "WINDOWED_EQUILIBRIUM_ALL_PASS" diff --git a/tests/test_windowed_improving.eigs b/tests/test_windowed_improving.eigs index d072cc5f..e0c6cc20 100644 --- a/tests/test_windowed_improving.eigs +++ b/tests/test_windowed_improving.eigs @@ -30,7 +30,7 @@ a is 99.0 a is 499.0 i1 is improving # #861 re-pin: value route: growth 3->499 is a linear/geometric runaway -> diverging; the ENTROPY descent this case drove reads "improving" only on the entropy channel (pinned via classify below for WI1) -want of ["WI1 monotone clean descent fires", i1, 0] +want of ["WI1 monotone clean descent fires", i1, false] # WI2: noisy descent with bounces still fires — net down AND majority (4/6) # genuine descents. The old absolute bounce cap rejected this; the @@ -44,7 +44,7 @@ b is 60.0 b is 4000.0 i2 is improving # #861 re-pin: value route: wild bouncing (4 deadband sign-flips) -> oscillating -want of ["WI2 noisy descent, net+majority, fires", i2, 0] +want of ["WI2 noisy descent, net+majority, fires", i2, false] # WI3: net entropy ROSE despite several descent steps -> does NOT fire. # Guards against the magnitude-blind failure: most steps tick down but the @@ -56,7 +56,7 @@ c is 6.0 c is 2.2 c is 1.6 i3 is improving -want of ["WI3 net-up despite descents does not fire", i3, 0] +want of ["WI3 net-up despite descents does not fire", i3, false] # WI4: steady descent INSIDE the gray band (dh_zero <= |dH| < dh_small) does # NOT fire — it reads `stable`. #187 mutual-exclusivity guard. @@ -67,7 +67,7 @@ d is 56.0 d is 58.0 d is 60.0 i4 is improving -want of ["WI4 gray-band steady descent does not fire", i4, 0] +want of ["WI4 gray-band steady descent does not fire", i4, false] # WI5: net DOWN but only a MINORITY (2/5) of steps are genuine descents -> does # NOT fire. Proves the proportional vote gates independently of sum<0. @@ -78,7 +78,7 @@ e is 4000.0 e is 1500.0 e is 600.0 i5 is improving -want of ["WI5 net-down but sub-majority does not fire", i5, 0] +want of ["WI5 net-down but sub-majority does not fire", i5, false] # WI6: flat sequence (all dH ~ 0) does not fire f is 5.0 @@ -87,7 +87,7 @@ f is 5.0 f is 5.0 f is 5.0 i6 is improving -want of ["WI6 flat does not fire", i6, 0] +want of ["WI6 flat does not fire", i6, false] # WI7: slow ascent (rising entropy, sum > 0) does not fire g is 99.0 @@ -95,14 +95,14 @@ g is 60.0 g is 40.0 g is 25.0 i7 is improving -want of ["WI7 slow ascent does not fire", i7, 0] +want of ["WI7 slow ascent does not fire", i7, false] # WI8: fewer than 3 samples (count == 2) does not fire (partial-window rule) h is 3.0 h is 50.0 h is 9999.0 i8 is improving -want of ["WI8 count < 3 does not fire", i8, 0] +want of ["WI8 count < 3 does not fire", i8, false] # #861: keep the entropy-window mechanics covered — WI1 is the canonical diff --git a/tests/test_windowed_oscillating.eigs b/tests/test_windowed_oscillating.eigs index 6e372a41..f539cb4a 100644 --- a/tests/test_windowed_oscillating.eigs +++ b/tests/test_windowed_oscillating.eigs @@ -29,7 +29,7 @@ a is 2.0 a is 10.0 a is 2.0 o1 is oscillating -want of ["WO1 large-amplitude alternation fires", o1, 1] +want of ["WO1 large-amplitude alternation fires", o1, true] # WO2: gray-band alternation fires too — flips gate at dh_zero, so |dH|~0.0057 # (between dh_zero and dh_small) still counts. @@ -41,7 +41,7 @@ b is 70.0 b is 75.0 b is 70.0 o2 is oscillating -want of ["WO2 gray-band alternation fires", o2, 1] +want of ["WO2 gray-band alternation fires", o2, true] # WO3: exactly FLIPS=4 flips (5 dH, perfect alternation) — the boundary, fires c is 2.0 @@ -51,7 +51,7 @@ c is 10.0 c is 2.0 c is 10.0 o3 is oscillating -want of ["WO3 exactly 4 flips fires", o3, 1] +want of ["WO3 exactly 4 flips fires", o3, true] # WO4: only 3 flips (4 dH) — below threshold, does NOT fire d is 2.0 @@ -60,7 +60,7 @@ d is 2.0 d is 10.0 d is 2.0 o4 is oscillating -want of ["WO4 only 3 flips does not fire", o4, 0] +want of ["WO4 only 3 flips does not fire", o4, false] # WO5: sub-noise wobble — alternating but |dH|~1.5e-5 < dh_zero, so no flip # counts. Stays out of `oscillating` (it reads equilibrium/converged-ish). @@ -73,7 +73,7 @@ e is 801.0 e is 800.0 o5 is oscillating # #861 re-pin: value route: 800<->801 is a REAL +/-1 perpetual oscillation — "sub-noise" only to the entropy signal (H is flat out there, the #294 blindness). #422 pins this class as oscillating; the entropy label (equilibrium-ish) is preserved via classify below -want of ["WO5 sub-noise wobble does not fire", o5, 1] +want of ["WO5 sub-noise wobble does not fire", o5, true] # WO6: monotone descent — no flips at all f is 3.0 @@ -83,7 +83,7 @@ f is 19.0 f is 99.0 f is 499.0 o6 is oscillating -want of ["WO6 monotone does not fire", o6, 0] +want of ["WO6 monotone does not fire", o6, false] # WO7: one reversal then steady — a single flip is not oscillation g is 2.0 @@ -94,13 +94,13 @@ g is 4.0 g is 3.0 g is 2.0 o7 is oscillating -want of ["WO7 one reversal does not fire", o7, 0] +want of ["WO7 one reversal does not fire", o7, false] # WO8: fewer than 3 samples (count == 2) does not fire (partial-window rule) h is 2.0 h is 10.0 h is 2.0 o8 is oscillating -want of ["WO8 count < 3 does not fire", o8, 0] +want of ["WO8 count < 3 does not fire", o8, false] print of "WINDOWED_OSCILLATING_ALL_PASS" diff --git a/tests/test_windowed_stable.eigs b/tests/test_windowed_stable.eigs index cb576672..092fd996 100644 --- a/tests/test_windowed_stable.eigs +++ b/tests/test_windowed_stable.eigs @@ -35,7 +35,7 @@ a is 46.0 a is 48.0 a is 50.0 # #861 re-pin: value route: +2/step linear climb -> diverging; entropy small-dH "stable" was the additive-runaway blindness (#422) -want of ["WS1 full-window drift fires", stable, 0] +want of ["WS1 full-window drift fires", stable, false] # WS2: full-window quiet at HIGH entropy -> stable (also equilibrium) b is 2.0 @@ -49,7 +49,7 @@ b is 2.0 b is 2.0 b is 2.0 b is 2.0 -want of ["WS2 quiet high-entropy fires", stable, 1] +want of ["WS2 quiet high-entropy fires", stable, true] # WS3: partial window (count == 9 < N) -> does NOT fire (full-window rule) c is 30.0 @@ -62,7 +62,7 @@ c is 42.0 c is 44.0 c is 46.0 c is 48.0 -want of ["WS3 partial window does not fire", stable, 0] +want of ["WS3 partial window does not fire", stable, false] # WS4: a single step exceeds dh_small -> does NOT fire d is 40.0 @@ -76,7 +76,7 @@ d is 54.0 d is 56.0 d is 58.0 d is 200.0 -want of ["WS4 step over dh_small does not fire", stable, 0] +want of ["WS4 step over dh_small does not fire", stable, false] # WS5: full-window quiet at LOW entropy -> NOT stable (entropy < h_low; converged) e is 1000000.0 @@ -90,7 +90,7 @@ e is 1000000.0 e is 1000000.0 e is 1000000.0 e is 1000000.0 -want of ["WS5 low-entropy quiet does not fire (converged)", stable, 1] +want of ["WS5 low-entropy quiet does not fire (converged)", stable, true] # WS6: gray-band oscillation (consecutive sign flips) -> NOT stable f is 70.0 @@ -104,7 +104,7 @@ f is 75.0 f is 70.0 f is 75.0 f is 70.0 -want of ["WS6 oscillation does not fire", stable, 0] +want of ["WS6 oscillation does not fire", stable, false] # WS7: quiet at entropy JUST ABOVE h_low (|x|=42, H~0.159) -> stable g is 42.0 @@ -118,7 +118,7 @@ g is 42.0 g is 42.0 g is 42.0 g is 42.0 -want of ["WS7 quiet just above h_low fires", stable, 1] +want of ["WS7 quiet just above h_low fires", stable, true] # WS8: quiet at entropy JUST BELOW h_low (|x|=100, H~0.080) -> NOT stable h is 100.0 @@ -132,6 +132,6 @@ h is 100.0 h is 100.0 h is 100.0 h is 100.0 -want of ["WS8 quiet just below h_low does not fire", stable, 1] +want of ["WS8 quiet just below h_low does not fire", stable, true] print of "WINDOWED_STABLE_ALL_PASS" diff --git a/tests/tsan_sandbox_snapshot_race.eigs b/tests/tsan_sandbox_snapshot_race.eigs index b09673a2..1999c047 100644 --- a/tests/tsan_sandbox_snapshot_race.eigs +++ b/tests/tsan_sandbox_snapshot_race.eigs @@ -7,7 +7,7 @@ define worker(n) as: local ok is 0 loop while i < 200: local r is sandbox_run of [[1, [0,0,0, 40], [7]], 1000] - if r["ok"] == 1: + if r["ok"]: ok is ok + 1 i is i + 1 return ok diff --git a/tests/ui_native_input.eigs b/tests/ui_native_input.eigs index 2abce5c8..3a7c0567 100644 --- a/tests/ui_native_input.eigs +++ b/tests/ui_native_input.eigs @@ -1,6 +1,6 @@ # Real X11 input reaches gfx_poll, toolkit dispatch and rendered slider pixels. load_file of "lib/ui.eigs" -assert of [(gfx_open of [320, 240, "ui-native-1263"]) == 1, "native fixture window"] +assert of [(gfx_open of [320, 240, "ui-native-1263"]), "native fixture window"] native_root is panel of ["native-root", 0, 0, 320, 240] native_slider is slider of ["native-slider", 20, 30, 200, 0, 100, 0, null] add_child of [native_root, native_slider] diff --git a/tests/ui_sdl_input.c b/tests/ui_sdl_input.c index 3e5ee694..c7e246a1 100644 --- a/tests/ui_sdl_input.c +++ b/tests/ui_sdl_input.c @@ -37,14 +37,14 @@ int main(void) { EigsState *state = eigs_open(); if (!state) return 2; int passed = 0; - int ok = eval_ok("assert of [(gfx_open of [80, 60, \"ui-input-1263\"]) == 1, \"open\"]\n"); + int ok = eval_ok("assert of [(gfx_open of [80, 60, \"ui-input-1263\"]) == true, \"open\"]\n"); const char *checks[] = { - "e.type == \"keydown\" and e.key == \"up\" and e.scancode == 82 and e.shift == 1 and e.ctrl == 0 and e.alt == 0", - "e.type == \"keyup\" and e.key == \"a\" and e.scancode == 4 and e.shift == 0 and e.ctrl == 1 and e.alt == 1", - "e.type == \"mousemove\" and e.x == 321 and e.y == 654 and e.shift == 0 and e.ctrl == 1 and e.alt == 1", - "e.type == \"mousedown\" and e.button == 3 and e.x == 111 and e.y == 222 and e.shift == 0 and e.ctrl == 1 and e.alt == 1", - "e.type == \"mouseup\" and e.button == 3 and e.x == 112 and e.y == 223 and e.shift == 0 and e.ctrl == 1 and e.alt == 1", - "e.type == \"wheel\" and e.x == -2 and e.y == 5 and e.mx == 17 and e.my == 23 and e.shift == 0 and e.ctrl == 1 and e.alt == 1", + "e.type == \"keydown\" and e.key == \"up\" and e.scancode == 82 and e.shift == true and e.ctrl == false and e.alt == false", + "e.type == \"keyup\" and e.key == \"a\" and e.scancode == 4 and e.shift == false and e.ctrl == true and e.alt == true", + "e.type == \"mousemove\" and e.x == 321 and e.y == 654 and e.shift == false and e.ctrl == true and e.alt == true", + "e.type == \"mousedown\" and e.button == 3 and e.x == 111 and e.y == 222 and e.shift == false and e.ctrl == true and e.alt == true", + "e.type == \"mouseup\" and e.button == 3 and e.x == 112 and e.y == 223 and e.shift == false and e.ctrl == true and e.alt == true", + "e.type == \"wheel\" and e.x == -2 and e.y == 5 and e.mx == 17 and e.my == 23 and e.shift == false and e.ctrl == true and e.alt == true", "e.type == \"resize\" and e.w == 640 and e.h == 480", "e.type == \"quit\"" }; diff --git a/tools/freestanding_smoke.sh b/tools/freestanding_smoke.sh index aba1c468..5a4b84cf 100755 --- a/tools/freestanding_smoke.sh +++ b/tools/freestanding_smoke.sh @@ -93,7 +93,7 @@ print of f"{v}" ' # Observer semantics intact. -check_line "observer predicates run" 0 "1" ' +check_line "observer predicates run" 0 "true" ' e is 5 loop while not converged: e is e * 0.5 diff --git a/tools/num_read_allowlist.txt b/tools/num_read_allowlist.txt new file mode 100644 index 00000000..dccfc64d --- /dev/null +++ b/tools/num_read_allowlist.txt @@ -0,0 +1,76 @@ +# tools/num_read_allowlist.txt -- every raw number token, reviewed (#1637 round 4). +# file|function|count|reason. tools/num_read_check.sh counts VAL_NUM_RAW / +# SLOT_NUM_RAW / VAL_NUM_OFFSET tokens per function and fails when a count +# differs (a new raw read, however it is spelled across lines, changes the +# count), when a function with raw tokens is missing here, or a row is stale. +# Builtins never belong here: they read through eigs_num_arg / eigs_list_num / +# eigs_opt_num / eigs_elem_num, which raise on a bool. +builtins_tensor.c|builtin_numerical_grad_rows|3|in-place writes of a matrix cell that tensor_cells_numeric proved a number before any mutation +builtins_tensor.c|builtin_tensor_matmul|1|writes the NaN-boxed null sentinel into a result buffer cell (EIGS_STRICT=0 path); a write, no Value is read +chunk.c|chunk_disassemble|1|debug disassembly prints a constant behind its own type == VAL_NUM test +chunk.c|const_equal|2|constant-pool dedup compares two constants after testing both are VAL_NUM +chunk.c|const_hash_value|1|constant-pool hash of a constant inside its type == VAL_NUM branch +eigenscript.c|chan_clone_rec|1|channel deep-copy, switch case VAL_NUM +eigenscript.c|compute_entropy_impl|1|observer entropy, switch case VAL_NUM +eigenscript.c|eigs_elem_num|1|checking accessor itself: reads only after type == VAL_NUM +eigenscript.c|eigs_num_arg_slow|1|checking accessor itself: reads only after type == VAL_NUM, raises otherwise +eigenscript.c|eigs_opt_num|1|checking accessor itself: reads only after type == VAL_NUM, raises on non-null non-numbers +eigenscript.c|is_truthy|1|truthiness, switch case VAL_NUM (a bool has its own case) +eigenscript.c|make_num|1|constructor: writes the number of a Value it just typed VAL_NUM +eigenscript.c|make_num_permanent|1|constructor: writes the number of a Value it just typed VAL_NUM +eigenscript.c|module_ns_project|2|mirror refresh: reads a slot after slot_is_num and writes a VAL_NUM mirror it owns exclusively +eigenscript.c|observer_entropy_now|1|observer entropy of a slot after slot_is_num +eigenscript.c|observer_slot_from_trajectory|9|trajectory snapshot fields each read right after its own type != VAL_NUM refusal +eigenscript.c|observer_slot_sample_gated|1|observer sample after newval->type == VAL_NUM +eigenscript.c|observer_slot_update|1|observer update after newval->type == VAL_NUM +eigenscript.c|promote_arena_scalar|1|arena promotion, inside its type == VAL_NUM branch +eigenscript.c|slot_from_value|1|NaN-boxing boundary, inside its VAL_NUM branch +eigenscript.c|slot_to_value|1|NaN-boxing boundary after slot_is_num +eigenscript.c|values_equal_impl|2|equality, switch case VAL_NUM after the bool/num pair raised +eigenscript.c|value_to_string|1|printing, switch case VAL_NUM +eigenscript.h|#define VAL_NUM_OFFSET|1|the raw member offset definition (JIT) +eigenscript.h|#define VAL_NUM_RAW|1|the raw read definition +eigenscript.h|eigs_num_arg|1|inline fast path of the checking accessor: reads only after type == VAL_NUM +eigs_embed.c|eigs_value_as_num|1|embedding API: reads only after type == VAL_NUM; a bool answers NaN and other non-numbers 0.0 (docs/EMBEDDING.md) +embed_smoke.c|main|7|C test program: builds and inspects slots it constructed as numbers +embed_smoke.c|test_worker_exit_lifecycle|2|C test program: inspects slots it constructed as numbers +jit.c|emit_load_numeric_operand|2|emits the load of data.num_ after the emitted type == VAL_NUM / refcount guard +jit.c|jit_compile_to_thunk|4|emits loads/stores of data.num_ behind emitted immediate-number checks; reads a VAL_NUM constant +jit_smoke.c|jit_helper_index_get|2|JIT smoke test program (stubbed helpers over slots it built as numbers) +jit_smoke.c|jit_helper_iter_next|1|JIT smoke test program (stubbed helpers over slots it built as numbers) +jit_smoke.c|jit_helper_set_name|4|JIT smoke test program (stubbed helpers over slots it built as numbers) +jit_smoke.c|run_exit_cases|3|JIT smoke test program (stubbed helpers over slots it built as numbers) +jit_smoke.c|run_index_bail_cases|2|JIT smoke test program (stubbed helpers over slots it built as numbers) +jit_smoke.c|run_reader_bail_cases|8|JIT smoke test program (stubbed helpers over slots it built as numbers) +jit_smoke.c|run_store_cases|4|JIT smoke test program (stubbed helpers over slots it built as numbers) +trace.c|write_slot|1|tape writer after slot_is_num +trace.c|write_value_ptr|1|tape writer, switch case VAL_NUM +trace.c|write_value_ptr_full|1|tape writer, switch case VAL_NUM +value_slot.h|#define SLOT_NUM_RAW|1|the raw slot read definition +value_slot.h|slot_from_num|1|constructor: writes a number into a fresh slot +value_slot.h|slot_truthy_immediate|1|truthiness after slot_is_num +vm.c|#define ARITH_FAST|2|arithmetic fast path after both operands tested VAL_NUM; NUM_REUSE writes +vm.c|#define INT_BINOP_R|2|integer op fast path after both slots slot_is_num +vm.c|#define NUM_BINOP|6|arithmetic after a->type == VAL_NUM && b->type == VAL_NUM; NUM_REUSE writes +vm.c|dict_set_cached_immediate|1|in-place dict value update, writes a VAL_NUM it exclusively owns +vm.c|jit_helper_dot_get|1|JIT helper: pushes a field value after its VAL_NUM test +vm.c|jit_helper_dot_set|1|JIT helper: number store after slot_is_num +vm.c|jit_helper_index_get|10|JIT helper: index read after slot_is_num / VAL_NUM tests on index and element +vm.c|jit_helper_index_set|13|JIT helper: index store after slot_is_num / VAL_NUM tests on index and value +vm.c|jit_helper_iter_next|3|JIT helper: loop state list [iterable, idx, snap] built by the VM, each read behind a VAL_NUM test +vm.c|jit_helper_local_dot_get|1|JIT helper: field value after its VAL_NUM test +vm.c|jit_helper_local_dot_set|1|JIT helper: number store after slot_is_num +vm.c|jit_helper_local_idx_dot_get|1|JIT helper: field value after its VAL_NUM test +vm.c|jit_helper_local_idx_get|1|JIT helper: element after its VAL_NUM test +vm.c|jit_helper_observe_assign_local|2|JIT helper: observer update after slot_is_num +vm.c|jit_helper_observe_name_post|2|JIT helper: observer update after slot_is_num +vm.c|loop_iter_store|1|writes the loop counter slot after slot_is_num +vm.c|obs_dump_scope|1|SIGUSR1 observer dump after slot_is_num +vm.c|obs_dump_value|2|SIGUSR1 observer dump after slot_is_num / case VAL_NUM +vm.c|slot_as_double|2|VM numeric operand read: returns 0 (not a number) unless slot_is_num or VAL_NUM +vm.c|slot_bridge_unwrap|1|NaN-boxing boundary after slot_is_num +vm.c|slot_truthy|1|truthiness after slot_is_num +vm.c|vm_leaf_accessor_exec|3|leaf-accessor fast path: constants the scanner admitted only as VAL_NUM (chunk_scan_leaf_accessor) and values behind VAL_NUM tests +vm.c|vm_run_ex|68|the interpreter loop: every opcode reads numbers behind slot_is_num / type == VAL_NUM dispatch; NUM_REUSE in-place writes; the iterator state list is VM-built +vm.c|vm_slot_lift|1|NaN-boxing boundary after slot_is_num +vm.c|vm_store_local_slot|2|in-place local number store after slot_is_num diff --git a/tools/num_read_check.sh b/tools/num_read_check.sh new file mode 100644 index 00000000..b3c41477 --- /dev/null +++ b/tools/num_read_check.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# num_read_check.sh -- #1637 round 4: raw number reads are TOKENS, and every +# token is reviewed. +# +# The Value union's number member is `num_` and the EigsSlot union's double is +# `d_`, so no unreviewed spelling compiles. Code reaches a number in exactly +# two ways: +# - the checking accessors eigs_num_arg / eigs_list_num / eigs_opt_num, +# which raise a type error on a bool or any other non-number; +# - the raw macros VAL_NUM_RAW(v) / SLOT_NUM_RAW(s) (and VAL_NUM_OFFSET for +# JIT-emitted loads), for a value whose type is already proven. +# This gate enumerates every raw token in src/*.c and src/*.h. It attributes +# each one to its enclosing function, or to its #define for a macro. Then it +# requires each (file, function) to appear in tools/num_read_allowlist.txt as +# `file|function|count|reason`, with EXACTLY that many tokens. So a new raw +# read -- a soft default split over any number of lines included -- changes +# a count and goes red until someone reviews it. +# +# It also fails on: +# - `data.num_` / `.d_` spelled out anywhere except the macro definitions +# (a bypass of the macros); +# - a bare `->data.builtin(` call: every builtin call must pass the bool +# gate, eigs_call_builtin; +# - an allowlist row that is stale, has the wrong count, or has no reason; +# - zero tokens examined. +# Prints `num-read: examined=N functions=F allowlisted=F violations=0`. +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SRC_DIR="${NUM_READ_SRC:-$ROOT/src}" +ALLOW="${NUM_READ_ALLOW:-$ROOT/tools/num_read_allowlist.txt}" +[ -f "$ALLOW" ] || { echo "num-read: FAIL: allowlist $ALLOW missing"; exit 1; } +TMP=$(mktemp "${TMPDIR:-/tmp}/num_read.XXXXXX") || exit 2 +trap 'rm -f "$TMP" "$TMP.b" "$TMP.c"' EXIT +: > "$TMP.b" + +for f in "$SRC_DIR"/*.c "$SRC_DIR"/*.h; do + [ -f "$f" ] || continue + awk -v FILE="$(basename "$f")" -v BYPASS="$TMP.b" ' + { + line = $0 + # strip comments (block comments carry state across lines) and + # string literals, so prose and messages never count + if (incmt) { if (index(line, "*/")) { line = substr(line, index(line, "*/") + 2); incmt = 0 } else line = "" } + gsub(/"([^"\\]|\\.)*"/, "\"\"", line) + while (match(line, /\/\*([^*]|\*[^\/])*\*\//)) line = substr(line, 1, RSTART - 1) " " substr(line, RSTART + RLENGTH) + if (index(line, "/*")) { line = substr(line, 1, index(line, "/*") - 1); incmt = 1 } + sub(/\/\/.*/, "", line) + } + # attribution: a #define names its macro (continuations stay with it); + # a column-0 line with a parameter list that is not a declaration starts + # a function; a column-0 "}" ends it + !incont && /^#[ \t]*define[ \t]/ { m = $0; sub(/^#[ \t]*define[ \t]+/, "", m); sub(/[^A-Za-z0-9_].*/, "", m); scope = "#define " m; inmac = 1 } + !incont && !/^#/ && /^[A-Za-z_][^;]*\(/ && $0 !~ /;[ \t]*$/ && $0 !~ /^(typedef|return|extern)/ { + fn = $0; sub(/\(.*/, "", fn); n = split(fn, w, /[ *]+/); scope = w[n]; inmac = 0 + } + { linemac = inmac } + { + s = line + while (match(s, /(VAL_NUM_RAW|SLOT_NUM_RAW)[ \t]*\(|VAL_NUM_OFFSET/)) { + tok = substr(s, RSTART, RLENGTH); sub(/[ \t]*\($/, "", tok) + printf "%s|%s|%s|%d\n", FILE, (scope == "" ? "" : scope), tok, NR + s = substr(s, RSTART + RLENGTH) + } + if (line ~ /data\.num_|[A-Za-z0-9_)\]]\.d_([^A-Za-z0-9_]|$)/ && scope !~ /^#define (VAL_NUM_RAW|SLOT_NUM_RAW|VAL_NUM_OFFSET)$/) + printf "%s:%d: %s\n", FILE, NR, $0 >> BYPASS + if (line ~ /->data\.builtin[ \t]*\(/) printf "%s:%d: ungated builtin call: %s\n", FILE, NR, $0 >> BYPASS + incont = ($0 ~ /\\$/) + if (inmac && !incont) { inmac = 0; scope = fnsave } + if (!inmac) fnsave = scope + } + /^}/ && !linemac { scope = ""; fnsave = "" } + ' "$f" +done > "$TMP" + +examined=$(grep -c . "$TMP") +violations=0; stale=0 +# per-(file,function) counts, compared both ways with the allowlist +cut -d'|' -f1,2 "$TMP" | sort | uniq -c | awk '{c = $1; $1 = ""; sub(/^ /, ""); print $0 "|" c}' > "$TMP.c" +functions=$(grep -c . "$TMP.c") +while IFS='|' read -r file fn count; do + row=$(awk -F'|' -v f="$file" -v g="$fn" '$1 == f && $2 == g' "$ALLOW") + if [ -z "$row" ]; then + violations=$((violations + 1)) + echo " VIOLATION: src/$file $fn: $count raw number token(s), function not on the allowlist" + grep "^$file|$fn|" "$TMP" | awk -F'|' '{printf " src/%s:%s %s\n", $1, $4, $3}' + continue + fi + want=$(printf '%s\n' "$row" | head -1 | cut -d'|' -f3) + if [ "$want" != "$count" ]; then + violations=$((violations + 1)) + echo " VIOLATION: src/$file $fn: $count raw number token(s), the allowlist reviewed $want" + fi +done < "$TMP.c" +while IFS='|' read -r file fn count reason; do + case "$file" in ''|\#*) continue ;; esac + if [ -z "$reason" ]; then echo " FAIL: allowlist row without a reason: $file|$fn"; stale=$((stale + 1)); fi + grep -qF -- "$file|$fn|$count" "$TMP.c" 2>/dev/null || + grep -q "^$file|$fn|" "$TMP.c" || + { echo " FAIL: stale allowlist row (no raw token there): $file|$fn"; stale=$((stale + 1)); } +done < "$ALLOW" +bypass=$(grep -c . "$TMP.b") +[ "$bypass" -gt 0 ] && sed 's/^/ VIOLATION: bypass: /' "$TMP.b" +violations=$((violations + bypass)) +allowlisted=$(grep -v '^#' "$ALLOW" | grep -c .) +echo "num-read: examined=$examined functions=$functions allowlisted=$allowlisted violations=$violations stale=$stale" +if [ "$examined" -le 0 ] || [ "$violations" -ne 0 ] || [ "$stale" -ne 0 ]; then + echo "num-read: FAIL"; exit 1 +fi +echo "num-read: PASS" diff --git a/tools/strict_differential.sh b/tools/strict_differential.sh index 15def4f0..30770b01 100755 --- a/tools/strict_differential.sh +++ b/tools/strict_differential.sh @@ -150,12 +150,12 @@ shape_abort() { shape_backend_classify() { # kind, captured rc+output, actual platform shape_receipt_ok "$2" || return 1 case "$1" in sdl|mixer) ;; *) return 1 ;; esac - if [ "$2" = $'0\nshape-backend: 1' ]; then echo present; return 0; fi + if [ "$2" = $'0\nshape-backend: true' ]; then echo present; return 0; fi [ "$3" = Darwin ] || return 1 - if [ "$1" = sdl ] && [ "$2" = $'0\ngfx_open: cannot load libSDL2\nshape-backend: 0' ]; then + if [ "$1" = sdl ] && [ "$2" = $'0\ngfx_open: cannot load libSDL2\nshape-backend: false' ]; then echo absent-sdl; return 0 fi - if [ "$1" = mixer ] && [ "$2" = $'0\naudio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0)\nshape-backend: 0' ]; then + if [ "$1" = mixer ] && [ "$2" = $'0\naudio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0)\nshape-backend: false' ]; then echo absent-mixer; return 0 fi return 1 @@ -349,162 +349,162 @@ shape_selftest() { fi } backend_case sdl-present-Darwin 0 present sdl '0 -shape-backend: 1' Darwin +shape-backend: true' Darwin backend_case sdl-present-Linux 0 present sdl '0 -shape-backend: 1' Linux +shape-backend: true' Linux backend_case sdl-exact-absent-Darwin 0 absent-sdl sdl '0 gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-absence-Linux-is-failure 1 '' sdl '0 gfx_open: cannot load libSDL2 -shape-backend: 0' Linux +shape-backend: false' Linux backend_case sdl-other-backend-diagnostic 1 '' sdl '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-wrong-child-rc-1 1 '' sdl '1 gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-wrong-child-rc-3 1 '' sdl '3 gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-wrong-child-rc-124 1 '' sdl '124 gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-wrong-child-rc--9 1 '' sdl '-9 gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-altered-diagnostic 1 '' sdl '0 gfx_open: cannot load libSDL2 changed -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-unrelated-first-line 1 '' sdl '0 unrelated diagnostic gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-unrelated-last-line 1 '' sdl '0 gfx_open: cannot load libSDL2 -shape-backend: 0 +shape-backend: false unrelated diagnostic' Darwin backend_case sdl-duplicate-diagnostic 1 '' sdl '0 gfx_open: cannot load libSDL2 gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-arbitrary-zero 1 '' sdl '0 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-success-with-absence-diagnostic 1 '' sdl '0 gfx_open: cannot load libSDL2 -shape-backend: 1' Darwin +shape-backend: true' Darwin backend_case sdl-wrong-result-marker 1 '' sdl '0 gfx_open: cannot load libSDL2 shape-backend: 2' Darwin backend_case sdl-missing-child-rc 1 '' sdl 'gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-duplicate-result-marker 1 '' sdl '0 gfx_open: cannot load libSDL2 -shape-backend: 0 -shape-backend: 0' Darwin +shape-backend: false +shape-backend: false' Darwin backend_case sdl-missing-result-marker 1 '' sdl '0 gfx_open: cannot load libSDL2' Darwin backend_case sdl-asan-veto 1 '' sdl '0 gfx_open: cannot load libSDL2 ERROR: AddressSanitizer: inert classifier text -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-lsan-veto 1 '' sdl '0 gfx_open: cannot load libSDL2 ERROR: LeakSanitizer: detected memory leaks -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-ubsan-veto 1 '' sdl '0 gfx_open: cannot load libSDL2 runtime error: inert classifier text -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case sdl-tsan-veto 1 '' sdl '0 gfx_open: cannot load libSDL2 WARNING: ThreadSanitizer: data race (inert text only) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-present-Darwin 0 present mixer '0 -shape-backend: 1' Darwin +shape-backend: true' Darwin backend_case mixer-present-Linux 0 present mixer '0 -shape-backend: 1' Linux +shape-backend: true' Linux backend_case mixer-exact-absent-Darwin 0 absent-mixer mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-absence-Linux-is-failure 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Linux +shape-backend: false' Linux backend_case mixer-other-backend-diagnostic 1 '' mixer '0 gfx_open: cannot load libSDL2 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-wrong-child-rc-1 1 '' mixer '1 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-wrong-child-rc-3 1 '' mixer '3 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-wrong-child-rc-124 1 '' mixer '124 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-wrong-child-rc--9 1 '' mixer '-9 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-altered-diagnostic 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) changed -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-unrelated-first-line 1 '' mixer '0 unrelated diagnostic audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-unrelated-last-line 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0 +shape-backend: false unrelated diagnostic' Darwin backend_case mixer-duplicate-diagnostic 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-arbitrary-zero 1 '' mixer '0 -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-success-with-absence-diagnostic 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 1' Darwin +shape-backend: true' Darwin backend_case mixer-wrong-result-marker 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) shape-backend: 2' Darwin backend_case mixer-missing-child-rc 1 '' mixer 'audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-duplicate-result-marker 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) -shape-backend: 0 -shape-backend: 0' Darwin +shape-backend: false +shape-backend: false' Darwin backend_case mixer-missing-result-marker 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0)' Darwin backend_case mixer-asan-veto 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) ERROR: AddressSanitizer: inert classifier text -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-lsan-veto 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) ERROR: LeakSanitizer: detected memory leaks -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-ubsan-veto 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) runtime error: inert classifier text -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case mixer-tsan-veto 1 '' mixer '0 audio_music: cannot load libSDL2_mixer (install libsdl2-mixer-2.0-0) WARNING: ThreadSanitizer: data race (inert text only) -shape-backend: 0' Darwin +shape-backend: false' Darwin backend_case unknown-kind-Darwin 1 '' other '0 -shape-backend: 1' Darwin +shape-backend: true' Darwin backend_case unknown-kind-Linux 1 '' other '0 -shape-backend: 1' Linux +shape-backend: true' Linux # Private inert calibration: removing platform ownership admits Linux # absence. Restore the actual classifier before the following control. saved=$(declare -f shape_backend_classify) eval "${saved/shape_backend_classify/shape_backend_original}" shape_backend_classify() { shape_backend_original "$1" "$2" Darwin; } - backend_case 'removed Linux-positive policy is detected (expected RED)' 0 absent-sdl sdl $'0\ngfx_open: cannot load libSDL2\nshape-backend: 0' Linux + backend_case 'removed Linux-positive policy is detected (expected RED)' 0 absent-sdl sdl $'0\ngfx_open: cannot load libSDL2\nshape-backend: false' Linux eval "$saved" unset -f shape_backend_original - backend_case 'restored Linux-positive policy refuses absence' 1 '' sdl $'0\ngfx_open: cannot load libSDL2\nshape-backend: 0' Linux + backend_case 'restored Linux-positive policy refuses absence' 1 '' sdl $'0\ngfx_open: cannot load libSDL2\nshape-backend: false' Linux echo "SHAPE_CAPTURE_SELFTEST: $passed passed, $failed failed, 88 declared" [ "$failed" = 0 ] && [ "$passed" = 88 ] } @@ -638,6 +638,8 @@ starts_with|print of (starts_with of [42, "x"]) store_delete|print of (store_delete of [42, "col", "k"])|store_delete: invalid store index_of|print of (index_of of [42, "x"]) list_index_of|print of (list_index_of of [42, 1]) +list_insert_at|print of (list_insert_at of [[1, 2], "i", 9]) +list_remove_at|print of (list_remove_at of [[1, 2], "i"]) ord|print of (ord of 42) proc_write|print of (proc_write of [42, 99]) exec_capture|print of (exec_capture of 42) @@ -798,7 +800,7 @@ token_name of an unknown id is "?"|print of (token_name of 9999) channel_closed of a reclaimed/unknown channel is 1|print of (channel_closed of ({"_channel_id": 99999})) json_build of null is the empty object|print of (json_build of null) random_hex of 0 is ""|print of f"[{random_hex of 0}]" -tensor_save preserves a zero-column tensor|assert of [(tensor_save of [[[], []], "@TMP@/zero-cols.tensor"]) == 1, "zero-column tensor_save"] +tensor_save preserves a zero-column tensor|assert of [(tensor_save of [[[], []], "@TMP@/zero-cols.tensor"]), "zero-column tensor_save"] EOF ) PINS="${PINS//@TMP@/$TMP}" @@ -972,7 +974,7 @@ cap_contract() { http) name=http_route; op='http_route of ["GET", "/strict-capability-probe", "ok"]'; answer='result == "route registered"' ;; net) name=net_close; op='net_close of -1'; answer='result == null' ;; db) name=db_connect; op='db_connect of null'; answer='(json_path of [result, "status"]) == "no_database"' ;; - model) name=eigen_model_loaded; op='eigen_model_loaded of null'; answer='result == 0' ;; + model) name=eigen_model_loaded; op='eigen_model_loaded of null'; answer='result == false' ;; # #1637: a bool gfx) name=gfx_text_width; op='gfx_text_width of ["m", 1]'; answer='(type of result) == "num" and result > 0' ;; esac message="$(cap_message "$cap")" diff --git a/tools/strict_shape_contracts.json b/tools/strict_shape_contracts.json index 8f054a83..14cf8603 100644 --- a/tools/strict_shape_contracts.json +++ b/tools/strict_shape_contracts.json @@ -147,7 +147,7 @@ ], "source_sha256": { "audio_convert_samples": [ - "8fbbf366557d1b169341d1b02f5016310055e5a104ee5b2d604edb2bd76023fa" + "e98a630393ad3c48b96a83d43c385c7f43e69657d73777fa8a5af913757ddd61" ], "builtin_audio_play": [ "093742db555c0b87c29dea9095751714c0320123fb999941490bd1357b72640a" @@ -229,14 +229,14 @@ ], "disposition": "exempt", "body_sha256": [ - "145c4f188e0808400fe540c77cc6e4baaa8041dbfdafedee1d72abbc448ce7b9" + "b838449be37f5d9d6d58b12d588dbb5deb389a90343061ea18e2bd579a68f057" ], "source_sha256": { "audio_convert_samples": [ - "8fbbf366557d1b169341d1b02f5016310055e5a104ee5b2d604edb2bd76023fa" + "e98a630393ad3c48b96a83d43c385c7f43e69657d73777fa8a5af913757ddd61" ], "builtin_audio_stream_push": [ - "145c4f188e0808400fe540c77cc6e4baaa8041dbfdafedee1d72abbc448ce7b9" + "b838449be37f5d9d6d58b12d588dbb5deb389a90343061ea18e2bd579a68f057" ], "gfx_bad_samples": [ "e7a582dc8b9ceffb2b939333e7467aa938420443f7595154a72be448b01d70b6" @@ -407,11 +407,11 @@ ], "disposition": "exempt", "body_sha256": [ - "13108f9e99f9e84c29d464e18e167a122f4ba1fbf70cb982eecc81e6c0b3fefa" + "54627fcd3cb9d0fde55e9169285491c1123b2a879be7a690949440e997514b09" ], "source_sha256": { "builtin_buf_from_list": [ - "13108f9e99f9e84c29d464e18e167a122f4ba1fbf70cb982eecc81e6c0b3fefa" + "54627fcd3cb9d0fde55e9169285491c1123b2a879be7a690949440e997514b09" ] } }, @@ -523,11 +523,14 @@ ], "disposition": "exempt", "body_sha256": [ - "8d48d81382bd58c9aa5b0fd162de822093b631278ed97aba98af4605b04d4ac4" + "a288dd09f5f2cd4af4fa8355b865cc7f480fb22d20dfb172b9dc7c9cdb13319b" ], "source_sha256": { "builtin_buffer": [ - "8d48d81382bd58c9aa5b0fd162de822093b631278ed97aba98af4605b04d4ac4" + "a288dd09f5f2cd4af4fa8355b865cc7f480fb22d20dfb172b9dc7c9cdb13319b" + ], + "eigs_list_num": [ + "d81d044498e21cfa0893aeb9977764b6d95f437e9ae59dfab7707f5d93693433" ] } }, @@ -634,10 +637,10 @@ "669dc58628bb911edaa53afcc37a9c2ca7dfd0faee8bef0613e72cfba6904c73" ], "db_build_query": [ - "00e39cea708a3efac5175d8bee3813cc22609e5d4d860439fd050abb76007123" + "b8b459c14d36d1e3c83f2d95248c7d000160d601ce5082429391ec4a46dd1e8c" ], "db_exec_from_arg": [ - "175f3418db40cd0f873e228489006cc68bb0970d729fa07f36be8e889c175210" + "cbcf24fbb55c5a6f9bfdbb6535dc22a89086dab2ed311d0fc0892eb6eb287145" ] } }, @@ -660,10 +663,10 @@ "083b679b60be2d040b1671ef418a0473fc9031f2456fe2f2f37869ec85330085" ], "db_build_query": [ - "00e39cea708a3efac5175d8bee3813cc22609e5d4d860439fd050abb76007123" + "b8b459c14d36d1e3c83f2d95248c7d000160d601ce5082429391ec4a46dd1e8c" ], "db_exec_from_arg": [ - "175f3418db40cd0f873e228489006cc68bb0970d729fa07f36be8e889c175210" + "cbcf24fbb55c5a6f9bfdbb6535dc22a89086dab2ed311d0fc0892eb6eb287145" ] } }, @@ -679,17 +682,17 @@ ], "disposition": "exempt", "body_sha256": [ - "13dd38a564ff983c74e4b992d7fa86b952ee55a662473a5fc0211d5c017adcaf" + "ac1ace66298435daa38e6f6ac83a6c8fcd5677c87261adac82489493ca967f26" ], "source_sha256": { "builtin_db_query_value": [ - "13dd38a564ff983c74e4b992d7fa86b952ee55a662473a5fc0211d5c017adcaf" + "ac1ace66298435daa38e6f6ac83a6c8fcd5677c87261adac82489493ca967f26" ], "db_build_query": [ - "00e39cea708a3efac5175d8bee3813cc22609e5d4d860439fd050abb76007123" + "b8b459c14d36d1e3c83f2d95248c7d000160d601ce5082429391ec4a46dd1e8c" ], "db_exec_from_arg": [ - "175f3418db40cd0f873e228489006cc68bb0970d729fa07f36be8e889c175210" + "cbcf24fbb55c5a6f9bfdbb6535dc22a89086dab2ed311d0fc0892eb6eb287145" ] } }, @@ -714,10 +717,10 @@ "e629cded8eb8323555969d974904261b1396e093245f761f50c45e3b62f8a6b3" ], "strict_numeric_byte_list": [ - "dcfe8257673becda013e3c060cc3c3d73a151e9ad0920e7d45024a71785fb85f" + "792de1634e3cda6e5ab40f7bef4d6d29d38ed5b6b9704a2e53d12332e560c892" ], "zlib_bytes_arg": [ - "45914e64f065c5d3f527890fe913d691743e0cacc5d6e26942905e0ae3b23089" + "4aeca3a4d6fc6d62d3304c832e8ecaf9061af1884b7106e9ef1381f21ded3f4f" ], "zlib_deflate_impl": [ "374d09087d4f62c26f227e4b4708db3b6a06ddc42c7f973b129fc44f0a47c90f" @@ -820,11 +823,14 @@ ], "disposition": "exempt", "body_sha256": [ - "d2ccc6c1236a035d23e45721350b2ff792b5e3e17f5f912aaf37f59e741e3851" + "6bda7c10ac12e205d51c0d8c90ee59d2e66cd136a513a64317a146ef32c10dfa" ], "source_sha256": { "builtin_exec_capture": [ - "d2ccc6c1236a035d23e45721350b2ff792b5e3e17f5f912aaf37f59e741e3851" + "6bda7c10ac12e205d51c0d8c90ee59d2e66cd136a513a64317a146ef32c10dfa" + ], + "eigs_list_num": [ + "d81d044498e21cfa0893aeb9977764b6d95f437e9ae59dfab7707f5d93693433" ] } }, @@ -852,11 +858,11 @@ ], "disposition": "exempt", "body_sha256": [ - "37376412c471a836809ea7adcdc475eae3043a4a888acfc7ee839582be90dad7" + "b12fd36f8b8e6f0885f5744abeee83bd83e1488254973a568993c8c37ed7b727" ], "source_sha256": { "builtin_f64_from_bytes": [ - "37376412c471a836809ea7adcdc475eae3043a4a888acfc7ee839582be90dad7" + "b12fd36f8b8e6f0885f5744abeee83bd83e1488254973a568993c8c37ed7b727" ] } }, @@ -1096,11 +1102,14 @@ ], "disposition": "exempt", "body_sha256": [ - "6317467ff1757666f65fd69bb3448e2ada22346ea7acd7f4b3f3bf7896239b9c" + "42802f4db44ca3853ec885ca9cc032cf90f7115366098a806d76db6cf5263b93" ], "source_sha256": { "builtin_http_early_bind": [ - "6317467ff1757666f65fd69bb3448e2ada22346ea7acd7f4b3f3bf7896239b9c" + "42802f4db44ca3853ec885ca9cc032cf90f7115366098a806d76db6cf5263b93" + ], + "eigs_opt_num": [ + "5bd33573efec2133743c045bc9827a72a8c9ea73f15a185468495764b5c37ecd" ] } }, @@ -1205,10 +1214,10 @@ "d89562220316e85c2ed110b250eee934769205aab21a2f3bc76a9804aa171060" ], "strict_numeric_byte_list": [ - "dcfe8257673becda013e3c060cc3c3d73a151e9ad0920e7d45024a71785fb85f" + "792de1634e3cda6e5ab40f7bef4d6d29d38ed5b6b9704a2e53d12332e560c892" ], "zlib_bytes_arg": [ - "45914e64f065c5d3f527890fe913d691743e0cacc5d6e26942905e0ae3b23089" + "4aeca3a4d6fc6d62d3304c832e8ecaf9061af1884b7106e9ef1381f21ded3f4f" ], "zlib_inflate_impl": [ "a54f1d8a2b596ca7c363f20303516358bf5992479e5aadc7ea6f2536e2089914" @@ -1239,11 +1248,11 @@ ], "disposition": "exempt", "body_sha256": [ - "9cac229bdc2f85508264289cf71021adf1acbfb9572b8ec71cd175d0d4089d72" + "260131ce8fc8a72effc5efef651d6e26ccc01e602088df2a90def3313a1c00e0" ], "source_sha256": { "builtin_json_build": [ - "9cac229bdc2f85508264289cf71021adf1acbfb9572b8ec71cd175d0d4089d72" + "260131ce8fc8a72effc5efef651d6e26ccc01e602088df2a90def3313a1c00e0" ] } }, @@ -1266,7 +1275,7 @@ "5815ee115524cd711b01ba1bca89f1e152cd2303c2214447e1482ffde5df1189" ], "eigs_json_encode_value": [ - "f620ecd4c3c82854e5d4bab72ebbbb20248e4246307402eba9b63c8030f39cc6" + "040243c0a02b8d5966f9dcaae4433181b5a847919f722bff8efbcbbe5aefbbe2" ] } }, @@ -1463,7 +1472,7 @@ "957d6367080150932b0eb91b77222af62118688b594ef3e894134f13c7ea1765" ], "minmax_reduce": [ - "94f24182cc76f490ff4d4a7282ab0d5f855a479c76d7968455f3049276aeed6e" + "4584cee7f2bdf4865b043d23abbee135d1f2e28d813942aa1d28f2daa688ae4b" ] } }, @@ -1486,7 +1495,7 @@ "895d02903f98e67d498faf4410e6d7d4cedc13d14f2d5699c86a24deccbea53c" ], "minmax_reduce": [ - "94f24182cc76f490ff4d4a7282ab0d5f855a479c76d7968455f3049276aeed6e" + "4584cee7f2bdf4865b043d23abbee135d1f2e28d813942aa1d28f2daa688ae4b" ] } }, @@ -1509,7 +1518,7 @@ "b38ee26c123f3a682f61cfacc3cd75d7dd6423aa2eafbceb49e64b2e63ee47bf" ], "call_eigs_fn": [ - "e591bda3f62de5ed6c4e1eade465dd2f6f60f6e2652c359da741f4fa08222fbe" + "dc81f6eda5eb6cb98ddd06c771f66f9d07fe79c59b825aab5ae8db00a2042138" ], "env_set_local": [ "bde886fc51cd3b927e7c8c487a7a0d270453133c4e598506927a2d549788afbf" @@ -1570,17 +1579,17 @@ ], "disposition": "exempt", "body_sha256": [ - "1d3492a790c1817c2ac4cec04e4ecc76fc99e093dde13138317826d2876efc74" + "db447ac43f129387dc0a10d8aa38aa337a936f67629bf0d6fa90cfd055c2972a" ], "source_sha256": { "builtin_net_accept": [ - "1d3492a790c1817c2ac4cec04e4ecc76fc99e093dde13138317826d2876efc74" + "db447ac43f129387dc0a10d8aa38aa337a936f67629bf0d6fa90cfd055c2972a" ], "net_lookup": [ - "5772fdad11980e8260bcd1fd75fc69465818c9bff583e35d9d551264bcd69bd0" + "80716b1becf33449a83c7700e1aac433cdcf96492fa0c7d5bbe29d1a4f3b58ea" ], "net_num_at": [ - "7208f7a66f6aaf3fbec29729e2baf24f16d82fa82025602d4fe99dd7fe333a88" + "21af63392d75ae60e71435ee2fb5eb314c704f34dd085aba813cf6bb71964ed9" ], "net_require_list": [ "70f27bee56a0ff146a34de05597e8738a16f02cbe766b461ddc89e945d654a97" @@ -1599,14 +1608,14 @@ ], "disposition": "exempt", "body_sha256": [ - "9affe0f7e4931f16f782536d5594ce3c3c92a605085998319a4c693d550aa57f" + "1714b537427a4503e5115af43594b01042000c94ca3ac6300782e08bc2d5edb7" ], "source_sha256": { "builtin_net_dial": [ - "9affe0f7e4931f16f782536d5594ce3c3c92a605085998319a4c693d550aa57f" + "1714b537427a4503e5115af43594b01042000c94ca3ac6300782e08bc2d5edb7" ], "net_num_at": [ - "7208f7a66f6aaf3fbec29729e2baf24f16d82fa82025602d4fe99dd7fe333a88" + "21af63392d75ae60e71435ee2fb5eb314c704f34dd085aba813cf6bb71964ed9" ], "net_require_list": [ "70f27bee56a0ff146a34de05597e8738a16f02cbe766b461ddc89e945d654a97" @@ -1625,14 +1634,14 @@ ], "disposition": "exempt", "body_sha256": [ - "aa4d4217baf4e40ef4dfca797c46b2febc79a371245071d6d8a6243219f0af35" + "0d7ef42e6ba9100ba2c1fa3d4ccb2234b8a8d5b041fb95567436bc36908f7c11" ], "source_sha256": { "builtin_net_recv": [ - "aa4d4217baf4e40ef4dfca797c46b2febc79a371245071d6d8a6243219f0af35" + "0d7ef42e6ba9100ba2c1fa3d4ccb2234b8a8d5b041fb95567436bc36908f7c11" ], "net_num_at": [ - "7208f7a66f6aaf3fbec29729e2baf24f16d82fa82025602d4fe99dd7fe333a88" + "21af63392d75ae60e71435ee2fb5eb314c704f34dd085aba813cf6bb71964ed9" ], "net_require_list": [ "70f27bee56a0ff146a34de05597e8738a16f02cbe766b461ddc89e945d654a97" @@ -1651,11 +1660,11 @@ ], "disposition": "exempt", "body_sha256": [ - "8ba28b7f1ac9cbd14aae454bd288b0c0f392e6001a5e19e67da05efb500065c0" + "c01f12132fa49a57b6759558b96f84d8b70d25cdfccb9941f555c8ceaae98bdf" ], "source_sha256": { "builtin_net_send": [ - "8ba28b7f1ac9cbd14aae454bd288b0c0f392e6001a5e19e67da05efb500065c0" + "c01f12132fa49a57b6759558b96f84d8b70d25cdfccb9941f555c8ceaae98bdf" ], "net_require_list": [ "70f27bee56a0ff146a34de05597e8738a16f02cbe766b461ddc89e945d654a97" @@ -1741,7 +1750,7 @@ "bcb3999873c5e085e20e6370e6ae81ddaf01a610069729a2a12dd5d402df301c" ], "value_to_string": [ - "f06a6f6d692e68a1e38ea3e4bc8ec6a0925aa6f036a604885839e762b8cd256e" + "00c08a962c4e4be86e9f6eb07d36d2ebd38b74e01e8ffd4e5c345bed69b1a0c5" ] } }, @@ -2005,11 +2014,11 @@ ], "disposition": "exempt", "body_sha256": [ - "2eaf1e9692dac0e5eb369f476fa84c108814caf685df33836d741a9cf606c611" + "71ae5b5c45df5188d25dc54707bb1024a2efc96f53f8505796b082827a4dc5f7" ], "source_sha256": { "builtin_set_at": [ - "2eaf1e9692dac0e5eb369f476fa84c108814caf685df33836d741a9cf606c611" + "71ae5b5c45df5188d25dc54707bb1024a2efc96f53f8505796b082827a4dc5f7" ] } }, @@ -2037,14 +2046,14 @@ ], "disposition": "exempt", "body_sha256": [ - "56aca0de08f2987b64d77feab03d06f38471a63c2bb5d1af962c3ee0e1071cfd" + "062be569d9375f1d5f273c7ed35fc2c30152e927c0cc1bbca272a0caadba4905" ], "source_sha256": { "builtin_set_observer_window": [ - "56aca0de08f2987b64d77feab03d06f38471a63c2bb5d1af962c3ee0e1071cfd" + "062be569d9375f1d5f273c7ed35fc2c30152e927c0cc1bbca272a0caadba4905" ], "obs_window_arg": [ - "2c6a614d2e35b22985d345c13441e3e67dc393d78367825e55800a6c304f1f1e" + "ae8de783a839a1b33081a93645adbe2b9277228152fa384a9956b1d2bd7d5cc5" ] } }, @@ -2299,7 +2308,7 @@ "54d7497ad4bc951a7f8b8e0e690f901fcab06ed74c92050e4e68be8c36921372" ], "value_to_string": [ - "f06a6f6d692e68a1e38ea3e4bc8ec6a0925aa6f036a604885839e762b8cd256e" + "00c08a962c4e4be86e9f6eb07d36d2ebd38b74e01e8ffd4e5c345bed69b1a0c5" ] } }, @@ -2315,14 +2324,14 @@ ], "disposition": "exempt", "body_sha256": [ - "7e50840f4c9305688c9e04d8fc833a55e33f846478dcca87c3ea26b8bd001f4e" + "804af03f97b739d64395d87f5f9aceda99c651abc92d38bee8b98f120bdfe6eb" ], "source_sha256": { "builtin_str_from_bytes": [ - "7e50840f4c9305688c9e04d8fc833a55e33f846478dcca87c3ea26b8bd001f4e" + "804af03f97b739d64395d87f5f9aceda99c651abc92d38bee8b98f120bdfe6eb" ], "strict_numeric_byte_list": [ - "dcfe8257673becda013e3c060cc3c3d73a151e9ad0920e7d45024a71785fb85f" + "792de1634e3cda6e5ab40f7bef4d6d29d38ed5b6b9704a2e53d12332e560c892" ] } }, @@ -2437,7 +2446,7 @@ "f6bb04ca6134dd41d1a67ee91ea2b9587e13477f13ed51afaa3834de1d4f2b78" ], "tensor_unary": [ - "63a65a35d38288af38d503fb84d3b5d5ac4df02d6eefb3042ef5581556a3fd43" + "ba28948736491f849e86f3d99069141b15dfda5f14ee9b3ee4ef728039852361" ] } }, @@ -2465,11 +2474,11 @@ ], "disposition": "exempt", "body_sha256": [ - "632d6d8f5103b751cd7d5c92cb5b302894eea66e50d83ffe50d587de9e294066" + "1a99f96780f83e71ed85eaa5c61c0d01ba9c14a0fd844f6bd1cfd26ffa0f5202" ], "source_sha256": { "builtin_tensor_leaky_relu": [ - "632d6d8f5103b751cd7d5c92cb5b302894eea66e50d83ffe50d587de9e294066" + "1a99f96780f83e71ed85eaa5c61c0d01ba9c14a0fd844f6bd1cfd26ffa0f5202" ], "flat_to_like": [ "9829b048a1af1233bfeae6329573e79d83cd7acb01e548d3737011a625fbdc02" @@ -2478,7 +2487,7 @@ "40100cc6cf4d92300cf1d3fbae23cb21adf4241bd01edecc9a683a07fb30bc01" ], "tensor_to_flat": [ - "c5cb36799390cdc54c0b9d68a87f8efb4001777fadd63b9da74a6461401ed034" + "5362e998b20c70cb9db3441550be28b5cc707f79d7ec0bc71e351ab43937357e" ] } }, @@ -2494,11 +2503,11 @@ ], "disposition": "exempt", "body_sha256": [ - "06f270d38dfe82b38652efd6c250fed359997ebaf4d7cece79b6bed8adf3d670" + "fd424edc6b03b9ab24aa2940a40654242ca70bbcf0fcab69642e88fc692d4bd3" ], "source_sha256": { "builtin_tensor_load": [ - "06f270d38dfe82b38652efd6c250fed359997ebaf4d7cece79b6bed8adf3d670" + "fd424edc6b03b9ab24aa2940a40654242ca70bbcf0fcab69642e88fc692d4bd3" ] } }, @@ -2521,7 +2530,7 @@ "ff086ad360320823ab29cb9cfbeed6ec340ae78f959cb18c187c227db55f947f" ], "tensor_unary": [ - "63a65a35d38288af38d503fb84d3b5d5ac4df02d6eefb3042ef5581556a3fd43" + "ba28948736491f849e86f3d99069141b15dfda5f14ee9b3ee4ef728039852361" ] } }, @@ -2537,11 +2546,11 @@ ], "disposition": "exempt", "body_sha256": [ - "4933701b5be382735103cb12ae6ab5b3285389675349362e3d7c44947b2f2f1d" + "4163fd8b2a8db59b4861cfe0f19f6bb57154e8c461f86f25915cb9b95028f0c6" ], "source_sha256": { "builtin_tensor_log_softmax": [ - "4933701b5be382735103cb12ae6ab5b3285389675349362e3d7c44947b2f2f1d" + "4163fd8b2a8db59b4861cfe0f19f6bb57154e8c461f86f25915cb9b95028f0c6" ] } }, @@ -2600,10 +2609,10 @@ "f6eec99bd0153900403028372a294cf84be9485ce98b7ad8843ce4e2a5d390ee" ], "tensor_flatten_recursive": [ - "8eec6fb6ef164c973bde8f7170a725e8aa599a4f70ea54c5fd5ae53004e6bc2f" + "611ed504e8a1821eaf122fcf87f4423f70856a4df74f2f82aebc32a7d187f421" ], "tensor_total": [ - "9f4e001a08dd6c876728b64308341acbc50ea0d733bd65e8bf94206ba8a09912" + "a36012ea9a660ce7ffc3e1be748c9412ef5553ba3bd0f91dce969f7b70ee605e" ] } }, @@ -2638,7 +2647,7 @@ "a345f0c30c4b1b80943b9c718c0002fc19981d0336a9a1ea0aca3b5966bf8462" ], "tensor_unary": [ - "63a65a35d38288af38d503fb84d3b5d5ac4df02d6eefb3042ef5581556a3fd43" + "ba28948736491f849e86f3d99069141b15dfda5f14ee9b3ee4ef728039852361" ] } }, @@ -2661,10 +2670,10 @@ "817e544211920e1216ff102393d66f3cb7a8def4a9ea05611872433b5ad8f87c" ], "tensor_flatten_recursive": [ - "8eec6fb6ef164c973bde8f7170a725e8aa599a4f70ea54c5fd5ae53004e6bc2f" + "611ed504e8a1821eaf122fcf87f4423f70856a4df74f2f82aebc32a7d187f421" ], "tensor_total": [ - "9f4e001a08dd6c876728b64308341acbc50ea0d733bd65e8bf94206ba8a09912" + "a36012ea9a660ce7ffc3e1be748c9412ef5553ba3bd0f91dce969f7b70ee605e" ] } }, @@ -2692,17 +2701,17 @@ ], "disposition": "exempt", "body_sha256": [ - "00ca744ab5b809802510f989c0dc4a9f04f2807555b5fac2bc8c567cd6da37df" + "e69663c14d78c3bda0a319969a0159a98b282bd90263ede6d152f060df8b96c8" ], "source_sha256": { "builtin_tensor_relu": [ - "00ca744ab5b809802510f989c0dc4a9f04f2807555b5fac2bc8c567cd6da37df" + "e69663c14d78c3bda0a319969a0159a98b282bd90263ede6d152f060df8b96c8" ], "flat_to_like": [ "9829b048a1af1233bfeae6329573e79d83cd7acb01e548d3737011a625fbdc02" ], "tensor_to_flat": [ - "c5cb36799390cdc54c0b9d68a87f8efb4001777fadd63b9da74a6461401ed034" + "5362e998b20c70cb9db3441550be28b5cc707f79d7ec0bc71e351ab43937357e" ] } }, @@ -2762,14 +2771,14 @@ ], "disposition": "exempt", "body_sha256": [ - "f56b3a2d9e6a80dd507efab368ce1adaf3f4821862a86fd0c93c4c8520db6a3b" + "8f880cc73ad6e7e48aa891ef2acb00a653c9b110ccea7c386c3189224fb2cd7a" ], "source_sha256": { "buf_dims": [ "edfe0613daa3910ade3374de6fb9fe4486e51f0fea899fc3ec891441e259010e" ], "builtin_tensor_softmax": [ - "f56b3a2d9e6a80dd507efab368ce1adaf3f4821862a86fd0c93c4c8520db6a3b" + "8f880cc73ad6e7e48aa891ef2acb00a653c9b110ccea7c386c3189224fb2cd7a" ], "flat_to_like": [ "9829b048a1af1233bfeae6329573e79d83cd7acb01e548d3737011a625fbdc02" @@ -2778,7 +2787,7 @@ "40100cc6cf4d92300cf1d3fbae23cb21adf4241bd01edecc9a683a07fb30bc01" ], "tensor_to_flat": [ - "c5cb36799390cdc54c0b9d68a87f8efb4001777fadd63b9da74a6461401ed034" + "5362e998b20c70cb9db3441550be28b5cc707f79d7ec0bc71e351ab43937357e" ] } }, @@ -2801,7 +2810,7 @@ "f38d9057e3c1e2a049e7cd4d4f38df6aace72af77d1a718b357b4f2a5bf4f54d" ], "tensor_unary": [ - "63a65a35d38288af38d503fb84d3b5d5ac4df02d6eefb3042ef5581556a3fd43" + "ba28948736491f849e86f3d99069141b15dfda5f14ee9b3ee4ef728039852361" ] } }, @@ -2836,10 +2845,10 @@ "196480c8b98743ea57d8bf3bc4bdf0b0fdc3ca70c56ef6cd1289342e15fcd544" ], "tensor_flatten_recursive": [ - "8eec6fb6ef164c973bde8f7170a725e8aa599a4f70ea54c5fd5ae53004e6bc2f" + "611ed504e8a1821eaf122fcf87f4423f70856a4df74f2f82aebc32a7d187f421" ], "tensor_total": [ - "9f4e001a08dd6c876728b64308341acbc50ea0d733bd65e8bf94206ba8a09912" + "a36012ea9a660ce7ffc3e1be748c9412ef5553ba3bd0f91dce969f7b70ee605e" ] } }, @@ -2933,7 +2942,7 @@ "dce6eda1020469efb73a67c34f60fa7cc66c03c617d1fb4bc96aaea536346ae3" ], "value_to_string": [ - "f06a6f6d692e68a1e38ea3e4bc8ec6a0925aa6f036a604885839e762b8cd256e" + "00c08a962c4e4be86e9f6eb07d36d2ebd38b74e01e8ffd4e5c345bed69b1a0c5" ] } }, @@ -2949,11 +2958,11 @@ ], "disposition": "exempt", "body_sha256": [ - "638954e93890cf0d3f8fedf6d1df580e4a9d7c030ce8edf80084e8677a11ff5e" + "3b2456c11794f2c97b3fdff67d7dec23ca93b74804b0c09172bbab4a3e628144" ], "source_sha256": { "builtin_type": [ - "638954e93890cf0d3f8fedf6d1df580e4a9d7c030ce8edf80084e8677a11ff5e" + "3b2456c11794f2c97b3fdff67d7dec23ca93b74804b0c09172bbab4a3e628144" ] } }, @@ -3002,7 +3011,7 @@ "fce8334d124e479de21cd22dfed262a8edcc656341a1f7c272507762481c57fc" ], "vm_desc_abi_error": [ - "4a356cfa1cde80a6de7cd79853084565c00ed4216a0cb91d998d0c206538b706" + "8f534bd94a4a3c67f17726c79e8ee59da0b48e97a297fba6335b153b94e14273" ] } }, @@ -3025,7 +3034,7 @@ "5556903dd96019edcff890e180e1e8f04ffc7101fe80b5fcf93520968ea3c180" ], "value_to_string": [ - "f06a6f6d692e68a1e38ea3e4bc8ec6a0925aa6f036a604885839e762b8cd256e" + "00c08a962c4e4be86e9f6eb07d36d2ebd38b74e01e8ffd4e5c345bed69b1a0c5" ] } }, @@ -3074,10 +3083,10 @@ "b68fb4aca23a1abfe47935e6c9e0cf8907e704a5b8621a19837643c7a27c4289" ], "strict_numeric_byte_list": [ - "dcfe8257673becda013e3c060cc3c3d73a151e9ad0920e7d45024a71785fb85f" + "792de1634e3cda6e5ab40f7bef4d6d29d38ed5b6b9704a2e53d12332e560c892" ], "zlib_bytes_arg": [ - "45914e64f065c5d3f527890fe913d691743e0cacc5d6e26942905e0ae3b23089" + "4aeca3a4d6fc6d62d3304c832e8ecaf9061af1884b7106e9ef1381f21ded3f4f" ], "zlib_deflate_impl": [ "374d09087d4f62c26f227e4b4708db3b6a06ddc42c7f973b129fc44f0a47c90f" @@ -3105,10 +3114,10 @@ "ede1e56b7d6bb385970e70743e836ed5d1c161d35f447ffad6b5c4a6b9115ddb" ], "strict_numeric_byte_list": [ - "dcfe8257673becda013e3c060cc3c3d73a151e9ad0920e7d45024a71785fb85f" + "792de1634e3cda6e5ab40f7bef4d6d29d38ed5b6b9704a2e53d12332e560c892" ], "zlib_bytes_arg": [ - "45914e64f065c5d3f527890fe913d691743e0cacc5d6e26942905e0ae3b23089" + "4aeca3a4d6fc6d62d3304c832e8ecaf9061af1884b7106e9ef1381f21ded3f4f" ], "zlib_inflate_impl": [ "a54f1d8a2b596ca7c363f20303516358bf5992479e5aadc7ea6f2536e2089914" diff --git a/tools/tape_kinds_check.sh b/tools/tape_kinds_check.sh new file mode 100644 index 00000000..db2ce882 --- /dev/null +++ b/tools/tape_kinds_check.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# tape_kinds_check.sh -- #1637: every taped builtin declares its return kinds. +# +# Replay refuses a recorded N value whose kind its builtin cannot return +# (src/trace.c, k_tape_kinds) -- and refuses a name that is in no table. A +# builtin that starts recording without a row would therefore fail only at +# replay, on a user's tape. This gate fails first, at build review: +# taped = every name src/*.c records or takes: TRACE_NONDET_TAKE/RET/ +# RECORD("x"), ARG_GUARD_TAPED/PRETAKE(..., "x", ...), +# trace_replay_take("x"), trace_nondet_value("x") +# declared = every {"x", ...} row of k_tape_kinds +# and requires taped == declared, both non-empty. The smoke/embedding test +# programs (embed_*.c, jit_smoke.c) record host names and declare them +# through eigs_trace_declare_kind, so they are not part of the core set. +# Prints `tape-kinds: taped=N declared=K missing=0 stale=0` and PASS/FAIL. +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SRC_DIR="${TAPE_KINDS_SRC:-$ROOT/src}" +TMP=$(mktemp "${TMPDIR:-/tmp}/tape_kinds.XXXXXX") || exit 2 +trap 'rm -f "$TMP" "$TMP.d"' EXIT + +for f in "$SRC_DIR"/*.c; do + case "$(basename "$f")" in embed_*.c|jit_smoke.c) continue ;; esac + awk ' + # A macro invocation may span lines; join it up to its closing ");". + function emit(s, m) { + while (match(s, /(TRACE_NONDET_(TAKE|RET|RECORD)|trace_replay_take|trace_nondet_value)[(]"[a-z_0-9]+"/)) { + m = substr(s, RSTART, RLENGTH); sub(/^[^"]*"/, "", m); sub(/"$/, "", m) + print m + s = substr(s, RSTART + RLENGTH) + } + } + /ARG_GUARD_(TAPED|PRETAKE)[(]/ && !/#define/ { acc = $0; inm = 1 } + inm && acc != $0 { acc = acc " " $0 } + inm && /[)];/ { + # the first string literal in the invocation is `who` + if (match(acc, /"[a-z_0-9]+"/)) print substr(acc, RSTART + 1, RLENGTH - 2) + inm = 0 + } + { emit($0) } + ' "$f" +done | sort -u > "$TMP" + +awk '/k_tape_kinds\[\] = [{]/ { on = 1; next } + on && /^[}];/ { on = 0 } + on && match($0, /[{]"[a-z_0-9]+"/) { print substr($0, RSTART + 2, RLENGTH - 3) }' \ + "$SRC_DIR/trace.c" | sort -u > "$TMP.d" + +taped=$(grep -c . "$TMP") +declared=$(grep -c . "$TMP.d") +missing=0; stale=0 +while IFS= read -r n; do + grep -qx -- "$n" "$TMP.d" || { echo " MISSING: '$n' records an N value but has no k_tape_kinds row"; missing=$((missing + 1)); } +done < "$TMP" +while IFS= read -r n; do + grep -qx -- "$n" "$TMP" || { echo " STALE: k_tape_kinds row '$n' names nothing taped"; stale=$((stale + 1)); } +done < "$TMP.d" +echo "tape-kinds: taped=$taped declared=$declared missing=$missing stale=$stale" +if [ "$taped" -gt 0 ] && [ "$declared" -gt 0 ] && [ "$missing" -eq 0 ] && [ "$stale" -eq 0 ]; then + echo "tape-kinds: PASS"; exit 0 +fi +echo "tape-kinds: FAIL"; exit 1