feat(v4.0.0): address faults -- an address outside memory is guarded (D-14)

Ruled 2026-10-04: guarded, an error shown, back to the prompt.

- The executor checks every address a programme uses (P, A, B) before
  using it.  Outside memory the opcode does nothing, the rest of its word
  is not executed, and P becomes the node's fault handler; a node with no
  handler stops.  v4_node_load/store never index outside memory.
- capsule/quit.v4: (FAULT), the host node's handler, prints
  "Address out of range", ends an open definition, prints ERROR and
  returns to the prompt.
- tests: every memory opcode and P in test_exec.c; from the prompt, from
  inside nested words and loops, in test_host_quit.c.

This closes the hole node.h described: a wild address used to index the
model's own memory gigabytes out of bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-04 12:04:59 -04:00
co-authored by Claude Opus 5.5
parent b5d644d498
commit b3d2d56d11
11 changed files with 277 additions and 74 deletions
+2 -1
View File
@@ -175,6 +175,7 @@ definition below depends on one, it says so.
| **D-11** | `Q./` semantics: division by zero, rounding, overflow (ruled 2026-10-02). | **Saturate and flag; round toward zero; saturate on overflow.** The quotient of `a * 2^16 / b` is rounded toward zero, like `SM/REM`. When it does not fit a Q value it is clamped to Q max or Q min by its sign. Division by zero returns Q max or Q min by the sign of the dividend (`0 / 0` gives 0) and sets the node's `NODE-ERROR` register (§7), which `VM-ERROR?` reads. v3 returned 0 on division by zero and saturated whenever the dividend was 2^48 or more, even when the quotient would have fit. |
| **D-12** | Q approximations outside their domain (ruled 2026-10-02). | **Return 0 and set `NODE-ERROR`.** `Q.SQRT` of a negative value and `Q.LOG` of zero or a negative value return 0 and set `NODE-ERROR` (§7), as `Q./` does on division by zero (D-11). v3 returned 0 for `ln(0)` without a flag and read negative arguments as large unsigned values. |
| **D-13** | Pictured-output hold buffer: size and errors (ruled 2026-10-03). | **63 characters, as v3; on error set `NODE-ERROR` and drop the character.** The buffer holds 63 characters at either cell width. `HOLD` of a value outside 0–255, or into a full buffer, stores nothing and sets `NODE-ERROR` (§7), which is v3's behaviour (it set its error flag and dropped the character). A full double in base 2 therefore does not fit, as in v3. |
| **D-14** | An address outside the node's memory (ruled 2026-10-04). | **Guarded: an address fault.** Every address a running programme uses is checked before it is used — `P` when an instruction word or an `@p` literal is fetched or `!p` stores, `A` for `@ @+ ! !+`, `B` for `@b !b`. Outside `0 … memory size − 1` the opcode does nothing (no fetch, no store, stacks and `A`, `B` untouched), the rest of its instruction word is not executed, and `P` becomes the node's **fault handler**. Nothing is pushed: the handler does not return to the programme. On the host node the handler is `(FAULT)` (`v4/capsule/quit.v4`): it prints `Address out of range`, ends any definition that was open, prints ` ERROR` and returns to the prompt, from however deep the fault was. A node with no handler stops. v3 printed ` ERROR` alone. What a mesh node's handler does — it has no console — comes with the mesh (step 2). Executed on the golden model (2026-10-04): `tests/test_exec.c` for every memory opcode and for `P`, `tests/test_host_quit.c` from the prompt. |
**Consequences of D-2 that every definition must respect.** The data stack holds 10 items and the
return stack 9, and every `call`, `FOR`, `DO` loop frame and `push` uses return-stack slots. Nesting
@@ -649,7 +650,7 @@ width and print a flood of spaces.
| `EMIT` | DEV | Console service: one-character message. Until the mesh exists it is a store to the `CONSOLE-TX` register (§7): `CONSOLE-TX b! !b`. As in v3, the low byte of the cell is the character. Executed on the golden model (2026-10-03). Clobbers `B`. |
| `KEY` | DEV | Console service: blocking receive. Until the mesh exists it reads the `CONSOLE-STATUS` and `CONSOLE-RX` registers (§7): `L: CONSOLE-STATUS b! @b if WAIT drop CONSOLE-RX b! @b ; WAIT: drop jump L` — it asks whether a character is pending until one is, then takes it. The character is 0–255. v3's `KEY` returned −1 at the end of its input; here there is no end of input, and `KEY` waits. Executed on the golden model (2026-10-03), including a transcript of the v3 binary, and shown still waiting after 5000 instruction words with nothing pending. Leaves its caller 9 data cells and 8 return entries. Clobbers `B`. |
| `?TERMINAL` | DEV | Console service: non-blocking status. Until the mesh exists: `CONSOLE-STATUS b! @b` — −1 when a character is pending, 0 when not, as v3; it takes nothing. Executed on the golden model (2026-10-03). Clobbers `B`. |
| `TYPE` | CAP | `( baddr u -- )`. Loop of `C@ EMIT`, or one string message to the console node: `-if OK drop drop NODE-ERROR b! -1 !b ; OK: if DONE over C@ EMIT push 1 + pop -1 + jump OK DONE: drop drop ;` — nothing for `u = 0`; for `u < 0` nothing is printed and `NODE-ERROR` is set, where v3 printed nothing and raised its error flag. The address range is not checked (out-of-range addressing is still open, `node.h`). Executed on the golden model (2026-10-03). Leaves its caller 6 data cells and 6 return entries. Clobbers `A` and `B`. |
| `TYPE` | CAP | `( baddr u -- )`. Loop of `C@ EMIT`, or one string message to the console node: `-if OK drop drop NODE-ERROR b! -1 !b ; OK: if DONE over C@ EMIT push 1 + pop -1 + jump OK DONE: drop drop ;` — nothing for `u = 0`; for `u < 0` nothing is printed and `NODE-ERROR` is set, where v3 printed nothing and raised its error flag. An address outside the node's memory is an address fault (D-14). Executed on the golden model (2026-10-03). Leaves its caller 6 data cells and 6 return entries. Clobbers `A` and `B`. |
| `CR` | CAP | `10 EMIT`, as `10 jump EMIT` — executed on the golden model (2026-10-03). Character 10, as v3; the console turns it into a new line. |
| `SPACE` | CAP | `BL EMIT`, as `32 jump EMIT` — executed on the golden model (2026-10-03). |
| `SPACES` | CAP | `( n -- )`: `-if L drop ; L: if DONE SPACE -1 + jump L DONE: drop ;` — `n` spaces, none for `n <= 0`, as v3. Executed on the golden model (2026-10-03), including a transcript of the v3 binary. Leaves its caller 7 data cells and 7 return entries. Clobbers `B`. |
+12
View File
@@ -11,6 +11,9 @@
\ ok> NOSUCH
\ UNKNOWN WORD: 'NOSUCH'
\ ERROR any line that sets NODE-ERROR ends so
\ ok> -1 @
\ Address out of range an address fault (D-14), from any depth
\ ERROR
\ The line itself is not sent back: the terminal shows what is typed.
\
\ THE RETURN STACK is circular (D-2): it has no bottom to be reset to, and
@@ -52,6 +55,15 @@ header QUIT
header ABORT
: ABORT (RESET) 1 jump (REPL)
\ ( -- ) where the node goes when a programme uses an address outside its
\ memory (D-14): the loader makes this word the node's fault handler. The
\ opcode that faulted did nothing; whatever was running is abandoned, as by
\ ABORT, and the line ends with ERROR.
: (FAULT)
$72646441 (EMIT4) $20737365 (EMIT4) $2074756F (EMIT4) \ "Address out "
$7220666F (EMIT4) $65676E61 (EMIT4) CR \ "of range"
2 jump (REPL)
\ ---- text in the source ---------------------------------------------------------
\ ." and ABORT" take the text up to the next " , which may be none at all
\ (input.v4's (PARSE)); with no closing " it is the rest of the line. Inside a definition they lay
+6 -1
View File
@@ -43,7 +43,12 @@ void v4_exec_reset(v4_exec_state *es);
/* Execute one instruction word: fetch at P, advance P, run the slots left to
* right until the word ends. A unext loop runs to completion inside one call,
* so the call does not return while R is nonzero at a `unext`. Returns the
* number of instructions retired. */
* number of instructions retired.
*
* An address outside node memory -- P at the fetch, or the address an opcode
* uses -- is a fault (node.h, D-14): that opcode and the rest of the word do
* not execute, and P is the node's fault handler on return. With no handler
* the node stops, and this does nothing and returns 0 from then on. */
unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h);
/* Execute a single opcode. The branch opcodes read their target from `w` and
+46 -31
View File
@@ -80,6 +80,12 @@ typedef struct {
unsigned input_len; /* characters fed */
unsigned input_pos; /* characters taken; input_len - input_pos are pending */
unsigned char input[V4_CONSOLE_CAP];
/* Address faults (D-14). See v4_node_fault_attach below. */
v4_cell fault_vector; /* word address of the handler, or -1: none */
v4_cell fault_addr; /* the address of the latest fault */
unsigned faults; /* how many there have been */
int stopped; /* non-zero: faulted with no handler */
} v4_node;
/* Zero P, A and B, empty the stacks, and clear memory. Installs every
@@ -97,37 +103,16 @@ int v4_node_guards_intact(const v4_node *n);
* and a narrower parameter would put a conversion between the register and the
* access on every one of the eight memory opcodes.
*
* PRECONDITION: 0 <= addr < V4_NODE_WORDS.
* An address outside 0 .. V4_NODE_WORDS-1 is never used to index memory
* (D-14, ruled 2026-10-04): v4_node_load gives 0 for it and v4_node_store
* does nothing. That is all these two do. They are what C code uses -- the
* assemblers, the tests -- and they do not count a fault; the executor does,
* for the addresses a running programme uses (see v4_node_fault_attach).
*
* Out-of-range addressing is not defined by DECOMPOSITION.md. D-1 fixes word
* addressing and says nothing about an address outside the node's memory; a
* F18 has no such case because its address space is the memory. Whether a
* conforming model should wrap, saturate, or fault is a real open question and
* is raised as a gap rather than decided here, because each answer changes the
* ISA's observable behaviour and inventing one silently would bake a guess into
* every later test.
*
* WHAT THE BOUNDARY HERE DOES AND DOES NOT COVER, since it is easy to
* overclaim and the distinction decides whether out-of-range addresses are
* actually protected:
*
* - It DOES cover a linear index error. An access to mem[-1] or
* mem[V4_NODE_WORDS] lands in the 5% band, because the band is immediately
* adjacent (tests/test_node.c asserts the adjacency with offsetof rather
* than assuming it). That access is inside the struct, so AddressSanitizer
* stays silent about it and v4_node_guards_intact() is what reports it.
*
* - It does NOT cover an out-of-range *word address*. A v4_cell address of
* -1 casts to unsigned 0xFFFFFFFF, which is 16 GB past mem at 32-bit cells
* -- far outside the struct and outside the band. So a P, A or B that runs
* off the end of memory is caught by neither the band nor, in a production
* build without sanitizers, by anything else here. It is a precondition
* violation and the range check that would catch it is the open question
* above, not the band.
*
* The band is worth its 10.2% for the first case regardless. The second case
* is a hole, and it is closed by ruling on out-of-range addressing, not by
* widening the band. */
* The 5% band on each side of mem is a different protection: it is where a
* linear index error in the model's own C -- mem[-1], mem[V4_NODE_WORDS] --
* would land, inside the struct where AddressSanitizer cannot see it, and
* v4_node_guards_intact() reports it. */
v4_cell v4_node_load(const v4_node *n, v4_cell addr);
void v4_node_store(v4_node *n, v4_cell addr, v4_cell value);
@@ -182,7 +167,37 @@ void v4_node_console_input_attach(v4_node *n, v4_cell rx, v4_cell status);
unsigned v4_node_console_feed(v4_node *n, const void *chars, unsigned len);
/* A data fetch: what @, @+ and @b read at `addr`. The two receive registers
* above when attached, memory otherwise. Same precondition as v4_node_load. */
* above when attached, memory otherwise; 0 outside memory, as v4_node_load. */
v4_cell v4_node_fetch(v4_node *n, v4_cell addr);
/* 1 if `addr` is a word of node memory. */
int v4_node_addr_ok(v4_cell addr);
/* ADDRESS FAULTS (DECOMPOSITION.md D-14, ruled 2026-10-04: an address outside
* the node's memory is guarded, an error is shown, and the node returns to
* its prompt).
*
* The executor checks every address a programme uses before using it: P when
* an instruction word or an `@p` literal is fetched or `!p` stores, A for
* `@ @+ ! !+`, B for `@b !b`. If it is outside memory there is a fault:
* - the opcode does nothing: no fetch, no store, the stacks and A and B
* untouched, nothing retired; the rest of its instruction word is not
* executed;
* - fault_addr is the address and faults is one more;
* - P becomes fault_vector, the address of the node's handler. Nothing is
* pushed: the handler does not return to the programme, it reports and
* goes back to the prompt (capsule/quit.v4, (FAULT)).
* With no handler attached (fault_vector -1) the node stops instead:
* `stopped` is set and v4_exec_step_word does nothing more until the node is
* reset or a handler is attached.
*
* v4_node_reset detaches the handler and clears the count. Attaching clears
* `stopped` and the count; `handler` must be a word of memory, or -1 to
* detach. */
void v4_node_fault_attach(v4_node *n, v4_cell handler);
/* For the executor: record a fault at `addr` and redirect or stop the node,
* as above. */
void v4_node_fault(v4_node *n, v4_cell addr);
#endif /* V4_NODE_H */
+1 -1
View File
@@ -12,7 +12,7 @@
* and step instruction words until the word returns. The stacks and A, B are
* left as the caller set them, so arguments go on the data stack first.
* Returns the number of instruction words stepped, or -1 if the word had not
* returned after `max_words`. */
* returned after `max_words` or the node stopped on an address fault (D-14). */
long v4_test_call(v4_node *n, v4_exec_state *es, v4_heat *h,
v4_cell entry, long max_words);
+23 -1
View File
@@ -35,6 +35,14 @@ static int ends_word(unsigned op)
return v4_op_is_branch(op) || op == V4_OP_SEMI || op == V4_OP_EX;
}
/* D-14: an address outside memory is a fault, and the opcode does nothing. */
static int faulted(v4_node *n, v4_cell addr)
{
if (v4_node_addr_ok(addr)) return 0;
v4_node_fault(n, addr);
return 1;
}
void v4_exec_reset(v4_exec_state *es)
{
es->anticlock = 0;
@@ -87,31 +95,39 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
break;
case V4_OP_FETCH_P:
if (faulted(n, n->p)) return;
v4_dstack_push(ds, v4_node_load(n, n->p));
n->p = add_wrap(n->p, 1);
break;
case V4_OP_FETCH_INC:
if (faulted(n, n->a)) return;
v4_dstack_push(ds, v4_node_fetch(n, n->a));
n->a = add_wrap(n->a, 1);
break;
case V4_OP_FETCH_B:
if (faulted(n, n->b)) return;
v4_dstack_push(ds, v4_node_fetch(n, n->b));
break;
case V4_OP_FETCH_A:
if (faulted(n, n->a)) return;
v4_dstack_push(ds, v4_node_fetch(n, n->a));
break;
case V4_OP_STORE_P:
if (faulted(n, n->p)) return;
v4_node_store(n, n->p, v4_dstack_pop(ds));
n->p = add_wrap(n->p, 1);
break;
case V4_OP_STORE_INC:
if (faulted(n, n->a)) return;
v4_node_store(n, n->a, v4_dstack_pop(ds));
n->a = add_wrap(n->a, 1);
break;
case V4_OP_STORE_B:
if (faulted(n, n->b)) return;
v4_node_store(n, n->b, v4_dstack_pop(ds));
break;
case V4_OP_STORE_A:
if (faulted(n, n->a)) return;
v4_node_store(n, n->a, v4_dstack_pop(ds));
break;
@@ -181,18 +197,24 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h)
{
v4_iword iw = fetch_iword(n, n->p);
v4_iword iw;
unsigned executed = 0;
unsigned slot = 0;
/* D-14: a node that faulted with no handler has stopped; and P itself
* may be what is outside memory. */
if (n->stopped || faulted(n, n->p)) return 0;
iw = fetch_iword(n, n->p);
n->p = add_wrap(n->p, 1);
while (slot < V4_SLOT_COUNT) {
unsigned op = v4_iword_op(iw, slot);
/* Decided before the opcode runs, because running it changes R. */
int again = (op == V4_OP_UNEXT) && (n->rs.r != 0);
unsigned faults = n->faults;
v4_exec_op(n, es, h, op, iw, slot);
if (n->faults != faults) break; /* P is the handler now, or the node has stopped */
executed++;
if (again) {
+26 -3
View File
@@ -13,6 +13,30 @@ void v4_node_reset(v4_node *n)
v4_rstack_reset(&n->rs);
v4_node_console_attach(n, -1);
v4_node_console_input_attach(n, -1, -1);
v4_node_fault_attach(n, -1);
}
int v4_node_addr_ok(v4_cell addr)
{
/* One unsigned compare covers both ends: a negative address is a very
* large unsigned one. */
return (v4_ucell)addr < (v4_ucell)V4_NODE_WORDS;
}
void v4_node_fault_attach(v4_node *n, v4_cell handler)
{
n->fault_vector = v4_node_addr_ok(handler) ? handler : (v4_cell)-1;
n->fault_addr = 0;
n->faults = 0;
n->stopped = 0;
}
void v4_node_fault(v4_node *n, v4_cell addr)
{
n->fault_addr = addr;
n->faults++;
if (n->fault_vector >= 0) n->p = n->fault_vector;
else n->stopped = 1;
}
int v4_node_guards_intact(const v4_node *n)
@@ -25,9 +49,7 @@ int v4_node_guards_intact(const v4_node *n)
v4_cell v4_node_load(const v4_node *n, v4_cell addr)
{
/* Precondition checked by the caller; see node.h. The unsigned compare is
* deliberate: `addr` is signed, and a negative address must not wrap into
* a large positive one and read the top of memory instead. */
if (!v4_node_addr_ok(addr)) return 0;
return n->mem[(unsigned)addr];
}
@@ -42,6 +64,7 @@ void v4_node_store(v4_node *n, v4_cell addr, v4_cell value)
n->console_dropped++;
return;
}
if (!v4_node_addr_ok(addr)) return;
n->mem[(unsigned)addr] = value;
}
+1 -3
View File
@@ -10,10 +10,8 @@ long v4_test_call(v4_node *n, v4_exec_state *es, v4_heat *h,
n->p = entry;
while (n->p != V4_TEST_HALT) {
if (words >= max_words) return -1;
/* A golden model must not index outside its own memory on a runaway
* P; node.h leaves out-of-range addressing undefined. */
if (n->p < 0 || (v4_ucell)n->p >= V4_NODE_WORDS) return -1;
(void)v4_exec_step_word(n, es, h);
if (n->stopped) return -1; /* an address fault with no handler (D-14) */
words++;
}
return words;
+105
View File
@@ -316,6 +316,110 @@ static void test_soak(void)
CHECK(v4_node_guards_intact(&n), "soak left the guards intact");
}
/* D-14: an address outside node memory is a fault. The opcode does nothing,
* the rest of its word is not executed, and P becomes the handler; with no
* handler the node stops. */
#define HANDLER ((v4_cell)40)
static void test_address_faults(void)
{
static const v4_cell bad[] = { -1, (v4_cell)V4_NODE_WORDS, (v4_cell)V4_NODE_WORDS + 1, MINC, MAXC, (v4_cell)(V4_NODE_WORDS * 4u) };
static const struct { unsigned op; int reg; const char *name; } m[] = {
{ V4_OP_FETCH_A, 'a', "@" }, { V4_OP_FETCH_INC, 'a', "@+" }, { V4_OP_FETCH_B, 'b', "@b" },
{ V4_OP_STORE_A, 'a', "!" }, { V4_OP_STORE_INC, 'a', "!+" }, { V4_OP_STORE_B, 'b', "!b" },
};
unsigned i, k;
for (i = 0; i < sizeof bad / sizeof bad[0]; i++)
for (k = 0; k < sizeof m / sizeof m[0]; k++) {
unsigned retired;
fresh();
v4_node_fault_attach(&n, HANDLER);
dpush(11); dpush(22);
n.a = m[k].reg == 'a' ? bad[i] : 7;
n.b = m[k].reg == 'b' ? bad[i] : 7;
n.mem[0] = word6(V4_OP_DUP, m[k].op, V4_OP_DROP, V4_OP_DROP, NOP, NOP);
n.p = 0;
retired = v4_exec_step_word(&n, &es, &h);
CHECK(n.faults == 1 && n.fault_addr == bad[i] && !n.stopped, "%s at %lld is a fault", m[k].name, (long long)bad[i]);
CHECK(n.p == HANDLER, "%s: P is the handler", m[k].name);
CHECK(retired == 1, "%s: only the dup before it retired (%u)", m[k].name, retired);
CHECK(n.ds.t == 22 && n.ds.s == 22, "%s: it neither pushed nor popped, and the drops after it did not run", m[k].name);
CHECK(n.a == (m[k].reg == 'a' ? bad[i] : 7) && n.b == (m[k].reg == 'b' ? bad[i] : 7), "%s: A and B untouched", m[k].name);
CHECK(v4_node_guards_intact(&n), "%s: guards intact", m[k].name);
}
/* the last word of memory is not a fault, and the first is not */
fresh(); v4_node_fault_attach(&n, HANDLER);
n.mem[V4_NODE_WORDS - 1u] = 77; n.a = (v4_cell)(V4_NODE_WORDS - 1u); run1(V4_OP_FETCH_A);
CHECK(n.faults == 0 && n.ds.t == 77, "the last word of memory can be fetched");
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(5); n.b = (v4_cell)(V4_NODE_WORDS - 1u); run1(V4_OP_STORE_B);
CHECK(n.faults == 0 && n.mem[V4_NODE_WORDS - 1u] == 5, "and stored");
/* P outside memory: nothing is fetched or executed */
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.p = bad[i];
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.fault_addr == bad[i] && n.p == HANDLER && n.ds.t == 1,
"P at %lld is a fault", (long long)bad[i]);
}
/* @p in the last word of memory: its literal would be past the end */
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.mem[V4_NODE_WORDS - 1u] = word6(V4_OP_FETCH_P, NOP, NOP, NOP, NOP, NOP);
n.p = (v4_cell)(V4_NODE_WORDS - 1u);
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.fault_addr == (v4_cell)V4_NODE_WORDS && n.p == HANDLER && n.ds.t == 1,
"a literal past the end of memory is a fault");
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.mem[V4_NODE_WORDS - 1u] = word6(V4_OP_STORE_P, NOP, NOP, NOP, NOP, NOP);
n.p = (v4_cell)(V4_NODE_WORDS - 1u);
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.p == HANDLER && n.ds.t == 1, "and so is !p there");
/* a jump out of memory faults at the next fetch */
fresh(); v4_node_fault_attach(&n, HANDLER);
rpush(-5);
n.mem[0] = word6(V4_OP_SEMI, NOP, NOP, NOP, NOP, NOP);
n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 1 && n.p == -5 && n.faults == 0, "; to an address outside memory is not yet a fault");
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.p == HANDLER && n.faults == 1 && n.fault_addr == -5, "the fetch there is");
/* the handler runs, and faults are counted */
fresh(); v4_node_fault_attach(&n, HANDLER);
n.mem[HANDLER] = word6(V4_OP_DUP, V4_OP_ADD, NOP, NOP, NOP, NOP);
dpush(21); n.a = -1; run1(V4_OP_FETCH_A);
CHECK(v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == 42, "the handler's code runs next");
n.a = -1; run1(V4_OP_FETCH_A); n.a = -1; run1(V4_OP_STORE_A);
CHECK(n.faults == 3, "each fault is counted");
v4_node_fault_attach(&n, HANDLER);
CHECK(n.faults == 0 && n.fault_vector == HANDLER, "attaching clears the count");
/* with no handler the node stops */
fresh();
CHECK(n.fault_vector == -1 && n.faults == 0 && !n.stopped, "a fresh node has no handler and has not stopped");
dpush(9); n.a = -1;
CHECK(run1(V4_OP_FETCH_A) == 0 && n.stopped && n.faults == 1 && n.ds.t == 9, "with no handler a fault stops the node");
n.mem[0] = word6(V4_OP_DUP, V4_OP_ADD, NOP, NOP, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.p == 0 && n.ds.t == 9, "a stopped node executes nothing");
v4_node_fault_attach(&n, HANDLER);
CHECK(!n.stopped && v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == 18, "attaching a handler lets it run again");
v4_node_fault_attach(&n, (v4_cell)V4_NODE_WORDS);
CHECK(n.fault_vector == -1, "a handler address outside memory detaches");
v4_node_reset(&n);
CHECK(n.fault_vector == -1 && !n.stopped && n.faults == 0, "reset detaches the handler");
/* the C accessors never index outside memory */
fresh();
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
v4_node_store(&n, bad[i], 123);
CHECK(v4_node_load(&n, bad[i]) == 0 && v4_node_fetch(&n, bad[i]) == 0 && !v4_node_addr_ok(bad[i]),
"load, fetch and store at %lld touch nothing", (long long)bad[i]);
}
CHECK(n.faults == 0 && v4_node_guards_intact(&n) && v4_node_addr_ok(0) && v4_node_addr_ok((v4_cell)(V4_NODE_WORDS - 1u)),
"and they are not faults: only a running programme faults");
}
int main(void)
{
printf("v4 exec tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
@@ -332,6 +436,7 @@ int main(void)
#if V4_CELL_BITS == 64
test_iword_in_wide_cell();
#endif
test_address_faults();
test_soak();
printf(" %d checks, %d failures\n", checks, failures);
+27 -1
View File
@@ -20,6 +20,7 @@
* the text and carries on. Compiled into a word, v3's crashes (SIGSEGV).
* - control words that do not match give ERROR alone; v3 names the word.
* - QUERY takes 80 characters (FORTH-79); v3's line is 255.
* - an address outside memory says so (D-14); v3 gives ERROR alone.
*/
#include "v4/text.h"
#include "v4/testcode.h"
@@ -38,7 +39,7 @@ static v4_node n;
static v4_exec_state es;
static v4_heat h;
static v4_text tx;
static v4_cell w_quit, w_key, w_key_end, capsule_latest;
static v4_cell w_quit, w_key, w_key_end, w_fault, capsule_latest;
static char out[V4_CONSOLE_CAP + 1];
static long last_steps;
@@ -71,6 +72,7 @@ static void boot_with(unsigned depth)
n.mem[NODE_ERROR] = 0;
v4_node_console_attach(&n, CONSOLE_TX);
v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST);
v4_node_fault_attach(&n, w_fault);
v4_dstack_reset(&n.ds);
v4_rstack_reset(&n.rs);
v4_exec_reset(&es);
@@ -180,6 +182,7 @@ int main(void)
w_quit = v4_text_word(&tx, "QUIT");
w_key = v4_text_word(&tx, "KEY");
w_key_end = v4_text_word(&tx, "CR"); /* the word after KEY in core.v4 */
w_fault = v4_text_word(&tx, "(FAULT)");
capsule_latest = v4_text_latest(&tx);
CHECK(w_key_end > w_key && w_key_end - w_key <= 12, "KEY is the few words before CR");
@@ -230,6 +233,29 @@ int main(void)
CHECK(is(say("67 EMIT\n"), "C ok\nok> ") && is(say("H3\n"), "UNKNOWN WORD: 'H3'\n ERROR\nok> "), "and the definition is gone");
CHECK(is(say(": H4 68 EMIT ; H4\n"), "D ok\nok> "), "the next definition compiles and runs");
/* ---- an address outside memory (D-14): a message, ERROR, and the prompt ---- */
{
static const char *const lines[] = {
"65 EMIT -1 @ 66 EMIT\n", "65 EMIT 5 -1 ! 66 EMIT\n", "65 EMIT 16384 @ 66 EMIT\n", "65 EMIT 5 16384 ! 66 EMIT\n",
"65 EMIT 99999999 C@ 66 EMIT\n", "65 EMIT 7 -4 C! 66 EMIT\n", "65 EMIT -1 EXECUTE 66 EMIT\n", "65 EMIT 1 -1 +! 66 EMIT\n",
"65 EMIT PAD -8 4 CMOVE 66 EMIT\n", "65 EMIT -9 COUNT 66 EMIT\n", "65 EMIT -9 3 TYPE 66 EMIT\n",
": F0 -1 @ ; : F1 F0 ; : F2 F1 ; : F3 65 EMIT F2 66 EMIT ; F3 67 EMIT\n",
": G0 9 0 DO I 3 = IF 0 -1 ! THEN LOOP ; : G1 65 EMIT G0 66 EMIT ; G1\n",
};
boot();
for (i = 0; i < sizeof lines / sizeof lines[0]; i++) {
unsigned before = n.faults;
CHECK(is(say(lines[i]), "AAddress out of range\n ERROR\nok> "), "fault %u", i);
CHECK(n.faults == before + 1 && !n.stopped && n.mem[STATE] == 0, "fault %u: one fault, and the node is at its prompt", i);
CHECK(is(say("1 2 + 48 + EMIT\n"), "3 ok\nok> "), "and the next line runs, after fault %u", i);
}
CHECK(is(say("16383 @ DROP 0 @ DROP 67 EMIT\n"), "C ok\nok> "), "the first and last words of memory are not faults");
CHECK(is(say(": H5 1 IF [ -1 @ ]\n"), "Address out of range\n ERROR\nok> ") && n.mem[STATE] == 0 && n.mem[CFP] == CFS_W,
"a fault while a definition is open ends it");
CHECK(is(say("H5\n"), "UNKNOWN WORD: 'H5'\n ERROR\nok> ") && is(say(": H6 68 EMIT ; H6\n"), "D ok\nok> "), "and the next definition compiles and runs");
CHECK(v4_node_guards_intact(&n), "guards intact after the faults");
}
/* ---- the text is in the definition, a counted string after the call ---- */
boot();
for (v4_cell k = n.mem[DP] / 4; k < DICT_END_W; k++) n.mem[k] = (v4_cell)-1; /* memory that was not zero */
+28 -32
View File
@@ -4,12 +4,10 @@
* place for a *linear* index error -- mem[-1], mem[V4_NODE_WORDS] -- which is
* inside the struct and therefore invisible to AddressSanitizer.
*
* It explicitly does NOT cover an out-of-range *word address*, where P, A or B
* has left the address space. That lands gigabytes away, outside any band.
* DECOMPOSITION.md does not say what such an access should do, so no policy is
* invented here; instead the limitation is measured and asserted, so the claim
* in node.h cannot rot. Closing that hole is a ruling on out-of-range
* addressing, not a wider band.
* It does not cover an out-of-range *word address*, where P, A or B has left
* the address space: as an index that would land gigabytes away, outside any
* band. D-14 guards that with a range check, tested here for the C accessors
* and in test_exec.c for a running programme.
*/
#include "v4/node.h"
@@ -171,32 +169,30 @@ static void test_linear_overrun_high_is_caught(void)
CHECK(v4_node_guards_intact(&n), "intact after re-reset");
}
static void test_out_of_range_word_address_is_not_covered_by_the_band(void)
static void test_out_of_range_word_address_touches_nothing(void)
{
/* What the band does NOT catch, asserted so the limitation stays visible
* and cannot be quietly forgotten.
*
* An out-of-range *word address* is a different animal from a linear index
* error. A v4_cell address of -1 becomes unsigned 0xFFFFFFFF, which is
* 2^32 words past mem -- 16 GB at 32-bit cells. That is far outside the
* struct, so no boundary can see it. The band was briefly documented as
* catching this; it does not, and the protection is a range check whose
* policy DECOMPOSITION.md does not define. This test records the gap by
* measuring it, so the number in node.h stays true. */
v4_cell neg = (v4_cell)-1;
uint64_t words_out = (uint64_t)(v4_ucell)neg;
uint64_t far = words_out * (uint64_t)sizeof(v4_cell);
printf(" out-of-range word address -1 lands %llu bytes past mem "
"(%.1f GB at this width): outside the band, as documented\n",
(unsigned long long)far, (double)far / 1073741824.0);
CHECK(far > (uint64_t)sizeof(v4_node),
"an out-of-range address should land outside the node");
/* The band is only V4_MEM_BOUND words, so even a modest overrun is caught
* only while it is within the band. Past that it is not, which is why the
* open question is a range check and not a wider band. */
CHECK(V4_MEM_BOUND < V4_NODE_WORDS / 2u,
"if the band were most of memory, this reasoning would be wrong");
/* A word address outside memory is a different thing from a linear index
* error: -1 as an index would be 2^32 words past mem, far outside the
* struct and any band. D-14 (2026-10-04) rules it guarded: load gives 0
* and store does nothing. (That a running programme faults there is the
* executor's business: test_exec.c.) */
static const v4_cell bad[] = { -1, -2, (v4_cell)V4_NODE_WORDS, (v4_cell)V4_NODE_WORDS + (v4_cell)V4_MEM_BOUND,
(v4_cell)V4_MSB, (v4_cell)(V4_MSB - 1u) };
v4_node *n = malloc(sizeof *n);
unsigned i;
if (!n) { CHECK(0, "malloc"); return; }
v4_node_reset(n);
for (i = 0; i < V4_NODE_WORDS; i++) n->mem[i] = (v4_cell)(i + 1000u);
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
CHECK(!v4_node_addr_ok(bad[i]), "%lld is not an address", (long long)bad[i]);
CHECK(v4_node_load(n, bad[i]) == 0, "a load at %lld gives 0", (long long)bad[i]);
v4_node_store(n, bad[i], 0x5A5A);
}
for (i = 0; i < V4_NODE_WORDS; i++)
if (n->mem[i] != (v4_cell)(i + 1000u)) { CHECK(0, "a store outside memory changed word %u", i); break; }
CHECK(v4_node_guards_intact(n), "and the boundaries are intact");
CHECK(v4_node_addr_ok(0) && v4_node_addr_ok((v4_cell)(V4_NODE_WORDS - 1u)), "the first and last words are addresses");
free(n);
}
static void test_ends_are_distinguishable(void)
@@ -257,7 +253,7 @@ int main(void)
test_boundary_is_adjacent_to_memory();
test_linear_overrun_low_is_caught();
test_linear_overrun_high_is_caught();
test_out_of_range_word_address_is_not_covered_by_the_band();
test_out_of_range_word_address_touches_nothing();
test_ends_are_distinguishable();
test_workload_never_false_alarms();