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
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