boot.t | ||
fennel.lua | ||
go.fnl | ||
README.md | ||
terra.fnl |
Garden
An experiment in combining Fennel and Terra.
Rationale
Idk, seemed cool
Usage
At the top of your file, include the following:
(import-macros {: def : q : ttype : static} :terra)
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
regular lua syntax. The static
macro allows you to define "global" terra variables.
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
Defines a function, compiling down to the terra
keyword.
Syntax:
(def [argname1 argtype1 argname2 argtype2... : rettype1 rettype2...] statement...)
Simple example:
(local add (def [x int y int : int]
(return (+ x y))))
; compiles to:
; local add = terra(x : int, y : int) : {int}
; return (x + y)
; end
(add 1 2) ; returns 3
To define a function as returning "void", simply end the argument list with a :
.
To make terra infer the return type, do not include a :
in the argument list at all.
Unlike Fennel, we do not implement implicit return semantics, and early returns are A-OK.
Sorry Phil.
q
Defines a terra quotation, compiling down to the `
operator if given one argument, and
quote
/ end
if given more than one.
(fn inc [x] (q (+ x 1))) ; compiles to: function(x) `(x + 1) end
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.
(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.
(local Variant (ttype {tag int
:union {number float
string [int8]
complex {real float imag float}}}))
; compiles to:
; local Variant = struct {
; tag : int,
; union {
; number : float,
; string : &int8,
; complex : struct {
; real : float,
; imag : float
; }
; }
; }
Function pointers
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.
(local callback (ttype (-> [int] int : int))) ; compiles to: local callback = { &int, int } -> { int }
(local callback (ttype (-> float))) ; compiles to: local callback = { float } -> {}
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.
(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,
you'll need to nest a call to ttype
. (I'm considering using `
for this purpose, but I might have
it consistently mean "create a quote" everywhere. Not sure.)
(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
(var name initial-value) ; compiles to: var name = initial-value
(var name type initial-value) ; compiles to: var name : type = initial-value
Define a local variable named var
, and set its initial value to initial-value
. You can
manually specify a type
, or you can let terra infer it from initial-value
. There is no
syntax for not initalizing the variable on declaration.
assignment
(set varname value) ; compiles to: varname = value
(set struct.field value) ; compiles to: struct.field = value
(tset struct (getfield) value) ; compiles to: struct.[getfield()] = value
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.
(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
field access
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
(cast type expr)
Cast an expression expr
to the type type
.
(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.
($ 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.
(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 |
return
Terra functions can return multiple values, or none. Returns must be explicitly
written with the return
form.
(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