Files
akbasic/docs/14-architecture.md
Logikoma 8a02674af5
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m34s
akbasic CI Build / coverage (push) Successful in 4m4s
akbasic CI Build / sanitizers (push) Successful in 6m59s
akbasic CI Build / akgl_build (push) Successful in 7m57s
akbasic CI Build / mutation_test (push) Successful in 23m28s
Move BASIC fixtures into the editable language corpus
Move every program and expectation out of tests/reference and register the unified tests/language corpus as local cases. Remove the old immutable-corpus protections from build, maintenance, and documentation paths.

Co-authored-by: andrew <andrew@aklabs.net>
2026-08-04 16:22:16 -04:00

839 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 14. Architecture
This chapter is for whoever has to understand the interpreter from the inside: to
embed it, to debug something it did, or to change it. Everything before this chapter
describes the language. This one describes the machine that runs it.
It is deliberately not a repeat of the API. **The headers under `include/akbasic/` are
the authority on every function's contract** — `doxygen Doxyfile` renders them, and each
one carries its parameters, its return, and the errors it raises. Where a rule is
subtle, the header comment states it at more length than a chapter should. What is here
instead is the shape: what talks to what, in what order, and why it was built that way.
| Question | Read |
|---|---|
| What does this function do? | the header, or the Doxygen it renders to |
| What does this verb do? | [Chapter 11](11-verb-reference.md), and `src/runtime_*.c` |
| Why is it built like this? | this chapter |
| What are the conventions for changing it? | [`MAINTENANCE.md`](../MAINTENANCE.md) |
| What is currently wrong with it? | [`TODO.md`](../TODO.md) |
## Three libraries and a driver
The build separates the interpreter from everything that could own a screen, and the
separation is in the build graph rather than in a comment.
```text
+---------------------------+ +---------------------------+
| basic (src/main.c) | | your game |
| argv, QUIT, | | window, renderer, |
| FINISH_NORETURN | | frame loop |
+-------------+-------------+ +-------------+-------------+
| |
+-------------v-------------+ |
| akbasic_frontend | | a host that already
| SDL window, font, event | | has a renderer skips
| pump, 256 steps a frame | | this target entirely
+-------------+-------------+ |
| |
+-------------v---------------------------------v-------------+
| akbasic_akgl sink | graphics | audio | input | sprites |
| function-pointer records that draw and play |
| through a renderer somebody else created |
+------------------------------+------------------------------+
|
+------------------------------v------------------------------+
| akbasic scanner -> parser -> evaluator, the pools, |
| the verb table, values, environments |
| no SDL, no malloc, no exit(), no globals |
+------------------------------+------------------------------+
|
+------------------------------v------------------------------+
| libakstdlib / libakerror |
+-------------------------------------------------------------+
```
| Target | What it is | A game links it? |
|---|---|---|
| `akbasic` | The interpreter. No SDL, nothing that terminates the process | Always |
| `akbasic_akgl` | The sink and the four device 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 |
| `basic` | The driver: argv, `QUIT`, and the one `FINISH_NORETURN` in the tree | — |
Where a thing lives:
| Area | Source |
|---|---|
| Tokens, AST leaves | `src/grammar.c`, `include/akbasic/grammar.h` |
| Scanner | `src/scanner.c` |
| Parser | `src/parser.c`, and the verbs with their own syntax in `src/parser_commands.c` |
| The step loop, evaluation, pools | `src/runtime.c` |
| Verb and function implementations | `src/runtime_*.c`, one file per verb group |
| The dispatch table | `src/verbs.c` |
| Scopes and block state | `src/environment.c` |
| Values, variables, symbol table | `src/value.c`, `src/variable.c`, `src/symtab.c` |
| Text sinks | `src/sink_stdio.c`, `src/sink_tee.c`, `src/sink_akgl.c` |
| Device backends | `src/graphics_akgl.c`, `src/audio_akgl.c`, `src/input_akgl.c`, `src/sprite_akgl.c` |
## One struct holds everything
There is no file-scope mutable state anywhere in the library. Every pool, every cursor
and every piece of subsystem state hangs off one `akbasic_Runtime`, which the caller
owns and passes by pointer. Two interpreters in one process do not interfere, which is
the whole reason for the rule.
It is several megabytes, because the pools are inline. Put it in static storage or in
your own state — never on a default stack. `src/main.c`, `tests/harness.h` and both
examples all do the same thing for the same reason.
`akbasic_Runtime` carries five kinds of thing, and it is worth knowing which is which
before you go reading it:
| Kind | Fields |
|---|---|
| The program | `source[]`, indexed **by line number**, and `sourcepath` |
| Pools | `environments[]`, `variables[]`, `functions[]`, `valuepool` |
| Execution state | `mode`, `run_finished_mode`, `environment`, `errclass`, `skiprestofline`, `trace`, `stopped` |
| Borrowed devices | `sink`, `graphics`, `audio`, `input`, `sprites` — any of the last four may be `NULL` |
| Per-subsystem BASIC state | `gfx`, `audio_state`, `input_state`, `sprite_state`, `format_state`, `console_state`, `data_state`, `disk_state` |
That last row is the one people ask about. `COLOR`, `TEMPO`, `SCALE`, the `PUDEF`
characters and the eight sprites' positions live on the *runtime*, not on the device
that renders them, because they are the program's state and not the hardware's. A host
that swaps one renderer for another does not expect the script's colours to go with it,
and `RSPPOS` answers correctly with no sprite device attached at all.
`source[]` being indexed by line number is worth pausing on. A line's number *is* its
array slot, so `GOTO 500` is one assignment — `environment->nextline = 500` — and no
search. Blank slots are skipped by the step loop. That is why line numbers are capped at
9998 and why a nine-line program still costs a 9999-entry array.
It is also why **a loaded line does not have to arrive with a number**. Execution needs a
slot, not a number somebody typed, so a line that comes without one is simply given the
slot after the last line filed. `akbasic_runtime_file_line()` is the one place that rule
lives, shared by `akbasic_runtime_load()`, `RUNSTREAM` and `DLOAD`; the step loop, the
branch verbs, the four prescans and `RENUMBER` never learn that anything changed, because
all of them already worked in slots. `akbasic_SourceLine::numbered` records which kind a
line was, and exactly one thing reads it — `akbasic_runtime_check_targets()`, which
refuses `GOTO 100` when line 100 is a number nobody wrote.
The prompt is deliberately outside all of this: `process_line_repl()` files a line only
when it *had* a number, because there the number is what separates program text from a
statement to run now.
## One step
`akbasic_runtime_step()` is the whole loop, unrolled to a single iteration. The
reference's `run()` did not return until the program quit; here the caller owns the loop
and the library owns exactly one turn of it.
```text
akbasic_runtime_step(rt)
|
+-- akbasic_play_service() release the next queued note if its time is up
+-- akbasic_sprite_service() advance continuous MOVSPR motion
+-- akbasic_collision_service() look for collisions where the sprites now are
|
+-- mode == QUIT ? ------------------------------------> return
|
+-- akbasic_input_service() GETKEY waiting? -----> return (no line runs)
+-- akbasic_ui_service() GETMENU waiting? -----> return (no line runs)
+-- akbasic_console_update_clock()
+-- akbasic_console_service() SLEEP or WAIT holding? -> return
|
+-- akbasic_runtime_zero() reset the per-line value pool
+-- akbasic_scanner_zero() reset the scanner cursor
|
+-- switch ( mode )
| RUNSTREAM -> process_line_runstream() read a line, file it
| REPL -> process_line_repl() read a line, run it or file it
| RUN -> service_interrupts() enter a handler if one is due
| process_line_run() run source[nextline]
|
+-- errclass set ? -> set_mode(run_finished_mode)
```
Three things in that order are deliberate.
**The service calls run before the `QUIT` check.** A program's last notes still come out
while a host keeps calling `step()` after the script has ended.
**They run before the blocking checks, too.** A program sitting in `GETKEY` still has its
music paced and its sprites moved. `GETKEY` holding the program means *this step
executes no source line* — it does not mean the step does not return. It always returns.
**Interrupts are serviced between lines and nowhere else.** A handler entered mid-statement
would have to return into the middle of a line, and the parser keeps no state that could
resume there.
`akbasic_runtime_run(rt, n)` is `step()` in a `while` with a budget: at most `n` steps,
then return regardless. That bound is the only thing standing between a script containing
`10 GOTO 10` and your frame rate. `n <= 0` means unbounded, which is right for a test and
wrong for a game. The SDL frontend uses 256 (`AKBASIC_FRONTEND_STEPS_PER_FRAME`); the
stdio driver uses 1, because it wants to refresh the clock between steps.
**That budget is visible to a script, and drawing is where it shows.** The frontend
presents the frame when `run()` comes back, and presenting discards the drawing buffer —
so a sequence of drawing verbs longer than one budget is cut in half by the present in
the middle of it, and an `SSHAPE` afterwards captures only what was issued since. Not a
transient artefact: a torn capture, over whatever the frame before left behind.
A script cannot read the budget, but it can *see* it: `akbasic_runtime_settime()` is
called once per frame by the host, so `TI#` changes on the first step of a batch and
nowhere else. Spinning until it changes is the only frame synchronisation this dialect
has, and it is what
[Chapter 18](18-tutorial-breakout-artwork.md#step-5-find-the-frame-boundary) is built on.
A host that gives the interpreter a larger budget makes more drawing fit; one that never
calls `settime()` takes the synchronisation away entirely.
Time comes in from outside. `akbasic_runtime_settime(rt, ms)` is how the interpreter
learns what time it is; it reads no clock, because it owns no loop and must not block. A
host that never calls it leaves time frozen at zero, and every duration then expires on
the step after it starts — audible, but never a hang. That is the intended way for it to
fail.
## Four modes
```text
akbasic_runtime_start(rt, mode)
|
+-------------------+-------------------+
| |
v v
RUNSTREAM REPL <---------------+
read a line, file it, read a line: file it if it |
execute nothing has a number, run it if not |
| | |
| end of input RUN, CONT | |
+---------------> RUN <---------------+ |
| |
| END, STOP, a BASIC error, or |
| running off the end of the source |
v |
run_finished_mode ---------- == REPL ----------+
|
| == QUIT, or the QUIT verb, or
v end of input at the prompt
QUIT
```
| Mode | What one step does | How it leaves |
|---|---|---|
| `AKBASIC_MODE_REPL` | Read one line from the sink. With a line number, file it; without one, **run it now** | `QUIT` verb, or end of input |
| `AKBASIC_MODE_RUN` | Execute `source[nextline]` | Running off the end, `END`, `STOP`, or a BASIC error |
| `AKBASIC_MODE_RUNSTREAM` | Read one line from the sink and file it. Executes nothing | End of input, which switches to `RUN` |
| `AKBASIC_MODE_QUIT` | Nothing | — |
`run_finished_mode` decides where a finished program lands. `akbasic_runtime_start(rt,
AKBASIC_MODE_REPL)` sets it to `REPL`, so a program that ends drops back to a prompt;
anything else sets it to `QUIT`, so `basic program.bas` exits. It is one field, and it is
the whole difference between an interactive session and a script runner.
**`akbasic_runtime_set_mode()` is not just an assignment.** Entering `REPL` prints
`READY`. Entering `RUN` does four prescans of the whole program first:
- **Labels.** Every `LABEL` in the source is filed before anything executes, textually
rather than by parsing. Without it a label would exist only from the moment its `LABEL`
statement ran, so `GOTO` could reach backwards and never forwards — and an interrupt
handler, which by definition sits where normal flow does not fall, could not be named
by label at all.
- **`DATA` items.** `READ` walks a cursor along a list built before the program runs, so a
`DATA` line above its `READ` is found.
- **`TYPE` declarations.** A declaration has to be in effect wherever control goes, so
`DIM P@ AS RECT` cannot run before `RECT` exists even if a branch skipped the lines that
declared it.
- **Branch targets.** The only one that reads nothing in and merely refuses: a numeric
`GOTO`, `GOSUB`, `RUN`, `RESTORE`, `TRAP` or `COLLISION` target naming a line the program
did not number. It shares `RENUMBER`'s walk in `src/renumber.c` rather than repeating it.
Every path into a run — `akbasic_runtime_start()`, the `RUN` verb, `CONT`, and the end of
a `RUNSTREAM` load — goes through that one function, which is why the prescans live there
and not in any of the four callers.
All four run inside one `ATTEMPT`, because **a prescan failure is the program's mistake
and not the host's** and has to leave as a BASIC error line rather than a raised context.
Three of them report against whichever line the loader stopped on and put the real one in
the message text; the branch-target scan points `environment->lineno` at the line it is
walking so the `? N :` prefix is right. `TODO.md` §5 item 64 records the difference.
The REPL's split between *file it* and *run it now* is one boolean: the scanner sets
`hadlinenumber` when the line it just read began with a number. A line typed with a
number is program text; a line typed without one is direct mode and runs immediately.
## From a line of text to an effect
Nothing is compiled and nothing is cached. Every time a line executes it is scanned and
parsed again, from the source text, into per-line pools that are reset on the way in.
```text
source[42] "IF A# = 5 THEN PRINT \"FIVE\""
|
| akbasic_scanner_scan() verb names via the dispatch table;
v no keyword maps of its own
environment->tokens[32] COMMAND(IF) IDENT_INT(A#) ASSIGNMENT LITERAL_INT(5)
| COMMAND(THEN) COMMAND(PRINT) LITERAL_STRING(FIVE)
|
| akbasic_parser_parse() one statement per call; the caller
v loops until the tokens are spent
environment->leaves[32] BRANCH
| / \
| (= A# 5) COMMAND PRINT
| |
| LITERAL_STRING "FIVE"
|
| akbasic_runtime_interpret() suppressed while this scope is
v skipping forward to a verb
akbasic_runtime_evaluate() switch on leaf type; a COMMAND leaf looks its
| handler up in the table and calls it
v
verb->exec() -> the sink, a device backend, or a variable
```
The statement loop lives in `process_line_run()` and `process_line_repl()`, not in the
parser: a line can hold several statements separated by colons, so the caller loops on
`akbasic_parser_is_at_end()`. **`akbasic_parser_parse()` may return `NULL` on success** —
that is a line that was nothing but separators — and a caller that does not skip it will
hand `NULL` to the interpreter.
`skiprestofline` is how a branch tells that loop to stop. BASIC 7.0 scopes everything
after `THEN` to the condition, but the parser takes only *one* statement per arm and the
rest arrive at the statement loop as ordinary top-level statements. So the branch raises a
flag and the loop obeys it. The rule is not "skip when false": the remainder belongs to
whichever arm was written last, so it is skipped exactly when *that* arm is the one not
taken. [Chapter 4](04-control-flow.md) has the truth table; the `AKBASIC_LEAF_BRANCH` case
in `akbasic_runtime_evaluate()` has the code.
Two details in that diagram catch people out:
**A lone `=` is scanned as `ASSIGNMENT`, not as equality.** The scanner cannot know
whether it is looking at a statement or a condition. The parser can: `akbasic_Parser`
carries a `comparing` flag, set around a condition and cleared afterwards, and while it is
set the relation rule accepts `ASSIGNMENT` as a seventh operator and rewrites it to
`EQUAL`. Outside a condition `=` has to stay an assignment or `FOR I# = 1 TO 5` never
initialises its counter.
**The token and leaf numbering is the Go reference's, on purpose.** `grammar.h` keeps the
original values so a debugging session against either implementation reads the same.
`akbasic_leaf_to_string()` renders a tree in prefix form — `(+ A# 42)`, `(group (+ A# 42))`
— which is the fastest way to see what the parser actually built. It is a plain function,
so you can call it from a debugger against any leaf pointer you have.
## The dispatch table
The Go reference resolved a verb by reflection: `MethodByName("Command" + NAME)`. C has
no reflection and none is being added. All three of its lookups — verbs, functions, and
verbs with their own parse path — collapse into one sorted table in `src/verbs.c`.
```c excerpt=include/akbasic/verbs.h
typedef struct
{
const char *name;
akbasic_TokenType tokentype;
int arity;
akbasic_ParseHandler parse;
akbasic_ExecHandler exec;
} akbasic_Verb;
```
One row per name, and both handlers are optional:
| Field | Meaning |
|---|---|
| `tokentype` | What the **scanner** gives this name. It has no keyword maps of its own |
| `arity` | Argument count for a function; `-1` where it does not apply |
| `parse == NULL` | The verb's rval parses as a plain expression |
| `parse != NULL` | The verb has its own syntax — `src/parser_commands.c` |
| `exec == NULL` | The token is consumed by another verb's parser and never evaluated alone: `THEN`, `ELSE`, `TO`, `STEP` |
`AKBASIC_TOK_COMMAND_IMMEDIATE` marks a verb the REPL may run against a line that *does*
carry a number — `RUN`, `LIST`, `NEW`, `DLOAD`. Everything else typed with a number is
filed as program text.
**The table is searched with `bsearch`, so it must stay sorted.** A mis-sorted row does
not fail to compile; it silently becomes an unfindable verb, and the symptom is
`Unknown command PRINT` a long way from the cause. `tests/verbs_table.c` asserts the
ordering, which is the only reason that is a safe thing to say.
## Nothing is allocated
Every object comes from a fixed pool inside the runtime. Exhausting one is a diagnosable
error naming the pool, not a crash and not a slow leak.
| Pool | Size | Lives on | Exhaustion says |
|---|---|---|---|
| `AKBASIC_MAX_SOURCE_LINES` | 9999 | runtime | `Line number N is outside 0..9998` |
| `AKBASIC_MAX_ENVIRONMENTS` | 32 | runtime | `Environment pool exhausted at line N (32 in use)` |
| `AKBASIC_MAX_VARIABLES` | 128 | runtime | `Maximum runtime variables reached` |
| `AKBASIC_MAX_FUNCTIONS` | 64 | runtime | `Maximum function definitions reached` |
| `AKBASIC_MAX_ARRAY_VALUES` | 4096 | runtime (`valuepool`) | `Array of N elements does not fit in the M remaining value slots` |
| — a scalar is not in that pool | 1 | **the variable** (`inlinevalue`) | — |
| `AKBASIC_MAX_TOKENS` | 32 | **environment** | `Line N has more than 32 tokens` |
| `AKBASIC_MAX_LEAVES` | 32 | **environment** | `No more leaves available` |
| `AKBASIC_MAX_VALUES` | 64 | **environment** | `Maximum values per line reached` |
The numbers are in `include/akbasic/types.h`, transcribed from the reference's `main.go`
plus three the Go version did not need because it called `make()`.
**The value pool is the one that needs a second sentence.** It is a bump allocator with
no free, so anything drawn from it is spent for the life of the run — and scope exit
returns a variable's *slot* without returning its storage. A scalar therefore does not
draw from it at all: `akbasic_variable_init()` points a one-element non-`@` variable at
its own `inlinevalue`, which is what makes a local, a `FOR` counter and a `DEF` parameter
free. Arrays and structures still spend, deliberately, because a pointer into a record is
allowed to outlive the scope that DIMmed it.
[Chapter 13](13-differences.md) states the same budget from a BASIC programmer's side.
The per-environment three are reset at the top of every line, which is what makes
"roughly 16 operations per line" a real limit and not a leak: a line that uses 30 leaves
is fine, and a line that needs 33 tokens is refused. Because they are per environment
rather than per runtime, a `FOR` body running inside a pushed scope gets its own 32.
`valuepool` is a bump allocator for array storage and it does not release. Nothing in
BASIC destroys a variable, so there is nothing to release *to* — but re-`DIM`ming an array
larger takes fresh slots and abandons the old ones, so a program that does that in a loop
will exhaust the pool. Bounded and diagnosable, which is the point.
## A scope is also the block state
`akbasic_Environment` does two jobs at once, and the second one is the surprising one. It
is a variable scope, chained to its parent — and it is the in-flight state of whatever
block structure is executing: the `FOR` bounds and step, the `DO`/`LOOP` condition, the
`GOSUB` return line, the `READ` cursor, and the line counters.
| What pushes a scope | What pops it |
|---|---|
| `FOR` — **during parsing**, not execution | the `NEXT` whose condition is met, or an `EXIT` |
| `GOSUB` | `RETURN` |
| A call to a multi-line user function | that function's `RETURN` |
| An interrupt firing | the handler's `RETURN` |
That `FOR` entry is not a typo. `akbasic_parse_for()` pushes the new environment while
parsing the line, parks `TO` and `STEP` in it as unevaluated leaves, and makes it active
only after parsing is finished — because the loop body is scanned against the *parent's*
token stream. If you are chasing a scope that appeared earlier than you expected, that is
why.
### `waitingForCommand`, and why loops work at all
A loop's condition is evaluated at the *bottom* of the structure, which leaves an obvious
problem: how does a zero-iteration `FOR` avoid running its body once? The answer is a
string on the environment.
```text
10 FOR I# = 5 TO 1 FOR pushes a scope, evaluates the condition,
20 PRINT "NEVER" finds it already met, and calls
30 NEXT I# wait_for_command(env, "NEXT")
step: line 20 interpret() sees the scope is waiting, and the leaf is not
COMMAND "NEXT" -> evaluates nothing, returns static true
step: line 30 the leaf *is* COMMAND "NEXT" -> runs. NEXT stops the wait,
pops the scope, and hands nextline back to the parent
```
`akbasic_runtime_interpret()` is where that is enforced, and it is the first thing it
does. Five places arm a wait, and between them they are the whole of block structure:
| Verb | Waits for | Because |
|---|---|---|
| `FOR` | `NEXT` | the condition was already met — zero iterations |
| `DO WHILE` / `DO UNTIL` | `LOOP` | the same, at the top of a `DO` |
| `EXIT` | `NEXT` or `LOOP` | where the loop *ends* is not known until a `NEXT` has run once |
| `IF ... THEN BEGIN` | `BEND` | the arm not taken has to skip *lines*, and `skiprestofline` only reaches the end of this one |
| a multi-line `DEF` | `RETURN` | the definition must not execute its own body |
Three consequences follow, and all three are things people report as bugs:
- **Block skipping walks source *lines*.** A whole `FOR ... NEXT` written on one line does
not loop, because there is no next line for the wait to skip to.
- **A `FOR` counter does not survive its loop.** It lives in the loop's own scope. On a
C128 it keeps its final value.
- **A host must use `akbasic_runtime_global()`.** `akbasic_environment_get()` creates in
whatever scope is active, and a script suspended part-way through a bounded
`akbasic_runtime_run()` is usually inside a `FOR` or `GOSUB` body — so the script reads
the value correctly inside the loop and gets `0` immediately after it, with nothing
raised anywhere.
## Values
`akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct
assignment with no allocator, no refcount and no lifetime question. It costs 256 bytes per
value; that is the trade, and it is recorded in `TODO.md` §6.
Type comes from the identifier's suffix and nothing else: `A#` integer, `A%` float, `A$`
string, and a bare name with no suffix is a *label*. That is `akbasic_leaf_identifier_type()`,
and it is why `ER` and `EL` are spelled `ER#` and `EL#` here.
One field on the runtime changes how identifiers evaluate. `eval_clone_identifiers` is
normally true, and evaluating `A#` then yields a clone drawn from the per-line value pool.
`POKE` and `POINTER` need the address of the real storage, so they clear it around their
own evaluation. If you are debugging an assignment that wrote to the wrong place, that
flag is the first thing to check.
## Structures are laid out exactly as arrays are
The whole storage model falls out of one decision: **a `TYPE` is declared**, so an
instance has a known slot count before the program runs.
That means a structure needs no pool of its own. `DIM R@ AS RECT` calls the same
`akbasic_valuepool_take()` that `DIM A#(10)` calls, and the variable's `values` run *is*
the instance. A field access is offset arithmetic against an offset the type descriptor
already knows. A nested value field **flattens into its container's run** — a `SEGMENT`
holding two `COORD`s and a string is five slots, not three — which is why the nesting is
free rather than a second indirection.
```text
DIM S@ AS SHAPE SHAPE: NAME$, ORIGIN@ AS COORD, AREA%
COORD: X#, Y#
variable S@
structtype ──► type table entry SHAPE (slotcount 4)
values ──────► ┌────────┬────────┬────────┬────────┐
│ NAME$ │ X# │ Y# │ AREA% │
└────────┴────────┴────────┴────────┘
off 0 off 1 off 2 off 3
└── ORIGIN@ is offset 1, two slots ──┘
```
`src/structtype.c` fills the table, and it does so **in three passes**, each for a case
the pass before cannot handle. Names first, so a field can refer to a type declared
further down. Then field lists, now able to resolve every reference. Then sizes, by
repeated resolution — a type whose fields are all sized can be sized, and repeating that
settles any legal ordering. **Whatever never resolves is a cycle of by-value containment**,
which is how "a `TYPE` cannot contain itself by value" is a diagnosis rather than an
assumption.
It is a *prescan*, run from `akbasic_runtime_set_mode()` beside the label and `DATA`
scans, for the reason all three are: a declaration has to be in effect wherever control
goes, including when a branch skips the lines that made it. The `TYPE` verb's whole job at
run time is to **jump past its own `END TYPE`**, because the field lines are declarations
and executing `W#` would evaluate a bare identifier and quietly create a global.
### Copy, and where it has to happen
A `STRUCT` value and a `POINTER` value carry the same thing: a type index and a base. What
differs is what *assignment* does with it.
**The copy cannot live in `akbasic_value_clone()`**, and this is the trap to know about.
Clone copies one slot, and one slot holds a *reference* to an instance rather than the
instance — so a structure going through clone would alias, which is precisely the
semantics the language does not have. `akbasic_environment_assign()` intercepts a
structure before that path and calls `akbasic_struct_copy()`, which walks the descriptor
and copies slot by slot. Deliberately not one `memcpy` of the run: a pointer field must
copy its reference where a value field must copy its slots, and only the descriptor knows
which is which.
Copy is therefore **deep through values and stops at pointers**, as it is for a C struct
holding a pointer. Since a `TYPE` cannot contain itself by value, copy depth is fixed by
the type graph before the program starts and no copy can recurse away. Only *rendering*
needs a runtime bound, because a pointer can make the graph cyclic — that is
`AKBASIC_MAX_STRUCT_DEPTH`, four, chosen so the bound bites before the 256-byte render
buffer does.
### A host structure is the same thing with its bytes somewhere else
`akbasic_host_register_type()