From cdaefcc941ba2a367666f78c1abd04f026a16174 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Fri, 31 Jul 2026 21:50:50 -0400 Subject: [PATCH] Write the usage guide as thirteen chapters in docs/ Organised the way the C128 Programmer's Reference Guide is: the language first, then each hardware area, then the reference sections. One markdown file per chapter. The verb and function references are generated from the interpreter's own dispatch table, with an assertion that every row is described, so they cannot drift out of step with what the program accepts. 98 verbs and 30 functions. Every example was run before it was written down, which caught three claims that were wrong: a whole FOR loop on one line prints nothing rather than looping once, MID and INSTR count from zero where a C128 counts from one, and a multi-line DEF returns a value the caller has to assign away. Chapter 13 is the list a BASIC 7.0 programmer needs -- roughly sixty documented differences, including the two known FOR defects and the fact that drawing does not survive a frame. Co-Authored-By: Claude Opus 5 (1M context) --- docs/01-introduction.md | 74 +++++++++++++ docs/02-getting-started.md | 115 +++++++++++++++++++ docs/03-the-language.md | 148 +++++++++++++++++++++++++ docs/04-control-flow.md | 176 ++++++++++++++++++++++++++++++ docs/05-strings-and-formatting.md | 128 ++++++++++++++++++++++ docs/06-graphics.md | 118 ++++++++++++++++++++ docs/07-sound.md | 94 ++++++++++++++++ docs/08-sprites.md | 130 ++++++++++++++++++++++ docs/09-files-and-disk.md | 110 +++++++++++++++++++ docs/10-embedding.md | 113 +++++++++++++++++++ docs/11-verb-reference.md | 121 ++++++++++++++++++++ docs/12-function-reference.md | 56 ++++++++++ docs/13-differences.md | 163 +++++++++++++++++++++++++++ docs/README.md | 46 ++++++++ 14 files changed, 1592 insertions(+) create mode 100644 docs/01-introduction.md create mode 100644 docs/02-getting-started.md create mode 100644 docs/03-the-language.md create mode 100644 docs/04-control-flow.md create mode 100644 docs/05-strings-and-formatting.md create mode 100644 docs/06-graphics.md create mode 100644 docs/07-sound.md create mode 100644 docs/08-sprites.md create mode 100644 docs/09-files-and-disk.md create mode 100644 docs/10-embedding.md create mode 100644 docs/11-verb-reference.md create mode 100644 docs/12-function-reference.md create mode 100644 docs/13-differences.md create mode 100644 docs/README.md diff --git a/docs/01-introduction.md b/docs/01-introduction.md new file mode 100644 index 0000000..5511d87 --- /dev/null +++ b/docs/01-introduction.md @@ -0,0 +1,74 @@ +# 1. Introduction + +## What akbasic is + +A BASIC interpreter written in C, styled after **Commodore BASIC 7.0** and +**Dartmouth BASIC**. It does three things: + +- **Runs a program from a file.** `basic program.bas` loads it, runs it, and exits. +- **Gives you a prompt.** `basic` with no arguments is a REPL: type lines with + numbers to build a program, type verbs without numbers to run them immediately. +- **Links into a program as a library.** A game can embed the interpreter, hand it a + script, and step it a frame at a time. That is the reason for most of the design + decisions you will notice. + +It is a rewrite of an earlier Go implementation, which is kept only as a reference for +questions about semantics. Its acceptance corpus is checked in here and runs on every +build. + +## What akbasic is not + +**It is not an emulator.** There is no 6502, no VIC-II, no SID, no 1541 and no +bank-switched memory. Verbs that only make sense against that hardware are either +reinterpreted for a modern machine — and Chapter 13 says exactly how — or refused by +name with the reason. Nothing is silently ignored. + +**It is not byte-compatible with a C128.** Error numbers are not Commodore's, floating +point is IEEE double rather than Commodore's five-byte format, and a handful of +statements parse differently. Again: Chapter 13. + +## Two builds + +The default build has no dependency on SDL and no graphics, sound, sprites or windowed +text. Everything else works, and the whole test suite runs on a machine with no SDL +installed at all. + +```sh +cmake -S . -B build +cmake --build build +``` + +The SDL build adds a window, the Commodore font, and the graphics, sound and sprite +devices. It needs [libakgl](https://source.starfort.tech/andrew/libakgl), which is +vendored as a submodule. + +```sh +git submodule update --init --recursive +cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON +cmake --build build-akgl +``` + +A verb that needs a device the build does not have does not crash and does not lie. It +reports itself by name: + +``` +? 10 : RUNTIME ERROR DRAW needs a graphics device and this runtime has none +``` + +That is the same message an embedded host sees when it deliberately withholds a +device — a game may want a script that can print but not draw. + +## Running the tests + +```sh +ctest --test-dir build --output-on-failure +``` + +The suite includes the original Go implementation's acceptance corpus, byte-compared, +plus this project's own tests for everything the corpus does not reach. + +## Where to go next + +Chapter 2 gets a program running. If you already write BASIC 7.0, read +**[Chapter 13](13-differences.md)** first — it is short, and it will save you the +three or four surprises that would otherwise find you one at a time. diff --git a/docs/02-getting-started.md b/docs/02-getting-started.md new file mode 100644 index 0000000..454bc13 --- /dev/null +++ b/docs/02-getting-started.md @@ -0,0 +1,115 @@ +# 2. Getting started + +## The prompt + +Run `basic` with no arguments and you get a prompt: + +``` +$ ./build/basic +READY +``` + +`READY` is printed whenever the interpreter is waiting for you, which is at startup +and after a program stops. + +Anything you type **with a line number** is stored as part of a program. Anything you +type **without** one runs immediately: + +``` +PRINT 2 + 2 + 4 +READY +``` + +## Your first program + +``` +10 PRINT "WHAT IS YOUR NAME" +20 INPUT "> " N$ +30 PRINT "HELLO, " + N$ +RUN +``` + +Type `LIST` to see it back, `RUN` to run it again, and `NEW` to throw it away. + +## Line numbers + +Lines are stored under their numbers and run in numeric order, so the gaps are what +let you insert later: + +``` +10 PRINT "FIRST" +30 PRINT "THIRD" +20 PRINT "SECOND" +LIST +10 PRINT "FIRST" +20 PRINT "SECOND" +30 PRINT "THIRD" +``` + +Typing a line number with nothing after it deletes that line. `DELETE 20-40` removes a +range, and `RENUMBER` tidies the whole program up — it rewrites every `GOTO` and +`GOSUB` to match, so it will not break your branches. + +`AUTO 10` turns on automatic numbering so you do not have to type them; `AUTO 0` turns +it off again. + +## Several statements on one line + +Statements are separated by colons: + +``` +10 A# = 1 : B# = 2 : PRINT A# + B# +``` + +There is one important limit: **block structures do not work inside a single line.** + +``` +10 FOR I# = 1 TO 3 : PRINT I# : NEXT I# +``` + +prints nothing at all. The loop body is skipped entirely, because the interpreter skips +forward a *line* at a time looking for the `NEXT` and never finds one on the line it is +already past. Write loops across several lines: + +``` +10 FOR I# = 1 TO 3 +20 PRINT I# +30 NEXT I# +``` + +The same applies to `DO`/`LOOP`. + +## Running a file + +```sh +$ ./build/basic program.bas +``` + +The file is read, stored, and run. It is exactly the same as typing the program in and +saving yourself the trouble. + +You can also pipe a program in: + +```sh +$ echo '10 PRINT "HI" +RUN' | ./build/basic +``` + +## Saving and loading + +``` +DSAVE "myprogram.bas" +DLOAD "myprogram.bas" +``` + +`SAVE` and `LOAD` are the same verbs under their other names. `VERIFY "myprogram.bas"` +compares what is in memory against the file and prints `OK` if they match. + +## Stopping + +`QUIT` ends the interpreter. `STOP` stops a *program* and returns you to the prompt, +where `CONT` resumes it from where it stopped. `END` also stops the program, but does +not arm `CONT`. + +In the SDL build, closing the window stops the program too. diff --git a/docs/03-the-language.md b/docs/03-the-language.md new file mode 100644 index 0000000..f4fd709 --- /dev/null +++ b/docs/03-the-language.md @@ -0,0 +1,148 @@ +# 3. The language + +## Variables carry their type in a suffix + +This is the first thing that will catch a C128 programmer. Every variable name ends in +a character that says what it holds: + +| Suffix | Type | Example | +|---|---|---| +| `#` | integer | `COUNT#`, `I#` | +| `%` | floating point | `RATE%`, `X%` | +| `$` | string | `NAME$` | + +``` +10 COUNT# = 42 +20 RATE% = 1.5 +30 NAME$ = "ADA" +``` + +There is no such thing as a variable with no suffix. A bare name is a **label** — see +Chapter 4 — so `GOTO DONE` and `LABEL DONE` are how the two meet. + +On a C128 the suffixes mean something different (`%` is integer, no suffix is float). +Here `%` is float and `#` is integer, following the Go implementation this was ported +from. Chapter 13 lists it with the other differences. + +Variable names are **case sensitive**. Verb and function names are not: `print`, +`Print` and `PRINT` are the same word. + +## Numbers + +Integers are 64-bit. Floats are IEEE doubles, so they print with six decimal places: + +``` +PRINT 1.5 + 1.500000 +``` + +Literals may be written in hexadecimal with a `0x` prefix. A leading zero is *not* +octal — `010` is ten, because a leading zero in a listing is far more often padding +than a base. + +## Strings + +Strings are up to 255 characters and are written in double quotes. There is no +escaping: a string cannot contain a double quote. + +`+` concatenates, and it will concatenate a string with a number: + +``` +PRINT "COUNT: " + 42 + COUNT: 42 +``` + +`*` repeats: + +``` +PRINT "-" * 20 + -------------------- +``` + +## Arrays + +`DIM` makes one. Subscripts start at zero and the number you give is the *count*, so +`DIM A#(3)` gives you `A#(0)` through `A#(2)`: + +``` +10 DIM A#(3) +20 A#(0) = 10 : A#(1) = 20 : A#(2) = 30 +30 PRINT A#(0) + A#(1) + A#(2) +``` + +Arrays can have several dimensions: `DIM GRID#(10, 10)`. `LEN(A#)` gives the total +number of elements. + +An array name used with no subscript means the whole array, which is what `SPRSAV` and +`SWAP` take. + +## Operators + +In order of precedence, tightest first: + +| Operators | Meaning | +|---|---| +| `^` | exponentiation | +| `-` (unary), `NOT` | negation, bitwise/logical not | +| `*` `/` | multiply, divide | +| `+` `-` | add and concatenate, subtract | +| `<` `<=` `>` `>=` `=` `==` `<>` | comparison | +| `AND` `OR` | bitwise, and logical | + +### `=` and `==` + +Both mean equality **inside a condition**: + +``` +10 IF A# = 5 THEN PRINT "FIVE" +20 IF A# == 5 THEN PRINT "ALSO FIVE" +``` + +Outside a condition `=` is assignment, which is why the distinction has to exist at +all. `==` works everywhere and is what the older programs in this repository use. + +### Truth + +A comparison yields **-1 for true and 0 for false**, which is Commodore's convention +and the reason `AND` and `OR` double as the logical operators: -1 is every bit set. + +Anything non-zero is true, so `IF A# THEN ...` works: + +``` +10 A# = 5 +20 IF A# THEN PRINT "NON-ZERO IS TRUE" +30 IF A# = 5 AND A# > 1 THEN PRINT "AND WORKS" +40 IF NOT (A# = 9) THEN PRINT "SO DOES NOT" +``` + +`AND` and `OR` are still bitwise on ordinary numbers: `PRINT 12 AND 10` gives `8`. + +## Comments + +`REM` comments to the end of the line. + +``` +10 REM This does nothing at all +``` + +## Functions you define yourself + +`DEF` makes a single-expression function: + +``` +10 DEF SQUARE(X#) = X# * X# +20 PRINT SQUARE(7) +``` + +A multi-line definition runs until `RETURN`, which is how you write a subroutine that +takes arguments: + +``` +10 DEF GREET(N$) +20 PRINT "HELLO, " + N$ +30 RETURN 0 +40 X# = GREET("WORLD") +``` + +`RETURN` carries the value back, so a multi-line `DEF` is a function even when you only +wanted the effect — assign the result somewhere to throw it away. diff --git a/docs/04-control-flow.md b/docs/04-control-flow.md new file mode 100644 index 0000000..947d35e --- /dev/null +++ b/docs/04-control-flow.md @@ -0,0 +1,176 @@ +# 4. Control flow + +## IF ... THEN ... ELSE + +``` +10 A# = 5 +20 IF A# = 5 THEN PRINT "FIVE" ELSE PRINT "NOT FIVE" +``` + +The condition is a whole expression, so `AND`, `OR` and `NOT` all work in one: + +``` +10 IF A# > 0 AND A# < 10 THEN PRINT "IN RANGE" +``` + +**Everything after `THEN` on the line belongs to the condition**, which matters as soon +as you put several statements on a line: + +| Line | Condition | What runs | +|---|---|---| +| `IF C THEN A : B` | true | `A`, `B` | +| `IF C THEN A : B` | false | nothing | +| `IF C THEN A ELSE B : D` | true | `A` | +| `IF C THEN A ELSE B : D` | false | `B`, `D` | + +The remainder always belongs to whichever arm was written *last*. + +### Blocks + +`BEGIN` and `BEND` make an `IF` span lines: + +``` +10 IF A# = 5 THEN BEGIN +20 PRINT "IN THE BLOCK" +30 PRINT "STILL IN IT" +40 BEND +50 PRINT "AFTER" +``` + +When the condition is false every line up to the `BEND` is skipped. + +## FOR ... NEXT + +``` +10 FOR I# = 1 TO 5 +20 PRINT I# +30 NEXT I# +``` + +`STEP` sets the stride, and a negative one counts down: + +``` +10 FOR I# = 10 TO 0 STEP -2 +20 PRINT I# +30 NEXT I# +``` + +The counter is an ordinary variable and the body may assign to it. Two things to know: + +- **The counter does not survive the loop.** It lives in the loop's own scope, so + reading it afterwards gives zero. On a C128 it keeps its final value. +- **A step that overshoots runs the body one extra time.** `FOR I = 1 TO 9 STEP 3` runs + with 1, 4, 7 *and 10*. Both are recorded as known defects; see Chapter 13. + +`EXIT` leaves the loop early: + +``` +10 FOR I# = 1 TO 100 +20 IF I# = 5 THEN EXIT +30 NEXT I# +``` + +## DO ... LOOP + +The condition can go on either end, or neither: + +``` +10 I# = 0 +20 DO WHILE I# < 3 +30 PRINT I# +40 I# = I# + 1 +50 LOOP +``` + +``` +10 I# = 0 +20 DO +30 PRINT I# +40 I# = I# + 1 +50 LOOP UNTIL I# = 3 +``` + +A condition on the `DO` is tested before the body, so the body may run zero times. A +condition on the `LOOP` is tested after, so it runs at least once. `DO` with no +condition at all loops forever until an `EXIT` or a `GOTO` leaves it. + +`EXIT` works here too. + +## GOTO and GOSUB + +``` +10 GOSUB 100 +20 PRINT "BACK" +30 END +100 PRINT "IN THE SUBROUTINE" +110 RETURN +``` + +`RETURN` goes back to the line after the `GOSUB`. + +## Labels + +A label is a name with no type suffix. It marks a line, and anything that takes a line +number takes a label instead: + +``` +10 GOTO SETUP +20 PRINT "SKIPPED" +100 LABEL SETUP +110 PRINT "ARRIVED" +``` + +**Labels are filed before the program runs**, so a forward `GOTO` works. This is worth +using for its own sake: a program written with labels is immune to `RENUMBER`, because +there is no number to rewrite. + +## ON + +`ON` picks the *n*th target from a list, counting from one: + +``` +10 ON CHOICE# GOTO 100, 200, 300 +20 PRINT "CHOICE WAS OUT OF RANGE" +``` + +Out of range is not an error — it falls through to the next statement, which is what +lets you write the check as the line after. `ON ... GOSUB` works the same way and +returns. + +## Trapping errors + +`TRAP` sends an error to a handler instead of stopping the program: + +``` +10 TRAP HANDLER +20 DIM Q#(2) +30 PRINT Q#(9) +40 PRINT "CARRIED ON" +50 END +100 LABEL HANDLER +110 PRINT "CAUGHT " + ERR(ER#) + " ON LINE " + EL# +120 RESUME NEXT +``` + +Two variables are set when the trap fires: **`ER#`** is the error code and **`EL#`** is +the line it happened on. `ERR(ER#)` gives the message text. On a C128 these are called +`ER` and `EL` with no suffix; this dialect has no bare variable names. + +`RESUME` comes in three forms: + +| Form | Where it goes | +|---|---| +| `RESUME` | back to the line that failed, and tries again | +| `RESUME NEXT` | on to the line after the one that failed | +| `RESUME (line)` | to a line you name | + +Bare `RESUME` only terminates if the handler fixed whatever was wrong — that is what +it is for. + +`TRAP` with no argument turns trapping off. An error *inside* a handler is reported +normally rather than re-entering it, so a broken handler cannot loop forever. + +## STOP, END and CONT + +`STOP` stops the program and returns to the prompt; `CONT` resumes from there. `END` +stops it without arming `CONT`. `QUIT` ends the interpreter itself. diff --git a/docs/05-strings-and-formatting.md b/docs/05-strings-and-formatting.md new file mode 100644 index 0000000..b4358ab --- /dev/null +++ b/docs/05-strings-and-formatting.md @@ -0,0 +1,128 @@ +# 5. Strings and formatting + +## The string functions + +| Function | What it gives you | +|---|---| +| `LEN(A$)` | how many characters, or how many elements in an array | +| `LEFT(A$, N#)` | the leftmost `N#` characters | +| `RIGHT(A$, N#)` | the rightmost `N#` characters | +| `MID(A$, START#, LENGTH#)` | a substring, counting from **zero** | +| `INSTR(A$, B$)` | where `B$` appears in `A$` counting from **zero**, or -1 | +| `CHR(N#)` | the character for a Unicode code point | +| `STR(N#)` | a number as a string | +| `VAL(A$)` | a string as a number | +| `HEX(N#)` | a number as hexadecimal text | +| `SPC(N#)` | that many spaces | + +`LEFT` and `RIGHT` clamp rather than failing, so asking for more characters than the +string has gives you the whole string. + +**`MID` and `INSTR` count from zero**, where a C128's `MID$` and `INSTR` count from one. +`MID("HELLO WORLD", 6, 5)` is `WORLD`, and `INSTR("HELLO WORLD", "WORLD")` is `6`. A +failed `INSTR` gives -1, not 0, so there is no ambiguity with a match at the start. + +``` +10 A$ = "HELLO WORLD" +20 PRINT LEN(A$) +30 PRINT LEFT(A$, 5) +40 PRINT RIGHT(A$, 5) +50 PRINT MID(A$, 6, 5) +60 PRINT INSTR(A$, "WORLD") +``` + +`VAL` refuses text that is not a number rather than quietly returning zero, so a +program can tell "the user typed 0" from "the user typed nonsense". + +## PRINT USING + +`PRINT USING` lays a value out in a fixed field, which is what makes columns line up. + +``` +10 PRINT USING "###.##"; 3.14159 +20 PRINT USING "TOTAL: $#,###.##"; 1234.5 +30 PRINT USING "####-"; -42 +``` + +``` + 3.14 +TOTAL: $1,234.50 + 42- +``` + +The field is built from these characters, and any text around it is printed as it +stands: + +| In the field | Means | +|---|---| +| `#` | one digit position | +| `.` | the decimal point | +| `,` | group the integer part in threes | +| `$` | a currency sign | +| `+` or `-` at the front | a sign position before the number | +| `+` or `-` at the end | a sign position after it | +| `=` | centre a string in the field | +| `>` | right-justify a string | + +**A value too wide for its field fills the field with `*`.** That is deliberate and it +is loud: printing more digits than the field asked for would push every later column +out of line. + +``` +10 PRINT USING "###"; 99999 +``` + +``` +*** +``` + +A negative number in a field with no sign position overflows the same way, because +printing `-5` as `5` would be worse. + +String fields centre and right-justify: + +``` +10 PRINT USING "=========="; "MID" +20 PRINT USING ">>>>>>>>>>"; "RIGHT" +``` + +``` + MID + RIGHT +``` + +One field per statement. `PRINT USING "### ###"; A, B` is not supported — see +Chapter 13. + +## PUDEF + +`PUDEF` changes the characters a numeric field pads and punctuates with. It takes up +to four, in this order: the leading blank, the thousands separator, the decimal point, +and the currency sign. + +``` +10 PUDEF "*" +20 PRINT USING "#####"; 42 +``` + +``` +***42 +``` + +Positions you leave out keep what they had, so the one-character form above changes +only the padding. + +## CHAR + +`CHAR` puts text at a character position rather than at the cursor: + +``` +10 CHAR 1, 10, 5, "HERE" +``` + +The arguments are colour, column, row and the text. It needs a text device with a +cursor, which means the SDL build — a terminal's cursor is not this library's to move, +and it refuses by name in the default build. + +The colour argument is accepted and ignored; the sink draws in one colour, chosen by +whoever built it. diff --git a/docs/06-graphics.md b/docs/06-graphics.md new file mode 100644 index 0000000..af1c5fb --- /dev/null +++ b/docs/06-graphics.md @@ -0,0 +1,118 @@ +# 6. Graphics + +Everything in this chapter needs the SDL build and a graphics device. Without one each +verb refuses by name: + +``` +? 10 : RUNTIME ERROR DRAW needs a graphics device and this runtime has none +``` + +## The coordinate space + +Drawing coordinates are **320 by 200**, with (0, 0) at the top left — BASIC 7.0's +hi-res screen. That is true whatever size the host's window is; stretching to fit is +the host's business, not the program's. `SCALE` maps a different range onto the same +screen. + +## Colour + +`COLOR` binds a *source* to a palette index, and the drawing verbs name the source +rather than the colour: + +``` +10 COLOR 1, 3 +20 DRAW 1, 10, 20 +``` + +Sources are numbered 0 to 6; palette indices are 1 to 16, as on a C128. That +indirection is BASIC 7.0's, and it is why every drawing verb's first argument is a +small number that is not a colour. + +## The verbs + +### GRAPHIC + +`GRAPHIC mode` chooses a screen mode; `GRAPHIC CLR` clears it. Mode 0 is text and +refuses to draw. + +### DRAW + +``` +10 DRAW 1, 10, 20 +20 DRAW 1, 0, 0 TO 100, 100 TO 200, 0 +``` + +One coordinate pair plots a point. Two or more, separated by `TO`, draw a polyline. A +bare `DRAW 1` plots wherever `LOCATE` left the pixel cursor. + +### BOX + +``` +10 BOX 1, 10, 10, 40, 40 +``` + +Corners, and an optional rotation angle. An unrotated `BOX` outlines rather than fills. + +### CIRCLE + +``` +10 CIRCLE 1, 160, 100, 50, 30 +``` + +Source, centre, then the two radii — so it draws ellipses. Further arguments give a +start angle, an end angle, a rotation and the degree increment, which is what makes it +an arc or a polygon. + +### PAINT + +``` +10 PAINT 1, 160, 100 +``` + +Flood-fills the region containing a point. If the region is too large for the fill's +own working space it stops and reports rather than leaving a half-painted screen with +no explanation. + +### LOCATE + +Moves the pixel cursor, which is where a bare `DRAW` plots and where a `BOX` with two +coordinates finishes. + +### SCALE + +``` +10 SCALE 1, 1023, 1023 +``` + +Turns on user coordinates and gives their maxima. With it on, your coordinates are +mapped onto the 320 by 200 screen. `SCALE 0` turns it off. + +### WIDTH + +`WIDTH 1` or `WIDTH 2` sets how thick a drawn line is. A thick line is drawn as +parallel passes; see Chapter 13. + +## Saving and stamping regions + +`SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back: + +``` +10 BOX 1, 0, 0, 20, 20 +20 SSHAPE A$, 0, 0, 20, 20 +30 GSHAPE A$, 100, 100 +``` + +**`A$` holds a handle, not the pixels.** On a C128 the string holds the bitmap, so a +program could save it to disk or take its `LEN`. Here a string is a fixed 255 bytes and +the region is a device surface, so what goes in the string is a reference to it — +`SHAPE:0`. You can pass it to `GSHAPE` and to `SPRSAV`, which is everything BASIC ever +does with one, but you cannot store it or measure it. + +## What is not here + +`FILTER` parses and then refuses: there is no filter stage to configure. See Chapter 7. + +The graphics verbs draw straight to the renderer rather than into a display list, so +anything drawn is overwritten by the text layer on the next frame. A program that wants +its drawing to persist has to redraw it. This is recorded as a defect rather than a +design; see Chapter 13. diff --git a/docs/07-sound.md b/docs/07-sound.md new file mode 100644 index 0000000..759bbaf --- /dev/null +++ b/docs/07-sound.md @@ -0,0 +1,94 @@ +# 7. Sound + +The sound verbs need the SDL build and an audio device. Without one they refuse by +name, and a machine with no sound card still runs the interpreter — only `SOUND` and +`PLAY` fail, and they say so. + +## SOUND + +``` +10 SOUND 1, 4000, 60 +``` + +Voice, frequency, duration. Voices are numbered from 1. The duration is in *jiffies* — +sixtieths of a second — as on a C128, so 60 is one second. + +Further arguments give a frequency sweep: a step, a direction and a range, which is +what makes a siren or a laser. + +**`SOUND` does not block.** On a C128 the note starts and the program carries on; here +the same is true, and it has to be — an embedded interpreter that stopped the host's +frame loop for a second would freeze the game. + +## PLAY + +`PLAY` takes a string of notes: + +``` +10 PLAY "C D E F G" +``` + +Within the string: + +| | | +|---|---| +| `A` to `G` | a note | +| `#` `$` | sharp, flat | +| `O` *n* | octave | +| `V` *n* | voice | +| `T` *n* | envelope preset | +| `U` *n* | volume | +| `W`, `H`, `Q`, `I`, `S` | whole, half, quarter, eighth, sixteenth notes | +| `R` | rest | +| `.` | dotted | + +Notes are queued and released in time, paced by the host's clock. A program that +`PLAY`s and then carries on works the way you expect; a program that `PLAY`s and then +immediately `QUIT`s may not hear all of it. + +`M` — measure — is accepted and does nothing. There is no bar-line accounting to do. + +## TEMPO + +``` +10 TEMPO 8 +``` + +How fast `PLAY` releases its queue. The number is the same range a C128 uses; the +constant that turns it into milliseconds is a calibration choice rather than a +transcription, so it may not match a real machine exactly. + +## ENVELOPE and VOL + +``` +10 ENVELOPE 0, 5, 9, 4, 6 +20 VOL 8 +``` + +`ENVELOPE` defines one of the ten presets `PLAY`'s `T` selects: attack, decay, sustain +and release. `VOL` sets the overall volume, 0 to 15. + +## FILTER + +`FILTER` parses and then **refuses at execution**: + +``` +? 10 : RUNTIME ERROR FILTER needs an audio device that can filter, and this one cannot +``` + +It sets the SID's filter cutoff, band switches and resonance. The audio backend +synthesises raw waveforms and mixes them; there is no filter stage to configure and +SDL supplies no primitive to build one from. Refusing is deliberate — a program that +asks for a low-pass and gets an unfiltered square wave has been lied to. + +This is the one verb still waiting on a capability from the graphics library. It is +filed there rather than worked around here. + +## The clock + +Everything with a duration — `SOUND`, `PLAY`, `TEMPO`, and `SLEEP` from Chapter 2's +neighbourhood — is paced by a clock the *host* provides. The standalone interpreter +sets it from a monotonic timer every step, so it just works. + +An embedded host that never sets the clock gets durations that expire immediately: +audible, but never a hang. That is the intended failure direction. diff --git a/docs/08-sprites.md b/docs/08-sprites.md new file mode 100644 index 0000000..7a83799 --- /dev/null +++ b/docs/08-sprites.md @@ -0,0 +1,130 @@ +# 8. Sprites + +Eight sprites, numbered 1 to 8, as on a C128. They need the SDL build. + +A sprite here is a real object in the host's graphics library, registered alongside +whatever the host's own game is drawing — so a game that embeds this interpreter can +see and manipulate a script's sprites. + +## Giving a sprite a picture + +`SPRSAV` does it, and it takes three kinds of source. + +### From an image file + +``` +10 SPRSAV "ship.png", 1 +``` + +The most useful form, and not one a C128 has. The path is tried against the working +directory first and then against the directory the running program was loaded from — so +a `.bas` stored beside its artwork works wherever you launch it from. Anything the +image library can decode will do. + +A sprite loaded this way takes **the image's own size**. It is not forced to 24 by 21. + +### From a saved region + +``` +10 BOX 1, 0, 0, 24, 21 +20 SSHAPE A$, 0, 0, 24, 21 +30 SPRSAV A$, 1 +``` + +Draw it with the graphics verbs, capture it, install it. The C128 documents `SPRSAV`'s +string as the `SSHAPE` data format at a fixed 24 by 21, so sharing the mechanism is +faithful rather than a shortcut. + +### From data + +``` +10 DIM P#(63) +20 FOR I# = 0 TO 62 +30 READ P#(I#) +40 NEXT I# +50 SPRSAV P#, 1 +60 DATA 255, 129, 129, ... +``` + +63 bytes: three per row, twenty-one rows, most significant bit leftmost. This is the +form a type-in listing uses. + +**It takes an integer array, not a string.** A C128 puts the raw bytes in a string; a +string here is NUL-terminated, so it cannot hold a zero byte and therefore cannot hold +a sprite. `DIM P#(63)` is exactly the right size. + +Writing a sprite back out — `SPRSAV 1, A$` — is refused. That would be a disk +operation. + +## Showing and moving + +``` +10 SPRITE 1, 1, 3 +20 MOVSPR 1, 100, 50 +``` + +`SPRITE n [,on] [,colour] [,priority] [,xexpand] [,yexpand] [,multicolour]`. Only the +number is required and **an argument you leave out is left alone**, so `SPRITE 1, 1` +turns sprite 1 on without disturbing its colour. + +`MOVSPR` has four forms, and the punctuation is what tells them apart: + +| Form | What it does | +|---|---| +| `MOVSPR 1, 100, 50` | put it at (100, 50) | +| `MOVSPR 1, +10, -20` | move it *by* that much | +| `MOVSPR 1, 10 ; 90` | move it 10 pixels along bearing 90 | +| `MOVSPR 1, 45 # 8` | set it moving along bearing 45 at speed 8 | + +A bearing is degrees **clockwise from straight up**, so 0 is north and 90 is east. In +the polar form the distance comes first and the angle second — the opposite order from +the continuous form, which is a trap worth remembering. + +The continuous form does not move anything on the statement itself. The sprite moves as +the program runs, paced by the host's clock. Speed 0 stops it. + +Coordinates are the same 320 by 200 space the drawing verbs use, not the VIC-II's +raster coordinates. + +## Collision + +``` +10 COLLISION 1, BUMPED +20 REM ... main loop ... +90 GOTO 20 +100 LABEL BUMPED +110 PRINT "HIT: " + BUMP(1) +120 RETURN +``` + +`COLLISION 1, target` calls a subroutine when two sprites overlap. The handler is +entered between lines and must end in `RETURN`, exactly like a `GOSUB` body. Omitting +the target disarms it. + +`BUMP(1)` returns a bitmask of which sprites have collided — bit 0 is sprite 1 — and +**reading it clears it**, which is what makes "has anything hit me since I last looked" +answerable. + +Only type 1, sprite-to-sprite, is implemented. Types 2 and 3 are refused by name. + +Collision is by bounding box, not by pixel: two sprites whose boxes overlap but whose +artwork does not are reported as colliding. + +## Reading state back + +| Function | Gives | +|---|---| +| `RSPPOS(n, 0)` | x | +| `RSPPOS(n, 1)` | y | +| `RSPPOS(n, 2)` | speed | +| `RSPRITE(n, f)` | one of `SPRITE`'s settings, in `SPRITE`'s own argument order | +| `RSPCOLOR(1)` or `RSPCOLOR(2)` | one of the shared multicolour registers | + +These read the interpreter's own state rather than asking the device, so they work +even with no device attached. + +## SPRDEF + +Not implemented, and deliberately. It is an interactive full-screen sprite editor +driven by single keystrokes, not something a program can call — and this interpreter +does not own the screen it would take over. The three `SPRSAV` forms replace it. diff --git a/docs/09-files-and-disk.md b/docs/09-files-and-disk.md new file mode 100644 index 0000000..91dc59b --- /dev/null +++ b/docs/09-files-and-disk.md @@ -0,0 +1,110 @@ +# 9. Files and disk + +There is no 1541. The disk verbs work against the filesystem where that means +something, and refuse by name where it does not. + +## Program storage + +``` +DSAVE "myprogram.bas" +DLOAD "myprogram.bas" +VERIFY "myprogram.bas" +``` + +`SAVE` and `LOAD` are the same verbs under their other names, and `DVERIFY` is +`VERIFY`. A program is saved as plain text with its line numbers, so you can edit it in +anything. + +`VERIFY` compares what is in memory against the file and prints `OK`, or reports how +many lines differ. + +## Files + +Ten channels, numbered 0 to 9. + +``` +10 DOPEN 1, "scores.txt", W +20 PRINT #1, "ADA 4000" +30 PRINT #1, "GRACE 3800" +40 DCLOSE 1 +50 DOPEN 2, "scores.txt" +60 INPUT #2, LINE$ +70 PRINT LINE$ +80 DCLOSE 2 +``` + +| Verb | What it does | +|---|---| +| `DOPEN n, "name"` | open for reading | +| `DOPEN n, "name", W` | open for writing | +| `APPEND n, "name"` | open for writing at the end | +| `DCLOSE n` | close one channel | +| `DCLOSE` | close all of them | +| `PRINT #n, expr` | write a line | +| `INPUT #n, VAR` | read a line | +| `RECORD n, r` | position at record `r` | + +**Note the space before the `#`.** A C128 writes `PRINT#1,A$`; here the `#` has to be +its own word, because `PRINT#` would otherwise scan as a variable name. See Chapter 13. + +Reading past the end of a file leaves the variable empty rather than raising, so a read +loop tests what it got: + +``` +10 DOPEN 1, "data.txt" +20 DO +30 INPUT #1, L$ +40 IF L$ = "" THEN EXIT +50 PRINT L$ +60 LOOP +70 DCLOSE 1 +``` + +Reading a channel opened for writing — or writing one opened for reading — is refused, +as is using a channel that is not open. + +`RECORD` counts *lines*, not fixed-length records: a filesystem file has no record +length to seek by, so it rewinds and reads forward. + +## Managing files + +``` +10 COPY "a.txt", "b.txt" +20 CONCAT "a.txt", "b.txt" +30 RENAME "b.txt", "c.txt" +40 SCRATCH "c.txt" +``` + +`COPY` duplicates, `CONCAT` appends one file to another, `RENAME` moves, `SCRATCH` +deletes. Deleting a file that is not there is reported rather than ignored. + +## Binary blocks + +``` +10 A$ = "BINARY DATA" +20 BSAVE "block.dat", POINTER(A$), POINTER(A$) + 12 +30 BLOAD "block.dat", POINTER(B$), 12 +``` + +`BSAVE` writes a range of memory and `BLOAD` reads one back. **`BLOAD` requires a +length**, unlike a C128's, because the address is a real address in this process and a +file longer than you expected would write past whatever you pointed at. + +## What is refused, and why + +| Verb | Why not | +|---|---| +| `HEADER` | formats a disk. On a filesystem that would have to mean "delete everything here" | +| `COLLECT` | validates a disk's block allocation map. There is no map | +| `BACKUP` | duplicates one disk onto another. There are no disks | +| `BOOT` | loads and runs a boot sector. There is no boot sector | +| `DIRECTORY` / `CATALOG` | needs a directory-reading wrapper the standard library does not have yet. Filed upstream | + +`DCLEAR` is the exception among the drive verbs: resetting a drive also closes its +channels, and closing the channels is real, so that is what it does. + +Each refusal names itself and says why: + +``` +? 10 : RUNTIME ERROR HEADER formats a disk, and there is no disk drive here -- only a filesystem +``` diff --git a/docs/10-embedding.md b/docs/10-embedding.md new file mode 100644 index 0000000..919e051 --- /dev/null +++ b/docs/10-embedding.md @@ -0,0 +1,113 @@ +# 10. Embedding + +The whole design of this interpreter is shaped by one requirement: a game must be able +to embed it as a scripting engine without giving up control. That produces four rules, +and they explain most of what looks unusual elsewhere in this guide. + +1. **Nothing in the library terminates the process.** Errors come back as + `akerr_ErrorContext *` for you to handle. +2. **The interpreter owns no window, no renderer and no event loop.** It draws through + whatever you already created. +3. **It never blocks.** `SLEEP`, `GETKEY`, `PLAY` and `WAIT` hold the program without + holding your frame rate. +4. **You can bound it.** A script with `10 GOTO 10` cannot take your game with it. + +## Linking + +```cmake +add_subdirectory(deps/akbasic EXCLUDE_FROM_ALL) +target_link_libraries(YOUR_GAME PRIVATE akbasic::akbasic) +``` + +| Target | What it is | Link it? | +|---|---|---| +| `akbasic` | The interpreter. No SDL, nothing that exits. | Always | +| `akbasic_akgl` | The graphics, sound, input and sprite backends, drawing through *your* renderer | If you want them | +| `akbasic_frontend` | The standalone program's host: creates the window, owns the loop | **No.** You are the host | + +## The shortest useful host + +```c +#include +#include + +static akbasic_Runtime RUNTIME; /* too big for a stack */ +static akbasic_TextSink SINK; +static akbasic_StdioSink SINKSTATE; + +akerr_ErrorContext AKERR_NOIGNORE *run_script(const char *source) +{ + PREPARE_ERROR(e); + + PASS(e, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL)); + PASS(e, akbasic_runtime_init(&RUNTIME, &SINK)); + PASS(e, akbasic_runtime_load(&RUNTIME, source)); + PASS(e, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN)); + + while ( RUNTIME.mode != AKBASIC_MODE_QUIT ) { + PASS(e, akbasic_runtime_settime(&RUNTIME, your_clock_ms())); + PASS(e, akbasic_runtime_run(&RUNTIME, 256)); /* 256 steps, then return */ + your_draw_a_frame(); + } + SUCCEED_RETURN(e); +} +``` + +`akbasic_runtime_run(rt, n)` runs at most `n` steps and returns. That bound is what +keeps a runaway script from owning your process. + +`akbasic_runtime_settime()` is how the interpreter knows what time it is. It reads no +clock of its own, because it owns no loop. If you never call it, every duration expires +immediately — audible, but never a hang. + +## Exchanging variables with a script + +Use `akbasic_runtime_global()`. It finds or creates the variable in the script's +outermost scope, which is the only place both of you can reliably see: + +```c +akbasic_Variable *health = NULL; +int64_t subscript[1] = { 0 }; + +PASS(e, akbasic_runtime_global(&RUNTIME, "HEALTH#", &health)); +PASS(e, akbasic_variable_set_integer(health, 100, subscript, 1)); +``` + +Do not reach for `akbasic_environment_get()`. A script suspended part-way through a +bounded run is usually inside a `FOR` or `GOSUB` body, and a variable created there +dies when the body pops — silently, with the script reading it correctly right up until +it stops. + +## Lending devices + +```c +PASS(e, akbasic_runtime_set_devices(&RUNTIME, &graphics, &audio, &input, &sprites)); +``` + +Any of them may be `NULL`, and that is how you withhold a capability: a script given no +audio backend gets an error from `SOUND` rather than silence. Each is a record of +function pointers, so you can supply your own and never link the graphics library at +all. + +`akbasic_akgl` provides implementations that draw through a renderer *you* created: + +```c +PASS(e, akbasic_graphics_init_akgl(&graphics, &gstate, my_renderer)); +PASS(e, akbasic_sprite_init_akgl(&sprites, &sstate, my_renderer, &gstate)); +``` + +Sprites become real actors in your registry, so your game can see them. + +## Where a script's errors go + +A BASIC-level error is reported through the sink and stops the script; it does not come +back to you as a failure. What comes back to you is an error in the *interpreter* — +pool exhaustion, a NULL argument — which is yours to handle. + +That is the split to hold on to: a script's mistakes are the script's problem, and your +program keeps running. + +## Reading it all + +`README.md` in the repository root carries the full API surface, the pool limits, and +the exact list of what each target pulls in. diff --git a/docs/11-verb-reference.md b/docs/11-verb-reference.md new file mode 100644 index 0000000..1808845 --- /dev/null +++ b/docs/11-verb-reference.md @@ -0,0 +1,121 @@ +# 11. Verb reference + +Every statement, alphabetically. This list is generated from the interpreter's own +dispatch table, so it cannot drift out of step with what the program actually accepts. + +A verb marked **Refused** parses and then reports why it cannot run — see Chapter 13 +for the reasoning in each case. + +| Verb | Form | What it does | +|---|---|---| +| `APPEND` | `APPEND n, "name"` | Open a file on channel `n` for writing at its end. | +| `AUTO` | `AUTO n` | Number lines automatically in steps of `n`. `AUTO 0` turns it off. | +| `BACKUP` | `BACKUP` | **Refused.** Duplicates one disk onto another; there are no disks. | +| `BEGIN` | `IF c THEN BEGIN` | Start a block that runs to `BEND`, so an `IF` can span lines. | +| `BEND` | `BEND` | End a `BEGIN` block. | +| `BLOAD` | `BLOAD "name", addr, len` | Read a file into memory. The length is required. | +| `BOOT` | `BOOT` | **Refused.** Loads and runs a boot sector; there is none. | +| `BOX` | `BOX src, x1, y1, x2, y2 [,angle]` | Outline a rectangle, optionally rotated. | +| `BSAVE` | `BSAVE "name", from, to` | Write a range of memory to a file. | +| `CATALOG` | `CATALOG` | **Refused.** The other name for `DIRECTORY`. | +| `CHAR` | `CHAR col, x, y, "text"` | Put text at a character cell. Needs a sink with a cursor. | +| `CIRCLE` | `CIRCLE src, x, y, rx, ry [,...]` | Draw an ellipse, arc or polygon. | +| `CLR` | `CLR` | Drop every variable and function, keeping the program. | +| `COLLECT` | `COLLECT` | **Refused.** Validates a disk's block allocation map. | +| `COLLISION` | `COLLISION 1 [,target]` | Call a subroutine when sprites collide. No target disarms it. | +| `COLOR` | `COLOR src, index` | Bind a colour source to a palette index, 1 to 16. | +| `CONCAT` | `CONCAT "a", "b"` | Append file `a` to file `b`. | +| `CONT` | `CONT` | Resume a program stopped by `STOP`. | +| `COPY` | `COPY "a", "b"` | Copy file `a` to file `b`. | +| `DATA` | `DATA v [, ...]` | Declare values for `READ`. Collected before the program runs. | +| `DCLEAR` | `DCLEAR` | Close every open channel. The half of a drive reset that means something. | +| `DCLOSE` | `DCLOSE [n]` | Close channel `n`, or every channel. | +| `DEF` | `DEF NAME(args) = expr` | Define a function. Multi-line definitions end in `RETURN`. | +| `DELETE` | `DELETE [n][-n]` | Delete lines, with the same range forms as `LIST`. | +| `DIM` | `DIM A#(n [,...])` | Make an array. Subscripts start at zero; `n` is the count. | +| `DIRECTORY` | `DIRECTORY` | **Refused.** Needs a directory-reading wrapper that does not exist yet. | +| `DLOAD` | `DLOAD "name"` | Load a program from a file. | +| `DO` | `DO [WHILE c | UNTIL c]` | Start a loop. The condition may be here, on the `LOOP`, or neither. | +| `DOPEN` | `DOPEN n, "name" [,W]` | Open a file on channel `n`. `W` opens it for writing. | +| `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. | +| `DSAVE` | `DSAVE "name"` | Save the program to a file. | +| `DVERIFY` | `DVERIFY "name"` | The other name for `VERIFY`. | +| `END` | `END` | Stop the program. Does not arm `CONT`. | +| `ENVELOPE` | `ENVELOPE n, a, d, s, r` | Define one of `PLAY`'s ten envelope presets. | +| `EXIT` | `EXIT` | Leave the innermost `FOR` or `DO` loop. | +| `FETCH` | `FETCH count, from, to` | Copy bytes. The same as `STASH`; there is no expansion RAM. | +| `FILTER` | `FILTER ...` | **Refused.** There is no filter stage in the audio backend. | +| `FOR` | `FOR V = a TO b [STEP c]` | Start a counted loop, ended by `NEXT`. | +| `GET` | `GET V` | Take a keystroke if one is waiting, without stopping. | +| `GETKEY` | `GETKEY V` | Wait for a keystroke, holding the program but not the host. | +| `GOSUB` | `GOSUB line` | Call a subroutine, returning on `RETURN`. | +| `GOTO` | `GOTO line` | Jump to a line or a label. | +| `GRAPHIC` | `GRAPHIC mode | CLR` | Choose a screen mode, or clear it. | +| `GSHAPE` | `GSHAPE A$, x, y` | Stamp a region saved by `SSHAPE`. | +| `HEADER` | `HEADER "name"` | **Refused.** Formats a disk. | +| `HELP` | `HELP` | Re-list the line the last error happened on. | +| `IF` | `IF c THEN s [ELSE s]` | Branch. Everything after `THEN` belongs to the condition. | +| `INPUT` | `INPUT ["prompt"] V` | Read a line from the user. | +| `INPUT#` | `INPUT #n, V` | Read a line from a channel. | +| `KEY` | `KEY [n, "text"]` | Define a function-key macro, or list them all. | +| `LABEL` | `LABEL NAME` | Mark this line with a name any branch can use. | +| `LET` | `LET V = expr` | Assign. Optional; assignment needs no verb. | +| `LIST` | `LIST [n][-n]` | List the program, or part of it. | +| `LOAD` | `LOAD "name"` | The other name for `DLOAD`. | +| `LOCATE` | `LOCATE x, y` | Move the pixel cursor. | +| `LOOP` | `LOOP [WHILE c | UNTIL c]` | End a `DO` loop. | +| `MOVSPR` | `MOVSPR n, ...` | Move a sprite. Four forms; see Chapter 8. | +| `NEW` | `NEW` | Erase the program and every variable. | +| `NEXT` | `NEXT V` | End a `FOR` loop and advance its counter. | +| `ON` | `ON e GOTO|GOSUB t [,...]` | Branch to the `e`th target, counting from one. | +| `PAINT` | `PAINT src, x, y` | Flood-fill the region containing a point. | +| `PLAY` | `PLAY "notes"` | Queue notes. Does not block. | +| `POKE` | `POKE addr, byte` | Write a byte to a real address. | +| `PRINT` | `PRINT [expr]` | Print a value and a newline. | +| `PRINT#` | `PRINT #n, expr` | Write a line to a channel. | +| `PUDEF` | `PUDEF "chars"` | Redefine what `PRINT USING` pads and punctuates with. | +| `QUIT` | `QUIT` | End the interpreter. | +| `READ` | `READ V [,...]` | Fill variables from the next `DATA` items. | +| `RECORD` | `RECORD n, r [,pos]` | Position a channel at record `r`. Records are lines. | +| `RENAME` | `RENAME "a", "b"` | Rename a file. | +| `RENUMBER` | `RENUMBER [start [,step [,from]]]` | Renumber lines, rewriting every branch to match. | +| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. | +| `RESUME` | `RESUME [NEXT | line]` | Return from a `TRAP` handler. | +| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. | +| `RUN` | `RUN [line]` | Run the program, optionally from a line. | +| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. | +| `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. | +| `SCNCLR` | `SCNCLR` | Clear the text screen. | +| `SCRATCH` | `SCRATCH "name"` | Delete a file. | +| `SLEEP` | `SLEEP seconds` | Pause. Holds the program, not the host. | +| `SOUND` | `SOUND v, freq, dur [,...]` | Play a tone on a voice. Does not block. | +| `SPRCOLOR` | `SPRCOLOR [c1] [,c2]` | Set the two shared multicolour registers. | +| `SPRITE` | `SPRITE n [,on] [,col] [,...]` | Configure a sprite. Omitted arguments are left alone. | +| `SPRSAV` | `SPRSAV source, n` | Give sprite `n` a picture. Three source forms; see Chapter 8. | +| `SSHAPE` | `SSHAPE A$, x1, y1 [,x2, y2]` | Save a screen region; `A$` receives a handle. | +| `STASH` | `STASH count, from, to` | Copy bytes. The same as `FETCH`. | +| `STOP` | `STOP` | Stop the program; `CONT` resumes it. | +| `SWAP` | `SWAP A, B` | Exchange two variables of the same type. | +| `SYS` | `SYS addr` | **Refused.** There is no 6502 and no ROM to call. | +| `TEMPO` | `TEMPO n` | How fast `PLAY` releases its queue. | +| `TRAP` | `TRAP [target]` | Send errors to a handler. No target disarms it. | +| `TROFF` | `TROFF` | Turn line tracing off. | +| `TRON` | `TRON` | Turn line tracing on; each line prints its number in brackets. | +| `VERIFY` | `VERIFY "name"` | Compare the program in memory against a file. | +| `VOL` | `VOL n` | Set the overall volume, 0 to 15. | +| `WAIT` | `WAIT addr, mask [,xor]` | Poll a byte until it matches. Holds the program. | +| `WIDTH` | `WIDTH 1|2` | How thick a drawn line is. | +| `WINDOW` | `WINDOW l, t, r, b [,clear]` | Constrain the text area. Needs a sink with a grid. | + +## Words that are not verbs + +`AND`, `ELSE`, `NOT`, `OR`, `REM`, `STEP`, `THEN`, `TO`, `UNTIL`, `USING` and `WHILE` +are reserved, but none of them is a statement on its own — each is consumed by the verb +it belongs to. + +## Deliberately absent + +`BANK`, `FAST`, `MONITOR` and `SPRDEF` have no table entry at all. The first three are +incompatible with a modern machine — there is no bank switching, no CPU speed to +control, and no machine-language monitor to drop into. `SPRDEF` is an interactive +full-screen editor rather than a programmable verb. diff --git a/docs/12-function-reference.md b/docs/12-function-reference.md new file mode 100644 index 0000000..bb7ddc3 --- /dev/null +++ b/docs/12-function-reference.md @@ -0,0 +1,56 @@ +# 12. Function reference + +Every function, alphabetically, generated from the interpreter's own dispatch table. + +A function is called with parentheses and yields a value; it cannot stand alone as a +statement. The arity column is how many arguments it takes — the interpreter checks it, +so a call with the wrong number is a syntax error rather than a surprise. + +| Function | Args | Form | What it gives | +|---|---|---|---| +| `ABS` | 1 | `ABS(n)` | The absolute value of an integer or float. | +| `ATN` | 1 | `ATN(n)` | Arctangent, in radians. | +| `BUMP` | 1 | `BUMP(1)` | Which sprites have collided, as a bitmask. **Reading clears it.** | +| `CHR` | 1 | `CHR(n)` | The character for a Unicode code point, as a string. | +| `COS` | 1 | `COS(n)` | Cosine, in radians. | +| `ERR` | 1 | `ERR(n)` | The message text for an error code. `ERR(ER#)` names the last one. | +| `HEX` | 1 | `HEX(n)` | An integer as hexadecimal text. | +| `INSTR` | 2 | `INSTR(A$, B$)` | Where `B$` appears in `A$`, counting from zero, or -1. | +| `LEFT` | 2 | `LEFT(A$, n)` | The leftmost `n` characters. Clamped to the string's length. | +| `LEN` | 1 | `LEN(x)` | The length of a string, or the element count of an array. | +| `LOG` | 1 | `LOG(n)` | Natural logarithm. | +| `MID` | 3 | `MID(A$, start, len)` | A substring, counting from zero. | +| `MOD` | 2 | `MOD(a, b)` | The remainder of `a / b`. Integers only. | +| `PEEK` | 1 | `PEEK(addr)` | The byte at a real address. | +| `POINTER` | 1 | `POINTER(V)` | The address of a variable's value. | +| `POINTERVAR` | 1 | `POINTERVAR(V)` | The address of the variable structure itself, metadata included. | +| `RAD` | 1 | `RAD(n)` | Degrees converted to radians. | +| `RIGHT` | 2 | `RIGHT(A$, n)` | The rightmost `n` characters. Clamped. | +| `RSPCOLOR` | 1 | `RSPCOLOR(n)` | One of `SPRCOLOR`'s two shared registers, 1 or 2. | +| `RSPPOS` | 2 | `RSPPOS(n, f)` | A sprite's x (0), y (1) or speed (2). | +| `RSPRITE` | 2 | `RSPRITE(n, f)` | One of a sprite's `SPRITE` settings, in `SPRITE`'s argument order. | +| `SGN` | 1 | `SGN(n)` | -1, 0 or 1 according to the sign. | +| `SHL` | 2 | `SHL(n, bits)` | Shift left. | +| `SHR` | 2 | `SHR(n, bits)` | Shift right. | +| `SIN` | 1 | `SIN(n)` | Sine, in radians. | +| `SPC` | 1 | `SPC(n)` | A string of `n` spaces. | +| `STR` | 1 | `STR(n)` | A number as a string. | +| `TAN` | 1 | `TAN(n)` | Tangent, in radians. | +| `VAL` | 1 | `VAL(A$)` | A string as a number. Refuses text that is not one. | +| `XOR` | 2 | `XOR(a, b)` | Bitwise exclusive or. | + +## Reserved variables + +Two things a C128 exposes as bare names are ordinary global variables here, because +this dialect has no variable name without a type suffix: + +| Here | On a C128 | Holds | +|---|---|---| +| `ER#` | `ER` | the code of the last trapped error | +| `EL#` | `EL` | the line it happened on | +| `TI#` | `TI` | jiffies — sixtieths of a second — since the host's clock started | +| `TI$` | `TI$` | the same time as `HHMMSS` | + +`ER#` carries this interpreter's error code, not a Commodore error number. There is no +correspondence to reproduce — the errors raised here are not the errors a 1985 ROM +raised — so print `ERR(ER#)` rather than the number. diff --git a/docs/13-differences.md b/docs/13-differences.md new file mode 100644 index 0000000..cb9e4ba --- /dev/null +++ b/docs/13-differences.md @@ -0,0 +1,163 @@ +# 13. Differences from BASIC 7.0 + +If you already write Commodore BASIC, this is the chapter to read first. Everything +here is deliberate, and everything is recorded in the repository's `TODO.md` with the +reasoning — this is the short version. + +## The language + +### Variables carry a type suffix, and the suffixes differ + +| | Integer | Float | String | +|---|---|---|---| +| C128 | `A%` | `A` | `A$` | +| akbasic | `A#` | `A%` | `A$` | + +There is **no such thing as an unsuffixed variable**. A bare name is a label. + +### `=` works in a condition, and `==` works everywhere + +`IF A = 5 THEN` does what you expect. `==` also means equality and is what the older +programs in this repository use. Outside a condition `=` is assignment, as always. + +### `AND`, `OR` and `NOT` in conditions + +These work, and a condition is a whole expression rather than a single comparison, so +`IF A = 1 AND B = 2 THEN` parses. Truth is nonzero, so `IF A THEN` works too. + +### `MID` and `INSTR` count from zero + +A C128 counts from one. A failed `INSTR` gives -1 rather than 0. + +### `THEN` needs a verb + +`IF X THEN 100` is not a jump. Write `IF X THEN GOTO 100`. + +### Strings are 255 characters and cannot contain a quote + +There is no escape character. + +### Numbers + +Integers are 64-bit and floats are IEEE doubles, so `PRINT 1.5` gives `1.500000`. A +leading zero is not octal; `0x` is hexadecimal. + +## Block structure + +**A whole loop on one line does not loop.** + +``` +10 FOR I# = 1 TO 3 : PRINT I# : NEXT I# +``` + +prints nothing. Block skipping walks source *lines*, so a `NEXT` on the same line as +its `FOR` is never reached. The same applies to `DO`/`LOOP`. Write loops across lines. + +## Two known defects in `FOR` + +Both are recorded, both have tests asserting the correct behaviour, and both are +waiting on a decision rather than on work: + +- **A step that overshoots runs the body one extra time.** `FOR I = 1 TO 9 STEP 3` runs + with 1, 4, 7 *and 10*. +- **`FOR I = 1 TO 1` does not run its body at all**, where every other BASIC runs it + once. + +The two errors cancel out for a step of 1, which is why they went unnoticed. Fixing +them would change the output of a checked-in acceptance file, which is not something +this project does silently. + +**A loop counter does not survive its loop.** It lives in the loop's own scope, so +reading it afterwards gives zero. + +## Direct mode + +A statement typed with no line number runs immediately, as it should. This was not +true until recently — the interpreter used to file everything but a handful of verbs as +program text. + +## Errors + +`ER` and `EL` are **`ER#` and `EL#`**, ordinary global variables. `ER#` holds this +interpreter's error code, which bears no relation to a Commodore error number. Print +`ERR(ER#)` for the text. + +## Graphics + +- **Coordinates are always 320 by 200**, whatever the window size. +- **`SSHAPE` puts a handle in the string, not the pixels.** You can pass it to `GSHAPE` + and `SPRSAV`; you cannot save it or take its `LEN`. +- **`WIDTH` is emulated** by drawing parallel passes. +- **Drawing does not persist across frames.** The verbs draw straight to the renderer, + so anything drawn is overwritten on the next frame unless the program redraws it. + This is a defect, not a design. +- **`CHAR` ignores its colour argument** and needs a text device with a cursor. + +## Sound + +- **`PLAY` and `SOUND` do not block.** The statement after them runs immediately. +- **`FILTER` is refused.** There is no filter stage to configure. +- **`PLAY`'s `M` is accepted and does nothing.** +- **`TEMPO`'s calibration is a choice**, not a transcription. + +## Sprites + +- **Coordinates are the 320 by 200 drawing space**, not the VIC-II's raster space. +- **`SPRSAV` takes an integer array**, not a string, for the data form — a string here + cannot hold a zero byte. It also takes an **image file path**, which a C128 cannot. +- **A sprite loaded from a file keeps the image's own size**, not 24 by 21. +- **`MOVSPR`'s speed unit is a choice.** The manual does not say what a unit is worth. +- **Collision is by bounding box**, not by pixel, and only type 1 exists. +- **Priority and multicolour are recorded but not drawn.** +- **`SPRDEF` is out of scope.** + +## Files + +- **`PRINT #` and `INPUT #` need a space before the `#`.** `PRINT#1` scans as a + variable name. +- **`RECORD` counts lines**, not fixed-length records. +- **`BLOAD` requires a length.** +- **`HEADER`, `COLLECT`, `BACKUP` and `BOOT` are refused.** They operate on a physical + disk. +- **`DIRECTORY` is refused** pending a wrapper in the standard library. + +## Machine + +- **`SYS` is refused.** There is no 6502 and no ROM. +- **`FETCH` and `STASH` are the same byte copy.** There is no expansion RAM to tell + them apart. +- **`POKE`, `PEEK` and `POINTER` use real process addresses.** A wrong one is a + segmentation fault, not an error message. +- **`BANK`, `FAST` and `MONITOR` do not exist.** + +## Formatting + +- **`PRINT USING` renders one field per statement.** `PRINT USING "### ###"; A, B` is + not supported. +- **Exponential fields (`^^^^`) are not implemented.** + +## Console + +- **`SLEEP` and `WAIT` hold the program without blocking the host.** `SLEEP` with no + host clock does nothing at all rather than waiting forever. +- **`TI` and `TI$` are `TI#` and `TI$`**, refreshed once per step. +- **`WAIT` polls ordinary process memory.** Nothing changes it but the host. +- **`KEY` stores macros and nothing expands them.** + +## Limits + +| | | +|---|---| +| Source lines | 9999 | +| Line length | 255 | +| String length | 255 | +| Variables | 128 | +| Array elements | 1024 per array, 4096 in total | +| Scopes | 32 | +| Labels | 64 | +| `DATA` items | 512 | +| File channels | 10 | +| Operations per line | roughly 16 | + +Every one is a fixed pool. Nothing in the interpreter calls `malloc`, which is what +makes it safe to embed in a game that cannot afford a surprise allocation. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..f722919 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,46 @@ +# The akbasic guide + +akbasic is a BASIC interpreter in the style of **Commodore BASIC 7.0** — the dialect +the C128 shipped with — and **Dartmouth BASIC**. It runs programs from a file or from +an interactive prompt, and it is also a C library you can link into a game so that +players can script it. + +These chapters follow the shape of the +[C128 Programmer's Reference Guide](http://www.jbrain.com/pub/cbm/manuals/128/C128PRG.pdf): +the language first, then each hardware area, then the reference sections. If you know +BASIC 7.0 you can skip to **[Chapter 13](13-differences.md)**, which is the list of +everything that behaves differently here and why. + +## Chapters + +| | | +|---|---| +| **[1. Introduction](01-introduction.md)** | What akbasic is, what it is not, and how to build it | +| **[2. Getting started](02-getting-started.md)** | The prompt, your first program, saving and loading | +| **[3. The language](03-the-language.md)** | Variables, types, arrays, operators, expressions | +| **[4. Control flow](04-control-flow.md)** | `IF`, `FOR`, `DO`, `GOSUB`, labels, `ON`, error trapping | +| **[5. Strings and formatting](05-strings-and-formatting.md)** | String functions, `PRINT USING`, `PUDEF` | +| **[6. Graphics](06-graphics.md)** | `GRAPHIC`, `DRAW`, `BOX`, `CIRCLE`, `PAINT`, shapes | +| **[7. Sound](07-sound.md)** | `SOUND`, `PLAY`, `ENVELOPE`, `VOL`, `TEMPO` | +| **[8. Sprites](08-sprites.md)** | `SPRITE`, `SPRSAV`, `MOVSPR`, collision | +| **[9. Files and disk](09-files-and-disk.md)** | Channels, `DOPEN`, program storage | +| **[10. Embedding](10-embedding.md)** | Driving the interpreter from C | +| **[11. Verb reference](11-verb-reference.md)** | Every statement, alphabetically | +| **[12. Function reference](12-function-reference.md)** | Every function, alphabetically | +| **[13. Differences from BASIC 7.0](13-differences.md)** | What a C128 programmer needs to know | + +## The shortest possible start + +``` +$ cmake -S . -B build && cmake --build build +$ ./build/basic +READY +10 FOR I# = 1 TO 5 +20 PRINT "HELLO " + I# +30 NEXT I# +RUN +``` + +Two things in that program are not Commodore BASIC and will catch you out +immediately: **variables carry a type suffix** (`I#` is an integer) and **`+` +concatenates a string with a number**. Chapter 3 explains both.