mirror of
https://github.com/Reuh/candran.git
synced 2025-10-27 17:59:30 +00:00
Compare commits
22 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d9a79c47d | |||
| 1e118381f8 | |||
| 7131c5c8b1 | |||
| f0cacd6f08 | |||
| 7b0563b9dc | |||
| 56f1901f9b | |||
| b32d4d5600 | |||
| 53f715cc6f | |||
| d4102f1af6 | |||
| 008e7732bc | |||
| 01e808e2e6 | |||
| ff6d7f8feb | |||
| e9ae8e21a3 | |||
| dd22f2de3d | |||
| b72aff807c | |||
| ea54376aa6 | |||
| 496a4ddafd | |||
| a0eda5bc72 | |||
| 3c84d1fbdd | |||
| 584dac17db | |||
| 6a1f745015 | |||
| ebd36d7103 |
18 changed files with 6200 additions and 5082 deletions
111
README.md
111
README.md
|
|
@ -6,12 +6,19 @@ Unlike Moonscript, Candran tries to stay close to the Lua syntax, and existing L
|
|||
|
||||
````lua
|
||||
#import("lib.thing") -- static import
|
||||
#local debug or= false
|
||||
#local DEBUG = false
|
||||
|
||||
#if DEBUG then
|
||||
# define("log(...)", "print(...)") -- macro: calls to log() will be replaced with print() in compiled code
|
||||
#else
|
||||
# define("log(...)", "") -- remove calls to log from the compiled code when DEBUG is true
|
||||
#end
|
||||
log("example macro") -- preprocessor macros
|
||||
|
||||
local function calculate(toadd=25) -- default parameters
|
||||
local result = thing.do()
|
||||
result += toadd
|
||||
#if debug then -- preprocessor conditionals
|
||||
#if DEBUG then -- preprocessor conditionals
|
||||
print("Did something")
|
||||
#end
|
||||
return result
|
||||
|
|
@ -76,7 +83,7 @@ Candran is released under the MIT License (see ```LICENSE``` for details).
|
|||
#### Quick setup
|
||||
Install Candran automatically using LuaRocks: ```sudo luarocks install candran```.
|
||||
|
||||
Or manually install LPegLabel (```luarocks install lpeglabel```, version 1.5 or above), download this repository and use Candran through the scripts in ```bin/``` or use it as a library with the self-contained ```candran.lua```.
|
||||
Or manually install LPegLabel and argparse (```luarocks install lpeglabel```, version 1.5 or above, and ```luarocks install argparse```, version 0.7 or above), download this repository and use Candran through the scripts in ```bin/``` or use it as a library with the self-contained ```candran.lua```.
|
||||
|
||||
You can optionally install lua-linenoise (```luarocks install linenoise```, version 0.9 or above) for an improved REPL, and luacheck (```luarocks install luacheck```, version 0.23.0 or above) to be able to use ```cancheck```. Installing Candran using LuaRocks will install linenoise and luacheck by default.
|
||||
|
||||
|
|
@ -90,6 +97,7 @@ Most editors should be able to use their existing Lua support for Candran code.
|
|||
* [sublime-candran](https://github.com/Reuh/sublime-candran) support the full Candran syntax
|
||||
* [SublimeLinter-cancheck-contrib](https://github.com/Reuh/SublimeLinter-contrib-cancheck) SublimeLinter plugin for Candran using ```cancheck```
|
||||
* [SublimeLinter-candran-contrib](https://github.com/Reuh/SublimeLinter-contrib-candran) SublimeLinter plugin for Candran using ```canc -parse``` (only checks for syntaxic errors, no linting)
|
||||
* **VS Code**: [vscode-candran](https://github.com/Reuh/vscode-candran) basic support for the Candran syntax
|
||||
* **Atom**: [language-candran](https://atom.io/packages/language-candran) support the full Candran syntax
|
||||
|
||||
For linting, if your editor support [luacheck](https://github.com/luarocks/luacheck), you should be able to replace it with ```cancheck``` (in this repository ```bin/cancheck```, or installed automatically if Candran was installed using LuaRocks), which is a wrapper around luacheck that monkey-patch it to support Candran.
|
||||
|
|
@ -442,15 +450,53 @@ Will output ````print("Bonjour")```` or ````print("Hello")```` depending of the
|
|||
The preprocessor has access to the following variables:
|
||||
* ````candran````: the Candran library table.
|
||||
* ````output````: the current preprocessor output string. Can be redefined at any time. If you want to write something in the preprocessor output, it is preferred to use `write(...)` instead of directly modifying `output`.
|
||||
* ````import(module[, [options])````: a function which import a module. This should be equivalent to using _require(module)_ in the Candran code, except the module will be embedded in the current file. _options_ is an optional preprocessor arguments table for the imported module (current preprocessor arguments will be inherited). Options specific to this function:
|
||||
* ````import(module[, [options])````: a function which import a module. This should be equivalent to using _require(module)_ in the Candran code, except the module will be embedded in the current file. Macros and preprocessor constants defined in the imported file (using `define` and `set`) will be made available in the current file. _options_ is an optional preprocessor arguments table for the imported module (current preprocessor arguments will be inherited). Options specific to this function:
|
||||
* ```loadLocal``` (default ```true```): ```true``` to automatically load the module into a local variable (i.e. ```local thing = require("module.thing")```)
|
||||
* ```loadPackage``` (default ```true```): ```true``` to automatically load the module into the loaded packages table (so it will be available for following ```require("module")``` calls).
|
||||
* ````include(filename)````: a function which copy the contents of the file _filename_ to the output.
|
||||
* ````write(...)````: write to the preprocessor output. For example, ````#write("hello()")```` will output ````hello()```` in the final file.
|
||||
* ```placeholder(name)```: if the variable _name_ is defined in the preprocessor environement, its content will be inserted here.
|
||||
* ```define(identifier, replacement)```: define a macro. See below.
|
||||
* ```set(identifier, value)```: set a preprocessor constant.
|
||||
* each arguments passed to the preprocessor is directly available in the environment.
|
||||
* and every standard Lua library.
|
||||
|
||||
#### Macros
|
||||
|
||||
Using `define(identifier, replacement)` in the preprocessor, you can define macros. `identifier` is expected to be string containing Candran/Lua code (representing either a identifier or a function call), and `replacement` can be either a string containing Candran/Lua code or a function.
|
||||
|
||||
There are two types of macros identifiers: variables, which replace every instance of the given identifier with the replacement; and functions, which will replace every call to this function with the replacement, also replacing its arguments. The `...` will be replaced with every remaining argument. Macros can not be recursive.
|
||||
|
||||
If `replacement` is a string, the macro will be replaced with this string, replacing the macros arguments in the string. If `replacement` is a function, the function will be called every time the macro is encoutered, with the macro arguments passed as strings, and is expected to return a string that will be used as a replacement.
|
||||
|
||||
If `replacement` is the empty empty, the macro will simply be removed from the compiled code.
|
||||
|
||||
```lua
|
||||
-- Variable macro
|
||||
#define("x", 42)
|
||||
print(x) -- 42
|
||||
|
||||
-- Function macros
|
||||
#define("f(x)", "print(x)")
|
||||
f(42) -- replaced with print(42)
|
||||
|
||||
#define("log(s, ...)", "print(s..": ", ...)")
|
||||
log("network", "error") -- network: error
|
||||
|
||||
#define("debug()", "")
|
||||
debug() -- not present in complied code
|
||||
|
||||
#define("_assert(what, err)", function(what, err)
|
||||
# return "if "..what.." then error("..err..") end"
|
||||
#end)
|
||||
_assert(5 = 2, "failed") -- replaced with if 5 = 2 then error("failed") end
|
||||
```
|
||||
|
||||
Candran provide some predefined macros by default:
|
||||
* `__STR__(expr)`: returns a string litteral representing the expression (e.g., `__STR__(5 + 2)` expands to `"5 + 2"`)
|
||||
* `__CONSTEXPR__(expr)`: calculate the result of the expression in the preprocessor, and returns a representation of the returned value, i.e. precalculate an expression at compile time
|
||||
You can disable these built-in macros using the `builtInMacros` compiler option.
|
||||
|
||||
Compile targets
|
||||
---------------
|
||||
Candran is based on the Lua 5.4 syntax, but can be compiled to Lua 5.4, Lua 5.3, Lua 5.2, LuaJIT, and Lua 5.1 compatible code.
|
||||
|
|
@ -482,23 +528,41 @@ The library can be used standalone through the ```canc``` (for compiling Candran
|
|||
|
||||
Preprocess and compile each _filename_ Candran files, and creates the assiociated ```.lua``` files in the same directories.
|
||||
|
||||
_options_ is of type ````-somearg -anotherarg thing=somestring other=5````, which will generate a Lua table ```{ somearg = true, anotherarg = true, thing = "somestring", other = 5 }```.
|
||||
_options_ is of type ````--no-map-lines -p --include module -d VAR 5````.
|
||||
|
||||
You can choose to use another directory where files should be written using the ```dest=destinationDirectory``` argument.
|
||||
You can choose to use another directory where files should be written using the `--destination` or `-d` option: ```--destination destinationDirectory```.
|
||||
|
||||
You can choose the output filename using ```out=filename```. By default, compiled files have the same name as their input file, but with a ```.lua``` extension.
|
||||
You can choose the output filename using `--output` or `-o` option: `--output filename`. By default, compiled files have the same name as their input file, but with a ```.lua``` extension.
|
||||
|
||||
```canc``` can write to the standard output instead of creating files using the ```-print``` argument.
|
||||
```canc``` can write to the standard output instead of creating files using the ```--print``` or `-p` argument.
|
||||
|
||||
You can choose to run only the preprocessor or compile using the ```-preprocess``` and ```-compile``` flags.
|
||||
You can choose to run only the preprocessor or compile using the ```--preprocess``` and ```--compile``` flags.
|
||||
|
||||
You can choose to only parse the file and check it for syntaxic errors using the ```-parse``` flag. Errors will be printed to stderr in a similar format to ```luac -p```.
|
||||
You can choose to only parse the file and check it for syntaxic errors using the ```--parse``` flag. Errors will be printed to stderr in a similar format to ```luac -p```.
|
||||
|
||||
The ```-ast``` flag is also available for debugging, and will disable preprocessing, compiling and file writing, and instead directly dump the AST generated from the input file(s) to stdout.
|
||||
The ```--ast``` flag is also available for debugging, and will disable preprocessing, compiling and file writing, and instead directly dump the AST generated from the input file(s) to stdout.
|
||||
|
||||
Instead of providing filenames, you can use ```-``` to read from standard input.
|
||||
|
||||
Use the ```-h``` or ```-help``` option to display a short help text.
|
||||
You can change the compiler target using `--target` or `-t`: `--target luajit`.
|
||||
|
||||
You can change the identation and newline string using `--indentation` and `--newline`: `--identation luajit`.
|
||||
|
||||
You can change Candran's built-in variable prefix using `--variable-prefix`: `--variable-prefix __CAN_`.
|
||||
|
||||
You can disable line mapping (error rewriting will not work) using `--no-map-lines`.
|
||||
|
||||
You can disable built-in macros using `--no-builtin-macros`.
|
||||
|
||||
You can define preprocessor constants using `--define` or `-D`: `--define VAR 5`. `VAR` will be available and set to 5 in the preprocessor. If you specify no value, it defaults to true.
|
||||
|
||||
You can statically import modules using `--import` or `-I`: `--import module`. The module will be imported in compiled files using `#import("module",{loadLocal=false})`.
|
||||
|
||||
You can disable error rewriting using `--no-rewrite-errors`.
|
||||
|
||||
You can change the chunkname using `--chunkname`: `--chunkname filename`. This will change the filenames are reported in errors. By default, try to use the current file name, or stdin when using `-`.
|
||||
|
||||
Use the ```-h``` or ```--help``` option to display the help text.
|
||||
|
||||
Example uses:
|
||||
|
||||
|
|
@ -506,15 +570,15 @@ The library can be used standalone through the ```canc``` (for compiling Candran
|
|||
|
||||
preprocess and compile _foo.can_ and write the result in _foo.lua_.
|
||||
|
||||
* ````canc indentation=" " foo.can````
|
||||
* ````canc --indentation " " foo.can````
|
||||
|
||||
preprocess and compile _foo.can_ with 2-space indentation (readable code!) and write the result in _foo.lua_.
|
||||
|
||||
* ````canc foo.can -verbose -print | lua````
|
||||
* ````canc foo.can -d verbose --print | lua````
|
||||
|
||||
preprocess _foo.can_ with _verbose_ set to _true_, compile it and execute it.
|
||||
preprocess _foo.can_ with _verbose_ set to _true_ in the preprocessor, compile it and execute it.
|
||||
|
||||
* ````canc -parse foo.can````
|
||||
* ````canc --parse foo.can````
|
||||
|
||||
checks foo.can for syntaxic errors.
|
||||
|
||||
|
|
@ -534,7 +598,9 @@ The library can be used standalone through the ```canc``` (for compiling Candran
|
|||
|
||||
Instead of providing a filename, you can use ```-``` to read from standard input.
|
||||
|
||||
Use the ```-h``` or ```-help``` option to display a short help text.
|
||||
Use similar options as `canc`.
|
||||
|
||||
Use the ```-h``` or ```-help``` option to display the help text.
|
||||
|
||||
* ```cancheck```
|
||||
|
||||
|
|
@ -551,7 +617,7 @@ local f = io.open("foo.can") -- read the file foo.can
|
|||
local contents = f:read("*a")
|
||||
f:close()
|
||||
|
||||
local compiled = candran.make(contents, { debug = true }) -- compile foo.can with debug set to true
|
||||
local compiled = candran.make(contents, { DEBUG = true }) -- compile foo.can with DEBUG set to true
|
||||
|
||||
load(compiled)() -- execute!
|
||||
|
||||
|
|
@ -567,8 +633,8 @@ The table returned by _require("candran")_ gives you access to:
|
|||
|
||||
##### Compiler & preprocessor
|
||||
* ````candran.VERSION````: Candran's version string (e.g. `"0.10.0"`).
|
||||
* ````candran.preprocess(code[, options])````: return the Candran code _code_, preprocessed with the _options_ options table; or nil, err in case of error.
|
||||
* ````candran.compile(code[, options])````: return the Candran code compiled to Lua with the _options_ option table; or nil, err in case of error.
|
||||
* ````candran.preprocess(code[, options])````: return the Candran code _code_, `macros` table. The code is preprocessed with the _options_ options table; `macros` is indented to be passed to `candran.compile` to apply the defined macros. In case of error, returns nil, error.
|
||||
* ````candran.compile(code[, options[, macros]])````: return the Candran code compiled to Lua with the _options_ option table and the macros `macros` (table returned by the preprocessor); or nil, err in case of error.
|
||||
* ````candran.make(code[, options])````: return the Candran code, preprocessed and compiled with the _options_ options table; or nil, err in case of error.
|
||||
|
||||
##### Code loading helpers
|
||||
|
|
@ -619,13 +685,16 @@ at the top of your main Lua file. If a Candran file is found when you call ```re
|
|||
You can give arbitrary options to the compiler and preprocessor, but Candran already provide and uses these with their associated default values:
|
||||
|
||||
```lua
|
||||
target = "lua53" -- compiler target. "lua53", "lua52", "luajit" or "lua51" (default is automatically selected based on the Lua version used).
|
||||
target = "lua53" -- compiler target. "lua54", "lua53", "lua52", "luajit" or "lua51" (default is automatically selected based on the Lua version used).
|
||||
indentation = "" -- character(s) used for indentation in the compiled file.
|
||||
newline = "\n" -- character(s) used for newlines in the compiled file.
|
||||
variablePrefix = "__CAN_" -- Prefix used when Candran needs to set a local variable to provide some functionality (example: to load LuaJIT's bit lib when using bitwise operators).
|
||||
mapLines = true -- if true, compiled files will contain comments at the end of each line indicating the associated line and source file. Needed for error rewriting.
|
||||
chunkname = "nil" -- the chunkname used when running code using the helper functions and writing the line origin comments. Candran will try to set it to the original filename if it knows it.
|
||||
rewriteErrors = true -- true to enable error rewriting when loading code using the helper functions. Will wrap the whole code in a xpcall().
|
||||
builtInMacros = true -- false to disable built-in macros __*__
|
||||
preprocessorEnv = {} -- environment to merge with the preprocessor environement
|
||||
import = {} -- list of modules to automatically import in compiled files (using #import("module",{loadLocal=false}))
|
||||
```
|
||||
|
||||
You can change the defaults used for these variables in the table `candran.default`.
|
||||
|
|
|
|||
71
bin/can
71
bin/can
|
|
@ -1,28 +1,31 @@
|
|||
#!/usr/bin/env lua
|
||||
|
||||
local candran = require("candran").setup()
|
||||
local cmdline = require("candran.cmdline")
|
||||
local util = require("candran.util")
|
||||
local argparse = require("argparse")
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local args = cmdline(arg)
|
||||
-- Parse args --
|
||||
|
||||
if args.help or args.h then
|
||||
print("Candran "..candran.VERSION.." interpreter by Reuh")
|
||||
print("Usage: "..arg[0].." [options] filename")
|
||||
print("Specify no options to start the REPL.")
|
||||
print("Use - instead of a filename to read from the standard input.")
|
||||
print("Interpreter options:")
|
||||
print(" -help or -h print this text")
|
||||
print("Default options:")
|
||||
for opt, val in pairs(candran.default) do
|
||||
if type(val) == "string" then val = val:gsub("\n", "\\n") end
|
||||
print((" %s=%q"):format(opt, val))
|
||||
end
|
||||
return
|
||||
end
|
||||
local parser = argparse()
|
||||
:name "can"
|
||||
:description("Candran "..candran.VERSION.." interpreter by Reuh.")
|
||||
:epilog "For more info, see https://github.com/Reuh/candran"
|
||||
|
||||
parser:argument("filename", "Candran file to run. Use - to read from standard input. Start the REPL if no filename given.")
|
||||
:args "?"
|
||||
|
||||
util.cli.addCandranOptions(parser)
|
||||
|
||||
local args = parser:parse()
|
||||
|
||||
local options = util.cli.makeCandranOptions(args)
|
||||
|
||||
-- Run --
|
||||
|
||||
-- stdin
|
||||
if arg[#arg] == "-" then
|
||||
local f, err = candran.load(io.read("*a"), "stdin", nil, args)
|
||||
if args.filename == "-" then
|
||||
local f, err = candran.load(io.read("*a"), "stdin", nil, options)
|
||||
if not f then
|
||||
io.stderr:write("can: "..err.."\n")
|
||||
os.exit(1)
|
||||
|
|
@ -33,8 +36,8 @@ if arg[#arg] == "-" then
|
|||
os.exit(1)
|
||||
end
|
||||
-- file
|
||||
elseif #args >= 1 then
|
||||
local f, err = candran.loadfile(args[1], nil, args)
|
||||
elseif args.filename then
|
||||
local f, err = candran.loadfile(args.filename, nil, options)
|
||||
if not f then
|
||||
io.stderr:write("can: "..err.."\n")
|
||||
os.exit(1)
|
||||
|
|
@ -47,6 +50,8 @@ elseif #args >= 1 then
|
|||
end
|
||||
-- REPL
|
||||
else
|
||||
candran.default = util.merge(candran.default, options)
|
||||
|
||||
-- Setup linenoise
|
||||
local s, l = pcall(require, "linenoise")
|
||||
if not s then -- pure Lua compatibility thingy
|
||||
|
|
@ -113,6 +118,22 @@ else
|
|||
print("Candran " .. candran.VERSION .. ", targeting " .. candran.default.target)
|
||||
candran.setup()
|
||||
|
||||
-- check errors in static import
|
||||
-- note: static imports will be run every line, as preprocessors macros and constants aren't kept between compilations...
|
||||
do
|
||||
local r, e = candran.load("local _", "stdin")
|
||||
if not r then
|
||||
print("In static import: "..e)
|
||||
candran.default.import = {}
|
||||
else
|
||||
r, e = pcall(r)
|
||||
if not r then
|
||||
print("In static import: "..e)
|
||||
candran.default.import = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- REPL loop
|
||||
local multiline = false -- true if wait for another line
|
||||
local buffer
|
||||
|
|
@ -152,15 +173,15 @@ else
|
|||
end
|
||||
|
||||
-- exec
|
||||
local t = { pcall(candran.load, buffer, "stdin") }
|
||||
if t[1] == false then
|
||||
if t[2]:match("expected '[end})]+' to close") then
|
||||
local r, e = candran.load(buffer, "stdin")
|
||||
if not r then
|
||||
if e:match("expected '[end})]+' to close") then
|
||||
multiline = true
|
||||
else
|
||||
print(t[2])
|
||||
print(e)
|
||||
end
|
||||
else
|
||||
t = { pcall(t[2]) }
|
||||
local t = { pcall(r) }
|
||||
if t[1] == false then
|
||||
print(t[2])
|
||||
elseif #t > 1 then
|
||||
|
|
|
|||
85
bin/canc
85
bin/canc
|
|
@ -1,48 +1,63 @@
|
|||
#!/usr/bin/env lua
|
||||
|
||||
local candran = require("candran")
|
||||
local cmdline = require("candran.cmdline")
|
||||
local parse = require("candran.can-parser.parser").parse
|
||||
local pp = require("candran.can-parser.pp")
|
||||
local util = require("candran.util")
|
||||
local argparse = require("argparse")
|
||||
|
||||
local args = cmdline(arg)
|
||||
-- Parse args --
|
||||
|
||||
if #arg < 1 or args.help or args.h then
|
||||
print("Candran "..candran.VERSION.." compiler by Reuh")
|
||||
print("Usage: "..arg[0].." [options] filenames...")
|
||||
print("Use - instead of filenames to read from the standard input. The output file will be named stdin.lua by default.")
|
||||
print("Compiler options:")
|
||||
print(" dest=\"directory\" where compiled files should be written")
|
||||
print(" out=\"name.lua\" output filename. By default, will use the same name as the input file with a .lua extension.")
|
||||
print(" -print write to the standard output instead of creating files")
|
||||
print(" -preprocess only run the preprocessor")
|
||||
print(" -compile only run the compiler")
|
||||
print(" -parse only parse the file and prints errors to stdout")
|
||||
print(" -ast (for debugging purposes) only parse the files and dump the AST to stdout")
|
||||
print(" -help or -h print this text")
|
||||
print("Default options:")
|
||||
for opt, val in pairs(candran.default) do
|
||||
if type(val) == "string" then val = val:gsub("\n", "\\n") end
|
||||
print((" %s=%q"):format(opt, val))
|
||||
end
|
||||
return
|
||||
end
|
||||
local parser = argparse()
|
||||
:name "canc"
|
||||
:description("Candran "..candran.VERSION.." compiler by Reuh.")
|
||||
:epilog "For more info, see https://github.com/Reuh/candran"
|
||||
|
||||
if arg[#arg] == "-" then
|
||||
table.insert(args, io.stdin)
|
||||
end
|
||||
parser:argument("filename", "Candran files to compile. Use - to read from standard input; the output file will then be named stdin.lua by default.")
|
||||
:args "+"
|
||||
|
||||
for _, file in ipairs(args) do
|
||||
parser:group("Output options",
|
||||
parser:option("-d --destination")
|
||||
:description "Where compiled files should be written"
|
||||
:argname "directory",
|
||||
|
||||
parser:option("-o --output")
|
||||
:description "Output filename. (default: same name as the input file with a .lua extension)"
|
||||
:argname "filename",
|
||||
|
||||
parser:flag("-p --print")
|
||||
:description "Write to the standard output instead of creating files",
|
||||
|
||||
parser:flag("--preprocess")
|
||||
:description "Only run the preprocessor",
|
||||
|
||||
parser:flag("--compile")
|
||||
:description "Only run the compiler",
|
||||
|
||||
parser:flag("--parse")
|
||||
:description "Only parse the file and prints syntax errors to stdout",
|
||||
|
||||
parser:flag("--ast")
|
||||
:description"(for debugging purposes) Only parse the files and dump the AST to stdout"
|
||||
)
|
||||
|
||||
util.cli.addCandranOptions(parser)
|
||||
|
||||
local args = parser:parse()
|
||||
|
||||
-- Compile --
|
||||
|
||||
for _, file in ipairs(args.filename) do
|
||||
-- Read
|
||||
local dest, input
|
||||
if file == io.stdin then
|
||||
dest = args.out or "stdin.lua"
|
||||
if file == "-" then
|
||||
dest = args.output or "stdin.lua"
|
||||
|
||||
input = io.read("*a")
|
||||
|
||||
args.chunkname = "stdin"
|
||||
else
|
||||
dest = args.out or (file:gsub("%.can$", "")..".lua")
|
||||
dest = args.output or (file:gsub("%.can$", "")..".lua")
|
||||
|
||||
local inputFile, err = io.open(file, "r")
|
||||
if not inputFile then
|
||||
|
|
@ -69,8 +84,10 @@ for _, file in ipairs(args) do
|
|||
end
|
||||
|
||||
-- Compile and output
|
||||
if args.dest then
|
||||
dest = args.dest .. "/" .. dest
|
||||
local options = util.cli.makeCandranOptions(args)
|
||||
|
||||
if args.destination then
|
||||
dest = args.destination .. "/" .. dest
|
||||
end
|
||||
|
||||
if not args.print then
|
||||
|
|
@ -79,7 +96,7 @@ for _, file in ipairs(args) do
|
|||
|
||||
local out = input
|
||||
if args.preprocess then
|
||||
local r, err = candran.preprocess(out, args)
|
||||
local r, err = candran.preprocess(out, options)
|
||||
if not r then
|
||||
io.stderr:write("canc: "..err.."\n")
|
||||
os.exit(1)
|
||||
|
|
@ -87,7 +104,7 @@ for _, file in ipairs(args) do
|
|||
out = r
|
||||
end
|
||||
if args.compile then
|
||||
local r, err = candran.compile(out, args)
|
||||
local r, err = candran.compile(out, options)
|
||||
if not r then
|
||||
io.stderr:write("canc: "..err.."\n")
|
||||
os.exit(1)
|
||||
|
|
@ -95,7 +112,7 @@ for _, file in ipairs(args) do
|
|||
out = r
|
||||
end
|
||||
if args.compile == nil and args.preprocess == nil then
|
||||
local r, err = candran.make(input, args)
|
||||
local r, err = candran.make(input, options)
|
||||
if not r then
|
||||
io.stderr:write("canc: "..err.."\n")
|
||||
os.exit(1)
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ local oldRunner = runner.new
|
|||
function runner.new(opts)
|
||||
-- Disable max line length checking (it is compiled code...)
|
||||
opts.max_line_length = false
|
||||
opts.ignore = { "4[23]1/__CAN_.*" }
|
||||
return oldRunner(opts)
|
||||
end
|
||||
|
||||
|
|
|
|||
93
candran.can
93
candran.can
|
|
@ -1,5 +1,10 @@
|
|||
local candran = {
|
||||
VERSION = "1.0.0"
|
||||
}
|
||||
package.loaded["candran"] = candran
|
||||
|
||||
#import("candran.util")
|
||||
#import("candran.cmdline")
|
||||
#import("candran.serpent")
|
||||
|
||||
#import("compiler.lua54")
|
||||
#import("compiler.lua53")
|
||||
|
|
@ -14,11 +19,6 @@
|
|||
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local candran = {
|
||||
VERSION = "0.14.0"
|
||||
}
|
||||
package.loaded["candran"] = candran
|
||||
|
||||
--- Default options.
|
||||
candran.default = {
|
||||
target = "lua54",
|
||||
|
|
@ -27,7 +27,10 @@ candran.default = {
|
|||
variablePrefix = "__CAN_",
|
||||
mapLines = true,
|
||||
chunkname = "nil",
|
||||
rewriteErrors = true
|
||||
rewriteErrors = true,
|
||||
builtInMacros = true,
|
||||
preprocessorEnv = {},
|
||||
import = {}
|
||||
}
|
||||
|
||||
-- Autodetect version
|
||||
|
|
@ -47,10 +50,20 @@ end
|
|||
-- @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={})
|
||||
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 = ""
|
||||
|
|
@ -85,7 +98,8 @@ function candran.preprocess(input, options={})
|
|||
preprocessor ..= "return output"
|
||||
|
||||
-- make preprocessor environement
|
||||
local env = util.merge(_G, options)
|
||||
local exportenv = {}
|
||||
local env = util.merge(_G, options.preprocessorEnv)
|
||||
--- Candran library table
|
||||
env.candran = candran
|
||||
--- Current preprocessor output
|
||||
|
|
@ -104,7 +118,10 @@ function candran.preprocess(input, options={})
|
|||
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)
|
||||
local modcontent = assert(candran.preprocess(f:read("*a"), 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)
|
||||
|
|
@ -140,6 +157,48 @@ function candran.preprocess(input, options={})
|
|||
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)
|
||||
|
|
@ -158,16 +217,17 @@ function candran.preprocess(input, options={})
|
|||
return nil, "in preprocessor: "..output
|
||||
end
|
||||
|
||||
return output
|
||||
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={})
|
||||
function candran.compile(input, options={}, macros)
|
||||
options = util.merge(candran.default, options)
|
||||
|
||||
local ast, errmsg = parser.parse(input, options.chunkname)
|
||||
|
|
@ -176,7 +236,7 @@ function candran.compile(input, options={})
|
|||
return nil, errmsg
|
||||
end
|
||||
|
||||
return require("compiler."..options.target)(input, ast, options)
|
||||
return require("compiler."..options.target)(input, ast, options, macros)
|
||||
end
|
||||
|
||||
--- Preprocess & compile code
|
||||
|
|
@ -188,7 +248,7 @@ end
|
|||
function candran.make(code, options)
|
||||
local r, err = candran.preprocess(code, options)
|
||||
if r then
|
||||
r, err = candran.compile(r, options)
|
||||
r, err = candran.compile(r, options, err)
|
||||
if r then
|
||||
return r
|
||||
end
|
||||
|
|
@ -203,7 +263,7 @@ local codeCache = {}
|
|||
function candran.loadfile(filepath, env, options)
|
||||
local f, err = io.open(filepath)
|
||||
if not f then
|
||||
return nil, "cannot open %s":format(err)
|
||||
return nil, "cannot open %s":format(tostring(err))
|
||||
end
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
|
|
@ -266,6 +326,7 @@ 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
|
||||
|
|
@ -321,7 +382,7 @@ function candran.searcher(modpath)
|
|||
if r then
|
||||
return r(modpath, filepath)
|
||||
else
|
||||
error("error loading candran module '%s' from file '%s':\n\t%s":format(modpath, filepath, s), 0)
|
||||
error("error loading candran module '%s' from file '%s':\n\t%s":format(modpath, filepath, tostring(s)), 0)
|
||||
end
|
||||
end, filepath
|
||||
end
|
||||
|
|
|
|||
10365
candran.lua
10365
candran.lua
File diff suppressed because it is too large
Load diff
|
|
@ -306,6 +306,11 @@ local function fixShortFunc (t)
|
|||
return t
|
||||
end
|
||||
|
||||
local function markImplicit (t)
|
||||
t.implicit = true
|
||||
return t
|
||||
end
|
||||
|
||||
local function statToExpr (t) -- tag a StatExpr
|
||||
t.tag = t.tag .. "Expr"
|
||||
return t
|
||||
|
|
@ -577,7 +582,7 @@ local G = { V"Lua",
|
|||
RetStat = tagC("Return", kw("return") * commaSep(V"Expr", "RetList")^-1);
|
||||
|
||||
PushStat = tagC("Push", kw("push") * commaSep(V"Expr", "RetList")^-1);
|
||||
ImplicitPushStat = tagC("Push", commaSep(V"Expr", "RetList"));
|
||||
ImplicitPushStat = tagC("Push", commaSep(V"Expr", "RetList")) / markImplicit;
|
||||
|
||||
NameList = tagC("NameList", commaSep(V"Id"));
|
||||
DestructuringNameList = tagC("NameList", commaSep(V"DestructuringId")),
|
||||
|
|
@ -757,6 +762,20 @@ local G = { V"Lua",
|
|||
BinOp = V"OrOp" + V"AndOp" + V"BOrOp" + V"BXorOp" + V"BAndOp" + V"ShiftOp" + V"ConcatOp" + V"AddOp" + V"MulOp" + V"PowOp";
|
||||
}
|
||||
|
||||
-- used to parse macro indentifier in define() preprocessor function
|
||||
local macroidentifier = {
|
||||
expect(V"MacroIdentifier", "InvalidStat") * expect(P(-1), "Extra"),
|
||||
|
||||
MacroIdentifier = tagC("MacroFunction", V"Id" * sym("(") * V"MacroFunctionArgs" * expect(sym(")"), "CParenPList"))
|
||||
+ V"Id";
|
||||
|
||||
MacroFunctionArgs = V"NameList" * (sym(",") * expect(tagC("Dots", sym("...")), "ParList"))^-1 / addDots
|
||||
+ Ct(tagC("Dots", sym("...")))
|
||||
+ Ct(Cc());
|
||||
|
||||
}
|
||||
for k,v in pairs(G) do if macroidentifier[k] == nil then macroidentifier[k] = v end end -- copy other rules from main syntax
|
||||
|
||||
local parser = {}
|
||||
|
||||
local validator = require("candran.can-parser.validator")
|
||||
|
|
@ -774,4 +793,15 @@ function parser.parse (subject, filename)
|
|||
return validate(ast, errorinfo)
|
||||
end
|
||||
|
||||
function parser.parsemacroidentifier (subject, filename)
|
||||
local errorinfo = { subject = subject, filename = filename }
|
||||
lpeg.setmaxstack(1000)
|
||||
local ast, label, errpos = lpeg.match(macroidentifier, subject, nil, errorinfo)
|
||||
if not ast then
|
||||
local errmsg = labels[label][2]
|
||||
return ast, syntaxerror(errorinfo, errpos, errmsg)
|
||||
end
|
||||
return ast
|
||||
end
|
||||
|
||||
return parser
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
-- started: 2008-04-12 by Shmuel Zeigerman
|
||||
-- license: public domain
|
||||
|
||||
local ipairs,pairs,setfenv,tonumber,loadstring,type =
|
||||
ipairs,pairs,setfenv,tonumber,loadstring,type
|
||||
local tinsert, tconcat = table.insert, table.concat
|
||||
|
||||
local function commonerror (msg)
|
||||
return nil, ("[cmdline]: " .. msg)
|
||||
end
|
||||
|
||||
local function argerror (msg, numarg)
|
||||
msg = msg and (": " .. msg) or ""
|
||||
return nil, ("[cmdline]: bad argument #" .. numarg .. msg)
|
||||
end
|
||||
|
||||
local function iderror (numarg)
|
||||
return argerror("ID not valid", numarg)
|
||||
end
|
||||
|
||||
local function idcheck (id)
|
||||
return id:match("^[%a_][%w_]*$") and true
|
||||
end
|
||||
|
||||
--[[------------------------------------------------------------------------
|
||||
Syntax:
|
||||
t_out = getparam(t_in [,options] [,params])
|
||||
|
||||
Parameters:
|
||||
t_in: table - list of string arguments to be processed in order
|
||||
(usually it is the `arg' table created by the Lua interpreter).
|
||||
|
||||
* if an argument begins with $, the $ is skipped and the rest is inserted
|
||||
into the array part of the output table.
|
||||
|
||||
* if an argument begins with -, the rest is a sequence of variables
|
||||
(separated by commas or semicolons) that are all set to true;
|
||||
example: -var1,var2 --> var1,var2 = true,true
|
||||
|
||||
* if an argument begins with !, the rest is a Lua chunk;
|
||||
example: !a=(40+3)*5;b=20;name="John";window={w=600,h=480}
|
||||
|
||||
* if an argument contains =, then it is an assignment in the form
|
||||
var1,...=value (no space is allowed around the =)
|
||||
* if value begins with $, the $ is skipped, the rest is a string
|
||||
example: var1,var2=$ --> var1,var2 = "",""
|
||||
example: var1,var2=$125 --> var1,var2 = "125","125"
|
||||
example: var1,var2=$$125 --> var1,var2 = "$125","$125"
|
||||
* if value is convertible to number, it is a number
|
||||
example: var1,var2=125 --> var1,var2 = 125,125
|
||||
* if value is true of false, it is a boolean
|
||||
example: var1=false --> var1 = false
|
||||
* otherwise it is a string
|
||||
example: name=John --> name = "John"
|
||||
|
||||
* if an argument neither begins with one of the special characters (-,!,$),
|
||||
nor contains =, it is inserted as is into the array part of the output
|
||||
table.
|
||||
|
||||
options (optional): a list of names of all command-line options and parameters
|
||||
permitted in the application; used to check that each found option
|
||||
is valid; no checks are done if not supplied.
|
||||
|
||||
params (optional): a list of names of all command-line parameters required
|
||||
by the application; used to check that each required parameter is present;
|
||||
no checks are done if not supplied.
|
||||
|
||||
Returns:
|
||||
On success: the output table, e.g. { [1]="./myfile.txt", name="John", age=40 }
|
||||
On error: nil followed by error message string.
|
||||
|
||||
--]]------------------------------------------------------------------------
|
||||
return function(t_in, options, params)
|
||||
local t_out = {}
|
||||
for i,v in ipairs(t_in) do
|
||||
local prefix, command = v:sub(1,1), v:sub(2)
|
||||
if prefix == "$" then
|
||||
tinsert(t_out, command)
|
||||
elseif prefix == "-" then
|
||||
for id in command:gmatch"[^,;]+" do
|
||||
if not idcheck(id) then return iderror(i) end
|
||||
t_out[id] = true
|
||||
end
|
||||
elseif prefix == "!" then
|
||||
local f, err = loadstring(command)
|
||||
if not f then return argerror(err, i) end
|
||||
setfenv(f, t_out)()
|
||||
elseif v:find("=") then
|
||||
local ids, val = v:match("^([^=]+)%=(.*)") -- no space around =
|
||||
if not ids then return argerror("invalid assignment syntax", i) end
|
||||
if val == "false" then
|
||||
val = false
|
||||
elseif val == "true" then
|
||||
val = true
|
||||
else
|
||||
val = val:sub(1,1)=="$" and val:sub(2) or tonumber(val) or val
|
||||
end
|
||||
for id in ids:gmatch"[^,;]+" do
|
||||
if not idcheck(id) then return iderror(i) end
|
||||
t_out[id] = val
|
||||
end
|
||||
else
|
||||
tinsert(t_out, v)
|
||||
end
|
||||
end
|
||||
if options then
|
||||
local lookup, unknown = {}, {}
|
||||
for _,v in ipairs(options) do lookup[v] = true end
|
||||
for k,_ in pairs(t_out) do
|
||||
if lookup[k]==nil and type(k)=="string" then tinsert(unknown, k) end
|
||||
end
|
||||
if #unknown > 0 then
|
||||
return commonerror("unknown options: " .. tconcat(unknown, ", "))
|
||||
end
|
||||
end
|
||||
if params then
|
||||
local missing = {}
|
||||
for _,v in ipairs(params) do
|
||||
if t_out[v]==nil then tinsert(missing, v) end
|
||||
end
|
||||
if #missing > 0 then
|
||||
return commonerror("missing parameters: " .. tconcat(missing, ", "))
|
||||
end
|
||||
end
|
||||
return t_out
|
||||
end
|
||||
163
candran/serpent.lua
Normal file
163
candran/serpent.lua
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
--[[
|
||||
Serpent source is released under the MIT License
|
||||
|
||||
Copyright (c) 2012-2018 Paul Kulchenko (paul@kulchenko.com)
|
||||
|
||||
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.
|
||||
]]
|
||||
local n, v = "serpent", "0.302" -- (C) 2012-18 Paul Kulchenko; MIT License
|
||||
local c, d = "Paul Kulchenko", "Lua serializer and pretty printer"
|
||||
local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
|
||||
local badtype = {thread = true, userdata = true, cdata = true}
|
||||
local getmetatable = debug and debug.getmetatable or getmetatable
|
||||
local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+
|
||||
local keyword, globals, G = {}, {}, (_G or _ENV)
|
||||
for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
|
||||
'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
|
||||
'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
|
||||
for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
|
||||
for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do
|
||||
for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end
|
||||
|
||||
local function s(t, opts)
|
||||
local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum
|
||||
local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
|
||||
local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
|
||||
local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring
|
||||
local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge)
|
||||
local numformat = opts.numformat or "%.17g"
|
||||
local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0
|
||||
local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)",
|
||||
-- tostring(val) is needed because __tostring may return a non-string value
|
||||
function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end
|
||||
local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s))
|
||||
or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
|
||||
or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end
|
||||
local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end
|
||||
local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
|
||||
and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end
|
||||
local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
|
||||
local n = name == nil and '' or name
|
||||
local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
|
||||
local safe = plain and n or '['..safestr(n)..']'
|
||||
return (path or '')..(plain and path and '.' or '')..safe, safe end
|
||||
local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding
|
||||
local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
|
||||
local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end
|
||||
table.sort(k, function(a,b)
|
||||
-- sort numeric keys first: k[key] is not nil for numerical keys
|
||||
return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
|
||||
< (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
|
||||
local function val2str(t, name, indent, insref, path, plainindex, level)
|
||||
local ttype, level, mt = type(t), (level or 0), getmetatable(t)
|
||||
local spath, sname = safename(path, name)
|
||||
local tag = plainindex and
|
||||
((type(name) == "number") and '' or name..space..'='..space) or
|
||||
(name ~= nil and sname..space..'='..space or '')
|
||||
if seen[t] then -- already seen this element
|
||||
sref[#sref+1] = spath..space..'='..space..seen[t]
|
||||
return tag..'nil'..comment('ref', level) end
|
||||
-- protect from those cases where __tostring may fail
|
||||
if type(mt) == 'table' and metatostring ~= false then
|
||||
local to, tr = pcall(function() return mt.__tostring(t) end)
|
||||
local so, sr = pcall(function() return mt.__serialize(t) end)
|
||||
if (to or so) then -- knows how to serialize itself
|
||||
seen[t] = insref or spath
|
||||
t = so and sr or tr
|
||||
ttype = type(t)
|
||||
end -- new value falls through to be serialized
|
||||
end
|
||||
if ttype == "table" then
|
||||
if level >= maxl then return tag..'{}'..comment('maxlvl', level) end
|
||||
seen[t] = insref or spath
|
||||
if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
|
||||
if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end
|
||||
local maxn, o, out = math.min(#t, maxnum or #t), {}, {}
|
||||
for key = 1, maxn do o[key] = key end
|
||||
if not maxnum or #o < maxnum then
|
||||
local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables
|
||||
for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end
|
||||
if maxnum and #o > maxnum then o[maxnum+1] = nil end
|
||||
if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end
|
||||
local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output)
|
||||
for n, key in ipairs(o) do
|
||||
local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
|
||||
if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing
|
||||
or opts.keyallow and not opts.keyallow[key]
|
||||
or opts.keyignore and opts.keyignore[key]
|
||||
or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types
|
||||
or sparse and value == nil then -- skipping nils; do nothing
|
||||
elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then
|
||||
if not seen[key] and not globals[key] then
|
||||
sref[#sref+1] = 'placeholder'
|
||||
local sname = safename(iname, gensym(key)) -- iname is table for local variables
|
||||
sref[#sref] = val2str(key,sname,indent,sname,iname,true) end
|
||||
sref[#sref+1] = 'placeholder'
|
||||
local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']'
|
||||
sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path))
|
||||
else
|
||||
out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1)
|
||||
if maxlen then
|
||||
maxlen = maxlen - #out[#out]
|
||||
if maxlen < 0 then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
local prefix = string.rep(indent or '', level)
|
||||
local head = indent and '{\n'..prefix..indent or '{'
|
||||
local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
|
||||
local tail = indent and "\n"..prefix..'}' or '}'
|
||||
return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level)
|
||||
elseif badtype[ttype] then
|
||||
seen[t] = insref or spath
|
||||
return tag..globerr(t, level)
|
||||
elseif ttype == 'function' then
|
||||
seen[t] = insref or spath
|
||||
if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end
|
||||
local ok, res = pcall(string.dump, t)
|
||||
local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level)
|
||||
return tag..(func or globerr(t, level))
|
||||
else return tag..safestr(t) end -- handle all other types
|
||||
end
|
||||
local sepr = indent and "\n" or ";"..space
|
||||
local body = val2str(t, name, indent) -- this call also populates sref
|
||||
local tail = #sref>1 and table.concat(sref, sepr)..sepr or ''
|
||||
local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or ''
|
||||
return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end"
|
||||
end
|
||||
|
||||
local function deserialize(data, opts)
|
||||
local env = (opts and opts.safe == false) and G
|
||||
or setmetatable({}, {
|
||||
__index = function(t,k) return t end,
|
||||
__call = function(t,...) error("cannot call functions") end
|
||||
})
|
||||
local f, res = (loadstring or load)('return '..data, nil, nil, env)
|
||||
if not f then f, res = (loadstring or load)(data, nil, nil, env) end
|
||||
if not f then return f, res end
|
||||
if setfenv then setfenv(f, env) end
|
||||
return pcall(f)
|
||||
end
|
||||
|
||||
local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
|
||||
return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
|
||||
load = deserialize,
|
||||
dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,
|
||||
line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,
|
||||
block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end }
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
local candran = require("candran")
|
||||
local util = {}
|
||||
|
||||
function util.search(modpath, exts={})
|
||||
|
|
@ -27,6 +28,20 @@ function util.load(str, name, env)
|
|||
end
|
||||
end
|
||||
|
||||
function util.recmerge(...)
|
||||
local r = {}
|
||||
for _, t in ipairs({...}) do
|
||||
for k, v in pairs(t) do
|
||||
if type(v) == "table" then
|
||||
r[k] = util.merge(v, r[k])
|
||||
else
|
||||
r[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
function util.merge(...)
|
||||
local r = {}
|
||||
for _, t in ipairs({...}) do
|
||||
|
|
@ -37,4 +52,74 @@ function util.merge(...)
|
|||
return r
|
||||
end
|
||||
|
||||
util.cli = {
|
||||
-- add option to set Candran options to an argparse parser
|
||||
addCandranOptions = function(parser)
|
||||
parser:group("Compiler options",
|
||||
parser:option("-t --target")
|
||||
:description "Target Lua version: lua54, lua53, lua52, luajit or lua51"
|
||||
:default(candran.default.target),
|
||||
|
||||
parser:option("--indentation")
|
||||
:description "Character(s) used for indentation in the compiled file"
|
||||
:default(candran.default.indentation),
|
||||
|
||||
parser:option("--newline")
|
||||
:description "Character(s) used for newlines in the compiled file"
|
||||
:default(candran.default.newline),
|
||||
|
||||
parser:option("--variable-prefix")
|
||||
:description "Prefix used when Candran needs to set a local variable to provide some functionality"
|
||||
:default(candran.default.variablePrefix),
|
||||
|
||||
parser:flag("--no-map-lines")
|
||||
:description "Do not add comments at the end of each line indicating the associated source line and file (error rewriting will not work)"
|
||||
)
|
||||
|
||||
parser:group("Preprocessor options",
|
||||
parser:flag("--no-builtin-macros")
|
||||
:description "Disable built-in macros",
|
||||
|
||||
parser:option("-D --define")
|
||||
:description "Define a preprocessor constant"
|
||||
:args("1-2")
|
||||
:argname{"name", "value"}
|
||||
:count("*"),
|
||||
|
||||
parser:option("-I --import")
|
||||
:description "Statically import a module into the compiled file"
|
||||
:argname("module")
|
||||
:count("*")
|
||||
)
|
||||
|
||||
parser:option("--chunkname")
|
||||
:description "Chunkname used when running the code"
|
||||
|
||||
parser:flag("--no-rewrite-errors")
|
||||
:description "Disable error rewriting when running the code"
|
||||
end,
|
||||
|
||||
-- convert parsed arguments to a Candran options table
|
||||
makeCandranOptions = function(args)
|
||||
local preprocessorEnv = {}
|
||||
for _, o in ipairs(args.define) do
|
||||
preprocessorEnv[o[1]] = tonumber(o[2]) or o[2] or true
|
||||
end
|
||||
|
||||
local options = {
|
||||
target = args.target,
|
||||
indentation = args.indentation,
|
||||
newline = args.newline,
|
||||
variablePrefix = args.variable_prefix,
|
||||
mapLines = not args.no_map_lines,
|
||||
chunkname = args.chunkname,
|
||||
rewriteErrors = not args.no_rewrite_errors,
|
||||
builtInMacros = not args.no_builtin_macros,
|
||||
preprocessorEnv = preprocessorEnv,
|
||||
import = args.import
|
||||
}
|
||||
return options
|
||||
end
|
||||
}
|
||||
|
||||
return util
|
||||
|
|
|
|||
|
|
@ -33,6 +33,6 @@ end
|
|||
|
||||
#local patch = output
|
||||
#output = ""
|
||||
#import("compiler.luajit", { patch = patch, loadPackage = false })
|
||||
#import("compiler.luajit", { preprocessorEnv = { patch = patch }, loadPackage = false })
|
||||
|
||||
return luajit
|
||||
|
|
|
|||
|
|
@ -30,6 +30,6 @@ end
|
|||
|
||||
#local patch = output
|
||||
#output = ""
|
||||
#import("compiler.lua53", { patch = patch, loadPackage = false })
|
||||
#import("compiler.lua53", { preprocessorEnv = { patch = patch }, loadPackage = false })
|
||||
|
||||
return lua53
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ end
|
|||
|
||||
#local patch = output
|
||||
#output = ""
|
||||
#import("compiler.lua54", { patch = patch, loadPackage = false })
|
||||
#import("compiler.lua54", { preprocessorEnv = { patch = patch }, loadPackage = false })
|
||||
|
||||
return lua54
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
local util = require("candran.util")
|
||||
|
||||
local targetName = "Lua 5.4"
|
||||
|
||||
return function(code, ast, options)
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
return function(code, ast, options, macros={functions={}, variables={}})
|
||||
--- Line mapping
|
||||
local lastInputPos = 1 -- last token position in the input code
|
||||
local prevLinePos = 1 -- last token position in the previous line of code in the input code
|
||||
|
|
@ -47,7 +51,8 @@ return function(code, ast, options)
|
|||
local states = {
|
||||
push = {}, -- push stack variable names
|
||||
destructuring = {}, -- list of variable that need to be assigned from a destructure {id = "parent variable", "field1", "field2"...}
|
||||
scope = {} -- list of variables defined in the current scope
|
||||
scope = {}, -- list of variables defined in the current scope
|
||||
macroargs = {} -- currently defined arguemnts from a macro function
|
||||
}
|
||||
-- Push a new value on top of the stack "name". Returns an empty string for chaining.
|
||||
local function push(name, state)
|
||||
|
|
@ -83,6 +88,9 @@ return function(code, ast, options)
|
|||
return var
|
||||
end
|
||||
|
||||
-- indicate if currently processing a macro, so it cannot be applied recursively
|
||||
local nomacro = { variables = {}, functions = {} }
|
||||
|
||||
--- Module management
|
||||
local required = {} -- { ["full require expression"] = true, ... }
|
||||
local requireStr = ""
|
||||
|
|
@ -536,7 +544,15 @@ return function(code, ast, options)
|
|||
end,
|
||||
-- Dots
|
||||
Dots = ()
|
||||
return "..."
|
||||
local macroargs = peek("macroargs")
|
||||
if macroargs and not nomacro.variables["..."] and macroargs["..."] then
|
||||
nomacro.variables["..."] = true
|
||||
local r = lua(macroargs["..."], "_lhs")
|
||||
nomacro.variables["..."] = nil
|
||||
return r
|
||||
else
|
||||
return "..."
|
||||
end
|
||||
end,
|
||||
-- Boolean{ <boolean> }
|
||||
Boolean = (t)
|
||||
|
|
@ -723,6 +739,37 @@ return function(code, ast, options)
|
|||
Call = (t)
|
||||
if t[1].tag == "String" or t[1].tag == "Table" then
|
||||
return "("..lua(t[1])..")("..lua(t, "_lhs", 2)..")"
|
||||
elseif t[1].tag == "Id" and not nomacro.functions[t[1][1]] and macros.functions[t[1][1]] then
|
||||
local macro = macros.functions[t[1][1]]
|
||||
local replacement = macro.replacement
|
||||
local r
|
||||
nomacro.functions[t[1][1]] = true
|
||||
if type(replacement) == "function" then
|
||||
local args = {}
|
||||
for i=2, #t do
|
||||
table.insert(args, lua(t[i]))
|
||||
end
|
||||
r = replacement(unpack(args))
|
||||
else
|
||||
local macroargs = util.merge(peek("macroargs"))
|
||||
for i, arg in ipairs(macro.args) do
|
||||
if arg.tag == "Dots" then
|
||||
macroargs["..."] = [for j=i+1, #t do t[j] end]
|
||||
elseif arg.tag == "Id" then
|
||||
if t[i+1] == nil then
|
||||
error("bad argument #%s to macro %s (value expected)":format(i, t[1][1]))
|
||||
end
|
||||
macroargs[arg[1]] = t[i+1]
|
||||
else
|
||||
error("unexpected argument type %s in macro %s":format(arg.tag, t[1][1]))
|
||||
end
|
||||
end
|
||||
push("macroargs", macroargs)
|
||||
r = lua(replacement)
|
||||
pop("macroargs")
|
||||
end
|
||||
nomacro.functions[t[1][1]] = nil
|
||||
return r
|
||||
elseif t[1].tag == "MethodStub" then -- method call
|
||||
if t[1][1].tag == "String" or t[1][1].tag == "Table" then
|
||||
return "("..lua(t[1][1]).."):"..lua(t[1][2], "Id").."("..lua(t, "_lhs", 2)..")"
|
||||
|
|
@ -757,7 +804,23 @@ return function(code, ast, options)
|
|||
end,
|
||||
-- Id{ <string> }
|
||||
Id = (t)
|
||||
return t[1]
|
||||
local r = t[1]
|
||||
local macroargs = peek("macroargs")
|
||||
if not nomacro.variables[t[1]] then
|
||||
nomacro.variables[t[1]] = true
|
||||
if macroargs and macroargs[t[1]] then -- replace with macro argument
|
||||
r = lua(macroargs[t[1]])
|
||||
elseif macros.variables[t[1]] ~= nil then -- replace with macro variable
|
||||
local macro = macros.variables[t[1]]
|
||||
if type(macro) == "function" then
|
||||
r = macro()
|
||||
else
|
||||
r = lua(macro)
|
||||
end
|
||||
end
|
||||
nomacro.variables[t[1]] = nil
|
||||
end
|
||||
return r
|
||||
end,
|
||||
-- AttributeId{ <string> <string>? }
|
||||
AttributeId = (t)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,6 @@ end
|
|||
|
||||
#local patch = output
|
||||
#output = ""
|
||||
#import("compiler.lua52", { patch = patch, loadPackage = false })
|
||||
#import("compiler.lua52", { preprocessorEnv = { patch = patch }, loadPackage = false })
|
||||
|
||||
return lua52
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ rockspec_format = "3.0"
|
|||
|
||||
package = "candran"
|
||||
|
||||
version = "0.14.0-1"
|
||||
version = "1.0.0-1"
|
||||
|
||||
description = {
|
||||
summary = "A simple Lua dialect and preprocessor.",
|
||||
|
|
@ -19,14 +19,15 @@ description = {
|
|||
|
||||
source = {
|
||||
url = "git://github.com/Reuh/candran",
|
||||
tag = "v0.14.0"
|
||||
tag = "v1.0.0"
|
||||
}
|
||||
|
||||
dependencies = {
|
||||
"lua >= 5.1",
|
||||
"lpeglabel >= 1.5.0",
|
||||
"linenoise >= 0.9",
|
||||
"luacheck >= 0.23.0"
|
||||
"luacheck >= 0.23.0",
|
||||
"argparse >= 0.7.0"
|
||||
}
|
||||
|
||||
build = {
|
||||
|
|
@ -25,7 +25,8 @@ dependencies = {
|
|||
"lua >= 5.1",
|
||||
"lpeglabel >= 1.5.0",
|
||||
"linenoise >= 0.9",
|
||||
"luacheck >= 0.23.0"
|
||||
"luacheck >= 0.23.0",
|
||||
"argparse >= 0.7.0"
|
||||
}
|
||||
|
||||
build = {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ return a
|
|||
|
||||
test("preprocessor placeholder function", [[
|
||||
#placeholder('foo')
|
||||
]], 5, { foo = "return 5" })
|
||||
]], 5, { preprocessorEnv = { foo = "return 5" } })
|
||||
|
||||
test("preprocessor options", [[
|
||||
#if not foo == "sky" then
|
||||
|
|
@ -151,6 +151,65 @@ return a
|
|||
#error("preprocessor should ignore long strings")
|
||||
]])
|
||||
|
||||
test("preprocessor macro remove function", [[
|
||||
#define("log(...)", "")
|
||||
log("test")
|
||||
return true
|
||||
]], true)
|
||||
|
||||
test("preprocessor macro replace function", [[
|
||||
#define("log(x)", "a = x")
|
||||
log("test")
|
||||
return a
|
||||
]], "test")
|
||||
|
||||
test("preprocessor macro identifier replace function", [[
|
||||
#define("test(x)", "x = 42")
|
||||
test(hello)
|
||||
return hello
|
||||
]], 42)
|
||||
|
||||
test("preprocessor macro replace function with vararg", [[
|
||||
#define("log(...)", "a, b, c = ...")
|
||||
log(1, 2, 3)
|
||||
assert(a == 1)
|
||||
assert(b == 2)
|
||||
assert(c == 3)
|
||||
return true
|
||||
]], true)
|
||||
|
||||
test("preprocessor macro replace function with vararg and arg", [[
|
||||
#define("log(x, ...)", "a, b, c = x, ...")
|
||||
log(1, 2, 3)
|
||||
assert(a == 1)
|
||||
assert(b == 2)
|
||||
assert(c == 3)
|
||||
return true
|
||||
]], true)
|
||||
|
||||
test("preprocessor macro replace variable", [[
|
||||
#define("a", "42")
|
||||
return a
|
||||
]], 42)
|
||||
|
||||
test("preprocessor macro prevent recursive macro", [[
|
||||
#define("f(x)", "x")
|
||||
local x = 42
|
||||
x = f(x)
|
||||
return x
|
||||
]], 42)
|
||||
|
||||
test("preprocessor macro replace variable with function", [[
|
||||
#define("a", function() return "42" end)
|
||||
return a
|
||||
]], 42)
|
||||
|
||||
test("preprocessor macro replace function with function", [[
|
||||
#define("test(x)", function(x) return ("%s = 42"):format(x) end)
|
||||
test(hello)
|
||||
return hello
|
||||
]], 42)
|
||||
|
||||
----------------------
|
||||
-- SYNTAX ADDITIONS --
|
||||
----------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue