2373 lines
82 KiB
Lua
2373 lines
82 KiB
Lua
--[[
|
|
Copyright (c) 2016-2018 Calvin Rose and contributors
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
this software and associated documentation files (the "Software"), to deal in
|
|
the Software without restriction, including without limitation the rights to
|
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
subject to the following conditions:
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
]]
|
|
|
|
-- Make global variables local.
|
|
local setmetatable = setmetatable
|
|
local getmetatable = getmetatable
|
|
local type = type
|
|
local assert = assert
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local tostring = tostring
|
|
local unpack = unpack or table.unpack
|
|
|
|
--
|
|
-- Main Types and support functions
|
|
--
|
|
|
|
local function deref(self) return self[1] end
|
|
|
|
local SYMBOL_MT = { 'SYMBOL', __tostring = deref }
|
|
local EXPR_MT = { 'EXPR', __tostring = deref }
|
|
local VARARG = setmetatable({ '...' }, { 'VARARG', __tostring = deref })
|
|
local LIST_MT = { 'LIST',
|
|
__tostring = function (self)
|
|
local strs = {}
|
|
for _, s in ipairs(self) do
|
|
table.insert(strs, tostring(s))
|
|
end
|
|
return '(' .. table.concat(strs, ', ', 1, #self) .. ')'
|
|
end
|
|
}
|
|
local SEQUENCE_MT = { 'SEQUENCE' }
|
|
|
|
-- Load code with an environment in all recent Lua versions
|
|
local function loadCode(code, environment, filename)
|
|
environment = environment or _ENV or _G
|
|
if setfenv and loadstring then
|
|
local f = assert(loadstring(code, filename))
|
|
setfenv(f, environment)
|
|
return f
|
|
else
|
|
return assert(load(code, filename, "t", environment))
|
|
end
|
|
end
|
|
|
|
-- Create a new list
|
|
local function list(...)
|
|
return setmetatable({...}, LIST_MT)
|
|
end
|
|
|
|
-- Create a new symbol
|
|
local function sym(str, scope, meta)
|
|
local s = {str, scope = scope}
|
|
if meta then
|
|
for k, v in pairs(meta) do
|
|
if type(k) == 'string' then s[k] = v end
|
|
end
|
|
end
|
|
return setmetatable(s, SYMBOL_MT)
|
|
end
|
|
|
|
-- Create a new sequence
|
|
local function sequence(...)
|
|
return setmetatable({...}, SEQUENCE_MT)
|
|
end
|
|
|
|
-- Create a new expr
|
|
-- etype should be one of
|
|
-- "literal", -- literals like numbers, strings, nil, true, false
|
|
-- "expression", -- Complex strings of Lua code, may have side effects, etc, but is an expression
|
|
-- "statement", -- Same as expression, but is also a valid statement (function calls).
|
|
-- "vargs", -- varargs symbol
|
|
-- "sym", -- symbol reference
|
|
local function expr(strcode, etype)
|
|
return setmetatable({ strcode, type = etype }, EXPR_MT)
|
|
end
|
|
|
|
|
|
local function varg()
|
|
return VARARG
|
|
end
|
|
|
|
local function isVarg(x)
|
|
return x == VARARG and x
|
|
end
|
|
|
|
-- Checks if an object is a List. Returns the object if is a List.
|
|
local function isList(x)
|
|
return type(x) == 'table' and getmetatable(x) == LIST_MT and x
|
|
end
|
|
|
|
-- Checks if an object is a symbol. Returns the object if it is a symbol.
|
|
local function isSym(x)
|
|
return type(x) == 'table' and getmetatable(x) == SYMBOL_MT and x
|
|
end
|
|
|
|
-- Checks if an object any kind of table, EXCEPT list or symbol
|
|
local function isTable(x)
|
|
return type(x) == 'table' and
|
|
x ~= VARARG and
|
|
getmetatable(x) ~= LIST_MT and getmetatable(x) ~= SYMBOL_MT and x
|
|
end
|
|
|
|
-- Checks if an object is a sequence (created with a [] literal)
|
|
local function isSequence(x)
|
|
return type(x) == 'table' and getmetatable(x) == SEQUENCE_MT and x
|
|
end
|
|
|
|
--
|
|
-- Parser
|
|
--
|
|
|
|
-- Convert a stream of chunks to a stream of bytes.
|
|
-- Also returns a second function to clear the buffer in the byte stream
|
|
local function granulate(getchunk)
|
|
local c = ''
|
|
local index = 1
|
|
local done = false
|
|
return function (parserState)
|
|
if done then return nil end
|
|
if index <= #c then
|
|
local b = c:byte(index)
|
|
index = index + 1
|
|
return b
|
|
else
|
|
c = getchunk(parserState)
|
|
if not c or c == '' then
|
|
done = true
|
|
return nil
|
|
end
|
|
index = 2
|
|
return c:byte(1)
|
|
end
|
|
end, function ()
|
|
c = ''
|
|
end
|
|
end
|
|
|
|
-- Convert a string into a stream of bytes
|
|
local function stringStream(str)
|
|
local index = 1
|
|
return function()
|
|
local r = str:byte(index)
|
|
index = index + 1
|
|
return r
|
|
end
|
|
end
|
|
|
|
-- Table of delimiter bytes - (, ), [, ], {, }
|
|
-- Opener keys have closer as the value, and closers keys
|
|
-- have true as their value.
|
|
local delims = {
|
|
[40] = 41, -- (
|
|
[41] = true, -- )
|
|
[91] = 93, -- [
|
|
[93] = true, -- ]
|
|
[123] = 125, -- {
|
|
[125] = true -- }
|
|
}
|
|
|
|
local function iswhitespace(b)
|
|
return b == 32 or (b >= 9 and b <= 13) or b == 44
|
|
end
|
|
|
|
local function issymbolchar(b)
|
|
return b > 32 and
|
|
not delims[b] and
|
|
b ~= 127 and -- "<BS>"
|
|
b ~= 34 and -- "\""
|
|
b ~= 39 and -- "'"
|
|
b ~= 59 and -- ";"
|
|
b ~= 44 and -- ","
|
|
b ~= 96 -- "`"
|
|
end
|
|
|
|
local prefixes = { -- prefix chars substituted while reading
|
|
[96] = 'quote', -- `
|
|
[64] = 'unquote' -- @
|
|
}
|
|
|
|
-- Parse one value given a function that
|
|
-- returns sequential bytes. Will throw an error as soon
|
|
-- as possible without getting more bytes on bad input. Returns
|
|
-- if a value was read, and then the value read. Will return nil
|
|
-- when input stream is finished.
|
|
local function parser(getbyte, filename)
|
|
|
|
-- Stack of unfinished values
|
|
local stack = {}
|
|
|
|
-- Provide one character buffer and keep
|
|
-- track of current line and byte index
|
|
local line = 1
|
|
local byteindex = 0
|
|
local lastb
|
|
local function ungetb(ub)
|
|
if ub == 10 then line = line - 1 end
|
|
byteindex = byteindex - 1
|
|
lastb = ub
|
|
end
|
|
local function getb()
|
|
local r
|
|
if lastb then
|
|
r, lastb = lastb, nil
|
|
else
|
|
r = getbyte({ stackSize = #stack })
|
|
end
|
|
byteindex = byteindex + 1
|
|
if r == 10 then line = line + 1 end
|
|
return r
|
|
end
|
|
local function parseError(msg)
|
|
return error(msg .. ' in ' .. (filename or 'unknown') .. ':' .. line, 0)
|
|
end
|
|
|
|
-- Parse stream
|
|
return function()
|
|
|
|
-- Dispatch when we complete a value
|
|
local done, retval
|
|
local function dispatch(v)
|
|
if #stack == 0 then
|
|
retval = v
|
|
done = true
|
|
elseif stack[#stack].prefix then
|
|
local stacktop = stack[#stack]
|
|
stack[#stack] = nil
|
|
return dispatch(list(sym(stacktop.prefix), v))
|
|
else
|
|
table.insert(stack[#stack], v)
|
|
end
|
|
end
|
|
|
|
-- Throw nice error when we expect more characters
|
|
-- but reach end of stream.
|
|
local function badend()
|
|
local accum = {}
|
|
for _, item in ipairs(stack) do
|
|
accum[#accum + 1] = item.closer
|
|
end
|
|
parseError(('expected closing delimiter%s %s'):format(
|
|
#stack == 1 and "" or "s",
|
|
string.char(unpack(accum))))
|
|
end
|
|
|
|
-- The main parse loop
|
|
repeat
|
|
local b
|
|
|
|
-- Skip whitespace
|
|
repeat
|
|
b = getb()
|
|
until not b or not iswhitespace(b)
|
|
if not b then
|
|
if #stack > 0 then badend() end
|
|
return nil
|
|
end
|
|
|
|
if b == 59 then -- ; Comment
|
|
repeat
|
|
b = getb()
|
|
until not b or b == 10 -- newline
|
|
elseif type(delims[b]) == 'number' then -- Opening delimiter
|
|
table.insert(stack, setmetatable({
|
|
closer = delims[b],
|
|
line = line,
|
|
filename = filename,
|
|
bytestart = byteindex
|
|
}, LIST_MT))
|
|
elseif delims[b] then -- Closing delimiter
|
|
if #stack == 0 then parseError 'unexpected closing delimiter' end
|
|
local last = stack[#stack]
|
|
local val
|
|
if last.closer ~= b then
|
|
parseError('unexpected delimiter ' .. string.char(b) ..
|
|
', expected ' .. string.char(last.closer))
|
|
end
|
|
last.byteend = byteindex -- Set closing byte index
|
|
if b == 41 then -- ; )
|
|
val = last
|
|
elseif b == 93 then -- ; ]
|
|
val = sequence()
|
|
for i = 1, #last do
|
|
val[i] = last[i]
|
|
end
|
|
else -- ; }
|
|
if #last % 2 ~= 0 then
|
|
parseError('expected even number of values in table literal')
|
|
end
|
|
val = {}
|
|
for i = 1, #last, 2 do
|
|
val[last[i]] = last[i + 1]
|
|
end
|
|
end
|
|
stack[#stack] = nil
|
|
dispatch(val)
|
|
elseif b == 34 or b == 39 then -- Quoted string
|
|
local start = b
|
|
local state = "base"
|
|
local chars = {start}
|
|
stack[#stack + 1] = {closer = b}
|
|
repeat
|
|
b = getb()
|
|
chars[#chars + 1] = b
|
|
if state == "base" then
|
|
if b == 92 then
|
|
state = "backslash"
|
|
elseif b == start then
|
|
state = "done"
|
|
end
|
|
else
|
|
-- state == "backslash"
|
|
state = "base"
|
|
end
|
|
until not b or (state == "done")
|
|
if not b then badend() end
|
|
stack[#stack] = nil
|
|
local raw = string.char(unpack(chars))
|
|
local formatted = raw:gsub("[\1-\31]", function (c) return '\\' .. c:byte() end)
|
|
local loadFn = loadCode(('return %s'):format(formatted), nil, filename)
|
|
dispatch(loadFn())
|
|
elseif prefixes[b] then -- expand prefix byte into wrapping form eg. '`a' into '(quote a)'
|
|
table.insert(stack, {
|
|
prefix = prefixes[b]
|
|
})
|
|
else -- Try symbol
|
|
local chars = {}
|
|
local bytestart = byteindex
|
|
repeat
|
|
chars[#chars + 1] = b
|
|
b = getb()
|
|
until not b or not issymbolchar(b)
|
|
if b then ungetb(b) end
|
|
local rawstr = string.char(unpack(chars))
|
|
if rawstr == 'true' then dispatch(true)
|
|
elseif rawstr == 'false' then dispatch(false)
|
|
elseif rawstr == '...' then dispatch(VARARG)
|
|
elseif rawstr:match('^:.+$') then -- keyword style strings
|
|
dispatch(rawstr:sub(2))
|
|
else
|
|
local forceNumber = rawstr:match('^%d')
|
|
local numberWithStrippedUnderscores = rawstr:gsub("_", "")
|
|
local x
|
|
if forceNumber then
|
|
x = tonumber(numberWithStrippedUnderscores) or
|
|
parseError('could not read token "' .. rawstr .. '"')
|
|
else
|
|
x = tonumber(numberWithStrippedUnderscores) or
|
|
sym(rawstr, nil, { line = line,
|
|
filename = filename,
|
|
bytestart = bytestart,
|
|
byteend = byteindex, })
|
|
end
|
|
dispatch(x)
|
|
end
|
|
end
|
|
until done
|
|
return true, retval
|
|
end, function ()
|
|
stack = {}
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Compilation
|
|
--
|
|
|
|
-- Create a new Scope, optionally under a parent scope. Scopes are compile time constructs
|
|
-- that are responsible for keeping track of local variables, name mangling, and macros.
|
|
-- They are accessible to user code via the '*compiler' special form (may change). They
|
|
-- use metatables to implement nesting via inheritance.
|
|
local function makeScope(parent)
|
|
return {
|
|
unmanglings = setmetatable({}, {
|
|
__index = parent and parent.unmanglings
|
|
}),
|
|
manglings = setmetatable({}, {
|
|
__index = parent and parent.manglings
|
|
}),
|
|
specials = setmetatable({}, {
|
|
__index = parent and parent.specials
|
|
}),
|
|
symmeta = setmetatable({}, {
|
|
__index = parent and parent.symmeta
|
|
}),
|
|
parent = parent,
|
|
vararg = parent and parent.vararg,
|
|
depth = parent and ((parent.depth or 0) + 1) or 0
|
|
}
|
|
end
|
|
|
|
-- Assert a condition and raise a compile error with line numbers. The ast arg
|
|
-- should be unmodified so that its first element is the form being called.
|
|
local function assertCompile(condition, msg, ast)
|
|
-- if we use regular `assert' we can't provide the `level' argument of zero
|
|
if not condition then
|
|
error(string.format("Compile error in '%s' %s:%s: %s",
|
|
isSym(ast[1]) and ast[1][1] or ast[1] or '()',
|
|
ast.filename or "unknown", ast.line or '?', msg), 0)
|
|
end
|
|
return condition
|
|
end
|
|
|
|
local GLOBAL_SCOPE = makeScope()
|
|
GLOBAL_SCOPE.vararg = true
|
|
local SPECIALS = GLOBAL_SCOPE.specials
|
|
local COMPILER_SCOPE = makeScope(GLOBAL_SCOPE)
|
|
|
|
local luaKeywords = {
|
|
'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function',
|
|
'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true',
|
|
'until', 'while'
|
|
}
|
|
for i, v in ipairs(luaKeywords) do
|
|
luaKeywords[v] = i
|
|
end
|
|
|
|
local function isValidLuaIdentifier(str)
|
|
return (str:match('^[%a_][%w_]*$') and not luaKeywords[str])
|
|
end
|
|
|
|
-- Allow printing a string to Lua, also keep as 1 line.
|
|
local serializeSubst = {
|
|
['\a'] = '\\a',
|
|
['\b'] = '\\b',
|
|
['\f'] = '\\f',
|
|
['\n'] = 'n',
|
|
['\t'] = '\\t',
|
|
['\v'] = '\\v'
|
|
}
|
|
local function serializeString(str)
|
|
local s = ("%q"):format(str)
|
|
s = s:gsub('.', serializeSubst):gsub("[\128-\255]", function(c)
|
|
return "\\" .. c:byte()
|
|
end)
|
|
return s
|
|
end
|
|
|
|
-- A multi symbol is a symbol that is actually composed of
|
|
-- two or more symbols using the dot syntax. The main differences
|
|
-- from normal symbols is that they cannot be declared local, and
|
|
-- they may have side effects on invocation (metatables)
|
|
local function isMultiSym(str)
|
|
if type(str) ~= 'string' then return end
|
|
local parts = {}
|
|
for part in str:gmatch('[^%.]+') do
|
|
parts[#parts + 1] = part
|
|
end
|
|
return #parts > 0 and
|
|
str:match('%.') and
|
|
(not str:match('%.%.')) and
|
|
str:byte() ~= string.byte '.' and
|
|
str:byte(-1) ~= string.byte '.' and
|
|
parts
|
|
end
|
|
|
|
-- Mangler for global symbols. Does not protect against collisions,
|
|
-- but makes them unlikely. This is the mangling that is exposed to
|
|
-- to the world.
|
|
local function globalMangling(str)
|
|
if isValidLuaIdentifier(str) then
|
|
return str
|
|
end
|
|
-- Use underscore as escape character
|
|
return '__fnl_global__' .. str:gsub('[^%w]', function (c)
|
|
return ('_%02x'):format(c:byte())
|
|
end)
|
|
end
|
|
|
|
-- Reverse a global mangling. Takes a Lua identifier and
|
|
-- returns the fennel symbol string that created it.
|
|
local function globalUnmangling(identifier)
|
|
local rest = identifier:match('^__fnl_global__(.*)$')
|
|
if rest then
|
|
return rest:gsub('_[%da-f][%da-f]', function (code)
|
|
return string.char(tonumber(code:sub(2), 16))
|
|
end)
|
|
else
|
|
return identifier
|
|
end
|
|
end
|
|
|
|
-- Creates a symbol from a string by mangling it.
|
|
-- ensures that the generated symbol is unique
|
|
-- if the input string is unique in the scope.
|
|
local function localMangling(str, scope, ast)
|
|
if scope.manglings[str] then
|
|
return scope.manglings[str]
|
|
end
|
|
local append = 0
|
|
local mangling = str
|
|
assertCompile(not isMultiSym(str), 'did not expect multi symbol ' .. str, ast)
|
|
|
|
-- Mapping mangling to a valid Lua identifier
|
|
if luaKeywords[mangling] or mangling:match('^%d') then
|
|
mangling = '_' .. mangling
|
|
end
|
|
mangling = mangling:gsub('-', '_')
|
|
mangling = mangling:gsub('[^%w_]', function (c)
|
|
return ('_%02x'):format(c:byte())
|
|
end)
|
|
|
|
local raw = mangling
|
|
while scope.unmanglings[mangling] do
|
|
mangling = raw .. append
|
|
append = append + 1
|
|
end
|
|
scope.unmanglings[mangling] = str
|
|
scope.manglings[str] = mangling
|
|
return mangling
|
|
end
|
|
|
|
-- Combine parts of a symbol
|
|
local function combineParts(parts, scope)
|
|
local ret = scope.manglings[parts[1]] or globalMangling(parts[1])
|
|
for i = 2, #parts do
|
|
if isValidLuaIdentifier(parts[i]) then
|
|
ret = ret .. '.' .. parts[i]
|
|
else
|
|
ret = ret .. '[' .. serializeString(parts[i]) .. ']'
|
|
end
|
|
end
|
|
return ret
|
|
end
|
|
|
|
-- Generates a unique symbol in the scope.
|
|
local function gensym(scope)
|
|
local mangling
|
|
local append = 0
|
|
repeat
|
|
mangling = '_' .. append .. '_'
|
|
append = append + 1
|
|
until not scope.unmanglings[mangling]
|
|
scope.unmanglings[mangling] = true
|
|
return mangling
|
|
end
|
|
|
|
-- Check if a binding is valid
|
|
local function checkBindingValid(symbol, scope, ast)
|
|
-- Check if symbol will be over shadowed by special
|
|
local name = symbol[1]
|
|
assertCompile(not scope.specials[name],
|
|
("symbol %s may be overshadowed by a special form or macro"):format(name), ast)
|
|
end
|
|
|
|
-- Declare a local symbol
|
|
local function declareLocal(symbol, meta, scope, ast)
|
|
checkBindingValid(symbol, scope, ast)
|
|
local name = symbol[1]
|
|
assertCompile(not isMultiSym(name), "did not expect mutltisym", ast)
|
|
local mangling = localMangling(name, scope, ast)
|
|
scope.symmeta[name] = meta
|
|
return mangling
|
|
end
|
|
|
|
-- If there's a provided list of allowed globals, don't let references
|
|
-- thru that aren't on the list. This list is set at the compiler
|
|
-- entry points of compile and compileStream.
|
|
local allowedGlobals
|
|
|
|
local function globalAllowed(name)
|
|
if not allowedGlobals then return true end
|
|
for _, g in ipairs(allowedGlobals) do
|
|
if g == name then return true end
|
|
end
|
|
end
|
|
|
|
-- Convert symbol to Lua code. Will only work for local symbols
|
|
-- if they have already been declared via declareLocal
|
|
local function symbolToExpression(symbol, scope, isReference)
|
|
local name = symbol[1]
|
|
local parts = isMultiSym(name) or {name}
|
|
local etype = (#parts > 1) and "expression" or "sym"
|
|
local isLocal = scope.manglings[parts[1]]
|
|
-- if it's a reference and not a symbol which introduces a new binding
|
|
-- then we need to check for allowed globals
|
|
assertCompile(not isReference or isLocal or globalAllowed(parts[1]),
|
|
'unknown global in strict mode: ' .. parts[1], symbol)
|
|
return expr(combineParts(parts, scope), etype)
|
|
end
|
|
|
|
|
|
-- Emit Lua code
|
|
local function emit(chunk, out, ast)
|
|
if type(out) == 'table' then
|
|
table.insert(chunk, out)
|
|
else
|
|
table.insert(chunk, {leaf = out, ast = ast})
|
|
end
|
|
end
|
|
|
|
-- Do some peephole optimization.
|
|
local function peephole(chunk)
|
|
if chunk.leaf then return chunk end
|
|
-- Optimize do ... end in some cases.
|
|
if #chunk == 3 and
|
|
chunk[1].leaf == 'do' and
|
|
not chunk[2].leaf and
|
|
chunk[3].leaf == 'end' then
|
|
return peephole(chunk[2])
|
|
end
|
|
-- Recurse
|
|
for i, v in ipairs(chunk) do
|
|
chunk[i] = peephole(v)
|
|
end
|
|
return chunk
|
|
end
|
|
|
|
-- correlate line numbers in input with line numbers in output
|
|
local function flattenChunkCorrelated(mainChunk)
|
|
local function flatten(chunk, out, lastLine, file)
|
|
if chunk.leaf then
|
|
out[lastLine] = (out[lastLine] or "") .. " " .. chunk.leaf
|
|
else
|
|
for _, subchunk in ipairs(chunk) do
|
|
-- Ignore empty chunks
|
|
if subchunk.leaf or #subchunk > 0 then
|
|
-- don't increase line unless it's from the same file
|
|
if subchunk.ast and file == subchunk.ast.file then
|
|
lastLine = math.max(lastLine, subchunk.ast.line or 0)
|
|
end
|
|
lastLine = flatten(subchunk, out, lastLine, file)
|
|
end
|
|
end
|
|
end
|
|
return lastLine
|
|
end
|
|
local out = {}
|
|
local last = flatten(mainChunk, out, 1, mainChunk.file)
|
|
for i = 1, last do
|
|
if out[i] == nil then out[i] = "" end
|
|
end
|
|
return table.concat(out, "\n")
|
|
end
|
|
|
|
-- Flatten a tree of indented Lua source code lines.
|
|
-- Tab is what is used to indent a block.
|
|
local function flattenChunk(sm, chunk, tab, depth)
|
|
if type(tab) == 'boolean' then tab = tab and ' ' or '' end
|
|
if chunk.leaf then
|
|
local code = chunk.leaf
|
|
local info = chunk.ast
|
|
-- Just do line info for now to save memory
|
|
if sm then sm[#sm + 1] = info and info.line or -1 end
|
|
return code
|
|
else
|
|
local parts = {}
|
|
for i = 1, #chunk do
|
|
-- Ignore empty chunks
|
|
if chunk[i].leaf or #(chunk[i]) > 0 then
|
|
local sub = flattenChunk(sm, chunk[i], tab, depth + 1)
|
|
if depth > 0 then sub = tab .. sub:gsub('\n', '\n' .. tab) end
|
|
table.insert(parts, sub)
|
|
end
|
|
end
|
|
return table.concat(parts, '\n')
|
|
end
|
|
end
|
|
|
|
-- Some global state for all fennel sourcemaps. For the time being,
|
|
-- this seems the easiest way to store the source maps.
|
|
-- Sourcemaps are stored with source being mapped as the key, prepended
|
|
-- with '@' if it is a filename (like debug.getinfo returns for source).
|
|
-- The value is an array of mappings for each line.
|
|
local fennelSourcemap = {}
|
|
-- TODO: loading, unloading, and saving sourcemaps?
|
|
|
|
local function makeShortSrc(source)
|
|
source = source:gsub('\n', ' ')
|
|
if #source <= 49 then
|
|
return '[fennel "' .. source .. '"]'
|
|
else
|
|
return '[fennel "' .. source:sub(1, 46) .. '..."]'
|
|
end
|
|
end
|
|
|
|
-- Return Lua source and source map table
|
|
local function flatten(chunk, options)
|
|
local sm = options.sourcemap and {}
|
|
chunk = peephole(chunk)
|
|
if(options.correlate) then
|
|
return flattenChunkCorrelated(chunk), {}
|
|
else
|
|
local ret = flattenChunk(sm, chunk, options.indent, 0)
|
|
if sm then
|
|
local key, short_src
|
|
if options.filename then
|
|
short_src = options.filename
|
|
key = '@' .. short_src
|
|
else
|
|
key = ret
|
|
short_src = makeShortSrc(options.source or ret)
|
|
end
|
|
sm.short_src = short_src
|
|
sm.key = key
|
|
fennelSourcemap[key] = sm
|
|
end
|
|
return ret, sm
|
|
end
|
|
end
|
|
|
|
-- Convert expressions to Lua string
|
|
local function exprs1(exprs)
|
|
local t = {}
|
|
for _, e in ipairs(exprs) do
|
|
t[#t + 1] = e[1]
|
|
end
|
|
return table.concat(t, ', ')
|
|
end
|
|
|
|
-- Compile side effects for a chunk
|
|
local function keepSideEffects(exprs, chunk, start, ast)
|
|
start = start or 1
|
|
for j = start, #exprs do
|
|
local se = exprs[j]
|
|
-- Avoid the rogue 'nil' expression (nil is usually a literal,
|
|
-- but becomes an expression if a special form
|
|
-- returns 'nil'.)
|
|
if se.type == 'expression' and se[1] ~= 'nil' then
|
|
emit(chunk, ('do local _ = %s end'):format(tostring(se)), ast)
|
|
elseif se.type == 'statement' then
|
|
emit(chunk, tostring(se), ast)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Does some common handling of returns and register
|
|
-- targets for special forms. Also ensures a list expression
|
|
-- has an acceptable number of expressions if opts contains the
|
|
-- "nval" option.
|
|
local function handleCompileOpts(exprs, parent, opts, ast)
|
|
if opts.nval then
|
|
local n = opts.nval
|
|
if n ~= #exprs then
|
|
local len = #exprs
|
|
if len > n then
|
|
-- Drop extra
|
|
keepSideEffects(exprs, parent, n + 1, ast)
|
|
for i = n + 1, len do
|
|
exprs[i] = nil
|
|
end
|
|
else
|
|
-- Pad with nils
|
|
for i = #exprs + 1, n do
|
|
exprs[i] = expr('nil', 'literal')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if opts.tail then
|
|
emit(parent, ('return %s'):format(exprs1(exprs)), ast)
|
|
end
|
|
if opts.target then
|
|
emit(parent, ('%s = %s'):format(opts.target, exprs1(exprs)), ast)
|
|
end
|
|
if opts.tail or opts.target then
|
|
-- Prevent statements and expression from being used twice if they
|
|
-- have side-effects. Since if the target or tail options are set,
|
|
-- the expressions are already emitted, we should not return them. This
|
|
-- is fine, as when these options are set, the caller doesn't need the result
|
|
-- anyways.
|
|
exprs = {}
|
|
end
|
|
return exprs
|
|
end
|
|
|
|
-- Compile an AST expression in the scope into parent, a tree
|
|
-- of lines that is eventually compiled into Lua code. Also
|
|
-- returns some information about the evaluation of the compiled expression,
|
|
-- which can be used by the calling function. Macros
|
|
-- are resolved here, as well as special forms in that order.
|
|
-- the 'ast' param is the root AST to compile
|
|
-- the 'scope' param is the scope in which we are compiling
|
|
-- the 'parent' param is the table of lines that we are compiling into.
|
|
-- add lines to parent by appending strings. Add indented blocks by appending
|
|
-- tables of more lines.
|
|
-- the 'opts' param contains info about where the form is being compiled.
|
|
-- Options include:
|
|
-- 'target' - mangled name of symbol(s) being compiled to.
|
|
-- Could be one variable, 'a', or a list, like 'a, b, _0_'.
|
|
-- 'tail' - boolean indicating tail position if set. If set, form will generate a return
|
|
-- instruction.
|
|
-- 'nval' - The number of values to compile to if it is known to be a fixed value.
|
|
local function compile1(ast, scope, parent, opts)
|
|
opts = opts or {}
|
|
local exprs = {}
|
|
|
|
-- Compile the form
|
|
if isList(ast) then
|
|
-- Function call or special form
|
|
local len = #ast
|
|
assertCompile(len > 0, "expected a function to call", ast)
|
|
-- Test for special form
|
|
local first = ast[1]
|
|
if isSym(first) then -- Resolve symbol
|
|
first = first[1]
|
|
end
|
|
local special = scope.specials[first]
|
|
if special and isSym(ast[1]) then
|
|
-- Special form
|
|
exprs = special(ast, scope, parent, opts) or expr('nil', 'literal')
|
|
-- Be very accepting of strings or expression
|
|
-- as well as lists or expressions
|
|
if type(exprs) == 'string' then exprs = expr(exprs, 'expression') end
|
|
if getmetatable(exprs) == EXPR_MT then exprs = {exprs} end
|
|
-- Unless the special form explicitly handles the target, tail, and nval properties,
|
|
-- (indicated via the 'returned' flag), handle these options.
|
|
if not exprs.returned then
|
|
exprs = handleCompileOpts(exprs, parent, opts, ast)
|
|
elseif opts.tail or opts.target then
|
|
exprs = {}
|
|
end
|
|
exprs.returned = true
|
|
return exprs
|
|
else
|
|
-- Function call
|
|
local fargs = {}
|
|
local fcallee = compile1(ast[1], scope, parent, {
|
|
nval = 1
|
|
})[1]
|
|
assertCompile(fcallee.type ~= 'literal',
|
|
'cannot call literal value', ast)
|
|
fcallee = tostring(fcallee)
|
|
for i = 2, len do
|
|
local subexprs = compile1(ast[i], scope, parent, {
|
|
nval = i ~= len and 1 or nil
|
|
})
|
|
fargs[#fargs + 1] = subexprs[1] or expr('nil', 'literal')
|
|
if i == len then
|
|
-- Add sub expressions to function args
|
|
for j = 2, #subexprs do
|
|
fargs[#fargs + 1] = subexprs[j]
|
|
end
|
|
else
|
|
-- Emit sub expression only for side effects
|
|
keepSideEffects(subexprs, parent, 2, ast[i])
|
|
end
|
|
end
|
|
local call = ('%s(%s)'):format(tostring(fcallee), exprs1(fargs))
|
|
exprs = handleCompileOpts({expr(call, 'statement')}, parent, opts, ast)
|
|
end
|
|
elseif isVarg(ast) then
|
|
assertCompile(scope.vararg, "unexpected vararg", ast)
|
|
exprs = handleCompileOpts({expr('...', 'varg')}, parent, opts, ast)
|
|
elseif isSym(ast) then
|
|
local e
|
|
-- Handle nil as special symbol - it resolves to the nil literal rather than
|
|
-- being unmangled. Alternatively, we could remove it from the lua keywords table.
|
|
if ast[1] == 'nil' then
|
|
e = expr('nil', 'literal')
|
|
else
|
|
e = symbolToExpression(ast, scope, true)
|
|
end
|
|
exprs = handleCompileOpts({e}, parent, opts, ast)
|
|
elseif type(ast) == 'nil' or type(ast) == 'boolean' then
|
|
exprs = handleCompileOpts({expr(tostring(ast), 'literal')}, parent, opts)
|
|
elseif type(ast) == 'number' then
|
|
local n = ('%.17g'):format(ast)
|
|
exprs = handleCompileOpts({expr(n, 'literal')}, parent, opts)
|
|
elseif type(ast) == 'string' then
|
|
local s = serializeString(ast)
|
|
exprs = handleCompileOpts({expr(s, 'literal')}, parent, opts)
|
|
elseif type(ast) == 'table' then
|
|
local buffer = {}
|
|
for i = 1, #ast do -- Write numeric keyed values.
|
|
local nval = i ~= #ast and 1
|
|
buffer[#buffer + 1] = exprs1(compile1(ast[i], scope, parent, {nval = nval}))
|
|
end
|
|
local keys = {}
|
|
for k, _ in pairs(ast) do -- Write other keys.
|
|
if type(k) ~= 'number' or math.floor(k) ~= k or k < 1 or k > #ast then
|
|
local kstr
|
|
if type(k) == 'string' and isValidLuaIdentifier(k) then
|
|
kstr = k
|
|
else
|
|
kstr = '[' .. tostring(compile1(k, scope, parent, {nval = 1})[1]) .. ']'
|
|
end
|
|
table.insert(keys, { kstr, k })
|
|
end
|
|
end
|
|
table.sort(keys, function (a, b) return a[1] < b[1] end)
|
|
for _, k in ipairs(keys) do
|
|
local v = ast[k[2]]
|
|
buffer[#buffer + 1] = ('%s = %s'):format(
|
|
k[1], tostring(compile1(v, scope, parent, {nval = 1})[1]))
|
|
end
|
|
local tbl = '{' .. table.concat(buffer, ', ') ..'}'
|
|
exprs = handleCompileOpts({expr(tbl, 'expression')}, parent, opts, ast)
|
|
else
|
|
assertCompile(false, 'could not compile value of type ' .. type(ast), ast)
|
|
end
|
|
exprs.returned = true
|
|
return exprs
|
|
end
|
|
|
|
-- SPECIALS --
|
|
|
|
-- For statements and expressions, put the value in a local to avoid
|
|
-- double-evaluating it.
|
|
local function once(val, ast, scope, parent)
|
|
if val.type == 'statement' or val.type == 'expression' then
|
|
local s = gensym(scope)
|
|
emit(parent, ('local %s = %s'):format(s, tostring(val)), ast)
|
|
return expr(s, 'sym')
|
|
else
|
|
return val
|
|
end
|
|
end
|
|
|
|
-- Implements destructuring for forms like let, bindings, etc.
|
|
-- Takes a number of options to control behavior.
|
|
-- var: Whether or not to mark symbols as mutable
|
|
-- declaration: begin each assignment with 'local' in output
|
|
-- nomulti: disallow multisyms in the destructuring. Used for (local) and (global).
|
|
-- noundef: Don't set undefined bindings. (set)
|
|
-- forceglobal: Don't allow local bindings
|
|
local function destructure(to, from, ast, scope, parent, opts)
|
|
opts = opts or {}
|
|
local isvar = opts.isvar
|
|
local declaration = opts.declaration
|
|
local nomulti = opts.nomulti
|
|
local noundef = opts.noundef
|
|
local forceglobal = opts.forceglobal
|
|
local forceset = opts.forceset
|
|
local setter = declaration and "local %s = %s" or "%s = %s"
|
|
|
|
-- Get Lua source for symbol, and check for errors
|
|
local function getname(symbol, up1)
|
|
local raw = symbol[1]
|
|
assertCompile(not (nomulti and isMultiSym(raw)),
|
|
'did not expect multisym', up1)
|
|
if declaration then
|
|
return declareLocal(symbol, {var = isvar}, scope, symbol)
|
|
else
|
|
local parts = isMultiSym(raw) or {raw}
|
|
local meta = scope.symmeta[parts[1]]
|
|
if #parts == 1 and not forceset then
|
|
assertCompile(not(forceglobal and meta),
|
|
'expected global, found var', up1)
|
|
assertCompile(meta or not noundef,
|
|
'expected local var ' .. parts[1], up1)
|
|
assertCompile(not (meta and not meta.var),
|
|
'expected local var', up1)
|
|
end
|
|
return symbolToExpression(symbol, scope)[1]
|
|
end
|
|
end
|
|
|
|
-- Compile the outer most form. We can generate better Lua in this case.
|
|
local function compileTopTarget(lvalues)
|
|
-- Calculate initial rvalue
|
|
local inits = {}
|
|
for _, x in ipairs(lvalues) do
|
|
table.insert(inits, scope.manglings[x] and x or 'nil')
|
|
end
|
|
local init = table.concat(inits, ', ')
|
|
local lvalue = table.concat(lvalues, ', ')
|
|
|
|
local plen = #parent
|
|
local ret = compile1(from, scope, parent, {target = lvalue})
|
|
if declaration then
|
|
if #parent == plen + 1 and parent[#parent].leaf then
|
|
-- A single leaf emitted means an simple assignment a = x was emitted
|
|
parent[#parent].leaf = 'local ' .. parent[#parent].leaf
|
|
else
|
|
table.insert(parent, plen + 1, { leaf = 'local ' .. lvalue .. ' = ' .. init, ast = ast})
|
|
end
|
|
end
|
|
return ret
|
|
end
|
|
|
|
-- Recursive auxiliary function
|
|
local function destructure1(left, rightexprs, up1, top)
|
|
if isSym(left) and left[1] ~= "nil" then
|
|
checkBindingValid(left, scope, left)
|
|
local lname = getname(left, up1)
|
|
if top then
|
|
compileTopTarget({lname})
|
|
else
|
|
emit(parent, setter:format(lname, exprs1(rightexprs)), left)
|
|
end
|
|
elseif isTable(left) then -- table destructuring
|
|
if top then rightexprs = compile1(from, scope, parent) end
|
|
local s = gensym(scope)
|
|
emit(parent, ("local %s = %s"):format(s, exprs1(rightexprs)), left)
|
|
for k, v in pairs(left) do
|
|
if isSym(left[k]) and left[k][1] == "&" then
|
|
assertCompile(type(k) == "number" and not left[k+2],
|
|
"expected rest argument in final position", left)
|
|
local subexpr = expr(('{(table.unpack or unpack)(%s, %s)}'):format(s, k),
|
|
'expression')
|
|
destructure1(left[k+1], {subexpr}, left)
|
|
return
|
|
else
|
|
if type(k) ~= "number" then k = serializeString(k) end
|
|
local subexpr = expr(('%s[%s]'):format(s, k), 'expression')
|
|
destructure1(v, {subexpr}, left)
|
|
end
|
|
end
|
|
elseif isList(left) then -- values destructuring
|
|
local leftNames, tables = {}, {}
|
|
for i, name in ipairs(left) do
|
|
local symname
|
|
if isSym(name) then -- binding directly to a name
|
|
symname = getname(name, up1)
|
|
else -- further destructuring of tables inside values
|
|
symname = gensym(scope)
|
|
tables[i] = {name, expr(symname, 'sym')}
|
|
end
|
|
table.insert(leftNames, symname)
|
|
end
|
|
if top then
|
|
compileTopTarget(leftNames)
|
|
else
|
|
local lvalue = table.concat(leftNames, ', ')
|
|
emit(parent, setter:format(lvalue, exprs1(rightexprs)), left)
|
|
end
|
|
for _, pair in pairs(tables) do -- recurse if left-side tables found
|
|
destructure1(pair[1], {pair[2]}, left)
|
|
end
|
|
else
|
|
assertCompile(false, 'unable to destructure ' .. tostring(left), up1)
|
|
end
|
|
if top then return {returned = true} end
|
|
end
|
|
|
|
return destructure1(to, nil, ast, true)
|
|
end
|
|
|
|
-- Unlike most expressions and specials, 'values' resolves with multiple
|
|
-- values, one for each argument, allowing multiple return values. The last
|
|
-- expression can return multiple arguments as well, allowing for more than the number
|
|
-- of expected arguments.
|
|
local function values(ast, scope, parent)
|
|
local len = #ast
|
|
local exprs = {}
|
|
for i = 2, len do
|
|
local subexprs = compile1(ast[i], scope, parent, {
|
|
nval = (i ~= len) and 1
|
|
})
|
|
exprs[#exprs + 1] = subexprs[1]
|
|
if i == len then
|
|
for j = 2, #subexprs do
|
|
exprs[#exprs + 1] = subexprs[j]
|
|
end
|
|
end
|
|
end
|
|
return exprs
|
|
end
|
|
|
|
-- Compile a list of forms for side effects
|
|
local function compileDo(ast, scope, parent, start)
|
|
start = start or 2
|
|
local len = #ast
|
|
local subScope = makeScope(scope)
|
|
for i = start, len do
|
|
compile1(ast[i], subScope, parent, {
|
|
nval = 0
|
|
})
|
|
end
|
|
end
|
|
|
|
-- Implements a do statement, starting at the 'start' element. By default, start is 2.
|
|
local function doImpl(ast, scope, parent, opts, start, chunk, subScope)
|
|
start = start or 2
|
|
subScope = subScope or makeScope(scope)
|
|
chunk = chunk or {}
|
|
local len = #ast
|
|
local outerTarget = opts.target
|
|
local outerTail = opts.tail
|
|
local retexprs = {returned = true}
|
|
|
|
-- See if we need special handling to get the return values
|
|
-- of the do block
|
|
if not outerTarget and opts.nval ~= 0 and not outerTail then
|
|
if opts.nval then
|
|
-- Generate a local target
|
|
local syms = {}
|
|
for i = 1, opts.nval do
|
|
local s = gensym(scope)
|
|
syms[i] = s
|
|
retexprs[i] = expr(s, 'sym')
|
|
end
|
|
outerTarget = table.concat(syms, ', ')
|
|
emit(parent, ('local %s'):format(outerTarget), ast)
|
|
emit(parent, 'do', ast)
|
|
else
|
|
-- We will use an IIFE for the do
|
|
local fname = gensym(scope)
|
|
local fargs = scope.vararg and '...' or ''
|
|
emit(parent, ('local function %s(%s)'):format(fname, fargs), ast)
|
|
retexprs = expr(fname .. '(' .. fargs .. ')', 'statement')
|
|
outerTail = true
|
|
outerTarget = nil
|
|
end
|
|
else
|
|
emit(parent, 'do', ast)
|
|
end
|
|
-- Compile the body
|
|
if start > len then
|
|
-- In the unlikely case we do a do with no arguments.
|
|
compile1(nil, subScope, chunk, {
|
|
tail = outerTail,
|
|
target = outerTarget
|
|
})
|
|
-- There will be no side effects
|
|
else
|
|
for i = start, len do
|
|
local subopts = {
|
|
nval = i ~= len and 0 or opts.nval,
|
|
tail = i == len and outerTail or nil,
|
|
target = i == len and outerTarget or nil
|
|
}
|
|
local subexprs = compile1(ast[i], subScope, chunk, subopts)
|
|
if i ~= len then
|
|
keepSideEffects(subexprs, parent, nil, ast[i])
|
|
end
|
|
end
|
|
end
|
|
emit(parent, chunk, ast)
|
|
emit(parent, 'end', ast)
|
|
return retexprs
|
|
end
|
|
|
|
SPECIALS['do'] = doImpl
|
|
SPECIALS['values'] = values
|
|
|
|
-- The fn special declares a function. Syntax is similar to other lisps;
|
|
-- (fn optional-name [arg ...] (body))
|
|
-- Further decoration such as docstrings, meta info, and multibody functions a possibility.
|
|
SPECIALS['fn'] = function(ast, scope, parent)
|
|
local fScope = makeScope(scope)
|
|
local fChunk = {}
|
|
local index = 2
|
|
local fnName = isSym(ast[index])
|
|
local isLocalFn
|
|
fScope.vararg = false
|
|
if fnName and fnName[1] ~= 'nil' then
|
|
isLocalFn = not isMultiSym(fnName[1])
|
|
if isLocalFn then
|
|
fnName = declareLocal(fnName, {}, scope, ast)
|
|
else
|
|
fnName = symbolToExpression(fnName, scope)[1]
|
|
end
|
|
index = index + 1
|
|
else
|
|
isLocalFn = true
|
|
fnName = gensym(scope)
|
|
end
|
|
local argList = assertCompile(isTable(ast[index]),
|
|
'expected vector arg list [a b ...]', ast)
|
|
local argNameList = {}
|
|
for i = 1, #argList do
|
|
if isVarg(argList[i]) then
|
|
assertCompile(i == #argList, "expected vararg in last parameter position", ast)
|
|
argNameList[i] = '...'
|
|
fScope.vararg = true
|
|
elseif(isSym(argList[i]) and argList[i][1] ~= "nil"
|
|
and not isMultiSym(argList[i][1])) then
|
|
argNameList[i] = declareLocal(argList[i], {}, fScope, ast)
|
|
elseif isTable(argList[i]) then
|
|
local raw = sym(gensym(scope))
|
|
argNameList[i] = declareLocal(raw, {}, fScope, ast)
|
|
destructure(argList[i], raw, ast, fScope, fChunk,
|
|
{ declaration = true, nomulti = true })
|
|
else
|
|
assertCompile(false, 'expected symbol for function parameter', ast)
|
|
end
|
|
end
|
|
for i = index + 1, #ast do
|
|
compile1(ast[i], fScope, fChunk, {
|
|
tail = i == #ast,
|
|
nval = i ~= #ast and 0 or nil
|
|
})
|
|
end
|
|
if isLocalFn then
|
|
emit(parent, ('local function %s(%s)')
|
|
:format(fnName, table.concat(argNameList, ', ')), ast)
|
|
else
|
|
emit(parent, ('%s = function(%s)')
|
|
:format(fnName, table.concat(argNameList, ', ')), ast)
|
|
end
|
|
emit(parent, fChunk, ast)
|
|
emit(parent, 'end', ast)
|
|
return expr(fnName, 'sym')
|
|
end
|
|
|
|
-- (lua "print('hello!')") -> prints hello, evaluates to nil
|
|
-- (lua "print 'hello!'" "10") -> prints hello, evaluates to the number 10
|
|
-- (lua nil "{1,2,3}") -> Evaluates to a table literal
|
|
SPECIALS['lua'] = function(ast, _, parent)
|
|
assertCompile(#ast == 2 or #ast == 3,
|
|
"expected 2 or 3 arguments in 'lua' special form", ast)
|
|
if ast[2] ~= nil then
|
|
table.insert(parent, {leaf = tostring(ast[2]), ast = ast})
|
|
end
|
|
if #ast == 3 then
|
|
return tostring(ast[3])
|
|
end
|
|
end
|
|
|
|
-- Wrapper for table access
|
|
SPECIALS['.'] = function(ast, scope, parent)
|
|
local len = #ast
|
|
assertCompile(len > 1, "expected table argument", ast)
|
|
local lhs = compile1(ast[2], scope, parent, {nval = 1})
|
|
if len == 2 then
|
|
return tostring(lhs[1])
|
|
else
|
|
local indices = {}
|
|
for i = 3, len do
|
|
local index = ast[i]
|
|
if type(index) == 'string' and isValidLuaIdentifier(index) then
|
|
table.insert(indices, '.' .. index)
|
|
else
|
|
index = compile1(index, scope, parent, {nval = 1})[1]
|
|
table.insert(indices, '[' .. tostring(index) .. ']')
|
|
end
|
|
end
|
|
-- extra parens are needed for table literals
|
|
if isTable(ast[2]) then
|
|
return '(' .. tostring(lhs[1]) .. ')' .. table.concat(indices)
|
|
else
|
|
return tostring(lhs[1]) .. table.concat(indices)
|
|
end
|
|
end
|
|
end
|
|
|
|
SPECIALS['global'] = function(ast, scope, parent)
|
|
assertCompile(#ast == 3, "expected name and value", ast)
|
|
if allowedGlobals then table.insert(allowedGlobals, ast[2][1]) end
|
|
destructure(ast[2], ast[3], ast, scope, parent, {
|
|
nomulti = true,
|
|
forceglobal = true
|
|
})
|
|
end
|
|
|
|
SPECIALS['set'] = function(ast, scope, parent)
|
|
assertCompile(#ast == 3, "expected name and value", ast)
|
|
destructure(ast[2], ast[3], ast, scope, parent, {
|
|
noundef = true
|
|
})
|
|
end
|
|
|
|
SPECIALS['set-forcibly!'] = function(ast, scope, parent)
|
|
assertCompile(#ast == 3, "expected name and value", ast)
|
|
destructure(ast[2], ast[3], ast, scope, parent, {
|
|
forceset = true
|
|
})
|
|
end
|
|
|
|
SPECIALS['local'] = function(ast, scope, parent)
|
|
assertCompile(#ast == 3, "expected name and value", ast)
|
|
destructure(ast[2], ast[3], ast, scope, parent, {
|
|
declaration = true,
|
|
nomulti = true
|
|
})
|
|
end
|
|
|
|
SPECIALS['var'] = function(ast, scope, parent)
|
|
assertCompile(#ast == 3, "expected name and value", ast)
|
|
destructure(ast[2], ast[3], ast, scope, parent, {
|
|
declaration = true,
|
|
nomulti = true,
|
|
isvar = true
|
|
})
|
|
end
|
|
|
|
SPECIALS['let'] = function(ast, scope, parent, opts)
|
|
local bindings = ast[2]
|
|
assertCompile(isList(bindings) or isTable(bindings),
|
|
'expected table for destructuring', ast)
|
|
assertCompile(#bindings % 2 == 0,
|
|
'expected even number of name/value bindings', ast)
|
|
assertCompile(#ast >= 3, 'missing body expression', ast)
|
|
local subScope = makeScope(scope)
|
|
local subChunk = {}
|
|
for i = 1, #bindings, 2 do
|
|
destructure(bindings[i], bindings[i + 1], ast, subScope, subChunk, {
|
|
declaration = true,
|
|
nomulti = true
|
|
})
|
|
end
|
|
return doImpl(ast, scope, parent, opts, 3, subChunk, subScope)
|
|
end
|
|
|
|
-- For setting items in a table
|
|
SPECIALS['tset'] = function(ast, scope, parent)
|
|
assertCompile(#ast > 3, ('tset form needs table, key, and value'), ast)
|
|
local root = compile1(ast[2], scope, parent, {nval = 1})[1]
|
|
local keys = {}
|
|
for i = 3, #ast - 1 do
|
|
local key = compile1(ast[i], scope, parent, {nval = 1})[1]
|
|
keys[#keys + 1] = tostring(key)
|
|
end
|
|
local value = compile1(ast[#ast], scope, parent, {nval = 1})[1]
|
|
local rootstr = tostring(root)
|
|
local fmtstr = (rootstr:match('^{')) and '(%s)[%s] = %s' or '%s[%s] = %s'
|
|
emit(parent, fmtstr:format(tostring(root),
|
|
table.concat(keys, ']['),
|
|
tostring(value)), ast)
|
|
end
|
|
|
|
-- The if special form behaves like the cond form in
|
|
-- many languages
|
|
SPECIALS['if'] = function(ast, scope, parent, opts)
|
|
local doScope = makeScope(scope)
|
|
local branches = {}
|
|
local elseBranch = nil
|
|
|
|
-- Calculate some external stuff. Optimizes for tail calls and what not
|
|
local wrapper, innerTail, innerTarget, targetExprs
|
|
if opts.tail or opts.target or opts.nval then
|
|
if opts.nval and opts.nval ~= 0 and not opts.target then
|
|
-- We need to create a target
|
|
targetExprs = {}
|
|
local accum = {}
|
|
for i = 1, opts.nval do
|
|
local s = gensym(scope)
|
|
accum[i] = s
|
|
targetExprs[i] = expr(s, 'sym')
|
|
end
|
|
wrapper = 'target'
|
|
innerTail = opts.tail
|
|
innerTarget = table.concat(accum, ', ')
|
|
else
|
|
wrapper = 'none'
|
|
innerTail = opts.tail
|
|
innerTarget = opts.target
|
|
end
|
|
else
|
|
wrapper = 'iife'
|
|
innerTail = true
|
|
innerTarget = nil
|
|
end
|
|
|
|
-- Compile bodies and conditions
|
|
local bodyOpts = {
|
|
tail = innerTail,
|
|
target = innerTarget,
|
|
nval = opts.nval
|
|
}
|
|
local function compileBody(i)
|
|
local chunk = {}
|
|
local cscope = makeScope(doScope)
|
|
keepSideEffects(compile1(ast[i], cscope, chunk, bodyOpts),
|
|
chunk, nil, ast[i])
|
|
return {
|
|
chunk = chunk,
|
|
scope = cscope
|
|
}
|
|
end
|
|
for i = 2, #ast - 1, 2 do
|
|
local condchunk = {}
|
|
local res = compile1(ast[i], doScope, condchunk, {nval = 1})
|
|
local cond = res[1]
|
|
--print(ast[i], res, cond)
|
|
local branch = compileBody(i + 1)
|
|
branch.cond = cond
|
|
branch.condchunk = condchunk
|
|
branch.nested = i ~= 2 and next(condchunk, nil) == nil
|
|
table.insert(branches, branch)
|
|
end
|
|
local hasElse = #ast > 3 and #ast % 2 == 0
|
|
if hasElse then elseBranch = compileBody(#ast) end
|
|
|
|
-- Emit code
|
|
local s = gensym(scope)
|
|
local buffer = {}
|
|
local lastBuffer = buffer
|
|
for i = 1, #branches do
|
|
local branch = branches[i]
|
|
local fstr = not branch.nested and 'if %s then' or 'elseif %s then'
|
|
local condLine = fstr:format(tostring(branch.cond))
|
|
if branch.nested then
|
|
emit(lastBuffer, branch.condchunk, ast)
|
|
else
|
|
for _, v in ipairs(branch.condchunk) do emit(lastBuffer, v, ast) end
|
|
end
|
|
emit(lastBuffer, condLine, ast)
|
|
emit(lastBuffer, branch.chunk, ast)
|
|
if i == #branches then
|
|
if hasElse then
|
|
emit(lastBuffer, 'else', ast)
|
|
emit(lastBuffer, elseBranch.chunk, ast)
|
|
end
|
|
emit(lastBuffer, 'end', ast)
|
|
elseif not branches[i + 1].nested then
|
|
emit(lastBuffer, 'else', ast)
|
|
local nextBuffer = {}
|
|
emit(lastBuffer, nextBuffer, ast)
|
|
emit(lastBuffer, 'end', ast)
|
|
lastBuffer = nextBuffer
|
|
end
|
|
end
|
|
|
|
if wrapper == 'iife' then
|
|
local iifeargs = scope.vararg and '...' or ''
|
|
emit(parent, ('local function %s(%s)'):format(tostring(s), iifeargs), ast)
|
|
emit(parent, buffer, ast)
|
|
emit(parent, 'end', ast)
|
|
return expr(('%s(%s)'):format(tostring(s), iifeargs), 'statement')
|
|
elseif wrapper == 'none' then
|
|
-- Splice result right into code
|
|
for i = 1, #buffer do
|
|
emit(parent, buffer[i], ast)
|
|
end
|
|
return {returned = true}
|
|
else -- wrapper == 'target'
|
|
emit(parent, ('local %s'):format(innerTarget), ast)
|
|
for i = 1, #buffer do
|
|
emit(parent, buffer[i], ast)
|
|
end
|
|
return targetExprs
|
|
end
|
|
end
|
|
|
|
-- (each [k v (pairs t)] body...) => []
|
|
SPECIALS['each'] = function(ast, scope, parent)
|
|
local binding = assertCompile(isTable(ast[2]), 'expected binding table', ast)
|
|
local iter = table.remove(binding, #binding) -- last item is iterator call
|
|
local bindVars = {}
|
|
local destructures = {}
|
|
for _, v in ipairs(binding) do
|
|
assertCompile(isSym(v) or isTable(v),
|
|
'expected iterator symbol or table', ast)
|
|
if(isSym(v)) then
|
|
table.insert(bindVars, declareLocal(v, {}, scope, ast))
|
|
else
|
|
local raw = sym(gensym(scope))
|
|
destructures[raw] = v
|
|
table.insert(bindVars, declareLocal(raw, {}, scope, ast))
|
|
end
|
|
end
|
|
emit(parent, ('for %s in %s do'):format(
|
|
table.concat(bindVars, ', '),
|
|
tostring(compile1(iter, scope, parent, {nval = 1})[1])), ast)
|
|
local chunk = {}
|
|
for raw, args in pairs(destructures) do
|
|
destructure(args, raw, ast, scope, chunk,
|
|
{ declaration = true, nomulti = true })
|
|
end
|
|
compileDo(ast, scope, chunk, 3)
|
|
emit(parent, chunk, ast)
|
|
emit(parent, 'end', ast)
|
|
end
|
|
|
|
-- (while condition body...) => []
|
|
SPECIALS['while'] = function(ast, scope, parent)
|
|
local len1 = #parent
|
|
local condition = compile1(ast[2], scope, parent, {nval = 1})[1]
|
|
local len2 = #parent
|
|
local subChunk = {}
|
|
if len1 ~= len2 then
|
|
-- Compound condition
|
|
emit(parent, 'while true do', ast)
|
|
-- Move new compilation to subchunk
|
|
for i = len1 + 1, len2 do
|
|
subChunk[#subChunk + 1] = parent[i]
|
|
parent[i] = nil
|
|
end
|
|
emit(parent, ('if %s then break end'):format(condition[1]), ast)
|
|
else
|
|
-- Simple condition
|
|
emit(parent, 'while ' .. tostring(condition) .. ' do', ast)
|
|
end
|
|
compileDo(ast, makeScope(scope), subChunk, 3)
|
|
emit(parent, subChunk, ast)
|
|
emit(parent, 'end', ast)
|
|
end
|
|
|
|
SPECIALS['for'] = function(ast, scope, parent)
|
|
local ranges = assertCompile(isTable(ast[2]), 'expected binding table', ast)
|
|
local bindingSym = assertCompile(isSym(table.remove(ast[2], 1)),
|
|
'expected iterator symbol', ast)
|
|
local rangeArgs = {}
|
|
for i = 1, math.min(#ranges, 3) do
|
|
rangeArgs[i] = tostring(compile1(ranges[i], scope, parent, {nval = 1})[1])
|
|
end
|
|
emit(parent, ('for %s = %s do'):format(
|
|
declareLocal(bindingSym, {}, scope, ast),
|
|
table.concat(rangeArgs, ', ')), ast)
|
|
local chunk = {}
|
|
compileDo(ast, scope, chunk, 3)
|
|
emit(parent, chunk, ast)
|
|
emit(parent, 'end', ast)
|
|
end
|
|
|
|
SPECIALS[':'] = function(ast, scope, parent)
|
|
assertCompile(#ast >= 3, 'expected at least 3 arguments', ast)
|
|
-- Compile object
|
|
local objectexpr = compile1(ast[2], scope, parent, {nval = 1})[1]
|
|
-- Compile method selector
|
|
local methodstring
|
|
local methodident = false
|
|
if type(ast[3]) == 'string' and isValidLuaIdentifier(ast[3]) then
|
|
methodident = true
|
|
methodstring = ast[3]
|
|
else
|
|
methodstring = tostring(compile1(ast[3], scope, parent, {nval = 1})[1])
|
|
objectexpr = once(objectexpr, ast[2], scope, parent)
|
|
end
|
|
-- Compile arguments
|
|
local args = {}
|
|
for i = 4, #ast do
|
|
local subexprs = compile1(ast[i], scope, parent, {
|
|
nval = i ~= #ast and 1 or nil
|
|
})
|
|
for j = 1, #subexprs do
|
|
args[#args + 1] = tostring(subexprs[j])
|
|
end
|
|
end
|
|
local fstring
|
|
if methodident then
|
|
fstring = objectexpr.type == 'literal'
|
|
and '(%s):%s(%s)'
|
|
or '%s:%s(%s)'
|
|
else
|
|
-- Make object first argument
|
|
table.insert(args, 1, tostring(objectexpr))
|
|
fstring = objectexpr.type == 'sym'
|
|
and '%s[%s](%s)'
|
|
or '(%s)[%s](%s)'
|
|
end
|
|
return expr(fstring:format(
|
|
tostring(objectexpr),
|
|
methodstring,
|
|
table.concat(args, ', ')), 'statement')
|
|
end
|
|
|
|
local function defineArithmeticSpecial(name, zeroArity, unaryPrefix)
|
|
local paddedOp = ' ' .. name .. ' '
|
|
SPECIALS[name] = function(ast, scope, parent)
|
|
local len = #ast
|
|
if len == 1 then
|
|
assertCompile(zeroArity ~= nil, 'Expected more than 0 arguments', ast)
|
|
return expr(zeroArity, 'literal')
|
|
else
|
|
local operands = {}
|
|
for i = 2, len do
|
|
local subexprs = compile1(ast[i], scope, parent, {
|
|
nval = (i == 1 and 1 or nil)
|
|
})
|
|
for j = 1, #subexprs do
|
|
operands[#operands + 1] = tostring(subexprs[j])
|
|
end
|
|
end
|
|
if #operands == 1 then
|
|
if unaryPrefix then
|
|
return '(' .. unaryPrefix .. paddedOp .. operands[1] .. ')'
|
|
else
|
|
return operands[1]
|
|
end
|
|
else
|
|
return '(' .. table.concat(operands, paddedOp) .. ')'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defineArithmeticSpecial('+', '0')
|
|
defineArithmeticSpecial('..', "''")
|
|
defineArithmeticSpecial('^')
|
|
defineArithmeticSpecial('-', nil, '')
|
|
defineArithmeticSpecial('*', '1')
|
|
defineArithmeticSpecial('%')
|
|
defineArithmeticSpecial('/', nil, '1')
|
|
defineArithmeticSpecial('//', nil, '1')
|
|
defineArithmeticSpecial('or', 'false')
|
|
defineArithmeticSpecial('and', 'true')
|
|
|
|
local function defineComparatorSpecial(name, realop, chainOp)
|
|
local op = realop or name
|
|
SPECIALS[name] = function(ast, scope, parent)
|
|
local len = #ast
|
|
assertCompile(len > 2, 'expected at least two arguments', ast)
|
|
local lhs = compile1(ast[2], scope, parent, {nval = 1})[1]
|
|
local lastval = compile1(ast[3], scope, parent, {nval = 1})[1]
|
|
-- avoid double-eval by introducing locals for possible side-effects
|
|
if len > 3 then lastval = once(lastval, ast[3], scope, parent) end
|
|
local out = ('(%s %s %s)'):
|
|
format(tostring(lhs), op, tostring(lastval))
|
|
if len > 3 then
|
|
for i = 4, len do -- variadic comparison
|
|
local nextval = once(compile1(ast[i], scope, parent, {nval = 1})[1],
|
|
ast[i], scope, parent)
|
|
out = (out .. " %s (%s %s %s)"):
|
|
format(chainOp or 'and', tostring(lastval), op, tostring(nextval))
|
|
lastval = nextval
|
|
end
|
|
out = '(' .. out .. ')'
|
|
end
|
|
return out
|
|
end
|
|
end
|
|
|
|
defineComparatorSpecial('>')
|
|
defineComparatorSpecial('<')
|
|
defineComparatorSpecial('>=')
|
|
defineComparatorSpecial('<=')
|
|
defineComparatorSpecial('=', '==')
|
|
defineComparatorSpecial('~=', '~=', 'or')
|
|
defineComparatorSpecial('not=', '~=', 'or')
|
|
|
|
local function defineUnarySpecial(op, realop)
|
|
SPECIALS[op] = function(ast, scope, parent)
|
|
assertCompile(#ast == 2, 'expected one argument', ast)
|
|
local tail = compile1(ast[2], scope, parent, {nval = 1})
|
|
return (realop or op) .. tostring(tail[1])
|
|
end
|
|
end
|
|
|
|
defineUnarySpecial('not', 'not ')
|
|
defineUnarySpecial('#')
|
|
|
|
-- Save current macro scope
|
|
local macroCurrentScope = GLOBAL_SCOPE
|
|
|
|
-- Covert a macro function to a special form
|
|
local function macroToSpecial(mac)
|
|
return function(ast, scope, parent, opts)
|
|
local oldScope = macroCurrentScope
|
|
macroCurrentScope = scope
|
|
local ok, transformed = pcall(mac, unpack(ast, 2))
|
|
macroCurrentScope = oldScope
|
|
assertCompile(ok, transformed, ast)
|
|
local result = compile1(transformed, scope, parent, opts)
|
|
return result
|
|
end
|
|
end
|
|
|
|
local function compile(ast, options)
|
|
options = options or {}
|
|
local oldGlobals = allowedGlobals
|
|
allowedGlobals = options.allowedGlobals
|
|
if options.indent == nil then options.indent = ' ' end
|
|
local chunk = {}
|
|
local scope = options.scope or makeScope(GLOBAL_SCOPE)
|
|
local exprs = compile1(ast, scope, chunk, {tail = true})
|
|
keepSideEffects(exprs, chunk, nil, ast)
|
|
allowedGlobals = oldGlobals
|
|
return flatten(chunk, options)
|
|
end
|
|
|
|
-- map a function across all pairs in a table
|
|
local function quoteTmap(f, t)
|
|
local res = {}
|
|
for k,v in pairs(t) do
|
|
local nk, nv = f(k, v)
|
|
if nk then
|
|
res[nk] = nv
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- make a transformer for key / value table pairs, preserving all numeric keys
|
|
local function entryTransform(fk,fv)
|
|
return function(k, v)
|
|
if type(k) == 'number' then
|
|
return k,fv(v)
|
|
else
|
|
return fk(k),fv(v)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- consume everything return nothing
|
|
local function no() end
|
|
|
|
local function mixedConcat(t, joiner)
|
|
local ret = ""
|
|
local s = ""
|
|
local seen = {}
|
|
for k,v in ipairs(t) do
|
|
table.insert(seen, k)
|
|
ret = ret .. s .. v
|
|
s = joiner
|
|
end
|
|
for k,v in pairs(t) do
|
|
if not(seen[k]) then
|
|
ret = ret .. s .. '[' .. k .. ']' .. '=' .. v
|
|
s = joiner
|
|
end
|
|
end
|
|
return ret
|
|
end
|
|
|
|
-- expand a quoted form into a data literal, evaluating unquote
|
|
local function doQuote (form, scope, parent, runtime)
|
|
local q = function (x) return doQuote(x, scope, parent, runtime) end
|
|
-- vararg
|
|
if isVarg(form) then
|
|
assertCompile(not runtime, "quoted ... may only be used at compile time", form)
|
|
return "_VARARG"
|
|
-- symbol
|
|
elseif isSym(form) then
|
|
assertCompile(not runtime, "symbols may only be used at compile time", form)
|
|
return ("sym('%s')"):format(deref(form))
|
|
-- unquote
|
|
elseif isList(form) and isSym(form[1]) and (deref(form[1]) == 'unquote') then
|
|
local payload = form[2]
|
|
local res = unpack(compile1(payload, scope, parent))
|
|
return res[1]
|
|
-- list
|
|
elseif isList(form) then
|
|
assertCompile(not runtime, "lists may only be used at compile time", form)
|
|
local mapped = quoteTmap(entryTransform(no, q), form)
|
|
return 'list(' .. mixedConcat(mapped, ", ") .. ')'
|
|
-- table
|
|
elseif type(form) == 'table' then
|
|
local mapped = quoteTmap(entryTransform(q, q), form)
|
|
return '{' .. mixedConcat(mapped, ", ") .. '}'
|
|
-- string
|
|
elseif type(form) == 'string' then
|
|
return serializeString(form)
|
|
else
|
|
return tostring(form)
|
|
end
|
|
end
|
|
|
|
SPECIALS['quote'] = function(ast, scope, parent)
|
|
assertCompile(#ast == 2, "quote only takes a single form")
|
|
local runtime, thisScope = true, scope
|
|
while thisScope do
|
|
thisScope = thisScope.parent
|
|
if thisScope == COMPILER_SCOPE then runtime = false end
|
|
end
|
|
return doQuote(ast[2], scope, parent, runtime)
|
|
end
|
|
|
|
local function compileStream(strm, options)
|
|
options = options or {}
|
|
local oldGlobals = allowedGlobals
|
|
allowedGlobals = options.allowedGlobals
|
|
if options.indent == nil then options.indent = ' ' end
|
|
local scope = options.scope or makeScope(GLOBAL_SCOPE)
|
|
local vals = {}
|
|
for ok, val in parser(strm, options.filename) do
|
|
if not ok then break end
|
|
vals[#vals + 1] = val
|
|
end
|
|
local chunk = {}
|
|
for i = 1, #vals do
|
|
local exprs = compile1(vals[i], scope, chunk, {
|
|
tail = i == #vals
|
|
})
|
|
keepSideEffects(exprs, chunk, nil, vals[i])
|
|
end
|
|
allowedGlobals = oldGlobals
|
|
return flatten(chunk, options)
|
|
end
|
|
|
|
local function compileString(str, options)
|
|
local strm = stringStream(str)
|
|
return compileStream(strm, options)
|
|
end
|
|
|
|
---
|
|
--- Evaluation
|
|
---
|
|
|
|
-- Convert a fennel environment table to a Lua environment table.
|
|
-- This means automatically unmangling globals when getting a value,
|
|
-- and mangling values when setting a value. This means the original
|
|
-- env will see its values updated as expected, regardless of mangling rules.
|
|
local function wrapEnv(env)
|
|
return setmetatable({}, {
|
|
__index = function(_, key)
|
|
if type(key) == 'string' then
|
|
key = globalUnmangling(key)
|
|
end
|
|
return env[key]
|
|
end,
|
|
__newindex = function(_, key, value)
|
|
if type(key) == 'string' then
|
|
key = globalMangling(key)
|
|
end
|
|
env[key] = value
|
|
end,
|
|
-- checking the __pairs metamethod won't work automatically in Lua 5.1
|
|
-- sadly, but it's important for 5.2+ and can be done manually in 5.1
|
|
__pairs = function()
|
|
local pt = {}
|
|
for key, value in pairs(env) do
|
|
if type(key) == 'string' then
|
|
pt[globalUnmangling(key)] = value
|
|
else
|
|
pt[key] = value
|
|
end
|
|
end
|
|
return next, pt, nil
|
|
end,
|
|
})
|
|
end
|
|
|
|
-- A custom traceback function for Fennel that looks similar to
|
|
-- the Lua's debug.traceback.
|
|
-- Use with xpcall to produce fennel specific stacktraces.
|
|
local function traceback(msg, start)
|
|
local level = start or 2 -- Can be used to skip some frames
|
|
local lines = {}
|
|
if msg then
|
|
table.insert(lines, msg)
|
|
end
|
|
table.insert(lines, 'stack traceback:')
|
|
while true do
|
|
local info = debug.getinfo(level, "Sln")
|
|
if not info then break end
|
|
local line
|
|
if info.what == "C" then
|
|
if info.name then
|
|
line = (' [C]: in function \'%s\''):format(info.name)
|
|
else
|
|
line = ' [C]: in ?'
|
|
end
|
|
else
|
|
local remap = fennelSourcemap[info.source]
|
|
if remap and remap[info.currentline] then
|
|
-- And some global info
|
|
info.short_src = remap.short_src
|
|
local mapping = remap[info.currentline]
|
|
-- Overwrite info with values from the mapping (mapping is now
|
|
-- just integer, but may eventually be a table)
|
|
info.currentline = mapping
|
|
end
|
|
if info.what == 'Lua' then
|
|
local n = info.name and ("'" .. info.name .. "'") or '?'
|
|
line = (' %s:%d: in function %s'):format(info.short_src, info.currentline, n)
|
|
elseif info.short_src == '(tail call)' then
|
|
line = ' (tail call)'
|
|
else
|
|
line = (' %s:%d: in main chunk'):format(info.short_src, info.currentline)
|
|
end
|
|
end
|
|
table.insert(lines, line)
|
|
level = level + 1
|
|
end
|
|
return table.concat(lines, '\n')
|
|
end
|
|
|
|
local function currentGlobalNames(env)
|
|
local names = {}
|
|
for k in pairs(env or _G) do
|
|
k = globalUnmangling(k)
|
|
table.insert(names, k)
|
|
end
|
|
return names
|
|
end
|
|
|
|
local function eval(str, options, ...)
|
|
options = options or {}
|
|
-- eval and dofile are considered "live" entry points, so we can assume
|
|
-- that the globals available at compile time are a reasonable allowed list
|
|
-- UNLESS there's a metatable on env, in which case we can't assume that
|
|
-- pairs will return all the effective globals; for instance openresty
|
|
-- sets up _G in such a way that all the globals are available thru
|
|
-- the __index meta method, but as far as pairs is concerned it's empty.
|
|
if options.allowedGlobals == nil and not getmetatable(options.env) then
|
|
options.allowedGlobals = currentGlobalNames(options.env)
|
|
end
|
|
local env = options.env and wrapEnv(options.env)
|
|
local luaSource = compileString(str, options)
|
|
local loader = loadCode(luaSource, env,
|
|
options.filename and ('@' .. options.filename) or str)
|
|
return loader(...)
|
|
end
|
|
|
|
local function dofileFennel(filename, options, ...)
|
|
options = options or {sourcemap = true}
|
|
if options.allowedGlobals == nil then
|
|
options.allowedGlobals = currentGlobalNames(options.env)
|
|
end
|
|
local f = assert(io.open(filename, "rb"))
|
|
local source = f:read("*all"):gsub("^#![^\n]*\n", "")
|
|
f:close()
|
|
options.filename = options.filename or filename
|
|
return eval(source, options, ...)
|
|
end
|
|
|
|
-- Implements a configurable repl
|
|
local function repl(options)
|
|
|
|
local opts = options or {}
|
|
-- This would get set for us when calling eval, but we want to seed it
|
|
-- with a value that is persistent so it doesn't get reset on each eval.
|
|
if opts.allowedGlobals == nil then
|
|
options.allowedGlobals = currentGlobalNames(opts.env)
|
|
end
|
|
|
|
local env = opts.env and wrapEnv(opts.env) or setmetatable({}, {
|
|
__index = _ENV or _G
|
|
})
|
|
|
|
local function defaultReadChunk(parserState)
|
|
io.write(parserState.stackSize > 0 and '.. ' or '>> ')
|
|
io.flush()
|
|
local input = io.read()
|
|
return input and input .. '\n'
|
|
end
|
|
|
|
local function defaultOnValues(xs)
|
|
io.write(table.concat(xs, '\t'))
|
|
io.write('\n')
|
|
end
|
|
|
|
local function defaultOnError(errtype, err, luaSource)
|
|
if (errtype == 'Lua Compile') then
|
|
io.write('Bad code generated - likely a bug with the compiler:\n')
|
|
io.write('--- Generated Lua Start ---\n')
|
|
io.write(luaSource .. '\n')
|
|
io.write('--- Generated Lua End ---\n')
|
|
end
|
|
if (errtype == 'Runtime') then
|
|
io.write(traceback(err, 4))
|
|
io.write('\n')
|
|
else
|
|
io.write(('%s error: %s\n'):format(errtype, tostring(err)))
|
|
end
|
|
end
|
|
|
|
-- Read options
|
|
local readChunk = opts.readChunk or defaultReadChunk
|
|
local onValues = opts.onValues or defaultOnValues
|
|
local onError = opts.onError or defaultOnError
|
|
local pp = opts.pp or tostring
|
|
|
|
-- Make parser
|
|
local bytestream, clearstream = granulate(readChunk)
|
|
local chars = {}
|
|
local read, reset = parser(function (parserState)
|
|
local c = bytestream(parserState)
|
|
chars[#chars + 1] = c
|
|
return c
|
|
end)
|
|
|
|
local envdbg = (opts.env or _G)["debug"]
|
|
-- if the environment doesn't support debug.getlocal you can't save locals
|
|
local saveLocals = opts.saveLocals ~= false and envdbg and envdbg.getlocal
|
|
local saveSource = table.
|
|
concat({"local ___i___ = 1",
|
|
"while true do",
|
|
" local name, value = debug.getlocal(1, ___i___)",
|
|
" if(name and name ~= \"___i___\") then",
|
|
" ___replLocals___[name] = value",
|
|
" ___i___ = ___i___ + 1",
|
|
" else break end end"}, "\n")
|
|
|
|
local spliceSaveLocals = function(luaSource)
|
|
-- we do some source munging in order to save off locals from each chunk
|
|
-- and reintroduce them to the beginning of the next chunk, allowing
|
|
-- locals to work in the repl the way you'd expect them to.
|
|
env.___replLocals___ = env.___replLocals___ or {}
|
|
local splicedSource = {}
|
|
for line in luaSource:gmatch("([^\n]+)\n?") do
|
|
table.insert(splicedSource, line)
|
|
end
|
|
-- reintroduce locals from the previous time around
|
|
local bind = "local %s = ___replLocals___['%s']"
|
|
for name in pairs(env.___replLocals___) do
|
|
table.insert(splicedSource, 1, bind:format(name, name))
|
|
end
|
|
-- save off new locals at the end - if safe to do so (i.e. last line is a return)
|
|
if (string.match(splicedSource[#splicedSource], "^ *return .*$")) then
|
|
if (#splicedSource > 1) then
|
|
table.insert(splicedSource, #splicedSource, saveSource)
|
|
end
|
|
end
|
|
return table.concat(splicedSource, "\n")
|
|
end
|
|
|
|
local scope = makeScope(GLOBAL_SCOPE)
|
|
|
|
local replCompleter = function(text, from, to)
|
|
local matches = {}
|
|
local inputFragment = string.lower(text):sub(from, to):gsub("[%s)(]*(.+)", "%1")
|
|
|
|
-- adds any matching keys from the provided generator/iterator to matches
|
|
local function addMatchesFromGen(next, param, state)
|
|
for k in next, param, state do
|
|
if #matches >= 40 then break -- cap completions at 40 to avoid overwhelming output
|
|
elseif inputFragment == k:sub(0, #inputFragment):lower() then table.insert(matches, k) end
|
|
end
|
|
end
|
|
addMatchesFromGen(pairs(env._ENV or env._G or {}))
|
|
addMatchesFromGen(pairs(env.___replLocals___ or {}))
|
|
addMatchesFromGen(pairs(SPECIALS or {}))
|
|
addMatchesFromGen(pairs(scope.specials or {}))
|
|
return matches
|
|
end
|
|
if options.registerCompleter then options.registerCompleter(replCompleter) end
|
|
|
|
-- REPL loop
|
|
while true do
|
|
chars = {}
|
|
local ok, parseok, x = pcall(read)
|
|
local srcstring = string.char(unpack(chars))
|
|
if not ok then
|
|
onError('Parse', parseok)
|
|
clearstream()
|
|
reset()
|
|
else
|
|
if not parseok then break end -- eof
|
|
local compileOk, luaSource = pcall(compile, x, {
|
|
sourcemap = opts.sourcemap,
|
|
source = srcstring,
|
|
scope = scope,
|
|
})
|
|
if not compileOk then
|
|
clearstream()
|
|
onError('Compile', luaSource) -- luaSource is error message in this case
|
|
else
|
|
if saveLocals then
|
|
luaSource = spliceSaveLocals(luaSource)
|
|
end
|
|
local luacompileok, loader = pcall(loadCode, luaSource, env)
|
|
if not luacompileok then
|
|
clearstream()
|
|
onError('Lua Compile', loader, luaSource)
|
|
else
|
|
local loadok, ret = xpcall(function () return {loader()} end,
|
|
function (runtimeErr)
|
|
onError('Runtime', runtimeErr)
|
|
end)
|
|
if loadok then
|
|
env._ = ret[1]
|
|
env.__ = ret
|
|
for i = 1, #ret do ret[i] = pp(ret[i]) end
|
|
onValues(ret)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local macroLoaded = {}
|
|
|
|
local module = {
|
|
parser = parser,
|
|
granulate = granulate,
|
|
stringStream = stringStream,
|
|
compile = compile,
|
|
compileString = compileString,
|
|
compileStream = compileStream,
|
|
compile1 = compile1,
|
|
mangle = globalMangling,
|
|
unmangle = globalUnmangling,
|
|
list = list,
|
|
sym = sym,
|
|
varg = varg,
|
|
scope = makeScope,
|
|
gensym = gensym,
|
|
eval = eval,
|
|
repl = repl,
|
|
dofile = dofileFennel,
|
|
macroLoaded = macroLoaded,
|
|
path = "./?.fnl;./?/init.fnl",
|
|
traceback = traceback,
|
|
version = "0.2.1",
|
|
}
|
|
|
|
local function searchModule(modulename)
|
|
modulename = modulename:gsub("%.", "/")
|
|
for path in string.gmatch(module.path..";", "([^;]*);") do
|
|
local filename = path:gsub("%?", modulename)
|
|
local file = io.open(filename, "rb")
|
|
if(file) then
|
|
file:close()
|
|
return filename
|
|
end
|
|
end
|
|
end
|
|
|
|
module.makeSearcher = function(options)
|
|
return function(modulename)
|
|
local opts = {}
|
|
for k,v in pairs(options or {}) do opts[k] = v end
|
|
local filename = searchModule(modulename)
|
|
if filename then
|
|
return function(modname)
|
|
return dofileFennel(filename, opts, modname)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- This will allow regular `require` to work with Fennel:
|
|
-- table.insert(package.loaders, fennel.searcher)
|
|
module.searcher = module.makeSearcher()
|
|
module.make_searcher = module.makeSearcher -- oops backwards compatibility
|
|
|
|
local function makeCompilerEnv(ast, scope, parent)
|
|
return setmetatable({
|
|
-- State of compiler if needed
|
|
_SCOPE = scope,
|
|
_CHUNK = parent,
|
|
_AST = ast,
|
|
_IS_COMPILER = true,
|
|
_SPECIALS = SPECIALS,
|
|
_VARARG = VARARG,
|
|
-- Expose the module in the compiler
|
|
fennel = module,
|
|
-- Useful for macros and meta programming. All of Fennel can be accessed
|
|
-- via fennel.myfun, for example (fennel.eval "(print 1)").
|
|
list = list,
|
|
sym = sym,
|
|
unpack = unpack,
|
|
gensym = function() return sym(gensym(macroCurrentScope)) end,
|
|
["list?"] = isList,
|
|
["multi-sym?"] = isMultiSym,
|
|
["sym?"] = isSym,
|
|
["table?"] = isTable,
|
|
["sequence?"] = isSequence,
|
|
["varg?"] = isVarg,
|
|
["get-scope"] = function() return macroCurrentScope end,
|
|
["in-scope?"] = function(symbol)
|
|
return macroCurrentScope.manglings[tostring(symbol)]
|
|
end
|
|
}, { __index = _ENV or _G })
|
|
end
|
|
|
|
local function macroGlobals(env, globals)
|
|
local allowed = {}
|
|
for k in pairs(env) do
|
|
local g = globalUnmangling(k)
|
|
table.insert(allowed, g)
|
|
end
|
|
if globals then
|
|
for _, k in pairs(globals) do
|
|
table.insert(allowed, k)
|
|
end
|
|
end
|
|
return allowed
|
|
end
|
|
|
|
local function addMacros(macros, ast, scope)
|
|
assertCompile(isTable(macros), 'expected macros to be table', ast)
|
|
for k, v in pairs(macros) do
|
|
scope.specials[k] = macroToSpecial(v)
|
|
end
|
|
end
|
|
|
|
local function loadMacros(modname, ast, scope, parent)
|
|
local filename = assertCompile(searchModule(modname),
|
|
modname .. " not found.", ast)
|
|
local env = makeCompilerEnv(ast, scope, parent)
|
|
local globals = macroGlobals(env, currentGlobalNames())
|
|
return dofileFennel(filename, { env = env, allowedGlobals = globals,
|
|
scope = COMPILER_SCOPE })
|
|
end
|
|
|
|
SPECIALS['require-macros'] = function(ast, scope, parent)
|
|
assertCompile(#ast == 2, "Expected one module name argument", ast)
|
|
local modname = ast[2]
|
|
if not macroLoaded[modname] then
|
|
macroLoaded[modname] = loadMacros(modname, ast, scope, parent)
|
|
end
|
|
addMacros(macroLoaded[modname], ast, scope, parent)
|
|
end
|
|
|
|
local function evalCompiler(ast, scope, parent)
|
|
local luaSource = compile(ast, { scope = makeScope(COMPILER_SCOPE) })
|
|
local loader = loadCode(luaSource, wrapEnv(makeCompilerEnv(ast, scope, parent)))
|
|
return loader()
|
|
end
|
|
|
|
SPECIALS['macros'] = function(ast, scope, parent)
|
|
assertCompile(#ast == 2, "Expected one table argument", ast)
|
|
local macros = evalCompiler(ast[2], scope, parent)
|
|
addMacros(macros, ast, scope, parent)
|
|
end
|
|
|
|
SPECIALS['eval-compiler'] = function(ast, scope, parent)
|
|
local oldFirst = ast[1]
|
|
ast[1] = sym('do')
|
|
local val = evalCompiler(ast, scope, parent)
|
|
ast[1] = oldFirst
|
|
return val
|
|
end
|
|
|
|
-- Load standard macros
|
|
local stdmacros = [===[
|
|
{"->" (fn [val ...]
|
|
(var x val)
|
|
(each [_ e (ipairs [...])]
|
|
(let [elt (if (list? e) e (list e))]
|
|
(table.insert elt 2 x)
|
|
(set x elt)))
|
|
x)
|
|
"->>" (fn [val ...]
|
|
(var x val)
|
|
(each [_ e (pairs [...])]
|
|
(let [elt (if (list? e) e (list e))]
|
|
(table.insert elt x)
|
|
(set x elt)))
|
|
x)
|
|
"-?>" (fn [val ...]
|
|
(if (= 0 (# [...]))
|
|
val
|
|
(let [els [...]
|
|
e (table.remove els 1)
|
|
el (if (list? e) e (list e))
|
|
tmp (gensym)]
|
|
(table.insert el 2 tmp)
|
|
`(let [@tmp @val]
|
|
(if @tmp
|
|
(-?> @el @(unpack els))
|
|
@tmp)))))
|
|
"-?>>" (fn [val ...]
|
|
(if (= 0 (# [...]))
|
|
val
|
|
(let [els [...]
|
|
e (table.remove els 1)
|
|
el (if (list? e) e (list e))
|
|
tmp (gensym)]
|
|
(table.insert el tmp)
|
|
`(let [@tmp @val]
|
|
(if @tmp
|
|
(-?>> @el @(unpack els))
|
|
@tmp)))))
|
|
:doto (fn [val ...]
|
|
(let [name (gensym)
|
|
form `(let [@name @val])]
|
|
(each [_ elt (pairs [...])]
|
|
(table.insert elt 2 name)
|
|
(table.insert form elt))
|
|
(table.insert form name)
|
|
form))
|
|
:when (fn [condition body1 ...]
|
|
(assert body1 "expected body")
|
|
`(if @condition
|
|
(do @body1 @...)))
|
|
:partial (fn [f ...]
|
|
(let [body (list f ...)]
|
|
(table.insert body _VARARG)
|
|
`(fn [@_VARARG] @body)))
|
|
:lambda (fn [...]
|
|
(let [args [...]
|
|
has-internal-name? (sym? (. args 1))
|
|
arglist (if has-internal-name? (. args 2) (. args 1))
|
|
arity-check-position (if has-internal-name? 3 2)]
|
|
(assert (> (# args) 1) "missing body expression")
|
|
(each [i a (ipairs arglist)]
|
|
(if (and (not (: (tostring a) :match "^?"))
|
|
(~= (tostring a) "..."))
|
|
(table.insert args arity-check-position
|
|
`(assert (~= nil @a)
|
|
(: "Missing argument %s on %s:%s"
|
|
:format @(tostring a)
|
|
@(or a.filename "unknown")
|
|
@(or a.line "?"))))))
|
|
`(fn @(unpack args))))
|
|
:match
|
|
(fn match [val ...]
|
|
;; this function takes the AST of values and a single pattern and returns a
|
|
;; condition to determine if it matches as well as a list of bindings to
|
|
;; introduce for the duration of the body if it does match.
|
|
(fn match-pattern [vals pattern unifications]
|
|
;; we have to assume we're matching against multiple values here until we
|
|
;; know we're either in a multi-valued clause (in which case we know the #
|
|
;; of vals) or we're not, in which case we only care about the first one.
|
|
(let [[val] vals]
|
|
(if (and (sym? pattern) ; unification with outer locals (or nil)
|
|
(or (in-scope? pattern)
|
|
(= :nil (tostring pattern))))
|
|
(values `(= @val @pattern) [])
|
|
;; unify a local we've seen already
|
|
(and (sym? pattern)
|
|
(. unifications (tostring pattern)))
|
|
(values `(= @(. unifications (tostring pattern)) @val) [])
|
|
;; bind a fresh local
|
|
(sym? pattern)
|
|
(do (if (~= (tostring pattern) "_")
|
|
(tset unifications (tostring pattern) val))
|
|
(values (if (: (tostring pattern) :find "^?")
|
|
true `(~= @(sym :nil) @val))
|
|
[pattern val]))
|
|
;; multi-valued patterns (represented as lists)
|
|
(list? pattern)
|
|
(let [condition `(and)
|
|
bindings []]
|
|
(each [i pat (ipairs pattern)]
|
|
(let [(subcondition subbindings) (match-pattern [(. vals i)] pat
|
|
unifications)]
|
|
(table.insert condition subcondition)
|
|
(each [_ b (ipairs subbindings)]
|
|
(table.insert bindings b))))
|
|
(values condition bindings))
|
|
;; table patterns)
|
|
(= (type pattern) :table)
|
|
(let [condition `(and (= (type @val) :table))
|
|
bindings []]
|
|
(each [k pat (pairs pattern)]
|
|
(if (and (sym? pat) (= "&" (tostring pat)))
|
|
(do (assert (not (. pattern (+ k 2)))
|
|
"expected rest argument in final position")
|
|
(table.insert bindings (. pattern (+ k 1)))
|
|
(table.insert bindings [`(select @k ((or unpack table.unpack)
|
|
@val))]))
|
|
(and (= :number (type k))
|
|
(= "&" (tostring (. pattern (- k 1)))))
|
|
nil ; don't process the pattern right after &; already got it
|
|
(let [subval `(. @val @k)
|
|
(subcondition subbindings) (match-pattern [subval] pat
|
|
unifications)]
|
|
(table.insert condition subcondition)
|
|
(each [_ b (ipairs subbindings)]
|
|
(table.insert bindings b)))))
|
|
(values condition bindings))
|
|
;; literal value
|
|
(values `(= @val @pattern) []))))
|
|
(fn match-condition [vals clauses]
|
|
(let [out `(if)]
|
|
(for [i 1 (# clauses) 2]
|
|
(let [pattern (. clauses i)
|
|
body (. clauses (+ i 1))
|
|
(condition bindings) (match-pattern vals pattern {})]
|
|
(table.insert out condition)
|
|
(table.insert out `(let @bindings @body))))
|
|
out))
|
|
;; how many multi-valued clauses are there? return a list of that many gensyms
|
|
(fn val-syms [clauses]
|
|
(let [syms (list (gensym))]
|
|
(for [i 1 (# clauses) 2]
|
|
(if (list? (. clauses i))
|
|
(each [valnum (ipairs (. clauses i))]
|
|
(if (not (. syms valnum))
|
|
(tset syms valnum (gensym))))))
|
|
syms))
|
|
;; wrap it in a way that prevents double-evaluation of the matched value
|
|
(let [clauses [...]
|
|
vals (val-syms clauses)]
|
|
(if (~= 0 (% (# clauses) 2)) ; treat odd final clause as default
|
|
(table.insert clauses (# clauses) (sym :_)))
|
|
;; protect against multiple evaluation of the value, bind against as
|
|
;; many values as we ever match against in the clauses.
|
|
(list (sym :let) [vals val]
|
|
(match-condition vals clauses))))
|
|
}
|
|
]===]
|
|
do
|
|
local env = makeCompilerEnv(nil, COMPILER_SCOPE, {})
|
|
for name, fn in pairs(eval(stdmacros, {
|
|
env = env,
|
|
scope = makeScope(COMPILER_SCOPE),
|
|
-- assume the code to load globals doesn't have any mistaken globals,
|
|
-- otherwise this can be problematic when loading fennel in contexts
|
|
-- where _G is an empty table with an __index metamethod. (openresty)
|
|
allowedGlobals = false,
|
|
})) do
|
|
SPECIALS[name] = macroToSpecial(fn)
|
|
end
|
|
end
|
|
SPECIALS['λ'] = SPECIALS['lambda']
|
|
|
|
return module
|