Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ exponent form such as `1E-03` or `1.23456789E+09`.
Commas advance to 14-column print zones. `TAB(n)` advances when `n` is to the
right of the current output column and otherwise emits no spacing. `SPC(n)`
emits `n` spaces regardless of the current column, and `POS(x)` reports that
column as a number. Both truncate their argument toward zero; a negative `SPC`
count is a runtime error. `TAB` and `SPC` are print-list directives rather than
column as a number. `TAB` and `SPC` truncate their argument toward zero and
accept `0` through `255`, the byte range Microsoft takes; anything outside it
is a runtime error. `TAB` and `SPC` are print-list directives rather than
values, so using either outside `PRINT` is a type error.

## Annotated structured-source extension
Expand Down
14 changes: 14 additions & 0 deletions pkg/interpreter/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,11 @@ func (e *Evaluator) evalSleepStatement(statement *SleepStatement) error {
return nil
}

// maxPrintPadding is the widest padding TAB and SPC accept. Microsoft BASIC
// takes both arguments as a byte, so a wider request is an illegal quantity
// rather than a very large allocation.
const maxPrintPadding = 255

// TabValue represents a target output column from TAB.
type TabValue struct {
Pos int
Expand Down Expand Up @@ -1096,6 +1101,12 @@ func (e *Evaluator) evalCallExpression(expression *CallExpression) (any, error)
if argument < 0 {
return nil, errors.New("TAB position cannot be negative")
}
// Anything below the next whole value truncates into range. Comparing
// before the conversion also avoids int() of an out-of-range float,
// which is undefined in Go.
if argument >= maxPrintPadding+1 {
return nil, fmt.Errorf("TAB position cannot exceed %d", maxPrintPadding)
}
return TabValue{Pos: int(argument)}, nil
case "SPC":
argument, err := e.singleNumberArgument(expression)
Expand All @@ -1105,6 +1116,9 @@ func (e *Evaluator) evalCallExpression(expression *CallExpression) (any, error)
if argument < 0 {
return nil, errors.New("SPC count cannot be negative")
}
if argument >= maxPrintPadding+1 {
return nil, fmt.Errorf("SPC count cannot exceed %d", maxPrintPadding)
}
return SpcValue{Count: int(argument)}, nil
case "POS":
if _, err := e.singleNumberArgument(expression); err != nil {
Expand Down
18 changes: 18 additions & 0 deletions pkg/interpreter/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,22 @@ func TestEvaluatorFormatsNumbersLikeMicrosoft(t *testing.T) {
}
}

func TestEvaluatorAcceptsTabAndSpcAtTheByteBoundary(t *testing.T) {
t.Parallel()

program := mustParse(t, `10 PRINT SPC(255);"A"
20 PRINT TAB(255);"B"
`)
var output bytes.Buffer
if err := NewEvaluator(program, &output).Run(); err != nil {
t.Fatalf("run: %v", err)
}
want := strings.Repeat(" ", 255) + "A\n" + strings.Repeat(" ", 255) + "B\n"
if got := output.String(); got != want {
t.Fatalf("output: got %q, want %q", got, want)
}
}

func TestEvaluatorNamedNextUnwindsAbandonedInnerLoops(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -673,6 +689,8 @@ func TestEvaluatorReportsRuntimeErrors(t *testing.T) {
{name: "negative sleep", program: mustParse(t, "10 SLEEP -1\n"), want: "SLEEP duration cannot be negative"},
{name: "negative tab", program: mustParse(t, "10 PRINT TAB(-1)\n"), want: "TAB position cannot be negative"},
{name: "negative spc", program: mustParse(t, "10 PRINT SPC(-1)\n"), want: "SPC count cannot be negative"},
{name: "tab beyond line width", program: mustParse(t, "10 PRINT TAB(256)\n"), want: "TAB position cannot exceed 255"},
{name: "spc beyond line width", program: mustParse(t, "10 PRINT SPC(256)\n"), want: "SPC count cannot exceed 255"},
{name: "spc argument count", program: mustParse(t, "10 PRINT SPC(1,2)\n"), want: "SPC expects 1 argument, got 2"},
{name: "pos argument count", program: mustParse(t, "10 PRINT POS(0,1)\n"), want: "POS expects 1 argument, got 2"},
{name: "string arithmetic", program: mustParse(t, "10 PRINT \"x\"+1\n"), want: "expected number"},
Expand Down