Skip to content

title: Compiler & Optimizer description: What happens between the d/BASIC you write and the compiled program file: old dialects brought in, meaning preserved, work removed, machine needs worked out, and the same bytes produced every time.


Compiler & optimizer

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

This page explains what happens between the d/BASIC source you write and the compiled program file, .dbc, that the runtime runs. You will learn how programs from older BASICs are brought in, what the optimizer will and will not change, how the compiler works out what a program needs from the machine, and which command-line tools to use. You do not need any of this to write a program. It helps when you want to know why programs are portable and what makes them fast.

d/BASIC's compiler is a new optimizing compiler built around one language and one bytecode format (DBC, the format inside every .dbc file). There is no "Windows dialect", no "Atari dialect", and no secret browser parser. Portability begins before optimization does.

The pipeline

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

Each stage has a different job:

  1. Dialect normalization recognizes historical BASIC forms that carry over and rewrites them into standard d/BASIC.
  2. Lexing identifies names, literals, keywords, punctuation, comments, line numbers and positions.
  3. Parsing builds the structure of the program without losing meaningful parentheses or labels.
  4. Record analysis fixes how values are laid out.
  5. Object analysis works out fields, access, inheritance, lifetime and method dispatch.
  6. Optimization and lowering choose the smallest bytecode shape that still means exactly what the program says.
  7. Self-validation reads the finished file back using the same rules the runtime applies when it loads one.

If the compiler's own output would be rejected as malformed, it produces no file at all. That last step turns "the compiler and the loader disagree" into a compiler bug caught at build time.

The dialect optimizer

Retro BASIC is a family of recognizable ideas spelled in incompatible local ways. The dialect optimizer is the door for migrating those programs, not a mode the runtime switches into.

It can normalize:

  • numbered-line control flow and compact statement separators;
  • implicit and suffixed types;
  • vendor-specific loop, text, graphics, sound and file idioms;
  • portable arithmetic and string forms;
  • calls whose old argument rules imply BYREF or forced values; and
  • machine-specific operations that have a registered equivalent among d/BASIC's intents (an intent is a named request such as "fill this rectangle" that the machine carries out however it can).

The output is ordinary d/BASIC. Once normalized, it travels through the same parser, optimizer, bytecode and compatibility checks as freshly written source.

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

This keeps an important rule: one language, no dialect switches. A migrated program can be reviewed and saved. The runtime never changes its grammar because it noticed a platform or a file's ancestry.

When an old machine operation has no honest equivalent, the optimizer stops and names the boundary. Guessing that one magic address "probably means screen colour" in every listing would be translation theatre.

The same bytes every time

The same source, imports, shared libraries and compiler version produce a byte-identical .dbc. The compiler does not embed a build path, the current time, an accident of hash-map ordering, or a host pointer.

That matters for more than tidy builds:

  • a package can hash exactly what will run;
  • the Windows, Linux, macOS, browser and d/OS runtimes can compare one file;
  • traces can expose a disagreement between runtimes; and
  • an optimization can be reviewed as a change to the bytecode rather than a hunch.

Reachability: do not pay for procedures that are never called

The compiler starts from the program's entry point, its startup and shutdown handlers, its event handlers and any reachable object methods, then follows direct calls. An ordinary procedure that can never be reached adds no work and no 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 about more than saving bytes. If unreachable code mentions a capability, the program should not be turned away for a request it can never make.

Library files are different: an exported procedure may be called by a program bound to it later, so a published shared library file, .dbl, keeps its full set of exports.

Constants become values, and identical ones are shared

CONST values become fixed values inside the file instead of storage that could change:

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

Identical string literals share one entry in the file's constant pool. Constants from a shared library that a program uses are copied into that program's own file.

Whenever the compiler rewrites a number, it uses d/BASIC's own arithmetic rules. It never invents a second, host-only arithmetic whose rounding could disagree with the runtime.

Floating-point expressions: fewer trips, same order

Consider:

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

On a desktop, each of those operations is cheap. On a small machine whose floating point lives behind a MATH provider (the part of the host that does the arithmetic), five separate trips across that boundary are not.

The optimizer takes the largest safe expression tree of one floating-point type, SINGLE or DOUBLE, with at least two operators, and turns it into one portable record that the runtime evaluates in order. Eligible operations are +, -, *, /, ^, unary minus, ABS, SQR, LOG, EXP, SIN, COS, TAN and ATN.

It stops at anything a program could observe:

  • a procedure or property call;
  • a conversion;
  • a comparison;
  • an array access, because bounds are checked;
  • RND, TIMER, or another intrinsic with state; or
  • a mix of numeric types.

A capable host runs the batch in one MATH crossing. A simpler host walks the same ordered record one step at a time. The compiled file is the same for both.

Whole-array verbs beat guessed loop vectorization

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

ARRAYFILL Cells%(), -1

The source already says what the whole operation means. ARRAYFILL becomes one instruction; the runtime may carry it out with vector hardware or in bounded, all-or-nothing slices. It is still work proportional to the array's size, and it may need a temporary array of the same size.

Likewise:

ARRAYSWAP Current%(), Previous%()

exchanges the two names, not the elements. It moves nothing and allocates no array.

These verbs help both readers and the optimizer: the clearest spelling is also the one the compiler can do the most with.

Typed array maths

MATH.ARRAY_MAP and MATH.ARRAY_REDUCE can be written into the compiled file in a compact typed form when the element type is known at compile time. Knowing the exact type lets the file declare only the capabilities it really needs instead of asking for every possible floating-point route just in case.

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

What you declare and what the compiler finds must agree

An APP ... NEEDS block is the program's declared maximum: the coarse promise that a packager, a launcher and a person can read. The compiler separately walks the reachable code and works out exactly which operations and versions the compiled file actually uses.

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

What the program actually reaches must fit inside what it declared. Declare too little and the compiler stops and tells you what the program reached beyond its promise. Declaring too much is allowed and still means something: the loader checks the whole declared maximum before starting, even if one particular build does not use every part.

The compiled file records:

  • the container and Service and intent versions it needs;
  • required capabilities and maths features;
  • stack and call-frame ceilings;
  • how much static and runtime memory it needs;
  • its entry points and handlers; and
  • exact shared-library dependencies.

The compiler keeps the two layers from drifting apart: the code cannot use more than NEEDS declares, and the file records both the declared promise and the exact vocabulary that justified its minimum version and capabilities.

The four costs to watch

Desktop folklore says "CPU time." Cross-platform d/BASIC needs a more useful model:

Cost Ask Better source shape
VM instructions how many steps does the program take? use a bulk verb when the operation is bulk
provider crossings how often does work cross a boundary into the host? keep pure floating-point expressions in one piece; use typed wrappers
allocation what temporary state must exist? swap ownership instead of copying when you can
redrawing how much of the visible surface must be repainted? setters + WorksSurfaceDamage + clipped painting

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

Optimize the idea, not the machine

Good d/BASIC performance habits:

  • use explicit integer types for integer work;
  • keep 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 the widgets' damage setters rather than repainting on every event;
  • ask for the live bounds and viewport size instead of hard-coding a machine; and
  • let the runtime pick the accelerator or the baseline display.

Avoid IF Platform$ = ... branches. They produce several barely tested programs inside one listing and stop the runtime from adapting on its own.

Error messages are part of the optimizer's job

An optimization must never turn an invalid request into a plausible one. If a call has the wrong number of arguments, an array shape does not fit, a class override does not match, or an old machine operation has no equivalent, compilation stops at the source position and names what is wrong.

Warnings and errors should teach the next valid move. They are not a sign that the compiler "gave up"; they are a sign 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 compiling from running and gives you the file to ship.
  • trace records the program's observable behaviour in a standard form, so two runtimes can be compared.

Test the successful output, the final status, and the cases where the program is meant to stop and say why. A portability claim is strongest when two independent runtimes produce the same trace from the same file.


Next: 6502 & the Large-Memory Model →