The BASIC language

aukimi's classic BASIC dialect: types, structures, the two shorthands, and the rules that catch everyone. 2 237 commands behind it.

In one line#

Aukimi ships a classic BASIC dialect: a full language, not a toy macro system, with typed numerics, records, methods, references, structured errors, coroutines and WebGPU compute. It lives in the Engine's TXT viewport mode and in the script editor's BASIC view.

JavaScript remains the canonical scripting API. BASIC is an optional frontend: both compile to the same private dispatcher, so a BASIC program and a JavaScript one reach exactly the same engine. Nothing is second class, and nothing is a separate runtime.

The rule that catches everyone#

A command call always carries parentheses.

mode = ClipSpaceMode()
SpritePosition(player, 320, 180)

This is not style. It is what lets the compiler tell a command from a variable, and it is why a misspelling is now caught at compile time:

You writeWhat used to happenWhat happens now
SpritePositon(1, 10, 20)compiled cleanly, the sprite did not move, and the failure surfaced later as ... is not definedcompile error, with the nearest catalogued spelling suggested
Blorp(1, 2)forwarded verbatim into the generated JavaScriptcompile error
ceci nest pas du basicemitted as a statementcompile error

A writing mistake used to present itself as an engine bug. Measured against the 84-demo corpus, the guard produced zero false positives and found two real silent failures, SetMusicSystemVolume and GetLightSensorExists, which the catalogue only knows as MusicSystemVolume and LightSensorExists.

Two leaks are left open on purpose: console.log("x") and window.x = 1 still pass. A guard for them was written, measured and withdrawn: BASIC runs inside a context that supplies names the compiler cannot enumerate, so refusing an unknown root rejected valid programs. A false positive there is worse than the leak.

The single most common surprise:

Print(...)draws on the screen, and a program calls it every frame
Console(...)writes to the editor's console, and does nothing else
Debug(...)the same command under PureBasic's name, for the muscle memory
ConsoleWarning, ConsoleError, ClearConsolethe rest of the log

Print in a frame loop would drown any real error in the console, which is why the two are separate commands rather than one with a flag.

Types#

Numeric types are real, and they behave identically on every target, the editor, the HTML export and the Bevy/native build:

  • Signed and unsigned 8, 16, 32 and 64-bit integers, with deterministic coercion, integer DIV, and wrapping overflow.
  • float32 and float64.
  • The classic sigils still work: name$ is a string, speed# a float.

OPTION EXPLICIT is opt-in, and classic implicit variables remain the default so imported programs still run. Turning it on is the prerequisite for the two features that cannot be safe without it, numeric typing and references because a misspelled assignment silently creating a variable defeats both.

Structures#

Records#

TYPE declares a record. They nest arbitrarily, and they have value semantics: assigning one deep-copies it, including nested arrays and collections.

Methods#

METHOD binds a function to an instance, with SELF inside it, New to construct and Delete to destroy. Deep copies stay method-safe.

References#

REF and BYREF are the same thing under two spellings, and both give a live read/write parameter, for a scalar, a record, a single field, an array, a list cursor or a map entry. References forward through calls, and coerce to fixed-width types at the boundary.

Handles#

@target takes a first-class reference. HANDLE and POINTER are the typed forms, with checked read, write, validity and release, and a handle to a deleted record is invalidated rather than left dangling.

Control and safety#

Structured errors: TRY / CATCH / FINALLY, nested, with THROW, protected RETHROW, and both structured and string catches.

Conditional compilation: #IF / #ELSEIF / #ELSE / #ENDIF, nested, with feature flags and explicit profiles for the editor, HTML, Bevy, native/wasm and debug/release. It is resolved before includes, on every target.

Rich functions: default values, OPTIONAL, a final PARAMARRAY (or ...), safe arity overloads, typed return coercion, and cloned record and array returns.

Multi-file programs#

#INCLUDE, #INSERT and IncludeFile make a program several files. The editor creates and imports project modules, completes include paths, keeps the dependency graph, and every runtime target is wired for it.

Coroutines, and the thing deliberately left out#

Cooperative tasks are label-based, with typed handles, a result and error state, cancellation at yields, and awaits on one task or a group. They share the game scheduler on every target.

Threads with shared memory are deliberately excluded. Not "not yet" excluded. A cooperative scheduler you can reason about beats a data race you cannot reproduce.

WebGPU compute#

Typed f32 / i32 / u32 storage buffers, raw WGSL pipelines, frame-ordered dispatch and memblock readback, real GPU compute from BASIC.

⚠️ Engine only. The HTML target raises an explicit capability error, and the Bevy target reports the incompatibility before the build rather than producing a binary that fails on the first dispatch.

Two shorthands for the same problem#

597 of the commands are named Set/Get<Object><Property>: Sprite 66, Tween 58, Text 32, Edit 29. The object's name is inside the function name and its id is the first argument of every line. Both shorthands below remove that repetition, and neither is a new API: the compiler rebuilds the real command name and looks it up in the catalogue.

WITH <family>, inside a block#

WITH SPRITE s
  \position 100, 200      ` SpritePosition(s, 100, 200)
  \size 64, 64            ` SpriteSize(s, 64, 64)
  x# = \x                 ` SpriteX(s)
ENDWITH

At the head of a statement \prop writes; anywhere else it reads. Position decides, so there is no second notation to learn. WITH hero, one token, is still the record block; WITH SPRITE s selects a catalogue family.

s.position(...), everywhere else#

s = CreateSprite(img)     ` the family is DEDUCED from the creation command
s.position(100, 200)      ` SpritePosition(s, 100, 200)
s.visible = 1             ` SpriteVisible(s, 1)
x# = s.x                  ` SpriteX(s)

The family comes from Create<F> / Load<F> / Clone<F>, or from an explicit s AS Sprite when the id arrives from elsewhere. Only variables whose family is known are rewritten: records keep their fields, lists keep their methods, SELF.field is untouched.

⚠️ A typed parameter is not recognised. FUNCTION move(s AS Sprite) does not give s a family, the pass reads statement-level declarations and assignments only. Stated here because it is a real limit, not a bug you should hunt.

⚠️ The catalogue drops the verb on properties, and keeps it on lifecycle. SpritePosition writes, SpriteX reads, and neither carries a Set or a Get: 4 commands in the whole catalogue begin with Set, and none begins with Get. The verb survives where something comes into being or ends: CreateSprite, LoadImage, DeleteSprite.

That is worth knowing because a misspelling is a compile error, and Set… is the misspelling people arrive with. The catalogue is the authority, and the reference is generated from it.

String interpolation, opt in#

Print($"score: {points}")        ` "score: " + Str(points)

{{ and }} write literal braces.

⚠️ The $ prefix is mandatory, and it is not decoration. Interpolating every literal broke hand-written JSON, which the world API takes routinely:

CreateTerrain("island", "{\"preset\":\"island\",\"size\":256}")

There the braces are data, not holes. An unprefixed string is left untouched whatever it contains.

Naming is showing#

A classic program makes hundreds of objects. Only the ones it NAMES enter the Scene Graph, SpriteName(id, "player") and its siblings are what put an object in the tree where the rest of the Engine can see it. LightName and ParticlesName do the same for a light and an emitter.

The tutorial Name what your program creates walks through it: the same scene, once with the tree empty and once with three entries in it.

The command catalogue#

2 237 commands, 2 550 public overloads, 43 categories. The largest:

3D286Studio106File61
Core200Input95Memblock43
Tweening149Input-Raw943D Particles41
3D Physics144Multiplayer91World40
Sprite135Text68Particles38
Platform1112D Physics66Sound37

plus JSON (36), Image (35), Skeleton (34), XML (34), Music (29), Preference (27), Extras (21), HTTP (21), String (21), Math (19), Regular Expression (19), Video (18), Dictionary (17), Benchmarking (15), Compute (14), Date (13), Maths (12), Time (12), StringBuilder (9), Compression (7), Font (8), Error (4), Array (3), Cipher (2), Sort (2).

Studio and Compute are Aukimi's own additions to the classic dialect the console commands, object naming, and the WebGPU pipeline.

The complete command reference lists all of them, with every parameter and every overload, and its own search, generated from the same catalogue this page counts, so it cannot fall behind it. The editor's own API panel reads that catalogue too.

This page documents the language; that one documents the commands.

Limits and common snags#

  • Parentheses are not optional on a command. The bare PureBasic form (Debug "x") is refused, because it reopens the variable/command ambiguity that the compile-time guard depends on.
  • A user function wins over a catalogued command of the same name, and a variable may share a command's name in a typed declaration, on the left of an assignment, or as the root of a path.
  • Variables and user functions are case-insensitive, and their symbol spaces are genuinely separate from the catalogue's.
  • Print is not a log. See above; it is the mistake that fills a console.
  • Compute does not export. Write the fallback path before you rely on it.