fennel-api(3)

๐Ÿ“ฆ
version 1.6.1 ยท 3 versions
, in alpine 3.24, arch rolling, epel 10, epel 9, fedora 43, fedora 44, nixos 26.05, nixpkgs unstable

๐Ÿ’– fennel-api ๐Ÿ’–

okay bestie, so this is the manual page for fennel-api โ€” a.k.a. Fennel's Lua API ๐Ÿโœจ. Yes, the page header says fennel-api(3) up top, that's just the man page's little name tag, but the actual vibe is "how do I embed Fennel in a Lua program". Let's get into it.


NAME

fennel-api โ€” Fennel's Lua API ๐Ÿ’…


DESCRIPTION

So the fennel module gives you these functions to use when you're embedding Fennel inside a Lua program. And like โ€” if you're writing a pure Fennel program, or you're on a system that already has Fennel support baked in, you probably don't need any of this, bestie. This is for the embedding girlies ๐Ÿซถ.

Only the fennel module is part of the public API, periodt. The other modules are implementation details and they're subject to change, so don't get attached ๐Ÿ’€. Most functions will error if something goes wrong.

Any time a function takes an options table, that table will usually accept these fields:

  • allowedGlobals โ€” a sequential table of strings, the names of globals the compiler will let you reference. Set it to false to disable checks entirely. Defaults to the contents of the env table if you gave one, or the current environment otherwise.
  • correlate โ€” when you set this, Fennel tries to emit Lua where the line numbers actually match up with your Fennel input code. Super useful when code that is not under your control is gonna print the stack traces. It's a debugging aid though, and it can't give exact numbers in every single case, so don't come for it ๐Ÿ˜ญ.
  • useMetadata โ€” turns metadata on or off, which is what lets you use the ,doc repl command. It's intended for development purposes (peep the performance note), and it defaults to true for REPL only.
  • requireAsInclude โ€” aliases any static require calls to the include special, so the module code gets embedded inline in the compiled output. If the module name isn't a string literal that can be resolved at compile time, it falls back to require at runtime. You can use this to embed both Fennel and Lua modules ๐Ÿ’–.
  • toBeClosed โ€” use Lua 5.4+ to-be-closed variables when compiling with-open, so you don't interfere with traces.
  • assertAsRepl โ€” replaces calls to assert with assert-repl so you get interactive debugging. iconic behavior honestly.
  • lambdaAsFn โ€” replaces lambda function definitions with fn.
  • env โ€” an environment table to run the code in; check the Lua manual for the deets.
  • compilerEnv โ€” an environment table to run compiler-scoped code in, for macro definitions and eval-compiler calls. Internal Fennel functions like list, sym, etc. will be exposed on top of this table. Defaults to a table containing limited known-safe globals. Pass _G to disable sandboxing (yes, that's the whole sandbox, gone, poof ๐Ÿซฃ).
  • unfriendly โ€” disable friendly compiler/parser error messages.
  • plugins โ€” list of compiler plugins.
  • error-pinpoint โ€” a list of two strings indicating what to wrap compile errors in.
  • keywords โ€” a table of the form {:keyword1 true :keyword2 true} containing symbols that should be treated as reserved Lua keywords.
  • global-mangle โ€” whether to mangle globals in compiler output; set it to false to turn global references that aren't valid Lua into _G['hello-world'].

You can pass the string "_COMPILER" as the value for env โ€” it'll cause the code to be run/compiled in a context that has all the compiler-scoped values available. That's useful for macro modules or compiler plugins โœจ. If you wanna add extra values to the environment in that case, use the extra-env key. You can also use extra-compiler-env to add fields to the compiler environment used for macros.

Note that only the fennel module is part of the public API. The other modules (fennel.utils, fennel.compiler, etc) should be considered compiler internals subject to change. no cap.

If you're embedding Fennel somewhere that ANSI escape codes don't get interpreted, you can set error-pinpoint to false to disable the highlighting of compiler and parse errors. ๐Ÿ’…


START A CONFIGURABLE REPL

fennel.repl([options])

Takes these additional options, bestie:

  • readChunk(state) โ€” a function that, when called, returns a line of code to run. It can be an incomplete expression, in which case it'll get called again until a complete expression can be constructed. The state argument is a table with a stack-size field which will be zero unless it's reading a continuation of previous input. Strings you return should end in newlines. It should return nil when there's no more source, which exits the repl.
  • onValues(values) โ€” a function called for every evaluation, with a sequence table containing string representations of each of the values that came out of the input.
  • onError(errType, err, luaSource) โ€” a function called on each error. errType is a string with the type of error: 'parse', 'compile', 'runtime', or 'lua'. err is the error message, and luaSource is the source of the generated lua code.
  • pp(x) โ€” a pretty-printer function to apply on values (default: fennel.view).
  • view-opts โ€” an options table passed to pp (default: {:depth 4}).
  • rawValues(...) โ€” a function which is passed the raw values from evaluation; like onValues but it receives the underlying data instead of the string representation.
Heads up: overriding readChunk/onValues will only affect input and output initiated by the repl directly. If the repl runs code that calls print, io.write, io.read, etc, those will still use stdio unless you override them in env.

By default, metadata will be enabled and you can view function signatures and docstrings with the ,doc command in the REPL ๐Ÿ‘‘.

In Fennel 1.4.1 fennel.repl was changed from a normal function to a callable table. It mostly behaves the same, but it can cause problems with certain functions that are very picky about functions ๐Ÿ˜ญ. Unfortunately this includes coroutine.create. You can pass fennel.repl.repl instead.

Customize REPL default options

Any fields set on fennel.repl โ€” which is actually a table with a __call metamethod rather than a function โ€” will be used as a fallback for any options passed to fennel.repl or assert-repl before defaults are applied, so you can customize the default behavior of (fennel.repl):

fennel.repl.onError = custom_error_handler

function_that_calls_assert_repl_somewhere()

-- In rare cases this needs to be temporary, overrides
-- can be cleared by simply clearing the entire table
for k in pairs(fennel.repl) do
  fennel.repl[k] = nil
end

EVALUATE A STRING OF FENNEL

local result = fennel.eval(str[, options[, ...]])

The options table may also contain:

  • filename โ€” override the filename that Lua thinks the code came from.

Any additional arguments beyond options get passed to the code and are available as .... ๐ŸŽ€


EVALUATE A FILE OF FENNEL

local result = fennel.dofile(filename[, options[, ...]])

Additional arguments beyond options are passed to the code and are available as .... Same deal as eval, just from a file ๐Ÿฆ‹.


USE LUA'S BUILT-IN REQUIRE FUNCTION

require("fennel").install().dofile("main.fnl")

This is the equivalent of this code:

local fennel = require("fennel")
table.insert(package.loaders or package.searchers, fennel.searcher)
fennel.dofile("main.fnl") -- require calls in main.fnl can load fennel modules

Normally Lua's require function only loads modules written in Lua, but you can install fennel.searcher into package.searchers (or in Lua 5.1, package.loaders) to teach it how to load Fennel code. slay.

If you'd rather change some of the options, you can use fennel.makeSearcher(options) to get a searcher function that's equivalent to fennel.searcher but overrides the default options table. You can provide a path field to set fennel.path.

The require function is different from fennel.dofile in that it searches the directories in fennel.path for .fnl files matching the module name โ€” and also in that it caches the loaded value to return on subsequent calls, while fennel.dofile will reload each time. The behavior of fennel.path mirrors that of Lua's package.path. There's also a fennel.macro-path which is used to look up macro modules.

If you install Fennel into package.searchers then you can use the repl's ,reload mod command to reload modules that have been loaded with require. obsessed ๐Ÿ’–


MACRO SEARCHERS

The compiler sandbox makes it so that the module system is also isolated from the rest of the system, so those require calls up there will not work from inside macros. However, there's a separate fennel.macro-searchers table which can be used to allow different modules to be loaded inside macros. By default it includes a searcher to load sandboxed Fennel modules and a searcher to load sandboxed Lua modules โ€” but if you disable the compiler sandbox you may wanna replace these with searchers that can load arbitrary modules.

The default fennel.macro-searchers functions also cannot load C modules. Here's an example of some code which would allow that to work:

table.insert(fennel["macro-searchers"], function(module_name)
  local filename = fennel["search-module"](module_name, package.cpath)
  if filename then
    local func = "luaopen_" .. module_name
    return function() return package.loadlib(filename, func) end, filename
  end
end)

Macro searchers store loaded macro modules in the fennel.macro-loaded table, which works the same as package.loaded but for macro modules. โœจ


GET FENNEL-AWARE STACK INFORMATION

The fennel.traceback function works like Lua's debug.traceback function, except it tracks line numbers from Fennel code correctly ๐Ÿ”ฅ.

If you're working on an application written in Fennel, you can override the default traceback function to replace it with Fennel's:

debug.traceback = fennel.traceback
Note that some systems print stack traces from C, which will not be affected.

The fennel.getinfo function works like Lua's debug.getinfo function, except it tracks line numbers from Fennel code correctly. Functions defined from Fennel will have the what field set to "Fennel" instead of "Lua" ๐Ÿ‘‘.

local mymodule = require("module")
print(fennel.getinfo(mymodule.func1).linedefined)

COMPILE FENNEL CODE TO LUA

Compile a file, AST, or byte iterator

local lua = fennel.compile(fennelSource[, options])

The first argument here can be a file, an AST (usually produced by fennel.parser), or a stateful iterator function of bytes.

Unlike the other functions, the compile functions default to performing no global checks โ€” though you can pass in an allowedGlobals table in options to enable it. Accepts filename in options like fennel.eval does, for error reporting purposes.

Compile a string of Fennel code

local lua = fennel.compileString(fennelcode[, options])

Also aliased to fennel.compile-string for convenience when calling from Fennel. ๐Ÿ’…


okay that's part 1 of 4 bestie, we are literally not done yet โ€” more incoming ๐Ÿซถโœจ

๐Ÿงš PARSE TEXT INTO AST NODES ๐Ÿงš

Okay bestie, so fennel.parser is literally a factory โ€” you call it, and it hands you back a function. Call that function over and over and you get successive AST nodes out of a string, one at a time. โœจ

The gag is it's an iterator function, so it slots right into Lua's for or Fennel's each. When a form gets read successfully it returns true and then the AST node. It returns nil when you hit the end. And if it can't parse your input? It raises an error, no cap. ๐Ÿ˜ญ

local parse = fennel.parser(text)
local ok, ast = assert(parse()) -- just get the first form

-- Or use in a for loop
for ok, ast in parse do
  if ok then
    print(fennel.view(ast))
  end
end

First argument is either a string or a function that returns one byte at a time. Then two optional arguments: a filename, and a table of options. The supported options are booleans and they all default to false:

  • unfriendly โ€” disables the enhanced parse error reporting. (Rude name, real option. ๐Ÿ’€)
  • comments โ€” includes comment nodes in the AST.
  • plugins โ€” (since 1.2.0) an optional list of compiler plugins.
Heads up: the list of common options at the top of this document do not apply here. Different vibe entirely. ๐Ÿซถ

๐Ÿ‘‘ AST NODE DEFINITION ๐Ÿ‘‘

The AST that comes back from the parser is data structures representing your code. If you pass AST nodes into fennel.view you get a string that should round-trip thru the parser and give you the same data back. Iconic. Same deal with tostring, except tostring does not work with non-sequence tables. ๐Ÿ™ˆ

fennel.ast-source takes an AST node and returns a table with source data around it โ€” filename, line number, et cetera โ€” if possible. Some AST nodes just cannot provide this, for instance numbers, strings, and booleans, or symbols constructed within macros using the sym function instead of backtick. They're doing their best. ๐Ÿฅบ

AST nodes can be any of these types:

๐ŸŽ€ list ๐ŸŽ€

A list represents a call to function/macro, or destructuring multiple return values in a binding context. It's a table, and you can spot it with the fennel.list? predicate function, or build one with fennel.list, which takes any number of arguments for the contents of the list.

Real talk: lists are compile-time constructs in Fennel. They do not exist at runtime, except in such cases as the compiler is in use at runtime. So don't go looking for them at 3am. ๐Ÿ’…

The list also contains these keys indicating where it was defined: filename, line, col, endcol, bytestart, and byteend. This data is used for stack traces and for pinpointing compiler error messages. And ngl, column numbers are based on character count, which does not always correspond to visual columns โ€” for instance "เธงเธ”" is three characters but only two visual columns. Character count girlies stay winning. โœจ

๐ŸŒธ sequence/key-value table ๐ŸŒธ

These are the table literals in Fennel code, produced by square brackets (sequences) or curly brackets (k/v tables). Sequences can be identified using the fennel.sequence? function and constructed using fennel.sequence. There is no predicate or constructor for k/v tables; any table which is not one of the other types is assumed to be one of these. Main character by default, honestly. ๐Ÿ‘‘

At runtime there is no difference between sequences and k/v tables which use monotonically increasing integer keys, but the parser is able to distinguish between them to improve error reporting. She's perceptive like that. ๐Ÿ”ฅ

Sequences and k/v tables have their source data in filename, line, etc keys of their metatable. The metatable for k/v tables also includes a keys sequence which tells you which order the keys appeared originally, since k/v tables are unordered and there would otherwise be no way to reconstruct this information. Memory? Restored. ๐Ÿง โœจ

๐Ÿฆ‹ symbol ๐Ÿฆ‹

Symbols typically represent identifiers in Fennel code. Spot them with fennel.sym? and construct them with fennel.sym, which takes a string name as its first argument and a source data table as the second. Symbols are represented as tables which store their source data (filename, line, col, etc) in fields on themselves. And unlike the other tables in the AST, they do not represent collections โ€” they're used as scalar types. Tiny little divas. ๐Ÿ’…

Symbols can refer not just directly to locals, but also to table references like tbl.x for field lookup or access.channel:deny for method invocation. The fennel.multi-sym? function will return a table containing the segments if the symbol is one of these, or nil otherwise. Multi-faceted fr fr. ๐ŸŒˆ

Note: nil is not a valid AST; code that references nil will have the symbol named "nil" which unfortunately prints in a way that is visually indistinguishable from actual nil. Traumatizing, honestly. ๐Ÿ˜ญ

The fennel.sym-char? function will tell you if a given character is allowed to be used in the name of a symbol. Vibe check, but for characters. ๐Ÿ’–

โญ vararg โญ

This is a special type of symbol-like construct โ€” ... โ€” indicating functions using a variable number of arguments. Its meaning is the same as in Lua. Identified with fennel.varg? and constructed with fennel.varg. Short, sweet, ate. ๐Ÿ”ฅ

๐Ÿ’œ number/string/boolean ๐Ÿ’œ

These are literal types defined by Lua. They cannot carry source data. That's it, that's the whole thing. No lore, no backstory. ๐ŸŽ€

๐Ÿ“ comment ๐Ÿ“

By default, ASTs will omit comments. But when the :comment field is set in the parser options, comments get included in the parsed values. Identify them with fennel.comment? and construct them with the fennel.comment function. They're represented as tables that have source data as fields inside them.

In most data contexts, comments just get included inline in a list or sequence. However, in a k/v table, this cannot be done, because k/v tables must have balanced key/value pairs, and including comments inline would imbalance these or cause keys to be considered as values and vice versa. So instead the comments are stored on the comments field of the metatable, keyed by the key or value they were attached to. She found a workaround, we love a problem solver. ๐Ÿซถโœจ


๐Ÿ” SEARCH THE PATH FOR A MODULE WITHOUT LOADING IT ๐Ÿ”

print(fennel.searchModule("my.mod", package.path))

If you just want to find the file path that a module would resolve to without actually loading it, you've got fennel.searchModule. The first argument is the module name, and the second argument is the path string to search. If none is provided, it defaults to Fennel's own path. So chill of her. ๐Ÿ˜Œ

Returns nil if the module is not found on the path. Not every search ends in a slay. ๐Ÿ’”

๐Ÿ”ฅ SERIALIZATION (VIEW) ๐Ÿ”ฅ

Okay so fennel.view is literally the glow-up function โœจ โ€” you hand it any Fennel data and it hands you back a representation that's suitable for feeding straight back into Fennel's parser. It handles tables, strings, numbers, and booleans, and it can even produce reasonable output from ASTs that came out of the parser. BUT bestie, for coroutines, compiled functions, and userdata it emits an unreadable placeholder โ€” because the parser literally cannot understand those. No cap. ๐Ÿ™ˆ

print(fennel.view({abc=123}[, options])
{:abc 123}

Heads up though: the list of common options at the top of this document does not apply here. Different options, different vibe. These are the ones it actually accepts:

  • one-line? (default: false) โ€” keeps the output string as a one-liner
  • depth (number, default: 128) โ€” limits how many levels deep it'll go
  • detect-cycles? (default: true) โ€” don't even try to traverse a looping table
  • metamethod? (default: true) โ€” uses the __fennelview metamethod if it finds one
  • empty-as-sequence? (default: false) โ€” renders empty tables as []
  • line-length (number, default: 80) โ€” the line length at which multi-line output for tables gets forced
  • byte-escape (function) โ€” if present, overrides the default behaviour of escaping special characters in decimal format (e.g. <ESC> -> \027). It's called with the signature (byte-escape byte view-opts), where byte is the char code for a special character
  • escape-newlines? (default: false) โ€” emits strings with \n instead of an actual newline
  • prefer-colon? (default: false) โ€” emits strings in colon notation whenever possible
  • utf8? (default: true) โ€” whether to use the utf8 module to compute string lengths
  • max-sparse-gap (number, default: 1) โ€” the maximum gap to fill in with nils in sparse sequential tables before switching to curly brackets
  • preprocess (function) โ€” if present, it's called on x (and recursively on each value in x), and the result is used for pretty printing; it takes the same arguments as fennel.view

Every single one of these options can be set to {:once some-value} to force its value to be some-value โ€” but only for the current level. After that, the option resets back to its default value. Or, if you're feeling fancy, {:once value :after other-value} works too, except the difference is that after first use, the option gets set to other-value instead of the default. ๐Ÿ’…


You can set a __fennelview metamethod on a table to override its serialization behaviour. It should take the table being serialized as its first argument, a function as its second argument, an options table as its third argument, and the current amount of indentation as its last argument:

(fn [t view options indent] ...)

The view function contains a pretty printer that can be used to serialize elements stored within the table being serialized. If your metamethod produces indented representation, you should pass the indent parameter to view increased by the amount of additional indentation you've introduced. This function has the same interface as the __fennelview metamethod, but in addition it accepts colon-string? as its last argument. If colon? is true, strings get printed as colon-strings when possible, and if its value is false, strings are always printed in double quotes. If it's omitted or nil, it defaults to the value of the :prefer-colon? option.

The options table contains the options described above, and also a visible-cycle? function, which takes a table being serialized, detects and saves information about a possible reachable cycle. You should use it in __fennelview to implement cycle detection. ๐Ÿ”„

The __fennelview metamethod should always return a table of correctly indented lines when it's producing multi-line output, or a string when it's always returning a single-line item. fennel.view will transform your data structure into the correct multi-line representation when needed. There's no need to concatenate the table manually ever โ€” fennel.view will apply the general rules for your data structure, depending on the current options. By default, multiline output is produced only when inner data structures contain newlines, or when returning the table of lines as a single-line result would make the width greater than the line-size option.

Multi-line representation can be forced by returning two values from __fennelview โ€” a table of indented lines as the first value, and true as the second value, which indicates that multi-line representation should be forced. โœจ

There's no need to incorporate indentation beyond what's needed to correctly align elements within the printed representation of your data structure. So like, if you want to print a multi-line table like this:

@my-table[1
          2
          3]

then __fennelview should return a sequence of lines:

["@my-table[1"
 "          2"
 "          3]"]

Note: since we've introduced an inner indent string of length 10, when calling the view function from within the __fennelview metamethod, indent must be increased by this amount of extra indentation in order to keep inner tables indented correctly.

Here's an implementation of such a pretty-printer for an arbitrary sequential table:

(fn pp-doc-example [t view options indent]
  (let [lines (icollect [i v (ipairs t)]
                (let [v (view v options (+ 10 indent))]
                  (if (= i 1) v
                      (.. "          " v))))]
    (doto lines
      (tset 1 (.. "@my-table[" (or (. lines 1) "")))
      (tset (length lines) (.. (. lines (length lines)) "]")))))

Setting a table's __fennelview metamethod to this function will give you correct results regardless of nesting:

>> {:my-table (setmetatable [[1 2 3 4 5]
                             {:smalls [6 7 8 9 10 11 12]
                              :bigs [500 1000 2000 3000 4000]}]
                            {:__fennelview pp-doc-example})
    :normal-table [{:c [1 2 3] :d :some-data} 4]}
{:my-table @my-table[[1 2 3 4 5]
                     {:bigs [500 1000 2000 3000 4000]
                      :smalls [6 7 8 9 10 11 12]}]
 :normal-table [{:c [1 2 3] :d "some-data"} 4]}

Note that even though we've only indented the inner elements of our table with 10 spaces, the result is correctly indented in terms of the outer table, and the inner tables also stay indented correctly. Iconic. ๐Ÿ‘‘

When using the :preprocess option or the __fennelview method, avoid modifying any tables in-place in the passed function. Since Lua tables are mutable and passed in without copying, any modification done in these functions will be visible outside of fennel.view. ๐Ÿ˜ญ

Using :byte-escape to override the special character escape format is intended for use-cases where it's known that the output will be consumed by something other than Lua/Fennel โ€” and it may result in output that Fennel can no longer parse. So, careful bestie. For example, to force the use of hex escapes:

(print (fennel.view {:clear-screen "\027[H\027[2J"}
                    {:byte-escape #(: "\\x%2x" :format $)}))
;; > {:clear-screen "\x1b[H\x1b[2J"}

While Lua 5.2+ supports hex escapes, PUC Lua 5.1 does not, so compiling this with Fennel later would result in an incorrect escape code in Lua 5.1.


๐ŸŒธ WORK WITH DOCSTRINGS AND METADATA ๐ŸŒธ

When you're running a REPL or using compile/eval with metadata enabled, each function declared with fn or ฮป/lambda will use the created function as a key on fennel.metadata to store the function's arglist and (if provided) its docstring. The metadata table is weakly-referenced by key, so each function's metadata gets garbage collected right along with the function itself. Clean. ๐Ÿงน

You can work with the API to view or modify this metadata yourself, or use the ,doc repl command to view function documentation.

In addition to direct access to the metadata tables, you can use the following methods:

  • fennel.metadata:get(func, key) โ€” get a value from a function's metadata
  • fennel.metadata:set(func, key, val) โ€” set a metadata value
  • fennel.metadata:setall(func, key1, val1, key2, val2, ...) โ€” set pairs
  • fennel.doc(func, fnName) โ€” print formatted documentation for a function using name. Utilized by the ,doc command, name is whatever symbol you operate on that's bound to the function.
local greet = fennel.eval('(ฮป greet [name] "Say hello" (print "Hello," name))',
                          {useMetadata = true})

fennel.metadata[greet]
-- > {"fnl/docstring" = "Say hello", "fnl/arglist" = ["name"]}

fennel.doc(greet, "greet")
-- > (greet name)
-- >   Say hello

fennel.metadata:set(greet, "fnl/docstring", "Say hello!!!")
fennel.doc(greet, "greet!")
--> (greet! name)
-->   Say hello!!!

Metadata performance note

Enabling metadata in the compiler/eval/REPL will cause every function to store a new table containing the function's arglist and docstring in the metadata table, weakly referenced by the function itself as a key.

This may have a performance impact in some applications due to the extra allocations and garbage collection associated with dynamic function creation. The impact hasn't been benchmarked, but enabling metadata is currently recommended for development purposes only. ๐Ÿซถ


๐Ÿฆ‹ DESCRIBE FENNEL SYNTAX ๐Ÿฆ‹

If you're writing a tool that does syntax highlighting or some other operations on Fennel code, the fennel.syntax function can hook you up with data about what forms and keywords to treat specially. So helpful.

local syntax = fennel.syntax()
print(fennel.view(syntax["icollect"]))
--> {:binding-form? true :body-form? true :macro? true}

The table has string keys and table values. Each entry will have one of "macro?", "global?", or "special?" set to true, indicating what type it is. Globals can also have "function?" set to true. Macros and specials can have "binding-form?" set to true, which indicates it accepts a [] argument that introduces new locals, and/or a "body-form?", which indicates whether it should be indented with two spaces instead of being indented like a function call. They can also have a "define?" key, indicating whether it introduces a new top-level identifier like local or fn. ๐ŸŒˆ


๐Ÿ’œ LOAD LUA CODE IN A PORTABLE WAY ๐Ÿ’œ

This one isn't Fennel-specific, but the loadCode function takes a string of Lua code along with an optional environment table and a filename string, and returns a function for the loaded code which will run inside that environment โ€” in a way that's portable across any Lua 5.1+ version. Slay.

local f = fennel.loadCode(luaCode, { x = y }, "myfile.lua")

๐Ÿ”ฎ DETECT LUA VM RUNTIME VERSION ๐Ÿ”ฎ

Okay so this one does a best effort detection of the Lua VM environment that's hosting Fennel ๐Ÿ’… and it's genuinely so useful if you wanna show an "About" dialog in your Fennel app that actually matches what the REPL and the --version CLI flag say. Consistency queen ๐Ÿ‘‘

(fennel.runtime-version)

print(fennel.runtimeVersion())
-- > Fennel 1.0.0 on PUC Lua 5.4

And if you just want the Fennel version by itself, the fennel.version field has you covered ๐Ÿ’–

(since 1.3.1)

Oh and if you pass an optional argument, it hands you the version info as a table instead ๐ŸŽ€

(fennel.runtime-version :as-table)
;; > {:fennel "1.3.1" :lua "PUC Lua 5.4"}

๐Ÿงš PLUGINS ๐Ÿงš

Bestie, listen to me carefully. Fennel's plugin system is extremely experimental and it exposes internals of the compiler in ways that no other part of the compiler does. It should be considered unstable. Changes to the compiler in future versions are likely to break plugins, and every single plugin should only be assumed to work with the specific versions of the compiler that they're tested against. The backwards-compatibility guarantees of the rest of Fennel do not apply to plugins. Periodt. ๐Ÿ’€

So. Compiler plugins let you extend the compiler's functionality in various ways โœจ A plugin is just a module containing various functions in fields named after different compiler extension points. When the compiler hits an extension point, it calls each plugin's function for that extension point โ€” if that plugin provided one โ€” and it passes along various arguments; usually the AST in question and the scope table. Each plugin function should normally just do side effects and return nil, or error out. And if a function returns non-nil, that causes the rest of the plugins for that given event to be skipped ๐Ÿ‘€

  • symbol-to-expression
  • call
  • do
  • fn
  • destructure
  • parse-error
  • assert-compile

Now, the destructure extension point is lowkey different from the others ๐Ÿฆ‹ because instead of just taking ast and scope, it takes a from, which is the AST for the value being destructured, and a to AST, which is the AST for the form being destructured to. This is most commonly a symbol but it can also be a list or a table. Versatile much? ๐Ÿ’…

The parse-error and assert-compile hooks can be used to override how Fennel behaves all the way down to the parser and compiler levels ๐Ÿ˜ณ Possible use-cases include building on top of fennel.view to serialize data with EDN-style tagging, or manipulating external s-expression-based syntax โ€” like tree-sitter queries, for example. Iconic behaviour honestly.

The scope argument is a table containing all the compiler's information about the current scope ๐ŸŒธ Most of the tables in there look up values in their parent scopes if they don't contain a key. It's giving inheritance, it's giving family tree ๐ŸŒณ

Plugins can also contain repl commands ๐Ÿ˜ญ and this part is so fun. If your plugin module has a field with a name beginning with "repl-command-", then that function becomes available as a comma command from within a repl session. It gets called with a table for the repl session's environment, a function which will read the next form from stdin (ignoring newlines and other whitespace), a function which is used to print normal values, and one which is used to print errors. Go off ๐Ÿซถ

(local fennel (require :fennel)
(fn locals [env read on-values on-error scope chars opts]
  "Print all locals in repl session scope."
  (on-values [(fennel.view env.___replLocals___)]))

{:repl-command-locals locals}

$ fennel --plugin locals-plugin.fnl
Welcome to Fennel 0.8.0 on Lua 5.4!
Use ,help to see available commands.
>> (local x 4)
nil
>> (local abc :xyz)
nil
>> ,locals
{
  :abc "xyz"
  :x 4
}

The docstring of the function gets used as its summary in the ,help command listing ๐Ÿ‘‘ And heads up: unlike other plugin hook fields, only the first plugin to provide a repl command will be used. No sharing, no takebacks ๐Ÿ™ˆ

๐ŸŒˆ Activation ๐ŸŒˆ

Plugins get activated by passing the --plugin argument on the command line ๐Ÿ’ซ which should be a path to a Fennel file containing a module that has some of the functions listed above. And if you're using the compiler programmatically, you can include a :plugins table in the options table passed to most compiler entry point functions. Slay, both ways work.

Your plugin should contain a :versions field ๐ŸŽ€ which either contains a list of strings indicating every version of Fennel which you have tested it with, or a string containing a pattern which is checked against Fennel's version with string.find. And if your plugin is used with a version of Fennel that doesn't match :versions, it will emit a warning โš ๏ธ You should also have a :name field with the plugin's name, bestie. Don't forget it.


๐Ÿ’– AUTHORS ๐Ÿ’–

Fennel Maintainers. Kings and queens, every one of them ๐Ÿ‘‘


And that's a wrap on fennel-api(3), bestie!! ๐ŸŽ‰ This page is from fennel 1.6.1, dated 2025-12-30, and it's the fennel-api(3) manual page โ€” so go forth, plug in, and compiler-extend responsibly ๐Ÿ’…โœจ ily, go read the source, mwah ๐Ÿฆ‹