In most software ecosystems, the compiler is self-hosting, which means that you need an existing compiler binary in order to build the compiler. This has significant implications, since it is not practically possible to audit these binaries. Plunder solves this problem at it's root by having an extremely tiny bootstrapping language built directly into the virtual machine, and then defining everything else in terms of that.
This bootstrapping language is The Plan Assembler (pet-named Wisp), which is tiny enough to be written by hand in less than a thousand lines of assembly. And because PLAN itself is also such a tiny formal system, the VM kernel needed to boot a PLAN ecosystem from scratch, can realistically be written entirely in assembly code. The result is that a Plunder has zero software supply-chain dependencies.
The Plan Assembler is basically just a notation for constructing PLAN values, and binding values to names. But because of it's novel and expressive syntax system, it's powerful macro system, and the general expressive power of PLAN itself, Wisp can extended itself into a fairly powerful language, and then more complex languages can be implemented in Wisp.
Wisp uses a Lisp-like syntax with some modifications which make it even more expressive, while also making it able to be parsed with far less code. This borrows some of the ideas from Rex Rex, but with much less features, making it extremely easy to parse.
Despite significant conceptual differences, most Wisp code strongly resembles Scheme. Here's a quick example.
(define (isJunk token)
(define tt (Tag tok))
(or (= tt TT_SPACE)
(= tt TT_CLOSE)
(= tt TT_ERROR)))
(define (mapLoop f x n i acc)
(if (>= i n) acc
(mapLoop f x n (inc i)
(Up i (f (Ix i x)) acc))))
(define (map f x)
(define n (Sz x))
(map-loop f x n 0
(Rep 0 0 n)))
Compared to Lisp expectations, Wisp syntax has a number of unusual properties.
(parenthesis), [square
braces], and {curly braces}.
juxta(posed).
'quoting, and `(quasi
,quoting).
fooBar) and operators (-=).
Because Wisp is designed to be implemented in assembly, and because every implementation of PLAN must include an implementation of Wisp, Wisp goes to extreme lengths to minimize implementation footprint, and to minimize the number of edge cases which are likely to diverge between implementations.
One of the ways that Wisp achives this is by eliminating error
cases. Error cases require a lot of code, and they are a
significant source of divergence between implementations since
they are hard to test. For example, Wisp does not require
strings to be terminated at the end of a file, and nesting
characters do not need to be matching: (foo] is
accepted. A separate tool can be used to validate these cases.
Another example of this is the lack of escape sequences in
string literals. Almost all languages support something
like "Say \"Hello World\", now\n".. The rules for
this tend to be complicated, and the specifics vary dramatically
between languages. Wisp eliminates all of this complexity by
simply having strings read until the next double-quote (or
end-of-file). Support for escaping can be implemented in user
code via a macro, for example: (putStr "Say \qHello
World\q, now\n").
Wisp also eliminates all of the special notations that are
usually baked into Lisp systems. Almost all lisps usually
have '(q u o t e s), and `(q u a s i ,q u o t
e s). #(vector
literals), :keywords, conditional
compilation, and the like. Wisp eliminates such forms from the
base language, but the juxtaposition system and the rune system
make it possible to define all of these features in user code by
defining macros.
While these design choices are extreme and unconventional, the result is an absurdly small system that is still very expressive and erognomic. Without these simplifications, a zero dependency, purely assembly implementation of Wisp would not be pragmatically feasible.
Macro hygiene is one really tricky corner case for Lisp metaprogramming, but Wisp manages to avoid all of these problems by reframing the foundations in a way that avoids the problem.
Macro hygiene can be broken down into three problems (TODO: am I missing one?):
add
function), what happens if the call-site happens to have
shaddowed that name? Without hygiene, the result will
accidentally refer to the local binding (For example, a
local variable named add).
Scheme implementations include a lot of machinery to enable hygeneric metaprogramming. Some basic mechanisms are in the Scheme standard, and implementations generally include their own more complex mechanisms in addition to that.
Common Lisp does not solve this problem. A couple of other mis-features (separate namespace for functions and values) result in the problem comming up less often, and in the other cases macro authors are just forced to program in an extremely defensive style.
Wisp simply eliminates the first two problems by construction, and the third problem is solved by threading a macro-expansion count through all macro-expansions within a file, and then macros use that to generate unique symbols. This combination of features is sufficient to implement Scheme-style hygenic macro systems as a normal macro, without any special baked in constructions.
data Wisp
= Expression [Wisp]
| Expand Plan [Wisp]
| Symbol Nat
| Constant Plan
First, note that this is not homoiconic. You can embed arbitrary PLAN values, but the notation is not a subset of PLAN.
Second, remember that PLAN is a hermetic system with no opaque or privalidged values, so there is no distinction between runtime and compile time. In particular, code truely is data: functions are just normal transparent data structures, just like everything else.
Third, remember that PLAN itself is a purely functional model, and that it doesn't not have any built-in concept of a symbol or namespace. Wisp itself has a symbol table, but it is only used to lookup symbols when reading forms, functions refer to each other by value, not by name.
The result of all of these unique features is that the hard problems of hygenic macros just go away.
In Lisp, a macro that wants to expand to a reference to a function
needs to somehow output a symbol which contains a stable
(unshadowable, unredefinable) name for that function. In Wisp,
you just expand to a Constant reference to the
function value itself. Similarly, if you want to expand to an
expression which references a specific macro, instead of expanding
to an s-expression, you expand to a (Expand macro
exprs..) form, where macro is not a name, but
a literal reference to a macro function.
The Wisp macro system is extremely simple, but it is able to support fully hygenic macros by changing how the problem is looked at, and taking advantage of some of the unique capabilities of PLAN.
(TODO: document the system here)