Skip to content

Render numbers the way Microsoft BASIC does - #124

Merged
scottdensmore merged 1 commit into
mainfrom
scottdensmore/fix/print-numeric-format
Aug 19, 2026
Merged

Render numbers the way Microsoft BASIC does#124
scottdensmore merged 1 commit into
mainfrom
scottdensmore/fix/print-numeric-format

Conversation

@scottdensmore

Copy link
Copy Markdown
Owner

Closes #119. Closes #120.

Behavior

Microsoft renders a number as sign-or-space, digits, trailing space, and go-basic emitted bare digits. It also leaked Go's float64 conventions into numeric output.

Before / after

$ printf '1\n' | ./bin/go-basic -seed 1 test/scripts/23-matches.bas | grep 'THERE ARE NOW'
before:  THERE ARE NOW20MATCHES REMAINING.
after:   THERE ARE NOW 20 MATCHES REMAINING.
Expression Before After
PRINT 1;2;3 123 1 2 3
PRINT -7 -7 -7
PRINT .5 0.5 .5
PRINT 1/3 0.3333333333333333 .333333333
PRINT 1E10 10000000000 1E+10
PRINT 1E-10 1e-10 1E-10

Boundaries, all covered by tests: .00999999.9999E-03, .01.01, 999999999999999999, 1E91E+09.

Design notes

  • basicNumberString is shared by PRINT and STR$. PRINT appends the trailing space; STR$ does not. Previously STR$ implemented the leading half of the rule and PRINT implemented none of it, so the two disagreed with each other — that inconsistency is what identified this as an oversight rather than a deliberate choice.
  • Nine significant digits matches Microsoft's five-byte float. That rounding subsumes the ad-hoc 1e-14 binary-residue heuristic, which is deleted rather than kept alongside it.
  • Fixed-point form outside [0.01, 1e9) switches to exponent form.

Test expectations

The expected transcripts were captured from this interpreter, so they encoded the defect — for example test/cli_test.go asserted A1POINT BAGELS BUFF!! where the original prints A 1 POINT BAGELS BUFF!!, and one literal (245.354056071945743.5) was a mid-number fragment of two columns run together. 148 expectations are updated across pkg/interpreter, cmd/go-basic, and test.

These were rewritten with tooling under two constraints, not by accepting output blindly:

  1. Every replacement had to match its original with spaces stripped, proving only spacing changed rather than wording. The ~13 that could not (because their digits changed) are the precision-sensitive ones, each verified individually against the nine-digit rule — e.g. 113.5528725660044113.552873.
  2. The structure-preserving rewriter re-evaluates its own output and rejects the edit if it does not equal the target.

Reviewer note: two deliberate assertion changes

  • strings.Count(transcript, " RIGHT") and the matching WRONG are re-anchored from 11 to 10 leading spaces. Their counts depend on the column, so this needed to stay column-anchored rather than become a bare word match.
  • Four Contains assertions lost incidental leading whitespace (e.g. " ***** END OF FIRST HALF *****…"). Their content assertions are unchanged and none of them feed a Count, so no assertion changed meaning, but they are marginally less strict than before.

Derivation

These expectations follow the documented Microsoft rules; they were not captured from a reference implementation. TestEvaluatorFormatsNumbersLikeMicrosoft pins the rules independently of any transcript, and every boundary above is asserted there. If the rules are ever checked against a real 6502 BASIC, that test is the single place to reconcile.

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

The gate caught a stale expectation in cmd/go-basic that focused runs over ./pkg/interpreter and ./test had missed.

Not fixed here

#122TAB/SPC allocate unbounded padding. Untouched by this change.

🤖 Generated with Claude Code

PRINT emitted bare digits, so numbers collided with adjacent literals:
`PRINT "THERE ARE NOW";N;"MATCHES REMAINING."` produced
`THERE ARE NOW20MATCHES REMAINING.` where every 8K BASIC produces
`THERE ARE NOW 20 MATCHES REMAINING.`. Microsoft renders a number as
sign-or-space, digits, then a trailing space. STR$ already implemented
the leading half of that rule, so PRINT and STR$ disagreed.

Numeric rendering also leaked Go's float64 conventions: a leading zero
before the decimal point, a lowercase exponent marker, no exponent form
for large magnitudes, and up to sixteen significant digits.

Add basicNumberString and formatNumber, shared by PRINT and STR$. Values
now carry nine significant digits, matching Microsoft's five-byte float;
fixed-point form drops the leading zero and trailing zeros; magnitudes
outside [0.01, 1e9) use exponent form such as 1E-03 or 1.23456789E+09.
Rounding to nine digits subsumes the ad-hoc binary-residue heuristic,
which is removed.

Expected transcripts were captured from this interpreter, so they encoded
the old spacing and are updated throughout. Each replacement had to match
its original with spaces stripped, proving only spacing changed; the
precision-sensitive ones were verified individually against the nine-digit
rule. The Count assertions on RIGHT and WRONG are re-anchored to their new
column, since their counts depend on it.

Closes #119
Closes #120

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@scottdensmore
scottdensmore merged commit d77eac3 into main Aug 19, 2026
3 checks passed
@scottdensmore
scottdensmore deleted the scottdensmore/fix/print-numeric-format branch August 19, 2026 15:25
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.

Numeric output leaks float64 formatting PRINT omits Microsoft numeric spacing

1 participant