diff --git a/docs/language-reference.md b/docs/language-reference.md index d3f4441..37c5891 100644 --- a/docs/language-reference.md +++ b/docs/language-reference.md @@ -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 diff --git a/pkg/interpreter/evaluator.go b/pkg/interpreter/evaluator.go index 9b45fa9..a40f8ae 100644 --- a/pkg/interpreter/evaluator.go +++ b/pkg/interpreter/evaluator.go @@ -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 @@ -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) @@ -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 { diff --git a/pkg/interpreter/evaluator_test.go b/pkg/interpreter/evaluator_test.go index 81c2290..bb2e59a 100644 --- a/pkg/interpreter/evaluator_test.go +++ b/pkg/interpreter/evaluator_test.go @@ -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() @@ -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"},