diff --git a/capsules/v4/nucleus-64.f18 b/capsules/v4/nucleus-64.f18 index 6f818911..b108c931 100644 Binary files a/capsules/v4/nucleus-64.f18 and b/capsules/v4/nucleus-64.f18 differ diff --git a/v4/Makefile b/v4/Makefile index e8ae96ce..20fc19fd 100644 --- a/v4/Makefile +++ b/v4/Makefile @@ -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= 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 ---- */ diff --git a/v4/tools/depthsweep.py b/v4/tools/depthsweep.py new file mode 100755 index 00000000..8cd1f278 --- /dev/null +++ b/v4/tools/depthsweep.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""depthsweep.py -- the hosted product at every depth of both stacks. + + depthsweep.py + +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)