Bumps all three ak* submodules to their current main, applies what their
upgrade notes require, and retires the two workarounds they make obsolete.
libakerror 2.0.1 -> 2.0.2 (63 commits). Two of its fixes were this
repository's own filed issues, and both workarounds are gone:
- It namespaces its embedded `coverage` target now, the same
CMAKE_SOURCE_DIR test it already applied to `mutation` (its issue #15).
The add_custom_target() shadow that renamed it on the way past could
only ever fire on a name the dependency has stopped using, so it is
deleted rather than left as dead code.
- It installs akerrorConfigVersion.cmake at SameMajorVersion (its issue
#16). MAINTENANCE.md said to add a `1.0` floor to our find_dependency
calls when this landed; the floor is now `2.0`, and we have no
find_dependency calls to add it to, so the paragraph says that instead
of an instruction nobody can follow.
Its IGNORE context also changed shape: `__akerr_last_ignored` was an extern
pointer, and is now a per-translation-unit `static akerr_last_ignored` holding
a copy, so the pool slot can be released. Nothing here referenced the symbol,
but it costs us 1.35 MiB of thread-local storage -- 38 TUs x 37,296 bytes,
measured as the entire TLS segment of build/basic, where 2.0.1 produced no TLS
segment at all -- plus 84 -Wunused-variable warnings. Filed upstream as
libakerror issue #37 and recorded in MAINTENANCE.md rather than patched here,
because patching a submodule forks it.
libakstdlib gains directory and file-metadata wrappers with no version bump.
aksl_snprintf keeps its `int *count` -- an intermediate commit removed it and
the merge put it back -- but now reports the required length on truncation
rather than 0. Every call site here reads it only after a successful return,
so nothing moved.
The directory wrappers close the gap DIRECTORY was refused for (libakstdlib
issue #10). The verb is still unwritten, so it still refuses, but it no longer
blames a wrapper that exists: the message is "DIRECTORY is not implemented
yet" and tests/disk_verbs.c asserts both that it says so and that it does not
name libakstdlib. What writing it would need is akbasic issue #55.
libakgl moves to the current main at 0.9.0. It registers libccd and tg as
submodules, so a tree that only ran `git submodule update --init --recursive`
before the bump needs it again or the configure fails on a missing
libccd/src/ccd/config.h.cmake.in.
Verified: 114/114 default, 116/116 under -DAKBASIC_WITH_AKGL=ON, docs_examples
green in both. libakerror's UPGRADING.md documents a 2.0.3 that project()
never stamped, so the version tables read 2.0.2 -- libakerror issue #38.
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
Co-Authored-By: Claude Code (Claude Opus 5, claude-opus-5[1m]) <noreply@anthropic.com>
252 lines
11 KiB
Markdown
252 lines
11 KiB
Markdown
# 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.
|
|
|
|
### Mixed arithmetic follows the left operand, not the wider type
|
|
|
|
**A C128 promotes to float. This does not.** An integer on the left converts the right
|
|
operand to an integer and throws its fraction away, so `A# * 0.45` is `0` where
|
|
`0.45 * A#` is `1.35`. It is consistent, it is inherited from the Go interpreter this was
|
|
ported from, and **nothing fails when you get it wrong** — the program computes something
|
|
else and carries on.
|
|
|
|
[Chapter 3](03-the-language.md#the-left-operand-decides-whether-the-arithmetic-is-integer-or-float)
|
|
has the two rules that keep you out of it. This is the difference from 7.0 most likely to
|
|
turn a working listing into a quietly wrong one.
|
|
|
|
### Structures are an addition
|
|
|
|
BASIC 7.0 has no records at all. `TYPE`/`END TYPE`, `DIM X@ AS T`, the `@` suffix, `.` and
|
|
`->`, `PTR TO` and `POINT ... AT` are all new here, and **[Chapter 16](16-structures.md)**
|
|
is the whole of it. Nothing about it changes an existing program.
|
|
|
|
Two consequences a C128 programmer should know. A structure assignment **copies**, like
|
|
every other assignment; sharing is spelled `POINT`. And a type name is a bare word, so it
|
|
shares a namespace with verbs and labels — `TYPE POINT` is refused because `POINT` is now a
|
|
verb.
|
|
|
|
## Block structure
|
|
|
|
**A whole loop on one line does not loop.**
|
|
|
|
```basic
|
|
10 FOR I# = 1 TO 3 : PRINT I# : NEXT I#
|
|
20 PRINT "DONE"
|
|
```
|
|
|
|
```output
|
|
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 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.
|
|
|
|
## Line numbers
|
|
|
|
**A program in a file does not need them.** Every BASIC this dialect descends from
|
|
required a number on every line; here that requirement belongs to the prompt alone,
|
|
where the number is the only thing separating program text from a statement to run now.
|
|
A program loaded from a file, or handed to the library as a string, may leave them out,
|
|
and a line without one is given the next number going. The two mix: a numbered line sets
|
|
where the next unnumbered one goes.
|
|
|
|
This is QuickBASIC's idea rather than the C128's, and it is here for the same reason
|
|
QuickBASIC had it — a program that branches by `LABEL` never names a line number, so the
|
|
numbers are maintenance with nothing on the other end of it.
|
|
|
|
Two consequences worth knowing:
|
|
|
|
- **`GOTO <number>` must name a number the program wrote.** In a file with no line
|
|
numbers `GOTO 100` would otherwise find the hundredth line and branch there. It is
|
|
refused before the program runs. `GOTO <label>` is unaffected.
|
|
- **`LIST` and `DSAVE` show the numbers that were handed out**, one apart. `RENUMBER`
|
|
before `DSAVE` if you want gaps to insert into.
|
|
|
|
An unnumbered program is capped at 9998 lines, which is the cap that already applied.
|
|
|
|
## 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, and see [Chapter 15](15-error-codes.md) for the whole list.
|
|
|
|
## Graphics
|
|
|
|
- **A coordinate is a window pixel, not one of 320 by 200.** A C128 listing therefore
|
|
draws in the top-left corner of a larger window; `SCALE 1, 319, 199` gives it the
|
|
whole window back. `RGR(1)` and `RGR(2)` report the size, and are ours rather than
|
|
7.0's — where 7.0's `RGR` takes only field 0, the `GRAPHIC` mode.
|
|
- **`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 persists, and the text layer covers it.** A drawing goes into a layer that
|
|
survives the frame, so a program draws its picture once and it stays — no redrawing, no
|
|
capturing it into a sprite. What is still true is that the text layer repaints every row
|
|
it owns, opaque, every frame, and by default it owns the whole window. `WINDOW` shrinks
|
|
it and hands the rest over. The two together are what makes a picture usable:
|
|
`WINDOW 0, 0, 39, 1` keeps a one-row status line and gives the drawing verbs everything
|
|
below it.
|
|
- **A drawing still has to fit in one batch.** The host runs a fixed number of source lines
|
|
and then presents, and an `SSHAPE` capture spanning that boundary comes back **half
|
|
drawn** — the part issued since the present, over whatever was there before. Measured
|
|
against the standalone frontend's 256 lines a batch: after synchronising to a jiffy edge,
|
|
220 lines of drawing survive a capture and 250 do not. This bites a *capture*, not the
|
|
drawing itself, so it matters far less than it did when capturing was the only way to
|
|
keep a picture. The only way a program can see the boundary is to watch `TI#`, which is
|
|
refreshed once per batch.
|
|
- **`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 window pixels**, not the VIC-II's raster space, and `SCALE` does
|
|
not apply to them.
|
|
- **`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 shape**, not by pixel. A sprite nobody has shaped collides with its
|
|
whole frame, expansion bits included, which is what a bounding box means here; `SPRHIT`
|
|
narrows that to a box, a circle or a capsule. None of them is pixel-exact.
|
|
- **`SPRHIT` and `RSPHIT` are an addition.** BASIC 7.0 has `COLLISION` and `BUMP` and
|
|
nothing else, and nothing about them changes an existing program.
|
|
- **Collision types 1 and 2 exist.** Type 2 means the rectangles `SOLID` registered, not a
|
|
screen read back — a C128 collides a sprite against the bitmap's set pixels and this
|
|
cannot, so the question is asked against geometry instead. Type 3 is still refused;
|
|
there is no light pen.
|
|
- **`SOLID` is an addition.** BASIC 7.0 has no static collision geometry at all, and it is
|
|
what lets a program collide with something that is not one of the eight sprites.
|
|
- **`RCOLLISION` is an addition, and nothing is resolved.** A contact is a *report*: it says
|
|
what was hit, which way is out and how far, and reversing the ball is still the program's
|
|
job. BASIC 7.0 has `BUMP` and a bitmask, and no way to ask any of this.
|
|
- **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** because it is not written yet. The standard-library
|
|
wrapper it was waiting on has landed, so the remaining work is the verb.
|
|
|
|
## 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 across every array and structure |
|
|
| 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.
|
|
|
|
**A scalar does not come out of the 4096.** It lives in the variable itself, so creating
|
|
one inside a `GOSUB` or a `FOR` — including the loop counter — costs nothing and can be
|
|
done for as long as the program runs. An *array* declared inside a scope does come out of
|
|
it and is not given back: the pool never frees, which is what lets a pointer into a
|
|
record outlive the scope that declared it. In practice that means `DIM` at the top rather
|
|
than in a loop, which is where you would have put it anyway.
|