Skip to content

Bound TAB and SPC padding to a byte - #125

Merged
scottdensmore merged 1 commit into
mainfrom
scottdensmore/fix/bounded-print-padding
Aug 19, 2026
Merged

Bound TAB and SPC padding to a byte#125
scottdensmore merged 1 commit into
mainfrom
scottdensmore/fix/bounded-print-padding

Conversation

@scottdensmore

Copy link
Copy Markdown
Owner

Closes #122.

Problem

TAB and SPC built their padding with strings.Repeat over an unchecked argument, so a single PRINT could allocate arbitrary memory.

$ printf '10 PRINT TAB(200000000);"X"\n' > /tmp/big.bas
$ /usr/bin/time -l ./bin/go-basic /tmp/big.bas > /dev/null
           205881344  maximum resident set size

Fix

Microsoft BASIC takes both arguments as a byte, so the authentic contract is 0 through 255 and a wider request is an illegal quantity. This is a fidelity correction rather than an arbitrary cap.

$ ./bin/go-basic /tmp/big.bas
go-basic: run /tmp/big.bas: BASIC line 10: TAB position cannot exceed 255

$ /usr/bin/time -l ./bin/go-basic /tmp/big.bas > /dev/null
             5177344  maximum resident set size

206 MB → 5.2 MB, which is just the Go runtime baseline.

Two details worth a look

  • The range check runs before the int() conversion. int() of an out-of-range float is undefined in Go, so SPC(1E30) would otherwise convert to a platform-dependent value. The obvious ordering — convert, then range-check — is subtly wrong here.
  • Truncation toward zero is unchanged. SPC(255.9) still emits 255 spaces. The comparison is argument >= maxPrintPadding+1 for exactly this reason, rather than > maxPrintPadding.

Risk

No pinned corpus program approaches the bound. The widest literal is TAB(60):

grep -rhoiE 'TAB\( *[0-9]+' .cache/basic-computer-games/5301155192d91d74d337899cecc59dbda59c4c17/ \
  | grep -oE '[0-9]+' | sort -n | tail -1   # 60

Computed arguments (TAB(I), TAB(H/12+29), TAB((63-4.5*Y)) are covered by the gate: the smoke tier and the deterministic gameplay tier both pass unchanged, so nothing exceeds 255 at runtime either.

Tests

  • TestEvaluatorAcceptsTabAndSpcAtTheByteBoundary — 255 still emits full padding for both.
  • tab beyond line width / spc beyond line width rows in TestEvaluatorReportsRuntimeErrors.

Commands run

Full CI gate, green on the committed state:

make corpus-smoke   PASS   all 112 byte-distinct variants passed
make fmt-check      PASS
make vet            PASS
make coverage-check PASS   coverage 83.1% meets 80.0% minimum
make fuzz           PASS
make build          PASS
make release-check  PASS   VERSION=ci
make lint           PASS
make vuln           PASS

🤖 Generated with Claude Code

A single PRINT could allocate arbitrary memory: TAB and SPC built their
padding with strings.Repeat over an unchecked argument, so
`PRINT TAB(200000000)` reserved 206 MB of spaces and larger values scaled
from there.

Microsoft BASIC takes both arguments as a byte, so the authentic contract
is 0 through 255 and a wider request is an illegal quantity. Reject
anything outside that range with a diagnostic rather than honoring it.

The comparison happens before the int conversion, because int() of an
out-of-range float is undefined in Go and 1E30 would otherwise convert to
a platform-dependent value. Truncation toward zero is unchanged, so
SPC(255.9) still emits 255 spaces.

No pinned corpus program exceeds the bound: the widest literal TAB is 60,
and the smoke and gameplay tiers pass unchanged.

Closes #122

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@scottdensmore
scottdensmore merged commit f09f570 into main Aug 19, 2026
3 checks passed
@scottdensmore
scottdensmore deleted the scottdensmore/fix/bounded-print-padding branch August 19, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TAB and SPC allocate unbounded padding

1 participant