[blog] | [projects] | [about] | [imprint]

ACE BASIC 3.0 - Classes IEEE and More

02 March 2026
 

ACE BASIC 3.0

The previous posts followed ACE BASIC from v2.5 through v2.9 -- AGA screens, GadTools, closures, MUI, structs, RTG graphics, tail-call optimization, and HTTP networking. Version 3.0 is a major release. It introduces an object system with generic functions, switches the floating-point format to IEEE 754, adds type-based pattern matching, atoms, multitasking primitives, and ships a set of new submodules including a JSON parser. There is a lot here, so let's go through it.

Object System

This is the headline feature. ACE BASIC now has classes with single inheritance and polymorphic dispatch via generic functions. The new keywords are CLASS, METHOD, EXTENDS, and GENERIC.

If you are familiar with Common Lisp's CLOS or Julia's multiple dispatch, the design will feel natural. Classes define data. Methods are standalone functions that take class instances as parameters. Generic declarations wire up the runtime dispatch.

Defining a class

A class groups data members together, much like a struct but with a type identity that enables runtime dispatch:

CLASS Disc
    LONGINT tag
    SINGLE radius
END CLASS

CLASS Rect
    LONGINT tag
    SINGLE w
    SINGLE h
END CLASS

Classes contain only data -- no methods are defined inside the class block. Instance creation and member access use the same -> syntax as structs:

DECLARE CLASS Disc d
d->radius = 5.0

DECLARE CLASS Rect r
r->w = 10.0
r->h = 3.0

Each instance carries a hidden type descriptor at offset 0. This is what the runtime uses for dispatch.

Methods and generic dispatch

Methods are defined outside the class. The first parameter is a typed class instance, which tells the runtime which class this method specialization belongs to:

METHOD Mark(Disc c)
    c->tag = 1
END METHOD

METHOD Mark(Rect r)
    r->tag = 2
END METHOD

Two methods with the same name, each taking a different class. To enable runtime dispatch, you declare a GENERIC:

GENERIC METHOD Mark(CLASS)
    ON Disc
    ON Rect
END GENERIC

The GENERIC declaration says: "Mark is a generic function that dispatches on one class argument. There are specializations for Disc and Rect." The CLASS placeholder in the signature marks the dispatched parameter. ON lists the concrete types that have specializations.

Now when you call Mark, the runtime checks the actual type of the argument and dispatches to the correct specialization:

Mark(d)    '..calls Mark(Disc c), sets d->tag to 1
Mark(r)    '..calls Mark(Rect r), sets r->tag to 2

Methods can return typed values, just like FUNCTION:

METHOD SINGLE CalcArea(Disc c)
    CalcArea = c->radius * c->radius
END METHOD

METHOD SINGLE CalcArea(Rect r)
    CalcArea = r->w * r->h
END METHOD

GENERIC SINGLE METHOD CalcArea(CLASS)
    ON Disc
    ON Rect
END GENERIC

SINGLE area
area = CalcArea(d)    '..25.0
area = CalcArea(r)    '..30.0

They can also take additional non-dispatched parameters:

METHOD LONGINT Scale(Disc c, LONGINT factor)
    Scale = 100 + factor
END METHOD

METHOD LONGINT Scale(Rect r, LONGINT factor)
    Scale = 200 + factor
END METHOD

GENERIC LONGINT METHOD Scale(CLASS, LONGINT)
    ON Disc
    ON Rect
END GENERIC

LONGINT s
s = Scale(d, 5)    '..105
s = Scale(r, 5)    '..205

Multiple dispatch

This is where things get interesting. A generic function can dispatch on more than one class parameter. Consider a collision detection system:

CLASS Disc
    SINGLE radius
END CLASS

CLASS Rect
    SINGLE w
    SINGLE h
END CLASS

GENERIC LONGINT METHOD Collide(CLASS, CLASS)
    ON Disc, Disc
    ON Disc, Rect
    ON Rect, Disc
    ON Rect, Rect
END GENERIC

METHOD LONGINT Collide(Disc a, Disc b)
    Collide = 11
END METHOD

METHOD LONGINT Collide(Disc a, Rect b)
    Collide = 12
END METHOD

METHOD LONGINT Collide(Rect a, Disc b)
    Collide = 21
END METHOD

METHOD LONGINT Collide(Rect a, Rect b)
    Collide = 22
END METHOD

DECLARE CLASS Disc c1, c2
DECLARE CLASS Rect r1, r2

result = Collide(c1, c2)    '..11 (disc-disc)
result = Collide(c1, r1)    '..12 (disc-rect)
result = Collide(r1, c1)    '..21 (rect-disc)
result = Collide(r1, r2)    '..22 (rect-rect)

The runtime dispatches on the types of both arguments simultaneously. Each combination of types maps to a different method specialization. This is genuine multiple dispatch -- the same mechanism found in CLOS and Julia, now available in ACE BASIC.

Inheritance

Classes support single inheritance with EXTENDS. Child classes inherit all parent members:

CLASS Shape
    LONGINT x
    LONGINT y
END CLASS

CLASS Rect EXTENDS Shape
    LONGINT w
    LONGINT h
END CLASS

CLASS ColorRect EXTENDS Rect
    LONGINT col
END CLASS

The memory layout follows the inheritance chain. Shape takes 12 bytes (4 for the type descriptor, 4 each for x and y). Rect adds w and h for 20 bytes. ColorRect adds col for 24 bytes. Parent members are always at the same offsets, so a Rect can be passed anywhere a Shape is expected.

Generic dispatch walks the parent chain. If a child class has no specialization for a generic function, the runtime walks up the inheritance tree until it finds one:

GENERIC LONGINT METHOD Info(CLASS)
    ON Shape
    ON Rect
END GENERIC

METHOD LONGINT Info(Shape s)
    Info = 1
END METHOD

METHOD LONGINT Info(Rect r)
    Info = 2
END METHOD

DECLARE CLASS Shape s1
DECLARE CLASS Rect r1
DECLARE CLASS ColorRect cr1

Info(s1)     '..returns 1 (direct match: Shape)
Info(r1)     '..returns 2 (direct match: Rect)
Info(cr1)    '..returns 2 (inherited: ColorRect -> Rect)

ColorRect has no Info specialization, so the runtime walks up: ColorRect -> Rect, finds a match, and dispatches there. If there were only a Shape specialization, a three-level walk (ColorRect -> Rect -> Shape) would find it.

Atom dispatch

The ATOM type (more on this below) can also participate in generic dispatch. This enables pattern matching on symbolic values mixed with class types:

CLASS Widget
    LONGINT id
END CLASS

CLASS Knob
    LONGINT id
END CLASS

GENERIC LONGINT METHOD React(CLASS, ATOM)
    ON Widget, #:click
    ON Widget, #:hover
    ON Knob, #:click
END GENERIC

METHOD LONGINT React(Widget w, #:click evt)
    React = 10 + w->id
END METHOD

METHOD LONGINT React(Widget w, #:hover evt)
    React = 20 + w->id
END METHOD

METHOD LONGINT React(Knob b, #:click evt)
    React = 30 + b->id
END METHOD

DECLARE CLASS Widget wg
DECLARE CLASS Knob bt
wg->id = 1
bt->id = 2

React(wg, #:click)    '..11
React(wg, #:hover)    '..21
React(bt, #:click)    '..32

Dispatch happens on both the class type and the atom value. This is a natural fit for event handling -- the class identifies the widget, the atom identifies the event kind.

TYPECASE

Related to the object system is TYPECASE, which provides type-based pattern matching with variable narrowing:

CLASS Animal
    LONGINT legs
END CLASS

CLASS Dog EXTENDS Animal
    LONGINT goodboy
END CLASS

SUB LONGINT CheckDog(Animal a)
  LONGINT result
  result = 0
  TYPECASE a
    CASE Dog
      result = a->goodboy
    CASE ELSE
      result = a->legs
  END TYPECASE
  CheckDog = result
END SUB

DECLARE CLASS Dog d
d->legs = 4
d->goodboy = 1

DECLARE CLASS Animal a
a->legs = 99

CheckDog(d)    '..returns 1 (matched Dog, reads goodboy)
CheckDog(a)    '..returns 99 (fell through to ELSE, reads legs)

Inside the CASE Dog branch, the variable a is narrowed to Dog type, so a->goodboy is accessible even though the SUB parameter is declared as Animal. The matching follows ISA semantics -- a Dog instance matches both CASE Dog and CASE Animal, so order matters. Put specific types first.

ATOM Type

Atoms are a new primitive type for lightweight symbolic constants. The literal syntax uses #: followed by a name:

ATOM status
status = #:ok

IF status = #:ok THEN
  PRINT "All good"
END IF

Atoms are compile-time constants that produce unique integer values via FNV-1a hashing. They are useful for tagging and dispatch -- anywhere you would otherwise define a set of CONST values. As shown above, atoms can also participate in generic method dispatch, which makes them especially powerful for event-driven patterns.

Atoms can also be dispatched on their own without classes:

GENERIC LONGINT METHOD Process(ATOM)
    ON #:ok
    ON #:fail
    ON #:retry
END GENERIC

METHOD LONGINT Process(#:ok result)
    Process = 1
END METHOD

METHOD LONGINT Process(#:fail result)
    Process = -1
END METHOD

METHOD LONGINT Process(#:retry result)
    Process = 0
END METHOD

Process(#:ok)      '..returns 1
Process(#:fail)    '..returns -1
Process(#:retry)   '..returns 0

IEEE 754 Floating Point

This is a breaking change, and a necessary one. ACE has used Motorola Fast Floating Point (FFP) since its original release in the early 1990s. FFP is a non-standard 32-bit format that was fast on the 68000 but is incompatible with everything else. No modern toolchain, library, or hardware uses it.

Version 3.0 migrates to IEEE 754 single-precision floating point throughout the compiler and runtime. All float literals, constants, and runtime operations now use the standard format. This means:

  • Float values are compatible with C libraries and OS functions that expect IEEE floats
  • The VBCC compiler (which replaced GCC in this release) handles IEEE floats natively
  • Math operations use the mathieeesingbas and mathieeesingtrans libraries instead of the FFP equivalents
  • Existing programs that rely on specific FFP bit patterns need to be recompiled

For most programs, recompiling is all that is needed. The syntax is identical -- SINGLE is still the type, and float literals look the same. The difference is under the hood. This change also fixed a crash when printing float values that was caused by K&R float parameter promotion mismatches between the FFP and IEEE calling conventions.

TASKPROC -- Multitasking Support

The Amiga is a multitasking operating system, and ACE can now launch Exec tasks. The TASKPROC keyword marks a zero-parameter SUB as a task entry point:

SUB BackgroundWork TASKPROC
  ' This runs as a separate Exec task
  ' Automatically saves/restores registers
  ' Calls Wait(0) before returning
END SUB

A TASKPROC SUB automatically saves and restores registers on entry and exit, and calls Wait(0) before returning to signal the parent that it is done. It takes no parameters and cannot be called directly from ACE code -- it is meant to be passed to the new taskutil.b submodule:

REM #using ace:submods/taskutil/taskutil.o

#include <submods/taskutil.h>

TaskLaunch("worker", @BackgroundWork, 4096)
' ... do other work ...
TaskTerminate("worker")

TaskLaunch creates an Exec task with the given name, entry point, and stack size. TaskGetData retrieves a task's data pointer for inter-task communication. TaskTerminate signals a task to shut down.

New Submodules

Version 3.0 ships seven new submodules. The most interesting ones build on the new object system.

Hashmap

The hashmap.b submodule implements a CLASS-based string-keyed hashmap with open addressing:

REM #using ace:submods/hashmap/hashmap.o

#include <submods/hashmap.h>

DECLARE CLASS Hashmap map
map = HmNew(32)

HmPut(map, "name", "ACE BASIC")
HmPut&(map, "version", 3)

PRINT HmGet$(map, "name")       '..prints "ACE BASIC"
PRINT HmGet&(map, "version")    '..prints 3

HmFree(map)

It stores typed values (string, integer, long, single, address) keyed by string. More examples are in the submods/hashmap/ folder.

Dynamic Array

The dynarray.b submodule provides a growable, type-tagged indexed collection:

REM #using ace:submods/dynarray/dynarray.o

#include <submods/dynarray.h>

DECLARE CLASS Dynarray arr
arr = DaNew(16)

DaAdd&(arr, 10)
DaAdd&(arr, 20)
DaAdd&(arr, 30)

PRINT DaGet&(arr, 0)    '..prints 10
PRINT DaSize(arr)        '..prints 3

DaFree(arr)

It supports iteration, a builder pattern, searching, higher-order functions, sorting, and automatic growth when elements are added beyond the initial capacity. Where the List submodule from v2.8 gives you linked-list semantics, Dynarray gives you indexed random access. More examples are in the submods/dynarray/ folder.

JSON

The json.b submodule is a complete JSON parser, generator, and pretty-printer. It uses Hashmap and Dynarray as its intermediate representation:

REM #using ace:submods/json/json.o
REM #using ace:submods/hashmap/hashmap.o
REM #using ace:submods/dynarray/dynarray.o

#include <submods/json.h>

ADDRESS root

root = JsonParse("{""name"":""ACE"",""version"":3,""features"":[""objects"",""ieee""]}")

PRINT JsonGetStr$(root, "name")     '..prints "ACE"
PRINT JsonGetLng&(root, "version")  '..prints 3

JsonPrettyPrint(root)
JsonFree(root)

Having JSON support means ACE programs can now parse configuration files, consume web API responses (using the HTTP client from v2.9), or generate structured output. The combination of HTTP client and JSON parser makes it possible to write practical network clients in ACE BASIC. More examples are in the submods/json/ folder.

Other submodules

  • fad.b (Files And Directories): Over 20 SUBs for file system operations -- existence checks, metadata queries, path manipulation, and directory iteration. Examples in submods/fad/.
  • iff.b: IFF ILBM picture loading, extracted from the built-in compiler commands into a standalone submodule.
  • testkit.b: Shared test assertion library used across all submodule test suites, eliminating duplicated test boilerplate.

Bounded String Operations

This is a safety improvement that happens under the hood. ACE's string operations (LET, MID$, LINE INPUT#, etc.) did not previously check destination buffer sizes. A string longer than the target buffer would silently overwrite adjacent memory -- the kind of bug that causes mysterious crashes hours later.

Version 3.0 adds bounded string operations at the runtime level. The compiler now emits the destination buffer size alongside string assignments, and the runtime's _strncpy and _strncat functions enforce the limit. This applies to string variable assignments, array element assignments, struct member assignments, and LINE INPUT# from files.

There is no syntax change. Existing code benefits automatically when recompiled.

Brief Mentions

  • VBCC Toolchain: The compiler build itself now uses the VBCC compiler instead of GCC. The runtime libraries have been rebuilt with VBCC as well. This simplifies the build process and aligns the whole toolchain around a single compiler.

  • EXIT WHILE / EXIT REPEAT: You can now break out of WHILE...WEND and REPEAT...UNTIL loops early, analogous to the existing EXIT FOR. A small quality-of-life addition.

  • FREE Statement: Per-block memory deallocation. FREE releases memory allocated by ALLOC for a specific block, while CLEAR ALLOC frees everything. This gives you finer control over memory lifetime.

  • SUB Tracing: The -t compiler flag and TRON/TROFF runtime commands let you trace SUB, FUNCTION, and METHOD entry and exit. Useful for debugging complex call chains and generic dispatch.

  • CyberGraphX Support: Screen mode 13 now also works with CyberGraphX in addition to Picasso96, broadening RTG hardware compatibility.

  • Struct SUB Parameters: You can now use struct type names directly as SUB parameter types (e.g. SUB Foo(MyStruct s)), and the compiler generates the pointer setup automatically. No more DECLARE STRUCT boilerplate at the top of every SUB that takes a struct.

  • Runtime Optimizations: Lookup tables, O(1) argument access, and dynamic allocation improvements in the runtime libraries.

  • Replaced ami.lib with amiga.lib: The custom ami.lib has been replaced with the standard amiga.lib for better compatibility, plus dedicated ace_clib.s and ieee_math.s modules for ACE-specific needs.

Conclusion

Version 3.0 is a very big release. The object system with classes, generic functions, and multiple dispatch turns ACE into a language where you can model problems with proper abstractions. The design follows the multimethod tradition -- classes define data, methods are standalone, dispatch happens at runtime based on actual types. IEEE 754 floats align ACE with every other toolchain and library in existence. TYPECASE and atoms provide clean pattern matching. The new submodules -- hashmap, dynarray, JSON -- show what the object system enables in practice. And bounded string operations make the runtime safer by default.

Combined with the HTTP client from v2.9, ACE can now fetch JSON from a web API, store the results in a hashmap, iterate with a dynamic array, and display them in a MUI interface. That is a long way from where this project started.

The project lives on GitHub. Bug reports and feature requests are welcome.

[atom/rss feed]