diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25e8c12..fd99418 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,8 +25,9 @@ corpus runs reuse the verified `.cache/` checkout. `go-basic` CLI. 2. Make the smallest product-code change that passes the test. 3. Refactor only while the tests remain green. -4. Update `README.md` and the compatibility documentation when behavior or - supported syntax changes. +4. Update the relevant user guide, language reference, and compatibility + documentation when behavior or supported syntax changes. Keep `README.md` + focused on project orientation and links into those documents. 5. Run the appropriate validation gates before opening a pull request. Keep interpreter behavior in `pkg/interpreter/`. The command under @@ -58,6 +59,19 @@ The full test suite uses the race detector and enforces at least 80% total statement coverage. Fuzzing is deliberately time-bounded; preserve useful regression seeds when a fuzzer finds a defect. +## Releases + +Tags matching `v*` trigger `.github/workflows/release.yml`. The workflow repeats +the quality, fuzz, compatibility, and vulnerability gates, then publishes +checksummed Linux (`amd64`, `arm64`), macOS (`amd64`, `arm64`), and Windows +(`amd64`) archives. + +Build and verify the same artifact set locally before tagging: + +```bash +make release-check VERSION=v0.1.0 +``` + ## Pull requests Use a concise, imperative, sentence-case title. Keep each change focused and diff --git a/README.md b/README.md index eb7a5cb..6f05389 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,28 @@ [![Go version](https://img.shields.io/github/go-mod/go-version/scottdensmore/go-basic)](go.mod) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +![A vintage computer running BASIC, connected to an abstract software pipeline](docs/assets/go-basic-hero.jpg) + A cross-platform interpreter for classic, line-numbered BASIC, written in Go. It targets Microsoft 8K BASIC behavior and is continuously exercised against all 112 byte-distinct original BASIC programs in the pinned [BASIC Computer Games](https://github.com/coding-horror/basic-computer-games) corpus. +## Why go-basic? + +- Run classic BASIC programs on Linux, macOS, and Windows. +- Get actionable source, BASIC-line, and runtime diagnostics. +- Reproduce programs with controlled random seeds and statement limits. +- Depend on a small Go codebase with no third-party runtime dependencies. +- Verify compatibility against every distinct source in a pinned historical + corpus. + ## Quick start -go-basic requires Go 1.26.6 or newer. Download a checksummed archive from the +Download a checksummed archive from the [latest release](https://github.com/scottdensmore/go-basic/releases/latest), or -build it from source: +build from source with Go 1.26.6 or newer: ```bash git clone https://github.com/scottdensmore/go-basic.git @@ -36,87 +47,27 @@ Hello World 5 25 ``` -On Windows, run `bin\go-basic.exe test\scripts\test.bas` after building with -`go build -o bin\go-basic.exe ./cmd/go-basic`. - -## Usage +## Documentation -```text -go-basic [-version] [-seed number] [-max-statements number] source.bas -``` - -| Option | Purpose | +| Guide | What it covers | | --- | --- | -| `-version` | Print build version information and exit. | -| `-seed number` | Seed `RND` for reproducible runs. | -| `-max-statements number` | Stop before executing more than this many statements; `0` is unlimited. | - -Programs read `INPUT` values from standard input and write program output to -standard output. Parse and runtime failures are written to standard error with -the source file and BASIC line context. - -## Language support - -| Area | Supported behavior | -| --- | --- | -| Values | Numeric and string scalars; explicitly or implicitly dimensioned numeric and string arrays | -| Arithmetic | `+`, `-`, `*`, `/`, right-associative `^` | -| Logic | Comparisons plus Microsoft-style 16-bit `NOT`, `AND`, and `OR` | -| Control flow | `IF...THEN`, `FOR...NEXT`, `GOTO`, `GOSUB...RETURN`, computed `ON...GOTO` and `ON...GOSUB`, `END`, `STOP` | -| Input/output | `INPUT`, `PRINT`, comma print zones, semicolon suppression, `TAB` | -| Data | `DIM`, `DATA`, `READ`, `RESTORE` | -| Numeric functions | `ABS`, `SGN`, `INT`, `SIN`, `COS`, `TAN`, `ATN`, `SQR`, `LOG`, `EXP`, `RND` | -| String functions | `LEFT$`, `RIGHT$`, `MID$`, `LEN`, `STR$`, `VAL`, `CHR$`, `ASC` | -| Other | `LET`, `DEF FN`, `REM`, colon-separated statements, and the `SLEEP` extension | - -The corpus methodology, acceptance criteria, and known upstream exception are -documented in [docs/compatibility.md](docs/compatibility.md). - -## Development - -The repository has no third-party runtime dependencies. Common workflows are: - -```bash -make fmt # format Go packages -make test # race-enabled unit and black-box CLI tests -make check # formatting, vet, coverage, and lint -make fuzz # bounded lexer and parser fuzzing -make vuln # pinned govulncheck scan -make build # build bin/go-basic -make corpus-smoke # all 112 pinned byte-distinct corpus variants -make corpus-playable # complete deterministic CLI gameplay suite -``` - -`make check` enforces an 80% total statement-coverage minimum. Tool binaries -are pinned by the Makefile and installed under the ignored `.tools/` directory; -the downloaded corpus is pinned by commit and cached under ignored `.cache/`. - -See [CONTRIBUTING.md](CONTRIBUTING.md) for the change workflow and validation -expectations. - -## Project layout - -```text -cmd/go-basic/ CLI entry point -cmd/corpus-*/ pinned corpus tooling -internal/corpus/ corpus acquisition, discovery, and bounded execution -pkg/interpreter/ lexer, parser, AST, structured lowering, and evaluator -test/ black-box CLI tests and BASIC fixtures -.github/workflows/ CI and release automation -``` - -## Releases - -Tags matching `v*` trigger the release workflow. It runs the complete quality, -fuzz, compatibility, and vulnerability gates, then publishes archives for Linux -(`amd64`, `arm64`), macOS (`amd64`, `arm64`), and Windows (`amd64`) with -`SHA256SUMS`. - -Maintainers can build and verify the same artifact set locally: - -```bash -make release-check VERSION=v0.1.0 -``` +| [Getting started](docs/getting-started.md) | Installation, CLI options, input, reproducibility, and errors | +| [How it works](docs/how-it-works.md) | Source preparation, lexing, parsing, evaluation, and package boundaries | +| [Language reference](docs/language-reference.md) | Supported syntax, statements, operators, functions, and extensions | +| [Compatibility](docs/compatibility.md) | Pinned corpus, acceptance tiers, results, and known upstream exception | +| [Contributing](CONTRIBUTING.md) | Development workflow, testing expectations, and pull requests | + +## Supported at a glance + +go-basic supports numeric and string values, arrays, arithmetic and 16-bit +logical operators, subroutines and loops, interactive input and formatted +printing, `DATA`/`READ`, user-defined numeric functions, and the common +Microsoft BASIC numeric and string function set. + +The [language reference](docs/language-reference.md) is the source of truth for +the implemented language surface. Passing the external corpus is strong +compatibility evidence, not a claim of universal support for every BASIC +dialect or hardware-specific feature. ## License diff --git a/docs/assets/go-basic-hero.jpg b/docs/assets/go-basic-hero.jpg new file mode 100644 index 0000000..9f39d29 Binary files /dev/null and b/docs/assets/go-basic-hero.jpg differ diff --git a/docs/compatibility.md b/docs/compatibility.md index 0d92d79..071670f 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -75,5 +75,6 @@ vulnerability gates before publishing artifacts. Passing the corpus is strong evidence for the supported language surface, but it is not a claim that every Microsoft BASIC dialect or hardware-specific -feature is implemented. Supported behavior is summarized in the -[README](../README.md) and enforced by the product and compatibility tests. +feature is implemented. Supported behavior is documented in the +[language reference](language-reference.md) and enforced by the product and +compatibility tests. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..ff46c86 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,98 @@ +# Getting started + +go-basic executes BASIC source files from a terminal. Prebuilt releases do not +require a Go installation; building the project from source requires Go 1.26.6 +or newer. + +## Install a release + +Open the [latest release](https://github.com/scottdensmore/go-basic/releases/latest) +and download the archive for your platform: + +| Platform | Architectures | Archive format | +| --- | --- | --- | +| Linux | `amd64`, `arm64` | `.tar.gz` | +| macOS | `amd64`, `arm64` | `.tar.gz` | +| Windows | `amd64` | `.zip` | + +Each release includes `SHA256SUMS`. Verify the downloaded archive against that +file before extracting it, then place `go-basic` or `go-basic.exe` somewhere on +your `PATH` if you want to invoke it from any directory. + +## Build from source + +Clone the repository and build the command: + +```bash +git clone https://github.com/scottdensmore/go-basic.git +cd go-basic +make build +``` + +The resulting executable is `bin/go-basic`. On Windows, build an `.exe` with: + +```text +go build -o bin\go-basic.exe ./cmd/go-basic +``` + +## Run a program + +The command accepts one BASIC source file: + +```text +go-basic [-version] [-seed number] [-max-statements number] source.bas +``` + +From a source checkout, run the included example: + +```bash +./bin/go-basic test/scripts/test.bas +``` + +On Windows, use: + +```text +bin\go-basic.exe test\scripts\test.bas +``` + +## Command options + +| Option | Purpose | +| --- | --- | +| `-version` | Print build version information and exit. | +| `-seed number` | Seed `RND` so a run can be reproduced. | +| `-max-statements number` | Stop before executing more than this many statements; `0` is unlimited. | + +The seed changes behavior only when it is explicitly supplied. A statement +limit is useful for historical programs that intentionally loop forever or +expect the user to interrupt them. + +## Input and output + +`INPUT` reads comma-separated values from standard input. Numeric variables +require numbers; names ending in `$` receive strings. Programs write to +standard output, so input and output can be redirected normally: + +```bash +./bin/go-basic game.bas < answers.txt > transcript.txt +``` + +An `INPUT` prompt defaults to `? `. Invalid input prints +`?REDO FROM START` and asks again, following classic BASIC behavior. + +## Diagnostics and exit status + +Read, source-preparation, parse, and runtime failures are written to standard +error. Parse errors identify the source position and BASIC line where possible; +runtime errors include the active BASIC line. Undefined jump targets remain +errors rather than being silently ignored. + +| Exit status | Meaning | +| --- | --- | +| `0` | The program completed successfully or `-version` was requested. | +| `1` | A file, preparation, parse, runtime, input, or output error occurred. | +| `2` | Command-line arguments were invalid. | + +For supported syntax, continue with the +[language reference](language-reference.md). To understand the execution +pipeline, see [how it works](how-it-works.md). diff --git a/docs/how-it-works.md b/docs/how-it-works.md new file mode 100644 index 0000000..ad5e04d --- /dev/null +++ b/docs/how-it-works.md @@ -0,0 +1,110 @@ +# How go-basic works + +go-basic keeps the command-line interface thin and puts language behavior in +`pkg/interpreter`. A source file moves through preparation, lexing, parsing, +and evaluation before its output reaches the terminal. + +```mermaid +flowchart LR + A["BASIC source file"] --> B["PrepareSource"] + B --> C["Lexer"] + C --> D["Parser"] + D --> E["Program AST"] + E --> F["Evaluator"] + G["stdin"] --> F + H["random source"] --> F + I["statement limit"] --> F + F --> J["stdout or a line-aware error"] +``` + +## 1. Command-line boundary + +`cmd/go-basic` owns operating-system concerns: flags, the source-file read, +standard streams, version output, and process exit status. It passes injected +input, output, randomness, and execution limits to the interpreter rather than +implementing BASIC behavior itself. + +This separation lets unit tests and the black-box CLI suite exercise the same +product implementation with deterministic dependencies. + +## 2. Source preparation + +`interpreter.PrepareSource` normally returns line-numbered Microsoft BASIC +unchanged. That preserves strict parsing and useful diagnostics for ordinary +programs. + +The pinned corpus also contains an explicitly annotated Checkers source. A +standalone `Sub_Start` marker opts that source into a small compatibility +lowering pass. The pass converts its `LOOP`/`ENDLOOP`, block +`IF`/`ENDIF`, `BREAK`, `THEN BREAK`, numeric labels, `#` comments, and `==` +equality notation into strict numbered BASIC. It then follows the same lexer, +parser, and evaluator path as every other program. + +Malformed or unclosed structured blocks fail during preparation. Merely using +the text `Sub_Start` inside a string does not enable the extension. + +## 3. Lexing + +The lexer reads the prepared source one byte at a time and emits tokens with +source line and column information. It recognizes BASIC keywords without +requiring whitespace between every token, preserves string contents, supports +numeric exponent forms, and reports illegal or unterminated input instead of +panicking. + +## 4. Parsing + +The parser builds an abstract syntax tree (AST): a `Program` containing a map +of BASIC line numbers to statements plus a sorted execution order. Duplicate +line numbers and unsupported syntax are diagnostics. + +Statements use dedicated parsing methods. Expressions use a Pratt parser so +operator precedence and right-associative exponentiation are encoded in one +place. Colon-separated statements become a sequence attached to their BASIC +line. + +## 5. Evaluation + +Before execution, the evaluator collects literal `DATA` values and flattens +statement sequences into an instruction list for each sorted BASIC line. It +then advances using a BASIC-line index and an index within that line. + +Runtime state includes: + +- case-insensitive scalar variables and typed arrays; +- a stack for nested `FOR`/`NEXT` loops; +- a return stack for `GOSUB`/`RETURN`; +- the current `DATA` read position; +- user-defined `DEF FN` functions; +- output-column tracking for comma zones, semicolons, and `TAB`; and +- injected input, random, sleep, line-observer, and statement-limit behavior. + +Control-flow statements update the execution position explicitly. A jump to a +missing line, a mismatched loop, invalid input type, out-of-range subscript, or +other runtime violation returns an error annotated with the active BASIC line. + +## Deterministic acceptance + +The ordinary test suite covers the lexer, parser, evaluator, and real CLI. Two +external-corpus tiers add broader evidence: + +- `make corpus-smoke` parses and executes all 112 pinned byte-distinct source + variants under deterministic bounds. +- `make corpus-playable` drives complete repeatable CLI scenarios and verifies + transcripts or meaningful gameplay milestones. + +The corpus is an acceptance suite rather than a runtime dependency. Its pinned +commit, inventory, termination rules, and known exception are documented in +[compatibility](compatibility.md). + +## Package map + +| Path | Responsibility | +| --- | --- | +| `cmd/go-basic/` | CLI adapter and exit behavior | +| `pkg/interpreter/` | Tokens, lexer, AST, parser, structured lowering, and evaluator | +| `cmd/corpus-*/` | Commands for fetching and running the pinned corpus | +| `internal/corpus/` | Corpus verification, discovery, and bounded execution | +| `test/` | Black-box CLI tests and BASIC fixtures | + +See the [language reference](language-reference.md) for the supported syntax +that flows through this pipeline. diff --git a/docs/language-reference.md b/docs/language-reference.md new file mode 100644 index 0000000..86f0b35 --- /dev/null +++ b/docs/language-reference.md @@ -0,0 +1,135 @@ +# Language reference + +go-basic targets the line-numbered Microsoft 8K BASIC family exercised by the +BASIC Computer Games corpus. Keywords and variable names are +case-insensitive. This document describes the implemented language surface; it +is not a claim of compatibility with every BASIC dialect. + +## Program form + +Ordinary source uses an integer line number followed by one or more statements: + +```basic +10 LET MESSAGE$="HELLO" +20 FOR I=1 TO 5 +30 PRINT MESSAGE$;" ";I +40 NEXT I +50 END +``` + +Lines execute in numeric order regardless of their order in the source file. +Duplicate line numbers are errors. Separate multiple statements on one line +with `:`. `REM` ignores the remainder of its BASIC line. `LET` is optional for +assignments. + +## Values, variables, and arrays + +| Form | Behavior | +| --- | --- | +| `A`, `TOTAL` | Numeric scalar; unset values read as `0` | +| `A$`, `NAME$` | String scalar; unset values read as an empty string | +| `A(I)`, `A(I,J)` | Numeric array reference | +| `A$(I)` | String array reference | +| `DIM A(20),B$(4,4)` | Explicit inclusive upper bounds, starting at zero | + +An array first referenced without `DIM` is created with an inclusive upper +bound of 10 in each supplied dimension. Array subscripts are truncated toward +zero after negative and non-finite values are rejected. The interpreter limits +each array to 1,000,000 elements. + +Numeric and string values do not coerce silently during assignment or +comparison. `+` concatenates two strings and adds two numbers. + +## Operators + +| Category | Operators and behavior | +| --- | --- | +| Arithmetic | `+`, `-`, `*`, `/`, right-associative `^` | +| Comparison | `=`, `<>`, `<`, `<=`, `>`, `>=` | +| Logic | `NOT`, `AND`, `OR` over signed 16-bit integer operands | +| Grouping | Parentheses | + +Comparisons return Microsoft-style truth values: `-1` for true and `0` for +false. Division by zero, non-real exponentiation, overflow, type mismatches, +and invalid logical operands are runtime errors. + +## Statements + +| Statement | Supported forms | +| --- | --- | +| Assignment | `LET A=1`, `A=1`, `A$(I)="X"` | +| Output | `PRINT`, comma print zones, semicolon suppression, adjacent items, `TAB` | +| 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` | +| Computed branch | `ON expression GOTO line,...` | +| Loops | `FOR variable=start TO end [STEP step]`, `NEXT`, `NEXT I,J` | +| Arrays | `DIM` with one or more numeric or string arrays | +| Data | `DATA`, `READ`, `RESTORE` | +| Functions | `DEF FNname(parameter)=expression` | +| Termination | `END`, `STOP` | +| Comments | `REM` | +| Extension | `SLEEP seconds` | + +`RESTORE` resets reading to the first `DATA` item. Line-targeted `RESTORE` is +not implemented. An `ON` selector outside the available positive target range +falls through without jumping; fractional or negative selectors are errors. + +## Numeric functions + +| Function | Purpose | +| --- | --- | +| `ABS(x)` | Absolute value | +| `SGN(x)` | Sign as `-1`, `0`, or `1` | +| `INT(x)` | Floor | +| `SIN(x)`, `COS(x)`, `TAN(x)` | Trigonometric functions in radians | +| `ATN(x)` | Arctangent in radians | +| `SQR(x)` | Square root | +| `LOG(x)` | Natural logarithm | +| `EXP(x)` | Natural exponential | +| `RND(x)` | Random value in `[0,1)` with classic repeat/reseed behavior | + +Supplying the CLI `-seed` option replaces the initial random source so a run +can be reproduced. + +## String functions + +| Function | Purpose | +| --- | --- | +| `LEFT$(text,n)` | Leftmost `n` bytes | +| `RIGHT$(text,n)` | Rightmost `n` bytes | +| `MID$(text,start[,length])` | One-based substring | +| `LEN(text)` | String length in bytes | +| `STR$(number)` | BASIC-formatted number converted to a string | +| `VAL(text)` | Numeric prefix converted to a number | +| `CHR$(number)` | Byte value `0..255` converted to a one-byte string | +| `ASC(text)` | Numeric value of the first byte | + +## Input and printing + +`INPUT` reads a line of comma-separated fields. It repeats the prompt after a +field-count or numeric-conversion error. A quoted prompt is followed by `? `; +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. + +## Annotated structured-source extension + +The interpreter includes a narrow source-preparation extension for an +annotated Checkers variant in the pinned corpus. A standalone `Sub_Start` +marker opts a file into lowering for `LOOP`/`ENDLOOP`, block `IF`/`ENDIF`, +`BREAK`, `THEN BREAK`, numeric labels, `#` comments, and `==` equality. + +Regular numbered BASIC does not use this lowering and remains strict. See +[how it works](how-it-works.md#2-source-preparation) for the pipeline details. + +## Compatibility boundary + +Unsupported or malformed syntax produces a diagnostic rather than being +silently omitted. Hardware-specific graphics, sound, memory access, and file +I/O are outside the currently implemented corpus-driven language surface. + +For the exact external inventory and acceptance rules, see +[compatibility](compatibility.md).