ACE BASIC 3.0 - Classes IEEE and More
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
mathieeesingbasandmathieeesingtranslibraries 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...WENDandREPEAT...UNTILloops early, analogous to the existingEXIT FOR. A small quality-of-life addition.FREE Statement: Per-block memory deallocation.
FREEreleases memory allocated byALLOCfor a specific block, whileCLEAR ALLOCfrees everything. This gives you finer control over memory lifetime.SUB Tracing: The
-tcompiler flag andTRON/TROFFruntime 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 moreDECLARE STRUCTboilerplate 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.libhas been replaced with the standardamiga.libfor better compatibility, plus dedicatedace_clib.sandieee_math.smodules 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.
-
[ACE BASIC 3.0 - Classes IEEE and More]
02-03-2026 -
[ACE BASIC - Structs RTG and More]
16-02-2026 -
[Developing with AI - Understanding the Context]
13-02-2026 -
[ACE BASIC - Closures MUI and More]
10-02-2026 -
[ACE BASIC - GadTools and More]
31-01-2026 -
[ACE BASIC - AGA Screen Support]
27-01-2026 -
[Polymorphism and Multimethods]
02-03-2023 -
[Global Day of CodeRetreat - recap]
07-11-2022 -
[House automation tooling - Part 4 - Finalized]
01-11-2022 -
[House automation tooling - Part 3 - London-School and Double-Loop]
02-07-2022 -
[Modern Programming]
14-05-2022 -
[House automation tooling - Part 2 - Getting Serial]
21-03-2022 -
[House automation tooling - Part 1 - CL on MacOSX Tiger]
07-03-2022 -
[Common Lisp - Oldie but goldie]
18-12-2021 -
[Functional Programming in (Common) Lisp]
29-05-2021 -
[Patterns - Builder-make our own]
13-03-2021 -
[Patterns - Builder]
24-02-2021 -
[Patterns - Abstract-Factory]
07-02-2021 -
[Lazy-sequences - part 2]
13-01-2021 -
[Lazy-sequences]
07-01-2021 -
[Thoughts about agile software development]
17-11-2020 -
[Test-driven Web application development with Common Lisp]
04-10-2020 -
[Wicket UI in the cluster - the alternative]
09-07-2020 -
[TDD - Mars Rover Kata Outside-in in Common Lisp]
03-05-2020 -
[MVC Web Application with Elixir]
16-02-2020 -
[Creating a HTML domain language in Elixir with macros]
15-02-2020 -
[TDD - Game of Life in Common Lisp]
01-07-2019 -
[TDD - classicist vs. London Style]
27-06-2019 -
[Wicket UI in the cluster - reflection]
10-05-2019 -
[Wicket UI in the Cluster - know how and lessons learned]
29-04-2019 -
[TDD - Mars Rover Kata classicist in Scala]
23-04-2019 -
[Burning your own Amiga ROMs (EPROMs)]
26-01-2019 -
[TDD - Game of Life in Clojure and Emacs]
05-01-2019 -
[TDD - Outside-in with Wicket and Scala-part 2]
24-12-2018 -
[TDD - Outside-in with Wicket and Scala-part 1]
04-12-2018 -
[Floating Point library in m68k Assembler on Amiga]
09-08-2018 -
[Cloning Compact Flash (CF) card for Amiga]
25-12-2017 -
[Writing tests is not the same as writing tests]
08-12-2017 -
[Dependency Injection in Objective-C... sort of]
20-01-2011