-
Notifications
You must be signed in to change notification settings - Fork 2
/
candran.can
408 lines (369 loc) · 11.8 KB
/
candran.can
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
local candran = {
VERSION = "1.0.0"
}
package.loaded["candran"] = candran
#import("candran.util")
#import("candran.serpent")
#import("compiler.lua54")
#import("compiler.lua53")
#import("compiler.lua52")
#import("compiler.luajit")
#import("compiler.lua51")
#import("candran.can-parser.scope")
#import("candran.can-parser.validator")
#import("candran.can-parser.pp")
#import("candran.can-parser.parser")
local unpack = unpack or table.unpack
--- Default options.
candran.default = {
target = "lua54",
indentation = "",
newline = "\n",
variablePrefix = "__CAN_",
mapLines = true,
chunkname = "nil",
rewriteErrors = true,
builtInMacros = true,
preprocessorEnv = {},
import = {}
}
-- Autodetect version
if _VERSION == "Lua 5.1" then
if package.loaded.jit then
candran.default.target = "luajit"
else
candran.default.target = "lua51"
end
elseif _VERSION == "Lua 5.2" then
candran.default.target = "lua52"
elseif _VERSION == "Lua 5.3" then
candran.default.target = "lua53"
end
--- Run the preprocessor
-- @tparam input string input code
-- @tparam options table arguments for the preprocessor. They will be inserted into the preprocessor environement.
-- @treturn[1] output string output code
-- @treturn[1] macros registered macros
-- @treturn[2] nil nil if error
-- @treturn[2] error string error message
function candran.preprocess(input, options={}, _env)
options = util.merge(candran.default, options)
local macros = {
functions = {},
variables = {}
}
-- add auto imports
for _, mod in ipairs(options.import) do
input =.. "#import(%q, {loadLocal=false})\n":format(mod)
end
-- generate preprocessor code
local preprocessor = ""
local i = 0
local inLongString = false
local inComment = false
for line in (input.."\n"):gmatch("(.-\n)") do
i += 1
-- Simple multiline comment/string detection
if inComment then
inComment = not line:match("%]%]")
elseif inLongString then
inLongString = not line:match("%]%]")
else
if line:match("[^%-]%[%[") then
inLongString = true
elseif line:match("%-%-%[%[") then
inComment = true
end
end
if not inComment and not inLongString and line:match("^%s*#") and not line:match("^#!") then -- exclude shebang
preprocessor ..= line:gsub("^%s*#", "")
else
local l = line:sub(1, -2)
if not inLongString and options.mapLines and not l:match("%-%- (.-)%:(%d+)$") then
preprocessor ..= ("write(%q)"):format(l .. " -- "..options.chunkname..":" .. i) .. "\n"
else
preprocessor ..= ("write(%q)"):format(line:sub(1, -2)) .. "\n"
end
end
end
preprocessor ..= "return output"
-- make preprocessor environement
local exportenv = {}
local env = util.merge(_G, options.preprocessorEnv)
--- Candran library table
env.candran = candran
--- Current preprocessor output
env.output = ""
--- Import an external Candran/Lua module into the generated file
-- Notable options:
-- * loadLocal (true): true to automatically load the module into a local variable
-- * loadPackage (true): true to automatically load the module into the loaded packages table
-- @tparam modpath string module path
-- @tparam margs table preprocessor options to use when preprocessessing the module
env.import = function(modpath, margs={})
local filepath = assert(util.search(modpath, {"can", "lua"}), "No module named \""..modpath.."\"")
-- open module file
local f = io.open(filepath)
if not f then error("can't open the module file to import") end
margs = util.merge(options, { chunkname = filepath, loadLocal = true, loadPackage = true }, margs)
margs.import = {} -- no need for recursive import
local modcontent, modmacros, modenv = assert(candran.preprocess(f:read("*a"), margs))
macros = util.recmerge(macros, modmacros)
for k, v in pairs(modenv) do env[k] = v end
f:close()
-- get module name (ex: module name of path.to.module is module)
local modname = modpath:match("[^%.]+$")
env.write(
"-- MODULE "..modpath.." --\n"..
"local function _()\n"..
modcontent.."\n"..
"end\n"..
(margs.loadLocal and ("local %s = _() or %s\n"):format(modname, modname) or "").. -- auto require
(margs.loadPackage and ("package.loaded[%q] = %s or true\n"):format(modpath, margs.loadLocal and modname or "_()") or "").. -- add to package.loaded
"-- END OF MODULE "..modpath.." --"
)
end
--- Include another file content in the preprocessor output.
-- @tparam file string filepath
env.include = function(file)
local f = io.open(file)
if not f then error("can't open the file "..file.." to include") end
env.write(f:read("*a"))
f:close()
end
--- Write a line in the preprocessor output.
-- @tparam ... string strings to write (similar to print)
env.write = function(...)
env.output ..= table.concat({...}, "\t") .. "\n"
end
--- Will be replaced with the content of the variable with the given name, if it exists.
-- @tparam name string variable name
env.placeholder = function(name)
if env[name] then
env.write(env[name])
end
end
env.define = function(identifier, replacement)
-- parse identifier
local iast, ierr = parser.parsemacroidentifier(identifier, options.chunkname)
if not iast then
return error("in macro identifier: %s":format(tostring(ierr)))
end
-- parse replacement value
if type(replacement) == "string" then
local rast, rerr = parser.parse(replacement, options.chunkname)
if not rast then
return error("in macro replacement: %s":format(tostring(rerr)))
end
-- when giving a single value as a replacement, bypass the implicit push
if #rast == 1 and rast[1].tag == "Push" and rast[1].implicit then
rast = rast[1][1]
end
replacement = rast
elseif type(replacement) ~= "function" then
error("bad argument #2 to 'define' (string or function expected)")
end
-- add macros
if iast.tag == "MacroFunction" then
macros.functions[iast[1][1]] = { args = iast[2], replacement = replacement }
elseif iast.tag == "Id" then
macros.variables[iast[1]] = replacement
else
error("invalid macro type %s":format(tostring(iast.tag)))
end
end
env.set = function(identifier, value)
exportenv[identifier] = value
env[identifier] = value
end
-- default macros
if options.builtInMacros then
env.define("__STR__(x)", function(x) return ("%q"):format(x) end)
local s = require("candran.serpent")
env.define("__CONSTEXPR__(expr)", function(expr)
return s.block(assert(candran.load(expr))(), {fatal = true})
end)
end
-- compile & load preprocessor
local preprocess, err = candran.compile(preprocessor, options)
if not preprocess then
return nil, "in preprocessor: "..err
end
preprocess, err = util.load(preprocessor, "candran preprocessor", env)
if not preprocess then
return nil, "in preprocessor: "..err
end
-- execute preprocessor
local success, output = pcall(preprocess)
if not success then
return nil, "in preprocessor: "..output
end
return output, macros, exportenv
end
--- Run the compiler
-- @tparam input string input code
-- @tparam options table options for the compiler
-- @tparam macros table defined macros, as returned by the preprocessor
-- @treturn[1] output string output code
-- @treturn[2] nil nil if error
-- @treturn[2] error string error message
function candran.compile(input, options={}, macros)
options = util.merge(candran.default, options)
local ast, errmsg = parser.parse(input, options.chunkname)
if not ast then
return nil, errmsg
end
return require("compiler."..options.target)(input, ast, options, macros)
end
--- Preprocess & compile code
-- @tparam code string input code
-- @tparam options table arguments for the preprocessor and compiler
-- @treturn[1] output string output code
-- @treturn[2] nil nil if error
-- @treturn[2] error string error message
function candran.make(code, options)
local r, err = candran.preprocess(code, options)
if r then
r, err = candran.compile(r, options, err)
if r then
return r
end
end
return r, err
end
local errorRewritingActive = false
local codeCache = {}
--- Candran equivalent to the Lua 5.3's loadfile funtion.
-- Will rewrite errors by default.
function candran.loadfile(filepath, env, options)
local f, err = io.open(filepath)
if not f then
return nil, "cannot open %s":format(tostring(err))
end
local content = f:read("*a")
f:close()
return candran.load(content, filepath, env, options)
end
--- Candran equivalent to the Lua 5.3's load funtion.
-- Will rewrite errors by default.
function candran.load(chunk, chunkname, env, options={})
options = util.merge({ chunkname = tostring(chunkname or chunk) }, options)
local code, err = candran.make(chunk, options)
if not code then
return code, err
end
codeCache[options.chunkname] = code
local f
f, err = util.load(code, "=%s(%s)":format(options.chunkname, "compiled candran"), env)
-- Um. Candran isn't supposed to generate invalid Lua code, so this is a major issue.
-- This is not going to raise an error because this is supposed to behave similarly to Lua's load function.
-- But the error message will likely be useless unless you know how Candran works.
if f == nil then
return f, "candran unexpectedly generated invalid code: "..err
end
if options.rewriteErrors == false then
return f
else
return function(...)
if not errorRewritingActive then
errorRewritingActive = true
local t = { xpcall(f, candran.messageHandler, ...) }
errorRewritingActive = false
if t[1] == false then
error(t[2], 0)
end
return unpack(t, 2)
else
return f(...)
end
end
end
end
--- Candran equivalent to the Lua 5.3's dofile funtion.
-- Will rewrite errors by default.
function candran.dofile(filename, options)
local f, err = candran.loadfile(filename, nil, options)
if f == nil then
error(err)
else
return f()
end
end
--- Candran error message handler.
-- Use it in xpcall to rewrite stacktraces to display Candran source file lines instead of compiled Lua lines.
function candran.messageHandler(message, noTraceback)
message = tostring(message)
if not noTraceback and not message:match("\nstack traceback:\n") then
message = debug.traceback(message, 2)
end
return message:gsub("(\n?%s*)([^\n]-)%:(%d+)%:", function(indentation, source, line)
line = tonumber(line)
local originalFile
local strName = source:match("^(.-)%(compiled candran%)$")
if strName then
if codeCache[strName] then
originalFile = codeCache[strName]
source = strName
end
else
if fi = io.open(source, "r") then
originalFile = fi:read("*a")
fi:close()
end
end
if originalFile then
local i = 0
for l in (originalFile.."\n"):gmatch("([^\n]*)\n") do
i = i +1
if i == line then
local extSource, lineMap = l:match(".*%-%- (.-)%:(%d+)$")
if lineMap then
if extSource ~= source then
return indentation .. extSource .. ":" .. lineMap .. "(" .. extSource .. ":" .. line .. "):"
else
return indentation .. extSource .. ":" .. lineMap .. "(" .. line .. "):"
end
end
break
end
end
end
end)
end
--- Candran package searcher function. Use the existing package.path.
function candran.searcher(modpath)
local filepath = util.search(modpath, {"can"})
if not filepath then
if _VERSION == "Lua 5.4" then
return "no candran file in package.path"
else
return "\n\tno candran file in package.path"
end
end
return (modpath) -- 2nd argument is not passed in Lua 5.1, so a closure is required
local r, s = candran.loadfile(filepath)
if r then
return r(modpath, filepath)
else
error("error loading candran module '%s' from file '%s':\n\t%s":format(modpath, filepath, tostring(s)), 0)
end
end, filepath
end
--- Register the Candran package searcher.
function candran.setup()
local searchers = if _VERSION == "Lua 5.1" then
package.loaders
else
package.searchers
end
-- check if already setup
for _, s in ipairs(searchers) do
if s == candran.searcher then
return candran
end
end
-- setup
table.insert(searchers, 1, candran.searcher)
return candran
end
return candran