Skip to content

Compiler & optimizer

The optimizer is conservative on purpose: it removes work and crossings, never observable sequence points.

D/BASIC's compiler is a fresh optimizing front end built around one canonical language and one DBC instruction contract. There is no “Windows dialect,” “Atari dialect,” or secret browser parser. Portability begins before optimization.

The pipeline

source → dialect normalization → lex → parse → record analysis → object analysis → optimize + lower → self-validate → DBC

Each stage has a different teaching purpose:

  1. Dialect normalization recognizes transferable historical BASIC forms and rewrites them into canonical D/BASIC.
  2. Lexing identifies names, literals, keywords, punctuation, comments, line numbers, and positions.
  3. Parsing builds the source structure without losing meaningful parentheses or labels.
  4. Record analysis fixes value layouts.
  5. Object analysis establishes fields, access, inheritance, lifetime, and dispatch slots.
  6. Optimization and lowering select the smallest semantic DBC shape that preserves the program.
  7. Self-validation parses the emitted image through the same container rules a runtime will enforce.

The compiler returns no image if its own output would be refused as malformed. That last step turns “the compiler and loader disagree” into a compiler defect at build time.

The dialect optimizer

Retro BASIC is a family of recognizable ideas expressed through incompatible local spellings. The dialect optimizer is the migration door, not a runtime mode.

It can normalize:

  • numbered-line control flow and compact statement separators;
  • implicit and suffixed types;
  • vendor loop, text, graphics, sound, and file idioms;
  • portable arithmetic and string forms;
  • calls whose old argument rules imply BYREF or forced values; and
  • machine-facing operations that have a registered semantic Intent equivalent.

The output is ordinary D/BASIC. Once normalized, it travels through the same parser, optimizer, bytecode, and conformance path as newly written source.

Atari BASIC ─┐
Applesoft ───┼─→ dialect optimizer → canonical D/BASIC → DBC
Commodore ───┤
Structured BASIC ┘

This preserves an important rule: one language, no dialect switches. A source migration can be reviewed and saved. The runtime never changes grammar because it detected a platform or a file's ancestry.

When an old machine operation has no honest semantic equivalent, the optimizer names the boundary. Guessing that one magic address “probably means screen color” on every listing would be translation theater.

Deterministic compilation

The same source, imports, shared-library artifacts, and compiler version produce byte-identical DBC. The compiler does not embed a build path, current time, hash-map iteration accident, or host pointer.

Determinism matters for more than tidy builds:

  • a package can hash exactly what will execute;
  • Windows, Linux, macOS, browser, and D/OS runners can compare one artifact;
  • conformance traces can expose runtime disagreement; and
  • an optimization can be reviewed as a bytecode change rather than a hunch.

Reachability: do not charge for dead procedures

The compiler begins from the program entry, lifecycle faces, event faces, and reachable object methods, then follows direct calls. Unreachable ordinary procedure bodies do not contribute executable work or capability requirements.

SUB NeverCalled ()
  DIM Bytes%(0 TO 0)
  DIM Digest%(0 TO 15)
  ' An unused experiment should not require its provider at load time.
  INTENT COMPUTE.SHA256(0, Bytes%(), Digest%())
END SUB

PRINT "HELLO"

This is more important than deleting bytes. If unreachable code mentions a capability, the program should not be refused for a request it can never make.

Library artifacts are different: exported procedures may be called by a future bound consumer, so a published .dbl retains its full export surface.

Constant materialization and pooling

CONST values become immutable image values instead of mutable storage:

CONST Margin = 12
CONST Title$ = "D/BASIC"

Identical string literals share one constant-pool entry. Shared-library constants used by a dependent are materialized into that dependent's own image.

The compiler uses canonical language math for any numeric rewrite. It never invents a second host-only arithmetic whose rounding could disagree with the runtime.

Float trees: remove crossings, keep order

Consider:

Distance# = SQR(X# * X# + Y# * Y#)

On a desktop, individual operations are cheap. On a small machine whose binary floating point lives behind a MATH provider, five separate crossings are not.

The optimizer promotes the largest safe homogeneous SINGLE or DOUBLE tree with at least two operators into one portable postfix evaluation record. Eligible work includes +, -, *, /, ^, unary minus, ABS, SQR, LOG, EXP, SIN, COS, TAN, and ATN.

It stops at an observation boundary:

  • a procedure or property call;
  • a conversion;
  • a comparison;
  • an array access with observable checks;
  • RND, TIMER, or another stateful intrinsic; or
  • a mixed numeric format.

A capable host executes the batch in one MATH crossing. A simpler host expands the same ordered record through scalar rows. The DBC does not fork.

Whole-array semantics beat guessed loop vectorization

The compiler does not have to prove that this loop has no alias or side effect:

ARRAYFILL Cells%(), -1

The source already states the semantic operation. ARRAYFILL lowers to one instruction; the runtime may implement it with vector hardware or bounded transactional scalar slices. It is still O(n) work and can require a temporary same-sized allocation.

Likewise:

ARRAYSWAP Current%(), Previous%()

is a binding exchange, not an element loop. It moves no elements and allocates no array.

These verbs are both usability and optimization features: the clearest spelling is also the most optimizable one.

Typed array math

MATH.ARRAY_MAP and MATH.ARRAY_REDUCE can use a compact typed DBC projection when the element format is known at compile time. That exact type lets the image declare only the capabilities it really needs instead of conservatively requesting every possible floating-point route.

This is a recurring D/BASIC design principle: precise source lets the compiler ask for less.

Declared needs and derived requirements agree

An APP ... NEEDS block is the application's coarse maximum promise: the product contract a packager, launcher, and person can inspect. The compiler separately walks reachable emitted code and derives the exact operations and versions the DBC actually contains.

APP "REPORT"
  LABEL "Report"
  NEEDS GFX, FILES
END APP

The actual reachable requirement must fit inside that declared maximum. An under-declaration refuses because the program reached beyond its contract. An over-declaration is legal and remains meaningful: the loader admits the declared maximum before starting, even if one build does not happen to reach every member.

The emitted image declares:

  • container and Service/Intent versions;
  • required capabilities and math features;
  • stack and call-frame ceilings;
  • static and runtime memory shape;
  • faces and handlers; and
  • exact shared-library dependencies.

The compiler prevents the two layers from drifting: emitted use cannot exceed NEEDS, and the DBC records both the admission promise and the exact vocabulary that justified its version and capability floors.

The four costs to see

Desktop folklore says “CPU time.” Cross-platform D/BASIC needs a more useful model:

Cost Ask Better source shape
VM instructions how many semantic steps? use a bulk verb when the operation is bulk
provider crossings how often does work cross a boundary? keep pure float trees visible; use typed wrappers
allocation what temporary state must exist? swap ownership instead of copying when appropriate
presentation damage how much visible surface must be repaired? setters + WorksSurfaceDamage + clipped painting

This model explains why an optimization can matter enormously on an 8-bit host and still be the cleanest code on Windows.

Optimize the idea, not the target

Good D/BASIC performance habits:

  • use explicit integral types for integral work;
  • retain a pure calculation in one expression when its order is clear;
  • use ARRAYFILL, ARRAYCOPY, and ARRAYSWAP to state whole-array intent;
  • keep external calls at clear boundaries;
  • use widgets' damage setters rather than repainting on every event;
  • query live bounds and viewport size instead of hard-coding a target; and
  • let the runner select the accelerator or composed route.

Avoid IF Platform$ = ... branches. They produce several barely tested applications inside one listing and prevent the runtime from adapting locally.

Diagnostics are part of the optimizer contract

An optimization must never turn an invalid request into a plausible one. If a call has the wrong arity, an array shape is incompatible, a class override does not match, or a legacy machine operation has no semantic translation, compilation stops at the source position with a named explanation.

Warnings and refusals should teach the next valid move. They are not evidence that the compiler “gave up”; they are evidence that it declined to invent meaning.

Test what the user can observe

The command-line tools support three useful levels:

dbasic run program.bas
dbasic build program.bas -o program.dbc
dbasic trace program.dbc -o program.trace
  • run answers whether the source produces the intended result.
  • build separates compilation from execution and yields the artifact to ship.
  • trace records canonical observable behavior for conformance and runtime comparison.

Test successful output, final status, and deliberate refusals. A portability claim is strongest when two independent runtimes produce the same trace from the same image.


Next: 6502 & the Large-Memory Model →