v32lua: Vircon32 Lua Compiler

A Vircon32-assembly targeting lua compiler

v32lua: Vircon32 Lua Compiler

Spanish version / en español French version / en francais

Target Architecture: Vircon32 Fantasy Console (32-bit)

Implementation Language: C (Flex/Bison + Custom Semantic Emitter)

Repository: github.com/wedge1020/v32lua

API Reference: doc/API.md — the full native Vircon32 API (sound, graphics, input, tilemaps, memory card, and raw I/O ports).

v32lua is a Lua compiler written in C that targets the Vircon32 fantasy console. Instead of embedding a heavyweight bytecode interpreter, v32lua parses Lua source code and compiles it directly into native Vircon32 assembly, and also produces the XML cartridge definition the console’s toolchain needs to package a ROM.

While it is not yet complete, one aim of development is to make v32lua a substitute (by no means a replacement) for the Vircon32 C Compiler in the Vircon32 development stack. Basically, pick your choice of language — C or Lua — and once it’s compiled down to assembly, you proceed with the build regardless of implementation language. As a result, efforts have been made to mimic various behaviours of the Vircon32 C Compiler to make compiler substitution more transparent.

Designed from the ground up with retro fantasy-console constraints in mind, v32lua features zero-cost hardware intrinsics, custom NaN-boxing, and — beyond the native Vircon32 API — two API compatibility layers so that carts written for TIC-80 and PICO-8 can compile and run on Vircon32 hardware with little to no source modification.

+------------------+     +-------------------+     +------------------+
| Source (.lua)    | --> | Lexer & Parser    | --> | AST Construction |
+------------------+     | (Flex / Bison)    |     +------------------+
                         +-------------------+              |
                                                            v
+------------------+     +-------------------+     +------------------+
| Cartridge Config | <-- | Vircon32 Assembly | <-- | Semantic Emitter |
| (.xml)           |     | Emitter (.asm)    |     |                  |
+------------------+     +-------------------+     +------------------+

Table of Contents


Getting Started

Requirements

Building the Compiler

The repository includes a root-level Makefile that manages building the compiler binary, running the test suite, and general project upkeep. To build the main compiler binary from source, run the default target from the repository root:

make

This produces the v32lua binary (under bin/), which turns a .lua source file into a Vircon32 .asm file plus an accompanying cartridge .xml. From there, assembling and packing follows the same steps as any other Vircon32 project (assemble → packrom → run under v32sim or on real hardware).

Reference Table of Makefile Targets

Target Description Core Actions & Dependencies
all Default target. Builds the main compiler executable. Invokes the compilation process natively inside the src/ subdirectory.
clean Standard workspace cleanup utility. Recursively wipes intermediate build artifacts out of src/ and removes generated files from testing/ and demos/.
install Installs the compiler binary onto the host system. Passes the target down to the src/ directory’s localized installation scripts.
tests Executes the automated compilation testing suite. Depends on the compiler binary (bin/v32lua) being built first, then triggers the test routines inside testing/.
demos Builds the collection of available demos. Depends on the compiler binary (bin/v32lua) being built first, then builds each demo under demos/.
asmcheck Validates assembly correctness. Requires bin/v32lua to be present, then processes assembly validations via the testing/ suite.
monofiles Builds streamlined monolithic file variants (used for pasting the whole project into a single-file conversation). Runs the monofile creation workflow sequentially inside both src/ and testing/.

Your First Cartridge

--#title "v32lua Tech Demo"
--#version "1.0"
--#texture tex_logo "logo.png"

x_pos = 160.0
y_pos = 120.0
speed = 2.5

function init()
    -- Set background clear color using zero-cost GPU port mappings
    ioports.gpu.bgcolor = 0xFF003366
    ioports.gpu.texture = tex_logo -- set texture
    ioports.gpu.region  = 0 -- set region

    -- define the region
    ioports.gpu.minX    = 0
    ioports.gpu.minY    = 0
    ioports.gpu.maxX    = 100
    ioports.gpu.maxY    = 50
    ioports.gpu.hotX    = 0
    ioports.gpu.hotY    = 0
end

function game_loop()

    -- Update state using pure floating-point math
    if ioports.inp.left > 1 then
        x_pos = x_pos - speed
    else if ioports.inp.right > 1 then
        x_pos = x_pos + speed
    end

    -- Direct hardware drawing
    ioports.gpu.x = x_pos
    ioports.gpu.y = y_pos
    ioports.gpu.draw()

    -- Table access and built-in string concatenation
    local frame = system.frames
    if frame > 1000 then
        local msg = "Demo Running: Frame " .. frame
        print(msg)
    end
end

Compile it with:

$ v32lua -o program.asm program.lua

v32lua emits program.asm and program.xml alongside it; hand those to the Vircon32 assembler and packrom to produce a runnable cartridge.

Command-Line Usage

$ v32lua [options] file

Available options:


API Compatibility Layers

v32lua supports three distinct API surfaces, selected with the --#api cartridge hint (native Vircon32 is the default when no --#api hint is present):

--#api "tic80"   -- opt into the TIC-80-compatible API surface
--#api "pico8"   -- opt into the PICO-8-compatible API surface

Only one API surface is active per cartridge; selecting tic80 or pico8 replaces the native call surface rather than adding to it.


Cartridge Resource Hints

v32lua allows you to embed Vircon32 cartridge metadata directly in your Lua source code using special --# line comments. The compiler parses these hints to auto-generate the project’s .xml ROM definition and assign sequential hardware resource IDs.

Supported hints:

Hint Purpose
--#version "X.Y" Sets the cartridge version field in the XML.
--#title "TITLE" Sets the cart title.
--#api "tic80" / --#api "pico8" Selects a compatibility API layer (see above).
--#texture NAME "path/image.png" Registers a texture resource and binds it to a compile-time constant NAME.
--#sound NAME "path/sound.vsnd" Registers a sound resource and binds it to a compile-time constant NAME.
--#tilemap NAME "path/map.csv" Registers a tilemap from a CSV file, embedded directly into the ROM image (see doc/API.md).
--#include "file.lua" Textually splices another Lua file in at this point, before parsing begins (see below).
--#version "1.1"
--#title "Space Grinder: Tech Demo"

-- Register textures (automatically binds 'bg_space' to ID 0, 'spr_ship' to ID 1)
--#texture bg_space "assets/background.png"
--#texture spr_ship "assets/player.png"

function init()
    -- Variables declared in hints are globally available in Lua at runtime!
    ioports.gpu.texture = bg_space
end

When compiled, v32lua outputs both the compiled .asm assembly and a complete Vircon32 XML cartridge definition file linking .vtex and .vsnd assets. With this, and the proper processing of any PNG and WAV data, you can proceed to the packrom step. Resource IDs are assigned in source order and are guaranteed to match their position in the generated XML.

Multi-File Projects (--#include)

Because Vircon32 carts are a fixed ROM assembled entirely at build time — there is no runtime filesystem — v32lua does not support real Lua’s dynamic require/dofile. Instead, --#include "file.lua" is a compile-time textual paste, resolved by a preprocessing pass before the lexer ever sees the file, exactly like C’s #include:

--#include "src/physics.lua"
--#include "src/entities.lua"

Compilation Pipeline

Compilation Flow

  1. Lexical & Syntax Analysis: Flex/Bison parses the Lua source into a typed Abstract Syntax Tree (AST).
  2. Symbol & Scope Resolution: Resolves variables across lexical scopes, mapping globals to sequential RAM addresses and locals to [BP - offset] stack frame positions.
  3. Code Generation: Emits Vircon32 assembly instructions, applying hardware intrinsic substitutions as it walks the AST.
  4. Cartridge Assembly: Emits the final .asm file, embeds runtime support routines, generates the read-only string data section, and outputs the .xml cartridge definition.

Compilation Stages (-v)

When -v is enabled, v32lua reports its progress through its pipeline stages:

  1. Stage 1: Lexer — Tokenizes the Lua source, stripping standard comments and processing string escape sequences (\n, \t, \r, \\, \").
  2. Stage 2: Preprocessor — Expands --#include directives and evaluates cartridge hints (--#...) and custom comment syntaxes.
  3. Stage 3: Parser — Constructs a complete Abstract Syntax Tree (AST) using a LALR(1) Bison grammar with strict operator precedence (PEMDAS + logic core).
  4. Stage 4: Semantic Analyzer — Executes a pre-pass to register global function and variable symbols and initialize the global scope.
  5. Stage 5: Emitter — Traverses the AST to generate Vircon32 assembly, applying register allocation and scope offsets, and finally outputs the cartridge XML configuration file.

Key Language & Compiler Features

Flexible Execution Models: main() vs. game_loop()

To accommodate different game architecture styles, the compiler supports two distinct entry point paradigms:

A program must declare at least one of main() or game_loop() — this is the designated entry point and its absence is a compile error.

NaN-Boxing: RAM vs. ROM Elements

v32lua uses a 32-bit tagging architecture that packs type metadata and payload pointers into unified values, keeping immutable ROM elements (string literals, function pointers) distinct from dynamic RAM heap objects (tables):

Data Type Hex Mask / Tag Architecture Description
Nil 0xFFC00000 Canonical representation for undefined/missing values.
Boolean False 0xFFC00001 Short-circuit falsy value.
Boolean True 0xFFC00002 Short-circuit truthy value.
ROM String 0x7FC00000 Pointers to read-only string data sections (__string_%d) in ROM.
Table / Boxed Object 0xFF800000 Boxed heap memory addresses (Bit 31=1, Bit 22=0).
Number IEEE 754 Float Unboxed native Vircon32 floating-point values for direct math.

Hardware Intrinsics & I/O Mapping

High-performance Vircon32 games cannot afford hash-table lookups for hardware manipulation. v32lua intercepts specific table member expressions and function calls and compiles them directly into native hardware I/O instructions:

See doc/API.md for the complete, authoritative reference — this README highlights the ideas, the API doc covers every call.

Developer Experience & Debug Tooling


Supported Lua Language Features

v32lua implements a subset of Lua, tailored specifically for game development on embedded hardware.

Variables & Scoping

In Lua parlance, functions are “first-class citizens”, and are effectively variables. That is borne out in v32lua as they both are transacted within the NaN-boxing scheme.

Multiple Assignment

The compiler natively supports multiple assignment and variable swapping without requiring explicit user temporaries:

local x, y, z = 10, 20, 30
x, y = y, x -- Synthesizes temporary register chains to safely swap values

Object-Oriented Programming & Tables

v32lua provides seamless syntactic sugar for table-based OOP models:

function Player.move(dx, dy) ... end
-- Desugars to: Player["move"] = __function_Player_move
Player:move(5, -2)
-- Desugars to: Player.move(Player, 5, -2)

Control Flow

Operators & Expressions

Functions & Multi-Value Returns

Functions can return multiple values simultaneously. The calling convention optimizes the first three returned expressions by placing them directly into registers R0, R2, and R3. Any additional return values (4th and beyond) are spilled directly onto the caller’s stack frame.

String Literal Pooling

All string literals declared in source code (e.g., "GAME OVER") are collected during compilation, deduplicated, and emitted into a dedicated data section at the end of the ROM (__string_0: string "GAME OVER"), preventing redundant ROM consumption.

Truthy & Falsy Short-Circuit Evaluation

In Lua, only nil and false evaluate to false in conditional expressions; every other value (including 0 and empty strings) is truthy. v32lua implements this via two high-speed assembly emission primitives:

When logical operators (and, or) are evaluated, the evaluated result is left intact in the destination register, preserving Lua’s idiom of returning the actual operand value rather than a strict boolean.


Hardware I/O & Compiler Intrinsics

One of the most powerful features of v32lua is its static intrinsic interception engine. When the compiler encounters table accesses or function calls matching specific system paths (e.g., ioports.gpu.clear()), it bypasses dynamic table lookups entirely and emits direct Vircon32 hardware I/O instructions (IN, OUT).

Automatic Type Casting Across I/O Boundaries

Because Lua variables are stored as NaN-boxed IEEE 754 floats while Vircon32 hardware ports expect 32-bit integers or booleans, v32lua automatically injects hardware conversion instructions during port reads and writes:

Comprehensive Intrinsics Reference Table

The full, authoritative reference for every intrinsic — ioports.gpu.*, ioports.inp.*, ioports.spu.*, ioports.tim.*, ioports.rng.*, ioports.car.*, ioports.mem.*, music.*/sfx.*, tilemap.*, memcard.*, and system.* — lives in doc/API.md, including port-ordering caveats, call signatures, and worked examples. A short sample of the most commonly used entries:

GPU Control & Drawing (ioports.gpu.*)

Lua Path / Intrinsic Vircon32 Port / Command Access Description & Behavior
ioports.gpu.texture GPU_SelectedTexture Read / Write Sets or reads the active texture ID used for drawing operations.
ioports.gpu.region GPU_SelectedRegion Read / Write Selects the texture sub-region (sprite frame) to render.
ioports.gpu.x / ioports.gpu.y GPU_DrawingPointX/Y Read / Write Screen coordinates for drawing placement.
ioports.gpu.minX/minY/maxX/maxY GPU_RegionMin/MaxX/Y Read / Write Defines the pixel boundaries of the active texture region.
ioports.gpu.hotX/hotY GPU_RegionHotSpotX/Y Read / Write Sets the drawing origin (hotspot) relative to the sprite region.
ioports.gpu.draw([mode]) GPU_Command Function Call Executes a hardware draw command: "zoom", "rotate", "rotozoom", or default.
ioports.gpu.clear([color]) GPU_ClearColor + GPU_Command Function Call Sets the clear color and wipes the screen. Supports preset color strings ("black", "white", "blue", "red", "green") or numeric hex values.

Gamepad & Input (ioports.inp.*)

Lua Path / Intrinsic Vircon32 Port / Command Access Description & Behavior
ioports.inp.gamepad INP_SelectedGamepad Read / Write Selects the active controller index (0-3) for input polling.
ioports.inp.status INP_GamepadConnected Read Only Returns a Lua boolean: is the selected gamepad connected.
ioports.inp.left/right/up/down INP_Gamepad* Read Only D-Pad directional state (> 0 pressed, < 0 released).
ioports.inp.A/B/X/Y/L/R/start INP_GamepadButton* Read Only Action/shoulder button state (> 0 pressed, < 0 released).
ioports.inp.inputs Custom Action Subroutine Read Only Collation intrinsic: polls all gamepad buttons/axes in one pass, collates them into a single 32-bit bitmask, and casts it to a Lua float.

System & Runtime Utilities

Lua Path / Intrinsic Vircon32 Instruction Access Description & Behavior
system.halt() HLT Function Call Emits the hardware HLT instruction, immediately terminating CPU execution or freezing the frame until the next interrupt/frame cycle.
system.wait() WAIT Function Call Emits the hardware WAIT instruction, pausing execution until the next interrupt/frame cycle.
system.frames / system.cycles TIM_FrameCounter / TIM_CycleCounter Read Only Running frame/cycle counters.
print(x, y, ...) __builtin_tostring + __builtin_print Function Call Coerces arguments to string representation and outputs them to the console debug terminal. First two parameters are the X, Y position on screen, in pixels.

Inline Assembly (__asm__ & __rawasm__)

For performance-critical inner loops or advanced Vircon32 hardware manipulation, v32lua provides direct inline assembly injection.

Standard Inline Assembly (__asm__)

The __asm__ directive allows embedding raw Vircon32 assembly strings directly inside Lua functions. Crucially, it supports variable interpolation, enabling seamless bridging between Lua scope symbols and assembly registers:

local speed = 5.0
__asm__( "MOV R0, {speed}\n" ..
         "FADD R0, 1.5\n" ..
         "MOV {speed}, R0" )

Raw Assembly (__rawasm__)

The __rawasm__ directive outputs the literal string directly to the assembly stream without safeties applied. This can be quite dangerous, and should only be used by the most knowledgeable and experienced of assembly users. It is also the basis of the compiler’s own unit-test harness: a test file is typically a function main() ... end wrapper around a sequence of __rawasm__ blocks with __debugN: labels for breakpointing under v32sim


Memory Map Reference

RAM Address Designation Usage
0 HEAP_POINTER Stores the dynamic starting address for runtime table/string allocations.
1, 2 FTOA_SCRATCH_PTR_A/B Reserved scratch words used by the float-to-string conversion routine.
3 to HEAP_START - 1 Global RAM Sequentially allocated slots for global Lua variables, resource IDs, and promoted top-level locals.
HEAP_START and up Dynamic Heap Runtime memory managed by the table allocator and string routines.
Stack Top (SP) Call Stack Function activation records, local variables, and saved register states.

HEAP_START is computed after all code generation has finished, so top-level statement codegen that registers late globals can never collide with the heap.


Compiler quirks and assumptions

While v32lua attempts to be a functional Lua compiler, it by no means is a full-to-specification implementation of the language. For one, there’s no bytecode virtual machine, nor interpreter — Lua compiles straight down to native assembly.

Further, there are some explicit deviations from a standard implementation of the language to better suit the freestanding environment of Vircon32:

Clearly, this effort is focused on making a tool for development on Vircon32, and not on being a fully-compliant Lua implementation. Efforts will be made to come as close as is possible and feasible, without sacrificing significant performance or veering away from being the tool it is intended to be.

Compiler Optimization

Early on in compiler development, all optimization code was removed and factored into a separate tool, v32opt. This is designed as a general purpose Vircon32 assembly optimizer, meant for use with the C compiler and lua compiler (along with handwritten assembly). Early tests have shown some mild improvements to performance, and potential space savings by eliminating redundant instructions.

At time of writing this tool is still very much in development, but is showing promise and will likely work for standard scenarios under the -O1, -O2, and even -O3 optimization levels. It is meant to be inserted into the build chain after the compiling and before assembling.

Roadmap / Not Yet Implemented

The following are known, deliberate gaps rather than bugs — either deferred to keep initial development moving, or awaiting a design decision:


AI utilization

NOTE: There was extensive AI use and interaction throughout this effort. A distinction should be made from “vibe coding”, but there is definitely a blur between human and AI. In the end, both benefit and could compensate for the other’s deficiencies.

This endeavour actually was not primarily about developing a compiler, it began as an honest attempt to get a feel for AI and its impact: its role and detriment to human thinking and education. That it has a compiler theme was merely to accentuate a point of interest. It has certainly been a learning experience. If sufficient compiler concepts and background knowledge weren’t sufficiently known going into this, the effort would have ended far less successfully.