fix(v4.0.0): EMIT ran past the output buffer; a message's first word was read before there was room for the rest
The two root causes behind what the reviews of the wait found, both in the code as it stands. Each with a check that failed first. EMIT stored its character and only then looked whether the buffer was exactly full. A flush that ended in a fault -- the stack too deep to begin a message -- left the buffer full, and the fault's own message was then put past its end, with (OUT^) never again at the end to be flushed. The 28 cells after the buffer happen to be unused, so nothing showed; the wait had put the ports there, and the node blocked writing to one. EMIT now sends a buffer it finds full before it stores. AWAIT, and (GATE) when a neighbour is writing, read a message's first word and then used stack that was not known to be there. With 27 or 28 values on the stack, or 28 or 29 calls deep, a line typed during AWAIT was taken and never answered. (ROOM) tries six cells and four return entries first: if they are not there the text ends 'Stack overflow' with nothing read. (GATE) tries the three cells its writing needs before a first word goes; it was safe before only by the order things were done. v4/tools/depthsweep.py runs every message path of the hosted product at every depth of both stacks: each line typed must be answered and the node must come back. hosted-check runs it. make -C v4 test and sanitize at both widths, hosted-check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6015c87157
commit
0d91608f39
Binary file not shown.
@@ -206,6 +206,7 @@ hosted-check: $(foreach i,$(HOSTED_ISAS),$(BINDIR)/boot-$(i).txt) $(BINDIR)/star
|
||||
@grep -q '^ok> Stack overflow$$' $(BINDIR)/deep-amd64.txt && grep -q '^ok> 3 ok$$' $(BINDIR)/deep-amd64.txt || { echo "hosted-check: a line that left 29 values on the stack was not an error the node came back from"; tail -4 $(BINDIR)/deep-amd64.txt; exit 1; }
|
||||
@printf '1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26\nWORDS\nDEPTH .\n' | $(BINDIR)/starforth4-amd64 > $(BINDIR)/deepwords-amd64.txt 2>&1; true
|
||||
@grep -q 'DUP' $(BINDIR)/deepwords-amd64.txt && grep -q '^ok> 26 ok$$' $(BINDIR)/deepwords-amd64.txt || { echo "hosted-check: WORDS with 26 values on the stack did not print and come back"; tail -4 $(BINDIR)/deepwords-amd64.txt | cut -c1-200; exit 1; }
|
||||
@python3 $(HERE)/tools/depthsweep.py $(BINDIR)/starforth4-amd64 $$(( $(HOST_DATA_RING) + 2 )) $$(( $(HOST_RET_RING) + 1 )) || { echo "hosted-check: at some depth of a stack a line was not answered, or the node did not come back"; exit 1; }
|
||||
@$(BINDIR)/starforth4-postfail < /dev/null > $(BINDIR)/postfail.txt; test $$? -ne 0 || { echo "hosted-check: a boot with a failing case did not fail"; exit 1; }
|
||||
@grep -q '^POST FAIL: fail.wrong out<5 5 $$' $(BINDIR)/postfail.txt || { echo "hosted-check: the failing case was not named"; cat $(BINDIR)/postfail.txt; exit 1; }
|
||||
@grep -q '^POST FAIL: fail.many out<QQQQ' $(BINDIR)/postfail.txt || { echo "hosted-check: the case that prints too much was not named"; exit 1; }
|
||||
|
||||
+22
-1
@@ -260,14 +260,27 @@ header CMOVE
|
||||
\ ( port to -- ) take in a message and keep it
|
||||
: (TAKE) (TAKE-HDR) jump (TAKE-KEEP)
|
||||
|
||||
\ ( -- ) ROOM TO TAKE A MESSAGE IN. Once a message's first word has been
|
||||
\ read the rest must be: whoever is writing it waits for that, and a fault
|
||||
\ in the middle leaves the two out of step. Taking one in and keeping it
|
||||
\ needs five cells of the data stack and four entries of the return stack.
|
||||
\ So before a first word is read from within text -- where the stacks may
|
||||
\ be as deep as the text has made them -- six and four are tried: if they
|
||||
\ are not there the fault comes here, with nothing read, and the text
|
||||
\ ends "Stack overflow" as it would have (MESH.md 7c.6).
|
||||
: (ROOM)
|
||||
0 0 0 0 0 0 drop drop drop drop drop drop
|
||||
0 push 0 push 0 push 0 push pop drop pop drop pop drop pop drop ;
|
||||
|
||||
\ ( port -- ) wait until a message may be begun on the port, by its
|
||||
\ address, taking in whatever is being written to this node meanwhile; and
|
||||
\ leave B at the port. The port is kept in (GATE-PORT), not on the stack:
|
||||
\ this is run with the stack as full as EMIT may be.
|
||||
: (GATE)
|
||||
(GATE-PORT) a! !
|
||||
0 0 0 drop drop drop \ what follows the message's first word needs three cells: tried here, before it goes
|
||||
L: (WRITERS) b! @b if QUIET
|
||||
(LOW) (PORT) + dup b! @b (TAKE) jump L
|
||||
(ROOM) (LOW) (PORT) + dup b! @b (TAKE) jump L
|
||||
QUIET: drop
|
||||
(GATE-PORT) a! @ (PORT) - (NEAR) + a! @ if ASK
|
||||
(ME) a! @ - -if GO jump ASK \ its number less this node's
|
||||
@@ -345,9 +358,17 @@ header CMOVE
|
||||
\ prints goes through EMIT. A is kept: the words that print have always
|
||||
\ been free to use it across an EMIT. As v3, only the low 8 bits of the
|
||||
\ character are printed.
|
||||
\ A BUFFER FOUND FULL is sent first. It is full when a flush of it ended
|
||||
\ in a fault -- the stack too deep to begin a message -- and what is printed
|
||||
\ next is that fault's own message: put where (OUT^) points, it would go
|
||||
\ past the buffer's end, and (OUT^) would never again be at the end to be
|
||||
\ flushed (MESH.md 7c.6).
|
||||
header EMIT
|
||||
: EMIT ( c -- )
|
||||
255 and a push push \ A, and then the character, to the return stack
|
||||
(OUT^) b! @b (OUT)+256 xor if STALE drop jump PUT
|
||||
STALE: drop (FLUSH-OUT)
|
||||
PUT:
|
||||
(OUT^) b! @b a! pop !+ a !b \ the character goes where (OUT^) points, which moves on
|
||||
a (OUT)+256 xor if FULL drop pop a! ;
|
||||
FULL: drop (FLUSH-OUT) pop a! ;
|
||||
|
||||
+2
-1
@@ -333,7 +333,8 @@ header AWAIT
|
||||
(A-QUEUE)
|
||||
(A-FOUND) a! @ if WAIT jump (A-END)
|
||||
WAIT: drop
|
||||
L: (PORT)+8 b! @b (PORT)+9 b! @b (PORT) + dup b! SWAP (TAKE-HDR)
|
||||
L: (ROOM) \ before a first word is read: core.v4
|
||||
(PORT)+8 b! @b (PORT)+9 b! @b (PORT) + dup b! SWAP (TAKE-HDR)
|
||||
(MQ-HDR) a! @ (ME) a! @ xor if A1 drop jump KEEP
|
||||
A1: drop (MQ-HDR)+2 a! @ \ for this node: its type
|
||||
-1 + if TEXT -2 + if ANSWER -1 + if NACK -1 + if GONE drop jump KEEP
|
||||
|
||||
@@ -1133,6 +1133,26 @@ int main(void)
|
||||
}
|
||||
printf(" %u values may wait on the stack while the next line is typed and interpreted\n", most);
|
||||
CHECK(most + 4u >= V4_DATA_DEPTH, "the prompt and the interpreter take no more than four cells of the stack");
|
||||
/* A flush of the output buffer that ends in a fault leaves the buffer full: what is printed next -- the
|
||||
* error's own message -- must not go past its end (MESH.md 7c.6). */
|
||||
{
|
||||
unsigned over = 0, worst = 0, at;
|
||||
for (k = most > 6 ? most - 6 : 0; k <= most + 3; k++) {
|
||||
char line[4 * V4_DATA_DEPTH + 16];
|
||||
size_t n_at = 0;
|
||||
unsigned m;
|
||||
boot_bare();
|
||||
for (m = 0; m < 28; m++) n.mem[OUT_W + 256 + m] = 0;
|
||||
for (m = 0; m < k; m++) n_at += (size_t)snprintf(line + n_at, sizeof line - n_at, "1 ");
|
||||
snprintf(line + n_at, sizeof line - n_at, "\n");
|
||||
(void)say(line);
|
||||
(void)say("WORDS\n");
|
||||
for (at = 0; at < 28; at++) if (n.mem[OUT_W + 256 + at] != 0) { over++; worst = k; break; }
|
||||
if (n.mem[OUT_PTR] > OUT_W + 256) { over++; worst = k; }
|
||||
CHECK(strcmp(say("1 2 + .\n"), "3 ok\nok> ") == 0, "WORDS with %u values on the stack: the node goes on", k);
|
||||
}
|
||||
CHECK(over == 0, "nothing was ever put past the end of the output buffer (it was, with %u values on the stack)", worst);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- how much of the data stack a line has ---- */
|
||||
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""depthsweep.py -- the hosted product at every depth of both stacks.
|
||||
|
||||
depthsweep.py <starforth4 binary> <data depth> <return depth>
|
||||
|
||||
Every path by which the node begins, writes or reads a message is run with
|
||||
the data stack holding 0 .. depth+2 values, and from 1 .. depth+2 calls
|
||||
deep. At each, every line typed must be answered -- " ok" or " ERROR" --
|
||||
and the node must come back: it may refuse with an error, but it may not
|
||||
hang, and it may not swallow a line. Where the stack is shallow the
|
||||
answers themselves are checked.
|
||||
|
||||
Why: docs/v4.0.0/MESH.md 7c.6. A fault that came after a message's first
|
||||
word had moved left it half done; and a fault inside a flush of the output
|
||||
buffer let EMIT run past the buffer's end. Nothing ran these paths on a
|
||||
deep stack.
|
||||
"""
|
||||
import subprocess, sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
binary, ddepth, rdepth = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
|
||||
cases = []
|
||||
|
||||
def run(lines):
|
||||
text = '\n'.join(lines) + '\n'
|
||||
try:
|
||||
r = subprocess.run([binary], input=text.encode(), capture_output=True, timeout=30)
|
||||
return r.stdout.decode('latin1'), False
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return (e.stdout or b'').decode('latin1'), True
|
||||
|
||||
def answers(out):
|
||||
# what follows POST: one answer to a line typed
|
||||
body = out.split('POST: PASSED', 1)[-1]
|
||||
return body.count(' ok\n') + body.count(' ERROR\n')
|
||||
|
||||
def check(name, k, lines, expect=None):
|
||||
cases.append((name, k, lines, expect))
|
||||
|
||||
def judge(case):
|
||||
name, k, lines, expect = case
|
||||
out, hung = run(lines)
|
||||
why = None
|
||||
if 'POST: PASSED' not in out: why = 'POST did not pass'
|
||||
elif hung: why = 'it hung'
|
||||
elif answers(out) != len(lines): why = '%d lines typed, %d answered' % (len(lines), answers(out))
|
||||
elif expect is not None and expect not in out: why = 'no "%s"' % expect.strip()
|
||||
if why is None: return None
|
||||
tail = out.split('POST: PASSED', 1)[-1][-160:].replace('\n', '|')
|
||||
return 'depthsweep: FAIL %s at %d: %s: ...%s' % (name, k, why, tail)
|
||||
|
||||
def vals(k): return ' '.join(['1'] * k)
|
||||
|
||||
# The stack shallow, and then every depth from well below where anything
|
||||
# changes to past its end. An error empties the stack, and a wait that a
|
||||
# typed line breaks ends in an error: so the line that broke it is always
|
||||
# done, whatever the depth.
|
||||
shallow = ddepth - 12
|
||||
for k in [0, 1, shallow] + list(range(ddepth - 10, ddepth + 3)):
|
||||
v = [vals(k)] if k else []
|
||||
sh = k <= shallow
|
||||
check('a number printed', k, v + ['7 .', '77 .'], '77 ok' if sh else None)
|
||||
check('WORDS', k, v + ['WORDS', '77 .'], 'DUP' if sh else None)
|
||||
check('a line typed during AWAIT', k, [': AW 5 AWAIT ;'] + v + ['AW', '77 .', '88 .'], '77 ok')
|
||||
check('SEND with no way', k, [': SD S" 1 DROP" 5 SEND ;'] + v + ['SD', '77 .'], 'Argument out of range' if sh else None)
|
||||
check('a write to an empty port', k, v + ['5 7 PORT!', '77 .'], 'No one on that port' if sh else None)
|
||||
check('an unknown word', k, v + ['NOSUCHWORD', '77 .'], 'UNKNOWN WORD' if sh else None)
|
||||
|
||||
def nest(n, inner):
|
||||
return [': N1 %s ;' % inner] + [': N%d N%d ;' % (i, i - 1) for i in range(2, n + 1)] + ['N%d' % n]
|
||||
|
||||
rshallow = rdepth - 14
|
||||
for n in [1, rshallow] + list(range(rdepth - 12, rdepth + 3)):
|
||||
sh = n <= rshallow
|
||||
check('a number printed, calls deep', n, nest(n, '7 .') + ['77 .'], '77 ok')
|
||||
check('WORDS, calls deep', n, nest(n, 'WORDS') + ['77 .'], '77 ok')
|
||||
check('a line typed during AWAIT, calls deep', n, nest(n, '5 AWAIT') + ['77 .', '88 .'], '77 ok')
|
||||
check('SEND with no way, calls deep', n, nest(n, 'S" 1 DROP" 5 SEND') + ['77 .'], '77 ok')
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
found = [f for f in pool.map(judge, cases) if f]
|
||||
for f in found: print(f)
|
||||
print('depthsweep: %d runs, %d failures' % (len(cases), len(found)))
|
||||
sys.exit(1 if found else 0)
|
||||
Reference in New Issue
Block a user