In the world of systems programming, assemblers are typically seen as direct,
low-level translators, converting human-readable mnemonics into machine code.
Metaprogramming, if it exists, is handled by a separate, often text-based,
preprocessor. ChrysaLisp fundamentally rejects this layered model. Its
assembler is not a distinct tool; it is a library of functions within the
ChrysaLisp language itself. This means that the full, Turing-complete power of
Lisp is available as the native metaprogramming and macro system for the
assembler. This document will explore this architectural paradigm, contrasting
it with traditional methods and using the :pixmap pixel format converter as
a detailed case study to demonstrate how this "assembler as Lisp" philosophy
enables the automatic generation of hyper-optimized, branchless code at
compile time.
To appreciate the ChrysaLisp approach, we must first understand the common methods for handling complex, repetitive, but performance-critical code in systems like C/C++.
Let's use the problem of pixel format conversion as an example. A graphics system might need to convert from a 16-bit RGB565 format to a 32-bit ARGB8888 format.
Approach A: The Runtime switch Statement
This is the most straightforward approach. A single function takes source and
destination buffers and their formats, using a large switch statement to
select the correct conversion logic at runtime.
void convert_pixel(uint32_t src_pixel, PixelFormat src_fmt, PixelFormat dst_fmt) {
switch (src_fmt) {
case RGB565:
switch (dst_fmt) {
case ARGB8888:
// ... logic for 565 -> 8888 ...
break;
// ... other destination formats ...
}
break;
// ... other source formats ...
}
}
- Flaw: This is disastrous for performance. The conversion logic for a
full bitmap is inside a tight loop. This
switchstatement introduces repeated, unpredictable branches, which are poison to modern CPUs with deep instruction pipelines.
Approach B: The C Preprocessor Macro
A developer might try to use the C preprocessor to generate code.
#define CONVERT_565_TO_8888(src_pixel) \
((((src_pixel >> 11) & 0x1F) * 255 / 31) << 16) | \
((((src_pixel >> 5) & 0x3F) * 255 / 63) << 8) | ...
// In the loop:
*dst++ = CONVERT_565_TO_8888(*src++);
- Flaw: The C preprocessor is just a text-substitution engine. It has no understanding of logic, types, or algorithms. It cannot, for example, algorithmically determine the correct bit shifts and scaling factors from a set of bitmasks. The developer must still hand-write and maintain a macro for every single conversion pair, which is tedious and error-prone.
Approach C: Hand-Written Functions
The final option is to manually write a specialized, optimized function for
every single conversion pair: convert_565_to_8888(), convert_555_to_8888(),
convert_888_to_565(), etc.
- Flaw: This gives the best runtime performance but is a maintenance nightmare. Adding a new pixel format requires writing N new functions. It leads to massive code bloat and a high probability of bugs in less-frequently used conversion paths.
ChrysaLisp finds all three approaches unacceptable and provides a fourth way.
In ChrysaLisp, there is no separate assembler.exe. The "assembler" is a set
of Lisp functions defined in files like lib/asm/vp.inc. When you write VP
assembly, you are actually writing a Lisp program.
A line of assembly like (vp-cpy-rr :r0 :r1) is a Lisp S-expression.
vp-cpy-rr is a Lisp function that, when called, emits the appropriate
bytecode for a register-to-register copy into an output stream.
This is the Lisp principle of homoiconicity: code is represented using the language's own primary data structures (lists). Because the assembler's source code is Lisp data, it can be manipulated by other Lisp functions before it is ever evaluated to produce bytecode.
This means the metaprogramming system for the assembler is Lisp itself. There is no need for a separate, limited preprocessor, because the full, Turing-complete power of Lisp is already available to generate and transform code at compile time.
This dynamic code generation is brilliantly showcased in
gui/pixmap/class.vp, specifically in the functions that handle pixel
format conversions. Let's analyze :pixmap :to_argb32.
The VP source file defines the method like this:
(def-method :pixmap :to_argb32)
; ... (register definitions)
(defun conv (...) ...) ; Helper Lisp function
(defun pipeline (...) ...) ; Helper Lisp function
(entry :pixmap :to_argb32 (list col pix))
(switch)
(vpcase `(,pix = 16))
(to-argb32
0b0000000000000000 ; Alpha mask
0b1111100000000000 ; Red mask
0b0000011111100000 ; Green mask
0b0000000000011111 ; Blue mask
)
(break)
; ... other cases
(endswitch)
(exit :pixmap :to_argb32 (list col))
(vp-ret)
(def-func-end)
The key thing to notice is that (to-argb32 ...) is not a VP instruction. It
is a Lisp function call that is executed by the assembler at compile
time. Its arguments are the bitmasks that define the 16-bit RGB565 format.
The to-argb32 function calls a helper Lisp function, conv, for each color
channel. This is the core code generator.
(defun conv (col sr dr sm dm)
(defq sw (width sm) dw (width dm)) ; Calculate source and dest bit widths
(cond
; ... logic to handle up-scaling ...
(:t ; left bit replicate
(defq ls (- (nlz sm) (nlz dm)) pipe
`((vp-cpy-rr ,col ,sr) (vp-and-cr ,sm ,sr)
(vp-shl-cr ,ls ,sr) (vp-cpy-rr ,sr ,dr))
; ...
)
(while (> dw 0)
(push pipe `(vp-shr-cr ,sw ,sr) `(vp-add-rr ,sr ,dr))
; ...
)
...
pipe) ; <--- RETURNS A LIST OF LISP FORMS
The conv function analyzes the source and destination masks (sm, dm) and
builds a list of Lisp S-expressions. Each expression in this list is a
call to a VP assembly emitter, like (vp-cpy-rr ,col ,sr). It
algorithmically determines the optimal shifts and masks required for the
conversion and returns this program fragment as a list.
The to-argb32 function collects the lists of instructions for each channel
from conv and passes them to pipeline. pipeline zips these lists
together and then, crucially, calls eval on each instruction form.
When (eval '(vp-cpy-rr :r1 :r2)) is executed, the Lisp function vp-cpy-rr
is called, which emits the final bytecode into the function being compiled.
At Compile Time:
-
The assembler executes
make.lispor a similar script. -
It begins processing
gui/pixmap/class.vp. -
Inside
def-method :pixmap :to_argb32, it hits the(to-argb32 ...)Lisp function call. -
to-argb32and its helpersconvandpipelinerun. They generate a list of assembly instructions, perfectly tailored for the RGB565 -> ARGB8888 conversion. -
evalis called on each instruction in the list, which executes the bytecode emitters (vp-shl-cr,vp-add-rr, etc.). -
The final, optimized, branchless VP bytecode is written into the body of the
:pixmap :to_argb32method in the output object file.
At Runtime:
When an application calls (. pixmap :to_argb32 pixel 16), it doesn't execute
the Lisp generator code. It executes the pre-generated, hyper-optimized VP
assembly. The runtime code has no switch statements, no ifs, no function
calls-just a flat sequence of bit-twiddling instructions to perform the
conversion at maximum speed.
The ChrysaLisp assembler is a profound demonstration of Lisp's homoiconic nature. By treating code as data, the system elevates its assembler from a simple translator to a powerful, programmable, logic-driven tool. It doesn't need a separate macro language because it has the entirety of Lisp at its disposal for compile-time computation and code generation.
The :pixmap pixel converter is the quintessential example of this power. It
solves a common graphics problem by achieving the "best of all worlds": the
runtime speed of hand-tuned assembly, the maintainability of a high-level
abstraction, and the code compactness of a generative approach. This is the
"Well, Don't Do That Then!" philosophy in action: instead of building complex
runtime solutions for pixel conversion, it simply generates the perfect,
simple code at compile time. It is a testament to an architecture where the
lines between compiler, assembler, and language are beautifully and
powerfully blurred.
ChrysaLisp is not just a language with an assembler; it's a platform that encourages the creation of an entire ecosystem of these embedded, domain-specific micro-compilers.
-
structureis a micro-compiler for data layouts. -
The
assignmacro is a micro-compiler for assignment operations, choosing the optimal load/store instruction based on its analysis of the source and destination operands. -
cscript.incprovides a micro-compiler for a C-like infix expression language. -
:pixmap :to_argb32is a micro-compiler for pixel format conversion.