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.