basilTeam / basil

Fast and flexible language exploring partial evaluation, context-sensitive parsing, and metaprogramming. Compiles JIT or AOT to native code.
BSD 3-Clause "New" or "Revised" License
124 stars 11 forks source link

Forms versus macros #33

Closed dumblob closed 2 years ago

dumblob commented 2 years ago

Recent commits added "macros". Naturally the question arises why Basil needs macros if it already has forms (which is a more generic concept capable of everything macros can and much more).

@elucent could you elaborate?

Note I didn't do any deep investigation on macros nor forms in Basil as it's all still very much in flux.

elucent commented 2 years ago

Forms (specifically, their ability to quote arguments implicitly, akin to fexprs) cover a lot of what traditional languages call macros, but not everything.

Let's define "code" in a little bit of a different sense than homoiconic languages usually do. It's generally true that with homoiconicity, "code is data". But even here, there's usually a rigid wall between what is considered "code" by default, and what is considered "data". If we plug (+ 1 2) into a Scheme interpreter, that (+ 1 2) is code. We can get its data representation by quoting it - (quote (+ 1 2)) returns the list (+ 1 2) as data. But that requires changing the code! Our program now has this quote expression in it that it didn't have before. Code and data are different things, even when we can represent either as either.

From this, I think we can define four quadrants:

Takes value as input Takes code as input
Returns value Traditional functions - factorial, +, etc. Fexprs, quoting forms - sufficient to implement things like if or quote
Returns code ??? ???

The bottom two boxes are a bit more niche, but are represented in the macro systems of several well-known languages:

So, this is the type of functionality macros are intended to provide - expressions that expand into other expressions. The way they actually work in Basil is actually pretty straightforward: macro values are essentially just functions, with the added attribute that they are macros. They are required to return lists, which are spliced into their enclosing s-expression (akin to Scheme's unquote-splicing) upon evaluation, instead of returning a value. The specific way this is implemented depends on a new built-in function, splice, that behaves similarly to Scheme's quasiquote (it's inserted implicitly around macro invocations, however, to simplify code).

With all this, we can implement all kinds of things that we couldn't otherwise:

  1. The identity macro in Basil behaves like unquote-splicing, as above, and behaves a bit like splats in other languages:

    macro unquote-splicing list? = list
    println (unquote-splicing [1 2 3]) # expands to (println 1 2 3)
  2. We can implement unquote just by wrapping our return value in a list:

macro unquote expr? = [expr]
println (unquote (quote 1 + 2)) # expands to (println 3)
  1. Using this pattern, we can generate code we previously couldn't. Most notably, we can now evaluate something and provide it to a quoted parameter:
def let :name? be :value? = def name = value # doesn't work! we'll always define the variable "name"
macro let :name? be :value? = [:def name := value] # with a macro, we can construct the expression properly
let x be 1
println x # prints 1

Note how in the last example we use quoted parameters in a macro - these aren't orthogonal to forms, they're a separate feature that synergizes with them very closely! So all told, I think these will drastically increase Basil's expressive power, while leveraging constructs that have mostly already been implemented - forms, functions, and eval.