docs/ and README.md carry 85 fenced blocks. Every one was checked by hand exactly once, when it was written, which is not a standard that survives a changing interpreter -- and four were already wrong: two transcripts showing a leading space PRINT does not emit, akbasic_TextSink in README.md missing the two members it had grown hours earlier, and FILTER's refusal quoted with wording the code does not use. tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds. BASIC programs and transcripts run and are byte-compared against an `output` block; C snippets compile with -fsyntax-only against the real include path, which CMake writes out because it is transitive through akerror, akstdlib and akgl; shell blocks run in a sandbox. Anything that would reconfigure the build tree, hit the network or re-enter the suite is tagged norun with the reason in MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision. An untagged block is a failure rather than a default, and the pass line reports what it executed by kind. Both exist because the way a harness like this dies is by quietly matching nothing and passing -- which it duly did on the first CTest run, where a generator expression evaluating to nothing still contributed an empty argument that the script read as a filename. The count is what caught it. The excerpt check earns its own mention: a block tagged `c excerpt=include/akbasic/sink.h` must still appear in that header, comments and whitespace ignored. Compiling it would only redefine the type, so a compile check could not have found the stale struct, and did not. Registered as the CTest case docs_examples in both configurations. Fixing the four wrong examples turned up two interpreter defects, fixed in the previous commit and recorded in TODO.md section 8. MAINTENANCE.md is new: the fence-tag reference, what to do when the case fails, and the conventions that until now only existed inside source comments -- the three test lists and how two of them invert "passed", the sorted verb table, that a golden file is never edited to suit this interpreter, and that a fix gets mutation-checked with a file copy rather than git checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.8 KiB
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#
20 PRINT "DONE"
DONE
The loop 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 3runs with 1, 4, 7 and 10. FOR I = 1 TO 1does 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.
SSHAPEputs a handle in the string, not the pixels. You can pass it toGSHAPEandSPRSAV; you cannot save it or take itsLEN.WIDTHis 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.
CHARignores its colour argument and needs a text device with a cursor.
Sound
PLAYandSOUNDdo not block. The statement after them runs immediately.FILTERis refused. There is no filter stage to configure.PLAY'sMis 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.
SPRSAVtakes 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.
SPRDEFis out of scope.
Files
PRINT #andINPUT #need a space before the#.PRINT#1scans as a variable name.RECORDcounts lines, not fixed-length records.BLOADrequires a length.HEADER,COLLECT,BACKUPandBOOTare refused. They operate on a physical disk.DIRECTORYis refused pending a wrapper in the standard library.
Machine
SYSis refused. There is no 6502 and no ROM.FETCHandSTASHare the same byte copy. There is no expansion RAM to tell them apart.POKE,PEEKandPOINTERuse real process addresses. A wrong one is a segmentation fault, not an error message.BANK,FASTandMONITORdo not exist.
Formatting
PRINT USINGrenders one field per statement.PRINT USING "### ###"; A, Bis not supported.- Exponential fields (
^^^^) are not implemented.
Console
SLEEPandWAIThold the program without blocking the host.SLEEPwith no host clock does nothing at all rather than waiting forever.TIandTI$areTI#andTI$, refreshed once per step.WAITpolls ordinary process memory. Nothing changes it but the host.KEYstores 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.