2023-12-03 04:16:24 +00:00
|
|
|
# Garden
|
|
|
|
An experiment in combining [Fennel](https://fennel-lang.org) and [Terra](https://terralang.org).
|
|
|
|
|
|
|
|
## Rationale
|
|
|
|
Idk, seemed cool
|
|
|
|
|
2023-12-07 03:47:47 +00:00
|
|
|
## Goals and Non-Goals
|
|
|
|
At its core are a few a largely non-opinionated macros that directly expose the functionality
|
|
|
|
that is only available through Terra's custom syntax. Terra code is not necessarily meant to
|
|
|
|
read like Fennel or have the same semantics. Early returns are fine, all variables are mutable,
|
|
|
|
break and continue and imperative loops are the norm.
|
|
|
|
|
|
|
|
Ideally it would be possible to build a much cleaner set of macros on top of these base macros.
|
|
|
|
But the goal isn't to design a nice low-level language, it's to expose a powerful compilation
|
|
|
|
target. It's assumed that if you want to take advantage of Terra, it's probably because you
|
|
|
|
want to do horrible code-generation tricks. Hosting it inside Fennel simply makes it more
|
|
|
|
comfortable to do so.
|
|
|
|
|
2023-12-03 04:16:24 +00:00
|
|
|
## Usage
|
|
|
|
At the top of your file, include the following:
|
|
|
|
```fennel
|
2023-12-03 16:49:11 +00:00
|
|
|
(import-macros {: def : q : ttype : static} :terra)
|
2023-12-03 04:16:24 +00:00
|
|
|
```
|
|
|
|
The `def` macro defines a new terra function. The `q` macro defines a quoted terra expression.
|
|
|
|
The `ttype` macro allows you to specify terra type definitions that can't be expressed with
|
2023-12-03 16:49:11 +00:00
|
|
|
regular lua syntax. The `static` macro allows you to define "global" terra variables.
|
2023-12-03 04:16:24 +00:00
|
|
|
|
|
|
|
Notably, all of these macros return values, and none of them define new variables, local or global.
|
|
|
|
I could maybe be persuaded to make `def` work like `fn` and optionally define a local, but for now,
|
|
|
|
eh, whatever.
|
|
|
|
|
|
|
|
### def
|
2023-12-05 01:40:52 +00:00
|
|
|
Defines a function, compiling down to the `terra` keyword. Can also be used to create an undefined
|
|
|
|
function, if called with no statements.
|
2023-12-03 04:16:24 +00:00
|
|
|
|
|
|
|
```fennel
|
2023-12-05 01:40:52 +00:00
|
|
|
; syntax:
|
2023-12-03 04:16:24 +00:00
|
|
|
(def [argname1 argtype1 argname2 argtype2... : rettype1 rettype2...] statement...)
|
2023-12-05 01:40:52 +00:00
|
|
|
(def [argtype1 argtype2... : rettype1 rettype2...])
|
2023-12-03 04:16:24 +00:00
|
|
|
|
2023-12-05 01:40:52 +00:00
|
|
|
; examples:
|
2023-12-03 04:16:24 +00:00
|
|
|
(local add (def [x int y int : int]
|
|
|
|
(return (+ x y))))
|
2023-12-03 16:49:11 +00:00
|
|
|
|
|
|
|
; compiles to:
|
|
|
|
; local add = terra(x : int, y : int) : {int}
|
|
|
|
; return (x + y)
|
|
|
|
; end
|
|
|
|
|
2023-12-03 04:16:24 +00:00
|
|
|
(add 1 2) ; returns 3
|
2023-12-05 01:40:52 +00:00
|
|
|
|
2023-12-09 03:36:07 +00:00
|
|
|
(local even? (def [uint32 : bool]))
|
|
|
|
(local odd? (def [n uint32 : bool]
|
2023-12-05 01:40:52 +00:00
|
|
|
(if (= n 0)
|
|
|
|
(return false)
|
2023-12-09 03:36:07 +00:00
|
|
|
(return (even? (- n 1))))))
|
|
|
|
(even?:adddefinition (def [n uint32 : bool]
|
|
|
|
(if (= n 0)
|
|
|
|
(return true)
|
|
|
|
(return (odd? (- n 1))))))
|
2023-12-05 01:40:52 +00:00
|
|
|
|
|
|
|
; compiles to:
|
2023-12-09 03:36:07 +00:00
|
|
|
; local terra even_3f :: { uint32 } -> { bool }
|
|
|
|
; local odd_3f = terra(n : uint32) : { bool }
|
|
|
|
; if n == 0 then return true else return even_3f(n - 1) end
|
2023-12-05 01:40:52 +00:00
|
|
|
; end
|
2023-12-09 03:36:07 +00:00
|
|
|
; even_3f:adddefinition(terra(n : uint32) : { bool }
|
|
|
|
; if n == 0 then return false else return odd_3f(n - 1) end
|
2023-12-05 01:40:52 +00:00
|
|
|
; end)
|
2023-12-03 04:16:24 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
To define a function as returning "void", simply end the argument list with a `:`.
|
2023-12-05 01:40:52 +00:00
|
|
|
To make terra infer the return type, do not include a `:` in the argument list at all.
|
|
|
|
Undefined functions can't have their return types inferred.
|
|
|
|
|
2023-12-03 04:16:24 +00:00
|
|
|
Unlike Fennel, we do not implement implicit return semantics, and early returns are A-OK.
|
|
|
|
Sorry Phil.
|
|
|
|
|
|
|
|
### q
|
2023-12-03 22:12:09 +00:00
|
|
|
Defines a terra quotation, compiling down to the `` ` `` operator if given one argument, and
|
2023-12-05 03:47:59 +00:00
|
|
|
`quote` / `in` / `end` if given more than one. The resulting quote can always be used as an
|
|
|
|
expression that returns a value; if `q` is passed more than one argument, the last form is
|
|
|
|
used as the `in` block. To force no value to be returned, you can pass `($)` as the last
|
|
|
|
argument, which corresponds to the empty tuple. Note that quotes inherently creates a new
|
|
|
|
temporary scope, so variables created in a quote will go out of scope by the end of the
|
|
|
|
quote.
|
2023-12-03 04:16:24 +00:00
|
|
|
|
2023-12-03 16:49:11 +00:00
|
|
|
```fennel
|
2023-12-05 03:47:59 +00:00
|
|
|
(fn inc [x] (q (+ x 1))) ; compiles to: function(x) return `(x + 1) end
|
|
|
|
(fn do-thing [f x]
|
|
|
|
(q (var z (+ ,x 1))
|
|
|
|
(f z)
|
|
|
|
($)))
|
|
|
|
; compiles to:
|
|
|
|
; function(f, x)
|
|
|
|
; return quote
|
|
|
|
; var z = [x] + 1
|
|
|
|
; f(z)
|
|
|
|
; in
|
|
|
|
; {}
|
|
|
|
; end
|
|
|
|
; end
|
2023-12-03 16:49:11 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
### Type syntax
|
|
|
|
In Terra, types are Lua values and can be constructed from regular Lua code, outside of
|
|
|
|
`terra` or `quote` blocks. (There are certain places inside these blocks where types can
|
|
|
|
be constructed as well.) However, because Terra significantly extends the syntax of Lua to
|
|
|
|
allow for convenient type construction, Garden also must provide a mini-language to support
|
|
|
|
it.
|
|
|
|
|
|
|
|
Inside `terra` or `quote` blocks, whenever you have a form that requires a type to be passed
|
|
|
|
to it, the compiler will automatically enter a type-compiling context. Outside of these blocks,
|
|
|
|
the `ttype` macro can be used.
|
|
|
|
|
|
|
|
#### Pointers and arrays
|
|
|
|
Pointers to types and arrays of types use the Fennel "sequence table" syntax. Alternatively,
|
|
|
|
the `&` operator can be used.
|
|
|
|
```fennel
|
|
|
|
(local intptr (ttype [int])) ; compiles to: &int
|
|
|
|
(local intptr2 (ttype (& int))) ; also compiles to: &int
|
|
|
|
(local intarray (ttype [int 16])) ; compiles to: int[16]
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Structs
|
|
|
|
Structs are defined using the Fennel "key-value table" syntax. For each row in the table,
|
|
|
|
the keys are Fennel symbols representing the name of the field, and the values are type
|
|
|
|
expressions. If, instead of a symbol, the compiler finds the string `:union`, the value
|
|
|
|
is expected to be another "key-value table" containing the same structure. Struct definitons
|
|
|
|
can be nested.
|
|
|
|
|
|
|
|
Note that the field names must be valid Lua symbols; no name-mangling is done. Might be
|
|
|
|
worth doing that at some point; it seems likely that I will be annoyed if we don't
|
|
|
|
auto-convert `-` to `_` at least.
|
|
|
|
|
|
|
|
```fennel
|
|
|
|
(local Variant (ttype {tag int
|
|
|
|
:union {number float
|
|
|
|
string [int8]
|
|
|
|
complex {real float imag float}}}))
|
|
|
|
; compiles to:
|
|
|
|
; local Variant = struct {
|
|
|
|
; tag : int,
|
2023-12-09 03:36:07 +00:00
|
|
|
; union {
|
2023-12-03 16:49:11 +00:00
|
|
|
; number : float,
|
|
|
|
; string : &int8,
|
|
|
|
; complex : struct {
|
|
|
|
; real : float,
|
|
|
|
; imag : float
|
|
|
|
; }
|
2023-12-09 03:36:07 +00:00
|
|
|
; }
|
2023-12-03 16:49:11 +00:00
|
|
|
; }
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Function pointers
|
2023-12-05 00:08:23 +00:00
|
|
|
Function pointer types are defined with the `->` form, which a variable number of types as arguments.
|
|
|
|
Similar to `def`, you can put a `:` in your argument list to delineate between input parameters and
|
|
|
|
output types. Unlike `def`, if no `:` is present it is assumed that the function does not return a value,
|
|
|
|
as inference is not possible.
|
2023-12-03 16:49:11 +00:00
|
|
|
|
|
|
|
```fennel
|
2023-12-05 00:08:23 +00:00
|
|
|
(local callback (ttype (-> [int] int : int))) ; compiles to: local callback = { &int, int } -> { int }
|
|
|
|
(local callback (ttype (-> float))) ; compiles to: local callback = { float } -> {}
|
2023-12-03 16:49:11 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
#### Tuples
|
|
|
|
Tuple types are defined in Terra with a generic Lua function call to `tuple` that takes a variable
|
|
|
|
number of types as parameters. This is supported directly, like any type declaration consisting of
|
|
|
|
a function call, but there is a shortened form using `$` to match the tuple instantiation syntax.
|
2023-12-03 04:16:24 +00:00
|
|
|
|
|
|
|
```fennel
|
2023-12-03 16:49:11 +00:00
|
|
|
(ttype (tuple int [int])) ; compiles to: tuple(int, &int)
|
|
|
|
(ttype ($ int [int])) ; compiles to: tuple(int, &int)
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Escaping
|
|
|
|
Arbitrary Fennel expressions can be evaluated in a type-compilation context using Fennel's `,` prefix,
|
|
|
|
which is normally used by macros. If you need to re-enter a type-compilation context after escaping,
|
2023-12-03 22:12:09 +00:00
|
|
|
you'll need to nest a call to `ttype`. (I'm considering using `` ` `` for this purpose, but I might have
|
2023-12-03 16:49:11 +00:00
|
|
|
it consistently mean "create a quote" everywhere. Not sure.)
|
|
|
|
|
|
|
|
```fennel
|
|
|
|
(ttype ($ [int] (fn-accepting-type [int]) ,(fn-accepting-seq [5]) (fn-accepting-seq-of-types ,[(ttype [int])])))
|
|
|
|
; compiles to:
|
|
|
|
; tuple(&int, fn_accepting_type(&int), fn_accepting_seq({ 5 }), fn_accepting_seq_of_types({ &int }))
|
|
|
|
```
|
|
|
|
|
|
|
|
### Terra syntax
|
|
|
|
|
|
|
|
#### var
|
|
|
|
```fennel
|
2023-12-07 03:47:47 +00:00
|
|
|
(var name initial-value) ; compiles to: var name = initial-value
|
|
|
|
(var name type initial-value) ; compiles to: var name : type = initial-value
|
|
|
|
(var (name1 name2) val1 val2) ; compiles to: var name1, name2 = val1, val2
|
|
|
|
(var {name1 type1 name2 type2} val1 val2) ; compiles to: var name1 : type1, name2 : type2 = val1, val2
|
|
|
|
(var {undefined type}) ; compiles to: var undefined : type
|
2023-12-03 04:16:24 +00:00
|
|
|
```
|
|
|
|
Define a local variable named `var`, and set its initial value to `initial-value`. You can
|
2023-12-07 03:47:47 +00:00
|
|
|
manually specify a `type`, or you can let terra infer it from `initial-value`. Both forms
|
|
|
|
require `initial-value` to be provided.
|
|
|
|
|
|
|
|
You can define multiple type-inferred variables at once with the `(name1 name2)` syntax, and
|
|
|
|
multiple explicitly-typed variables with the `{name1 type1 name2 type2}` syntax. Both of those
|
|
|
|
forms accept any number of initial values. If no values are provided, the variables are left
|
|
|
|
uninitialized. If one value is passed, Terra will treat it as a tuple destructuring. Otherwise
|
|
|
|
you should probably pass the same number of values as names.
|
2023-12-03 04:16:24 +00:00
|
|
|
|
2023-12-03 22:12:09 +00:00
|
|
|
#### assignment
|
|
|
|
```fennel
|
|
|
|
(set varname value) ; compiles to: varname = value
|
|
|
|
(set struct.field value) ; compiles to: struct.field = value
|
2023-12-07 03:47:47 +00:00
|
|
|
(set (field1 field2) tuple) ; compiles to: field1, field2 = tuple
|
2023-12-03 22:12:09 +00:00
|
|
|
(tset struct (getfield) value) ; compiles to: struct.[getfield()] = value
|
|
|
|
```
|
2023-12-03 04:16:24 +00:00
|
|
|
|
2023-12-05 00:08:23 +00:00
|
|
|
#### Pointers and arrays
|
|
|
|
Dereferencing a pointer or accessing an element in an array uses the same syntax as defining a pointer
|
|
|
|
or array type - the Fennel sequence literal. To take a reference to a value, you can use the `&` form.
|
|
|
|
|
|
|
|
```fennel
|
|
|
|
(def [ptr [int]] (return [ptr])) ; compiles to: terra (ptr : &int) return @ptr end
|
|
|
|
(def [arr [int 8]] (return [arr 5])) ; compiles to: terra (arr : int[8]) return arr[5] end
|
|
|
|
(def [nested [[int]]] (return [nested 0 3])) ; compiles to: terra (nested : &&int) return nested[0][3] end
|
|
|
|
(def [nested [[int]]] (return [[nested] 3])) ; compiles to: terra (nested : &&int) return (@nested)[3] end
|
|
|
|
(def [ptr [int] : [int]] (return (& [ptr 1]))) ; compiles to: terra (ptr : &int): &int return &ptr[1] end
|
|
|
|
```
|
|
|
|
|
2023-12-03 22:12:09 +00:00
|
|
|
#### field access
|
2023-12-03 16:49:11 +00:00
|
|
|
```fennel
|
2023-12-03 22:12:09 +00:00
|
|
|
struct.field ; compiles to: struct.field
|
|
|
|
(struct.func) ; compiles to: struct.func()
|
|
|
|
(obj:method) ; compiles to: obj:method()
|
|
|
|
(. struct (getfield)) ; compiles to: struct.[getfield()]
|
|
|
|
(: obj (getmethod)) ; compiles to: struct:[getmethod()]()
|
|
|
|
```
|
|
|
|
|
|
|
|
#### cast
|
|
|
|
```fennel
|
|
|
|
(cast type expr)
|
|
|
|
```
|
|
|
|
Cast an expression `expr` to the type `type`.
|
|
|
|
|
|
|
|
```fennel
|
|
|
|
(cast [int] voidptr) ; compiles to: ([&int]voidptr)
|
|
|
|
(cast [int8] (C.malloc (* (sizeof int8) 16))) ; compiles to: ([&int8]C.malloc(sizeof(int8) * 16))
|
|
|
|
```
|
|
|
|
|
|
|
|
#### tuple literal
|
|
|
|
`$` can be used to create a tuple.
|
|
|
|
|
|
|
|
```fennel
|
|
|
|
($ 5 2.5 :hello) ; compiles to: { 5, 2.5, "hello" }
|
|
|
|
(var pair ($ int int) ($ 5 10)) ; compiles to: var pair : { int, int } = { 5, 10 }
|
|
|
|
```
|
|
|
|
|
|
|
|
#### struct literal
|
|
|
|
A fennel key-value table literal is interpreted as an anonymous struct literal.
|
|
|
|
If you "call" a struct type with a struct literal, it will coerce it to the given
|
|
|
|
type.
|
|
|
|
|
|
|
|
```fennel
|
|
|
|
(local Complex (ttype {real float imag float}))
|
|
|
|
; compiles to:
|
|
|
|
; local Complex = struct { real : float, imag : float }
|
|
|
|
|
|
|
|
(def [: Complex] (return (Complex { real 5 imag 1 })))
|
|
|
|
; compiles to:
|
|
|
|
; terra (): Complex
|
|
|
|
; return Complex({ real = 5, imag = 1 })
|
|
|
|
; end
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Primitive operators
|
|
|
|
|
|
|
|
| Fennel | Terra | Meaning |
|
|
|
|
| ------------ | ------------ | ----------------------------------- |
|
|
|
|
| `(+ x y)` | `x + y` | add x and y |
|
|
|
|
| `(- x y)` | `x - y` | subtract y from x |
|
|
|
|
| `(/ x y)` | `x / y` | divide x by y |
|
|
|
|
| `(* x y)` | `x * y` | multiply x and y |
|
|
|
|
| `(% x y)` | `x % y` | x modulo y |
|
|
|
|
| `(< x y)` | `x < y` | x is less than y |
|
|
|
|
| `(<= x y)` | `x <= y` | x is less than or equal to y |
|
|
|
|
| `(= x y)` | `x == y` | x is equal to y |
|
|
|
|
| `(not= x y)` | `x ~= y` | x is not equal to y |
|
|
|
|
| `(> x y)` | `x > y` | x is greater than y |
|
|
|
|
| `(>= x y)` | `x >= y` | x is greater than or equal to y |
|
|
|
|
| `(and x y)` | `x and y` | x AND y (boolean or bitwise) |
|
|
|
|
| `(or x y)` | `x or y` | x OR y (boolean or bitwise) |
|
|
|
|
| `(not x)` | `not x` | NOT x (boolean or bitwise) |
|
|
|
|
| `(^ x y)` | `x ^ y` | x XOR y (bitwise) |
|
|
|
|
| `(<< x y)` | `x << y` | arithmetic shift x left by y bits |
|
|
|
|
| `(>> x y)` | `x >> y` | arithmetic shift x right by y bits |
|
|
|
|
|
2023-12-05 00:08:23 +00:00
|
|
|
#### return
|
|
|
|
Terra functions can return multiple values, or none. Returns must be explicitly
|
|
|
|
written with the `return` form.
|
|
|
|
|
|
|
|
```fennel
|
|
|
|
(def [: int] (return 5)) ; compiles to: terra (): { int } return 5 end
|
|
|
|
(def [: float float] (return -1.5 1.5)) ; compiles to: terra (): { float float } return -1.5, 1.5 end
|
|
|
|
```
|
|
|
|
|
2023-12-03 22:12:09 +00:00
|
|
|
#### Fennel escaping
|