Files
LithosAnanake/v4/tools/depthsweep.py
T
rajamesandClaude Opus 5.5 0d91608f39 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>
2026-10-08 11:46:05 -04:00

85 lines
3.8 KiB
Python
Executable File

#!/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)