Skip to content

Releases

UltimateBasic 1.6.7

Choose a tag to compare

@github-actions github-actions released this 10 Nov 13:24
eaf48f9

What's Changed

  • Fix debugger source lines when subs come before the main program by @zstarczali in #68
  • Add palette statement for Ultimate 64 / C64U colour palette by @zstarczali in #69
  • ci: add manual workflow to publish packages to FTP by @zstarczali in #70
  • ci: add optional GitHub release step to package workflow by @zstarczali in #71

Full Changelog: 1.6.6...1.6.7

UltimateBasic 64 1.6.6

Choose a tag to compare

@zstarczali zstarczali released this 18 Oct 12:31
3da3c2c

ltimate Basic v1.6.6 – Release Notes

2026-10-18 – Unreleased (after 1.6.6)

New

  • option strings n [, m]: a computed string variable holds up to n
    characters (1-255, default 80), a string-array element up to m
    (default 31). Each variable keeps its own buffer.
  • Compile-time warnings: a byte variable that gets a constant it
    cannot hold (b = 300, b = a + 1000) is reported with its line.
    Warnings never stop the build.
  • With --basic-print, STR$ gives the C64 BASIC text for every number
    (" 7", "-42", " 3.5"); without it a byte keeps "007".
  • Struct types: field defaults (var hp: word = 100 - the array's
    contents at program start), nested struct fields (e[i].pos.x),
    fixed-length string fields (var name: string * 20), struct
    parameters (sub heal(p AS TEntity) ... heal(e[i])), and the QBasic
    form TYPE ... name AS type ... END TYPE. A struct element can be at
    most 256 bytes (a compile error says so).
    Changed
  • In a SUB / FUNCTION closed by END SUB / END FUNCTION, END alone on
    its line ends the program, as in QBasic (also inside IF ... END IF).
    Blocks in such routines are closed QBasic-style.
    Fixed
  • / with a WORD, INTEGER or negative divisor used only the low byte
    of the divisor: -700 / -7 gave 0, 50000 / (v + 0) was wrong. It is
    a 16-bit division now, truncated toward zero.
  • END and BYE inside a sub or function returned to the caller
    instead of ending the program.
  • A variable index into a struct array was multiplied in 8 bits, so
    elements beyond byte offset 255 were read and written wrongly.
  • Word struct fields were cut to 8 bits inside expressions, and
    word + x / word - x dropped the high byte of a 16-bit x (a word
    array element, a word function).

2026-10-17 – Release 1.6.6: PRINT USING, string order, floats

New

  • PRINT USING "format"; values (QBasic subset): numeric fields with

    digits, . point, , thousands separators, a leading + (always a

    sign) and a trailing - (sign after the number); a number that
    does not fit is printed with a % in front. String fields & (whole
    string), ! (first character) and \ \ (as wide as the field);
    _ prints the next character as it is. Values cycle through the
    fields; a trailing ; or , keeps the cursor on the line. The format
    must be a string literal.
  • Strings can be ordered: < > <= >= compare in PETSCII order, a
    shorter string first when it starts the other ("AB" < "ABC").
  • RND as a floating-point number: in a statement that computes in
    floating point, rnd / rnd() is 0 <= x < 1 (d = rnd, x = rnd() * 6).
    rnd(n) and integer statements keep the 0-255 / 0..n-1 generator.
  • MOD with a SINGLE / DOUBLE operand works in floating point, like
    VB / C fmod: 7.5 mod 2 = 1.5, -7.5 mod 2 = -1.5; x mod 0 = x.
  • Floating point in IRQ / NMI handlers: a handler sub (irq, nmi,
    cia_timer) that uses SINGLE / DOUBLE saves and restores the
    runtime state, so it no longer disturbs the main program. Handlers
    without floating point cost nothing extra.
  • --basic-print (or a line "option basic print" in the program):
    C64 BASIC PRINT spacing - numbers print with a leading space (or
    their minus sign) and a trailing space (print 5 gives " 5 "), a ,
    between items moves to the next 10-column zone, a trailing , keeps
    the line. Without it the output is unchanged.
    Changed
  • Programs carry only the floating-point routines they use: a small
    program with + * and PRINT went from 4603 to 2293 bytes,
    examples/float_demo.ub from 6319 to 5660.
  • MOD with a WORD or INTEGER operand is a real 16-bit remainder with
    the sign of the dividend, like QBasic: -7 mod 3 = -1, 7 mod -3 = 1,
    (w + 5) mod 7 is no longer cut to 8 bits.
  • SHR of an INTEGER is an arithmetic shift: -8 shr 1 = -4. WORD and
    byte SHR stay logical.
    Fixed
  • A literal from 32768 to 65535 in a floating-point statement was
    read as a negative 16-bit pattern: d = 40000 gave -25536. It keeps
    its value now; integer statements are unchanged.

UltimateBasic 64 1.6.5

Choose a tag to compare

@zstarczali zstarczali released this 17 Oct 10:09
22404a6

Ultimate Basic v1.6.5 – Release Notes

2026-10-17 – Release 1.6.5: 16-bit address expressions

Fixed

  • An address made of a 16-bit constant plus a byte variable was
    computed in 8 bits: peek($07F8 + k) read $00F8 + k, and
    poke $07F8 + k, v wrote into zero page. Addresses are now 16-bit
    in peek, poke, peek16, poke16, fill, memcopy, drawmem, save and
    reu (also the fill / memcopy length, e.g. 40 * rows).
    constant + byte compiles to LDA / STA $07F8,Y.
  • poke ptr + i, v with a word pointer adds in 16 bits.
  • peek(k) with a byte variable returned k itself; it now reads
    memory at $00kk.
  • poke16 addr, base + k wrote only the low byte of the value.

UltimateBasic 64 1.6.2

Choose a tag to compare

@zstarczali zstarczali released this 04 Oct 18:27
c00aaa3

Ultimate Basic v1.6.2 – Release Notes

2026-10-04 – Release 1.6.2: tracker instruments

New (tune ... end)

  • imod id, vibdelay, vibspeed, vibdepth, pwmspeed, pwmmin, pwmmax:
    instrument vibrato (triangle LFO after a delay, depth scaled to the
    note's semitone) and pulse width modulation between min and max.
  • ifilt id, cutend, sweep, pingpong: filter cutoff sweep from the inst
    cutoff toward cutend, sweep per frame, optionally back and forth.
  • igate id, gatetimer, hardrestart, firstwave: gate released gatetimer
    frames before the next note, hard restart also zeroes ADSR, firstwave
    is written on a note's first frame ($09 = test + gate).
  • itab id, speed, loop, "WW +n" / ".. -n" / "WW =n": wave / arpeggio
    table (up to 32 steps), one step every speed frames, loop step or 255.
  • inst ctrl may carry ring modulation ($04) / sync ($02); modevol bit 7
    = voice 3 off.
    Changed
  • The player now works every frame (tables, vibrato, PWM, gate timer,
    filter sweep). Filter routing is per voice: the voice that starts a
    filter-routed instrument owns cutoff / resonance / mode, $D417 routes
    every voice whose instrument is filter-routed. A tune blob is about
    2.5 KB of player code plus its data.
  • Same algorithm as the Visual Assembler SID editor's player; the new
    tests/tune_player.rs compares the SID registers frame by frame with
    a trace of the editor's engine.

2026-10-03 – Release 1.6.1: smooth scrolling maps

New

  • map view px, py: shows the loaded map at a pixel position, once per
    frame (instead of wait raster). XSCROLL / YSCROLL with 38 columns /
    24 rows, two screen buffers ($0400 / $3C00) swapped at char steps,
    the next char cell built two frames ahead, color RAM copied in two
    halves behind / ahead of the raster beam: smooth and tear-free in
    every direction. Sprite pointers are copied to both buffers.
  • map load reads a VisualAssembler Level Editor project (.bin) and
    composes its screens into one map (max 6x10 screens, 255x255 cells).
  • map load "file", addr: the map data goes to a fixed address (like
    incbin "file", addr) instead of inline in the code.
  • Compile errors: map view without map load, maps smaller than 40x25,
    program code reaching into $3C00-$3FFF.
  • Example: examples/level_scroll_demo.ub.

Fixes

  • A byte product or shift assigned to a word variable inside a sum
    was truncated to 8 bits (py = cy * 8 + fy gave 67 instead of 323
    for cy = 40, fy = 3); it is now computed in 16 bits, like a lone
    product. w = b shl n keeps 16 bits too.
  • A constant multiplier above 255 was cut to 8 bits (cy * 300); the
    16-bit operand now always takes the multiplicand side.

UltimateBasic 64 1.6.0

Choose a tag to compare

@zstarczali zstarczali released this 03 Oct 14:32
cc44002

Ultimate Basic v1.6.0 – Release Notes

2026-10-03 – Release 1.6.0: optimised 6502 code generation

Performance

  • Conditions (if / while / until / select) compile to CMP + branch
    instead of materialising a 0/1 value and testing it again
    (if x == 10: 24 -> 9 bytes). x > n becomes x >= n+1; and / or of
    comparisons short-circuit when the right side is side-effect free;
    while loops are rotated (test at the bottom); select compares the
    variable in place.
  • Constants and byte variables are used directly as operands
    (ADC #n, CMP zp) instead of being staged in scratch zero page; poke to a
    constant or word address stores directly; identity operations vanish.
  • * by a constant: shifts / shift-add (x * 8 -> 3x ASL); otherwise a
    shift-add loop of at most 8 rounds (the old repeated-addition loop
    needed ~16 cycles per unit of the multiplier).
  • / and mod by a power of two: LSR / AND #(n-1); otherwise an 8-round
    shift-subtract loop (the old loop ran once per unit of the quotient).
  • x = x +/- 1|2 -> INC / DEC (word: 16-bit INC / DEC for +/-1).
  • Arrays with a variable index use base,Y addressing instead of a
    zero-page pointer.
  • for loops are rotated (no JMP per iteration); constant limits / steps
    are immediates and use no zero page.
  • A-register reuse: the leading LDA of a statement is skipped when it
    reloads the value the previous statement just stored.

Fixes

  • Division by zero used to hang: now x / 0 = 255 and x mod 0 = x.
  • for i = 0 to 255 looped forever; the step now detects the 8-bit
    wrap-around in both directions, so the loop runs 256 times and stops.

Tests

  • Full 6502 emulator for tests (tests/common/cpu6502.rs), semantic tests
    that run compiled programs (tests/codegen_semantics.rs), code-shape /
    cycle tests (tests/codegen_opt.rs) and an ignored differential fuzzer
    against a reference compiler (tests/differential_fuzz.rs).

2026-09-26 – chain: load and start another program

New language elements

  • Added chain "FILE" [, device]: loads another program to its own load
    address (normally $0801, right over the running program) and RUNs it, as
    if LOAD"FILE",8,1 and RUN had been typed. For boot / title programs.
    A 46-byte position-independent loader and the file name (1-16
    characters) are copied to the cassette buffer $033C. Before the load the
    I/O is reset (VIC IRQs off, SID volume 0, IOINIT, RESTOR, $01 = $37);
    after it the end-of-program pointer $2D/$2E is set, BASIC is
    re-initialised ($E453, $E3BF - a self-extracting cruncher such as
    Exomizer sfx may have used BASIC's zero page) and the program is RUN
    ($A659, $A7AE). Without a device the drive the program came from ($BA)
    is used, else drive 8. If the file cannot be loaded, the program
    continues with the statement after chain.

Fixes

  • load: the secondary address was swapped. load "FILE" used secondary
    0 with X/Y = 0 and loaded the file to $0000; load "FILE", addr used
    secondary 1 and ignored addr. Now load "FILE" loads to the file's own
    address (secondary 1) and load "FILE", addr to addr (secondary 0).
  • --d64: directory entries were written 30 bytes apart instead of 32, so
    every file after the first had a broken entry (e.g. --add data.prg
    showed up as a garbled name and could not be loaded). Also fixed: disk ID
    and DOS type were at the wrong BAM offsets; file data only used tracks
    1-17 (a larger disk crashed the build instead of reporting it); sectors
    are now allocated like the 1541 DOS does (tracks 17->1, then 19->35,
    interleave 10). A full disk is reported as "D64 error: disk full ...".
    New tests: tests/d64_tests.rs reads the image back (BAM, directory,
    sector chains) and compares every file.

2026-09-25 – Initialised arrays

New language elements

  • Added data <array>: v, v, ...: fills a declared array / array_word
    with constant values. Further lines continue where the previous one
    stopped; word arrays take 16-bit values (stored lo, hi). The values are
    stored after the code and copied into the array once at program start
    (right after the arrays are zeroed), so the table is writable and indexed
    like any other array (sinus[i]). These lines do not affect read.
    An undeclared name or more values than the array holds is a compile-time
    error. This is what the Visual Assembler Curve Editor's "Export to
    UltimateBasic" and "Export UltimateBasic demo" generate.

Ultimate Basic v1.5.7

Choose a tag to compare

@zstarczali zstarczali released this 22 Sep 10:38
b32397f

Ultimate Basic v1.5.7 – Release Notes

2026-09-21 – Tracker tunes and an inline-assembler fix

New language elements

  • Added tune ... end: describe a 3-voice SID tracker tune (instruments,
    order list, per-voice pattern rows such as "C-4 01 V24") directly in the
    source. The compiler builds a self-contained, zero-page-free player at
    compile time and defines sid_init / sid_play, so music play|stop| pause|resume work exactly as with load sid. Vibrato, slide, note cut
    and speed change are supported. tune at $addr moves the blob (default
    $8000). This is what the Visual Assembler SID editor's "Export to
    UltimateBasic" now generates.

Fixes

  • Inline asm { }: label,X, label,Y and JMP (label) were assembled
    as plain absolute instructions (the index/indirect mode was dropped), so
    table lookups such as LDA table,X read the wrong byte. The addressing
    mode is now kept.
  • music play: the CIA1 IRQ wrapper acknowledged the timer by WRITING $01 to
    $DC0D, which clears the interrupt mask, so the music IRQ stopped after the
    first tick. It now acknowledges by reading $DC0D.

2026-09-21 – Custom charsets, code layout and non-blocking sound

New language elements

  • Added charset on / charset off. charset addr (default $3800) used to
    only set the destination of chardef; charset on now points the VIC-II
    at that RAM set (read-modify-write of $D018 bits 1-3, the screen-matrix
    bits are kept) and charset off restores the ROM set ($1000, or $1800
    after lowercase). The base must be a multiple of $800 inside VIC bank 0.
  • Added a build-time check that generated code does not overlap the charset:
    charset on claims the 2 KB set and every chardef its 8 bytes; a
    program that reaches into them stops with "the program code (...)
    overlaps the charset area ...". Data in an org gap is not counted.
  • Added org addr. Placed between sub/fn definitions it continues the
    subroutine code at addr and zero-fills the gap, which stays free for
    data (fonts, sprites, tables). Subs never fall through, so this is safe;
    org below the current code end is an error.
  • Extended incbin with incbin "file", addr, skip [, len]: skip a header
    (e.g. a .prg load address) and/or take only a slice. incbin ..., addr can
    now be written into an org gap.
  • Added sfx channel, freq, frames [, wave]: a non-blocking SID note. It
    sets attack 0 / sustain 0 / release 0 and picks the decay from frames
    (1 frame = 20 ms) at compile time, re-triggers the voice and returns at
    once. wave is 16 (triangle), 32 (saw, default), 64 (pulse) or 128
    (noise). Unlike sound it is safe inside a game loop.

Fixes

  • sound waited on raster line 0, which $D012 (low 8 bits) shows twice per
    frame (lines 0 and 256), so notes were half as long as requested; two of
    its branches were also patched over their opcode, crashing the program
    for any non-zero duration. It now waits on raster line 200 like delay.
  • word assignments computed products in 8 bits: w += 300 * level,
    w = w + 100 * level and w = 100 * level + 900 now keep all 16 bits.
  • print a / 10 (and other arithmetic on a float) printed the raw Q8.8
    value as an integer; it now prints N.DD.
  • Arrays are zeroed once at program start (RAM contents at power-up are
    random; array(N) previously started with whatever was there).
  • Running out of permanent zero page ($02-$4F) is now a compile-time
    error ("out of zero page"). Before, variables and for limit/step
    bytes silently spilled into the scratch area and broke loops. Sequential
    for loops now release their two limit/step bytes when the loop ends
    (if the body declared no variables), so many loops no longer exhaust it.
  • Manual corrections: % binary literals were documented but never
    supported by the lexer (use $hex or decimal); the charset example used
    the $D018 value of another address ($1E selects $3800, $1A selects $2800).
  • Tests: a missing ROL zp opcode in the test CPU was added, and the koala
    tests no longer race on a shared temp file.

UltimateBasic 64 1.5.6

Choose a tag to compare

@zstarczali zstarczali released this 09 Sep 19:06
14fc8a6

Ultimate Basic v1.5.6 – Release Notes

2026-09-06 – Hires draw color + multicolor shapes

  • Added color pen c: sets a persistent hires drawing color (0-15). The
    plot/line/rect/circle/paint helpers stamp it into the video-matrix
    foreground nibble of every cell they touch; the background nibble is
    preserved. Default is white (1), so programs that never call color pen
    render exactly as before. Emitted only when color pen is used, so the
    plot helper is unchanged for existing programs.
  • Added multicolor shape commands mline, mrect, and mcircle (joining
    mplot). The trailing color argument is the 2-bit source (0-3); the
    actual colors come from the cell palette (screen / color RAM), exactly as
    for mplot. They reuse the hires Bresenham/midpoint routines via a shared
    draw-target switch, plotting each pixel through the mplot helper. mline
    and mrect require x in 0-159; mcircle clips off-screen points.

UltimateBasic 64 1.5.5

Choose a tag to compare

@zstarczali zstarczali released this 26 Aug 20:12
4d93336

Ultimate Basic v1.5.5 – Release Notes

2026-08-26 – Magic Desk CRT export

  • Added .crt output support: when the output filename ends in .crt, the
    compiler now writes a Commodore 64 cartridge image instead of a raw PRG.
  • The cartridge uses the same banked Magic Desk type-19 layout as
    VisualAssembler: 64-byte header, bank 0 boot loader, and the compiled PRG
    payload copied into RAM on startup.
  • Existing .prg and .d64 output paths are unchanged.
  • Added integration tests covering the CRT header, bank layout, payload copy,
    and boot stub.

2026-08-18 – Struct types (type ... endtype)

  • New user-defined struct types with type NAME ... endtype blocks. Fields
    are declared inside as var name: int, var name: word, or var name: float.
    Element size is the sum of field widths (no padding).
    type TEntity
    var x: int
    var y: int
    var hp: word # 1 + 1 + 2 = 4 bytes per element
    endtype
  • Array-of-struct declarations look like regular arrays:
    var enemies: TEntity = array(N) ; allocates N * elem_size bytes at $C000+
  • Field access on both sides of assignment:
    enemies[0].x = 5 ; constant idx → STA absolute
    enemies[i].hp = enemies[i].hp + 1 ; variable idx → shift-add mul + (ptr),Y
  • Constant-index writes/reads fold to bare LDA/STA absolute. Variable-index
    access emits a small shift-and-add multiplier for idx * elem_size (uses
    ASL A chains for power-of-two sizes, or a shift-add sequence for arbitrary
    sizes via two scratch zero-page bytes), then indexed (ptr),Y access.
  • Word and float fields write both bytes (little-endian): STA addr / STA addr+1
    for constant idx, STA (ptr),Y ; INY ; STA (ptr),Y for variable idx.
  • Float fields use Q8.8 fixed-point format (hi byte = integer, lo byte =
    fraction × 256). Reading a float field into a float variable works via
    gen_word_assign; printing a float struct field uses print_fixed ("N.DD").
  • The parser resolves arr[i].field into Stmt::StructSet and Expr::StructGet
    nodes that carry (field_offset, field_kind, elem_size) — codegen is
    self-contained and needs no type table lookup.
  • Not yet implemented in this release: struct-typed sub/fn parameters,
    default field values (parsed for compatibility with the proposal but not
    initialized at allocation time), nested struct fields, multi-dimensional
    struct arrays, string fields. Errors reported: struct-typed var without
    array(N) initializer, and unknown-field access.
  • Added examples/type_demo.ub and CPU-run integration tests for constant
    and variable index, int, word, and float field kinds, and the unknown-field
    error.

2026-08-18 – Count-down for-loop fix

  • for / loop with a negative constant step (e.g. for i = 20 to 0 step -2)
    used to compile but never enter the body, and — for values that wrap past 0 —
    loop forever. The exit test was unsigned-only, so counting down from 20 to 0
    kept re-entering after the 254 (= -2 mod 256) wrap-around.
  • The compiler now inspects the sign of a constant step at code generation:
    • positive step or default +1 → BCC/BEQ to body, unchanged (exit when var > to)
    • negative constant step → BCS to body (exit when var < to), plus a
      post-increment BCS loop_top ; JMP exit that catches the ADC underflow
      (carry clear) when var wraps below 0. This is what keeps
      for i = N to 0 step -k finite.
  • A default-step for/loop where from > to (e.g. loop i = 10 to 1, no
    step) used to silently skip the body — 0 iterations, no diagnostic. It is
    now a compile-time error:
    "for-loop: from (10) > to (1) with default step +1 loops 0 times
    — use 'step -1' to count down"
  • Non-constant step (variable or expression) is still treated as positive at
    compile time. Use a while loop when you need count-down with a runtime step.
  • Added examples/countdown_demo.ub (working forms) and
    examples/countdown_errors_demo.ub (the compile-time error form).
  • Added integration tests: a codegen check that count-down loops emit BCS
    for the body branch, a check that count-up loops still emit BCC + BEQ,
    should_panic coverage of the from>to default-step error, and CPU-run tests
    that execute the loop and verify it terminates with the expected iteration count.

2026-08-18 – --explicit CLI flag

  • Added the --explicit build flag:
    ub build myprog.ub --explicit
    With the flag, every var, sub-parameter, and fn-parameter declaration
    must carry a :type annotation. Untyped declarations become compile-time
    errors:
    line 12: 'explicit' mode: 'var loose' has no type
    — use 'var loose: int|word|float|string'
    line 14: 'explicit' mode: parameter 'a' has no type
    — use 'a: int|word|float|string'
  • Array declarations (array(N), array_word(N)) and const NAME = value
    are unaffected — their type is fixed by the declaration form.
  • The flag is CLI-only; no source-level directive keyword. Existing programs
    compile unchanged without the flag. Add it to a Makefile / build script to
    enforce typed style project-wide.
  • Added examples/explicit_demo.ub (annotated code that builds under the flag)
    and examples/explicit_errors_demo.ub (loose code that builds without the
    flag but reports errors with it).
  • Added integration tests covering the typed-ok case, untyped var / sub-param /
    fn-param errors, and the without-flag regression.

UltimateBasic 64 1.5.3

Choose a tag to compare

@zstarczali zstarczali released this 18 Aug 15:57
f0d6896

Ultimate Basic v1.5.3 – Release Notes

2026-08-18 – Multi-dimensional arrays

  • Arrays can now be declared with more than one dimension using a
    comma-separated dimension list, for both byte and word arrays:
    var grid = array(8, 8) # 8x8 = 64 bytes
    var wm = array_word(4, 4) # 4x4 word elements = 32 bytes
  • Elements are indexed with a comma-separated subscript list and stored
    row-major; the flat offset is base + i0*(d1*..) + i1*(d2*..) + ... :
    grid[row, col] = 42
    var v = grid[row, col]
  • Any number of dimensions is supported. Dimensions must be compile-time
    constants (integer literals or const values).
  • When every subscript is a constant the address is folded at compile time to a
    single absolute store/load; a variable subscript emits the row*stride + col
    computation followed by an indexed (ptr),Y access.
  • A single subscript into a multi-dimensional array is still allowed and is
    treated as a flat/linear index (e.g. grid[10]).
  • Indexing with a subscript count that is neither 1 nor the declared rank is
    reported as a compile-time error.
  • Implemented entirely in the parser (multi-index folds to one flat index
    expression), so code generation is unchanged.
  • Added examples/array2d_demo.ub and integration tests covering constant and
    variable indices, row-major layout, word-element stride, linear access,
    const-derived dimensions, and the wrong-rank error.

2026-08-18 – PETSCII bracket encoding fix

  • Fixed string encoding of the '[' , ']' and '^' characters, which previously
    fell through to the unknown-character placeholder and printed as '?'.
  • They now emit their correct C64 codes ($5B, $5D, $5E). These code points are
    identical in the uppercase/graphics and lowercase charsets, so the fix is
    correct in both uppercase and lowercase modes.
  • Added a regression test for bracket encoding in both charset modes.

UltimateBasic 64 1.5.2

Choose a tag to compare

@zstarczali zstarczali released this 15 Aug 20:04
8390576

Ultimate Basic v1.5.2 – Release Notes

2026-08-15 – Readable assembly codegen listings

  • Added the --asm build option:
    ub build demo.ub --asm
    A successful build writes demo.asm next to the selected .prg output.
  • The listing is generated from compiler metadata rather than being only a bare
    disassembly of the completed PRG.
  • UB statement boundaries are retained as comments alongside their emitted code.
  • Zero-page variables, arrays, subroutines, BASIC labels, relative branches, and
    in-program JMP/JSR targets receive readable symbols or generated labels.
  • Each instruction includes its final C64 address and exact machine-code bytes.
  • Compiler-generated helpers are included in final memory order, while unknown
    opcodes are preserved as .byte directives.
  • Helper entry points now use descriptive ub_helper_* labels.
  • Known data regions (DATA, maps, sprite/character definitions, lookup tables,
    incbins, SID and Koala payloads) are emitted as named .byte blocks instead of
    being disassembled as instructions; long zero gaps use .fill.
  • Output uses reassemblable KickAssembler syntax, including .label constants and
    a valid * = origin program counter directive.
  • Added automated listing tests and full README.md, MANUAL.md, CLI-help, and
    developer-guide documentation.

Ultimate Basic v1.5.1 – Release Notes

2026-08-15 – Koala Painter image import

  • Added compile-time loading of standard and raw Koala Painter images:
    koala load "picture.kla"
    koala show
    koala hide
  • Added file, memory-layout, and SID-conflict diagnostics plus emulation-backed
    tests and full manual documentation.

2026-08-15 – Character tile maps

  • Added compile-time loading and validation of self-describing .ubmap files:
    map load "level.ubmap"
    VisualAssembler me-map .bin exports are also accepted directly, without an
    .ubmap conversion step.
  • Added 40x25 viewport rendering from arbitrary map origins:
    map draw map_x, map_y
    Character codes go to $0400 and optional per-cell colors go to $D800.
  • Added writable runtime access with map_tile(), map_color(), map set, and
    map color, allowing game logic and destructible maps without reloading data.
  • Added normal and multicolor character-map modes. Multicolor maps configure
    $D016 MCM and the three shared colors in $D021-$D023.
  • Documented the byte-exact UBMP v1 format, flags, row-major arrays, viewport
    constraints, and charset relationship in MANUAL.md and CLAUDE.md.
  • Added emulation-backed tests for normal/multicolor setup, screen and color-RAM
    rendering, tile/color mutation and queries, and invalid-file diagnostics.

2026-08-12 – Debug symbol exports

  • Added side-effect-free software bounding-box collision detection:
    var hit = box_hit(l1, t1, r1, b1, l2, t2, r2, b2)
    It returns 1 for overlapping or edge-touching inclusive AABBs and 0 otherwise,
    without reading or clearing the VIC-II $D01E/$D01F collision registers.
  • Extended sprite_frame with an optional animation frame expression:
    sprite_frame id, base_addr, frame
    Consecutive frames occupy 64-byte slots; the original two-argument syntax is
    unchanged and remains backward compatible.
  • Added emulation-backed integration tests for overlap, separation, touching edges,
    and runtime animation-frame pointer selection.
  • Added the --debug build option:
    ub build demo.ub --debug
    A successful build now writes three debugger files next to the .prg, using
    the output file's stem.
  • Added KickAssembler-compatible .sym export. It can be imported by assembler
    source and contains resolved symbols in .label name = $address form.
  • Added C64Debugger/RetroDebugger .dbg export in KickAssembler debug-dump XML
    format. It contains the UltimateBasic program segment, source filename, and
    resolved address labels.
  • Added VICE .vs monitor-command export. It contains al C:address .label
    commands and can be loaded with VICE's -moncommands option or ll command.
  • Debug exports include program_start, program_end, zero-page variables,
    arrays, subroutines, and BASIC labels.
  • BASIC labels are now retained with their final machine-code addresses in the
    compiler memory map, making them available to all debug exporters.
  • Symbol names are sanitized for debugger compatibility, and source paths are
    XML-escaped in .dbg output.
  • Added unit coverage for all three symbol syntaxes and documented the new CLI
    option and file formats in README.md, MANUAL.md, and CLAUDE.md.
  • Current limitation: .dbg provides segments and address symbols, but does not
    yet contain instruction-to-source-line mappings for source-level stepping.