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
9 changes: 7 additions & 2 deletions docs/language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ and invalid logical operands are runtime errors.
| Statement | Supported forms |
| --- | --- |
| Assignment | `LET A=1`, `A=1`, `A$(I)="X"` |
| Output | `PRINT`, comma print zones, semicolon suppression, adjacent items, `TAB` |
| Output | `PRINT`, comma print zones, semicolon suppression, adjacent items, `TAB`, `SPC` |
| Input | `INPUT A`, `INPUT "PROMPT";A$`, multiple scalar or array targets |
| Branching | `IF expression THEN line`, `IF expression THEN statement`, `GOTO` |
| Subroutines | `GOSUB`, `RETURN`, computed `ON expression GOSUB` |
Expand Down Expand Up @@ -88,6 +88,7 @@ falls through without jumping; fractional or negative selectors are errors.
| `LOG(x)` | Natural logarithm |
| `EXP(x)` | Natural exponential |
| `RND(x)` | Random value in `[0,1)` with classic repeat/reseed behavior |
| `POS(x)` | Current output column, counting from zero; `x` is ignored |

Supplying the CLI `-seed` option replaces the initial random source so a run
can be reproduced.
Expand All @@ -113,7 +114,11 @@ without one, the prompt is simply `? `.

`PRINT` starts a new line unless its final item is followed by `;` or `,`.
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.
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
values, so using either outside `PRINT` is a type error.

## Annotated structured-source extension

Expand Down
30 changes: 26 additions & 4 deletions pkg/interpreter/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,11 +696,14 @@ func (e *Evaluator) evalPrintStatement(statement *PrintStmt) error {
return err
}
text := formatValue(value)
if tab, ok := value.(TabValue); ok {
switch directive := value.(type) {
case TabValue:
text = ""
if tab.Pos > e.OutputColumn {
text = strings.Repeat(" ", tab.Pos-e.OutputColumn)
if directive.Pos > e.OutputColumn {
text = strings.Repeat(" ", directive.Pos-e.OutputColumn)
}
case SpcValue:
text = strings.Repeat(" ", directive.Count)
}
if _, err := io.WriteString(e.Out, text); err != nil {
return fmt.Errorf("write output: %w", err)
Expand Down Expand Up @@ -806,6 +809,11 @@ type TabValue struct {
Pos int
}

// SpcValue represents a run of spaces from SPC.
type SpcValue struct {
Count int
}

func (e *Evaluator) evalExpression(expression Expression) (any, error) {
switch value := expression.(type) {
case *IntegerLiteral:
Expand Down Expand Up @@ -1086,6 +1094,20 @@ func (e *Evaluator) evalCallExpression(expression *CallExpression) (any, error)
return nil, errors.New("TAB position cannot be negative")
}
return TabValue{Pos: int(argument)}, nil
case "SPC":
argument, err := e.singleNumberArgument(expression)
if err != nil {
return nil, err
}
if argument < 0 {
return nil, errors.New("SPC count cannot be negative")
}
return SpcValue{Count: int(argument)}, nil
case "POS":
if _, err := e.singleNumberArgument(expression); err != nil {
return nil, err
}
return float64(e.OutputColumn), nil
case "SIN":
argument, err := e.singleNumberArgument(expression)
if err != nil {
Expand Down Expand Up @@ -1464,7 +1486,7 @@ func formatValue(value any) string {
return exact
case string:
return typed
case TabValue:
case TabValue, SpcValue:
return ""
default:
return fmt.Sprint(typed)
Expand Down
28 changes: 28 additions & 0 deletions pkg/interpreter/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,31 @@ func TestEvaluatorUsesMicrosoftPrintZones(t *testing.T) {
}
}

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

program := mustParse(t, `10 PRINT "A";SPC(5);"B"
20 PRINT "C";SPC(0);"D"
30 PRINT POS(0)
40 PRINT "EF";: PRINT POS(0)
50 PRINT "G",: PRINT POS(0)
60 PRINT TAB(4);SPC(2);POS(0)
`)
var output bytes.Buffer
if err := NewEvaluator(program, &output).Run(); err != nil {
t.Fatalf("run: %v", err)
}
want := "A" + strings.Repeat(" ", 5) + "B\n" +
"CD\n" +
"0\n" +
"EF2\n" +
"G" + strings.Repeat(" ", 13) + "14\n" +
strings.Repeat(" ", 6) + "6\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 @@ -606,6 +631,9 @@ func TestEvaluatorReportsRuntimeErrors(t *testing.T) {
{name: "division by zero", program: mustParse(t, "10 PRINT 1/0\n"), want: "division by zero"},
{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: "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"},
{name: "mixed comparison", program: mustParse(t, "10 PRINT \"x\"=1\n"), want: "type mismatch in comparison"},
{name: "string assignment type mismatch", program: mustParse(t, "10 A$=1\n"), want: "string variable A$ requires a string value"},
Expand Down
15 changes: 15 additions & 0 deletions pkg/interpreter/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ func TestLexerRecognizesArctangent(t *testing.T) {
}
}

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

lexer := NewLexer("10 PRINT SPC(3);POS(0)\n")
want := []TokenType{
NUMBER, PRINT, SPC, LPAREN, NUMBER, RPAREN, SEMICOLON, POS, LPAREN, NUMBER, RPAREN, EOL,
EOF,
}
for index, wantType := range want {
if token := lexer.NextToken(); token.Type != wantType {
t.Fatalf("token %d: got %s (%q), want %s", index, token.Type, token.Literal, wantType)
}
}
}

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

Expand Down
2 changes: 2 additions & 0 deletions pkg/interpreter/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ func NewParser(lexer *Lexer) *Parser {
parser.prefixParseFuncs[NOT] = parser.parsePrefixExpression
parser.prefixParseFuncs[LPAREN] = parser.parseGroupedExpression
parser.prefixParseFuncs[TAB] = parser.parseCallExpression
parser.prefixParseFuncs[SPC] = parser.parseCallExpression
parser.prefixParseFuncs[POS] = parser.parseCallExpression
parser.prefixParseFuncs[SIN] = parser.parseCallExpression
parser.prefixParseFuncs[COS] = parser.parseCallExpression
parser.prefixParseFuncs[TAN] = parser.parseCallExpression
Expand Down
6 changes: 6 additions & 0 deletions pkg/interpreter/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ const (
SLEEP TokenType = "SLEEP"
// TAB moves PRINT output to a target column.
TAB TokenType = "TAB"
// SPC emits a run of spaces in a PRINT list.
SPC TokenType = "SPC"
// POS reports the current PRINT output column.
POS TokenType = "POS"
// SIN computes a sine value.
SIN TokenType = "SIN"
// COS computes a cosine value.
Expand Down Expand Up @@ -160,6 +164,8 @@ var keywords = map[string]TokenType{
"print": PRINT,
"sleep": SLEEP,
"tab": TAB,
"spc": SPC,
"pos": POS,
"sin": SIN,
"cos": COS,
"tan": TAN,
Expand Down