1
0
Fork 0
mirror of https://github.com/Reuh/candran.git synced 2025-10-27 17:59:30 +00:00

Compare commits

...

44 commits

Author SHA1 Message Date
8d9a79c47d
Update README.md 2024-12-22 16:29:26 +01:00
1e118381f8 Ignore shadowing warning in cancheck for temporary Candran variables 2021-12-24 00:05:53 +01:00
7131c5c8b1 Candran 1.0.0 2021-06-23 20:59:04 +02:00
f0cacd6f08 Update tests 2021-06-18 15:33:04 +02:00
7b0563b9dc Fix can handling of --import 2021-06-18 15:30:23 +02:00
56f1901f9b Fix #set changing global state 2021-06-18 15:26:33 +02:00
b32d4d5600 Update README 2021-06-18 15:17:07 +02:00
53f715cc6f Add #set to define preprocessors constants from an imported file 2021-06-18 14:56:19 +02:00
d4102f1af6 Handle static import errors in can 2021-06-18 14:23:45 +02:00
008e7732bc Add static import 2021-06-18 14:09:10 +02:00
01e808e2e6 Fix Lua < 5.4 compiler with new preprocessor env 2021-06-17 20:18:38 +02:00
ff6d7f8feb Fix require cycle 2021-06-17 19:48:32 +02:00
e9ae8e21a3 Use argparse to parse CLI args, separate preprocessor constants from options 2021-06-17 19:45:53 +02:00
dd22f2de3d Rename built-in macros to __*__, add flag to disable built-in macros 2021-06-17 18:03:34 +02:00
b72aff807c Update README 2021-06-11 14:29:12 +02:00
ea54376aa6 Add constexpr and __STR__ default macros 2021-06-11 14:17:55 +02:00
496a4ddafd Add support for macro replacement using Lua functions 2021-06-11 13:46:31 +02:00
a0eda5bc72 Merge branch 'master' into macros 2021-06-11 13:15:51 +02:00
3c84d1fbdd Fix string.format errors on Lua 5.1 when using booleans or nil 2021-06-09 14:16:28 +02:00
584dac17db Typo 2021-06-07 17:24:25 +02:00
6a1f745015 Update README 2021-06-07 17:17:36 +02:00
ebd36d7103 Add macro support in preprocessor 2021-06-07 17:04:26 +02:00
66f1a5a3c2 Candran v0.14 2021-05-17 15:37:20 +02:00
012b5c1d5a Typo 2021-05-17 15:29:54 +02:00
f5752cd231 Allow rewriting without new traceback 2021-05-17 15:19:55 +02:00
a3f05f5046 Update year 2021-05-17 14:57:34 +02:00
a9dc5ab254 Update README 2021-05-17 14:57:24 +02:00
d65c11e8d9 Fix Candran loader 2021-05-17 14:33:24 +02:00
b00068c766 Load candran before lua files 2021-05-17 14:21:14 +02:00
81f5f8fbbb Improve error handling 2021-05-17 13:59:37 +02:00
d68c6fab0d Candran v0.13 2020-12-24 18:18:11 +01:00
98efdc95f4 Improve cancheck column detection for identifiers 2020-12-24 18:17:27 +01:00
24a3e0ed69 Update README 2020-12-24 17:26:57 +01:00
7e4b46ba7d Add Lua 5.2 target 2020-12-24 17:25:28 +01:00
bf4cadc349 Colorize tests output and fix Lua5.1 compatibility 2020-12-24 17:24:36 +01:00
c8aa4f2a08 Improve error reporting in candran.searcher 2020-12-24 16:24:26 +01:00
c8c35e93a8 Fix unpack/table.unpack rename in 5.3/5.4 2020-08-28 23:41:33 +02:00
cecb2aea03 Lua <5.4: Only error when the variable is declared with an attribute 2020-07-04 23:42:51 +02:00
10be62a2fe Lua 5.4 support and const and close shortcut 2020-06-30 21:29:13 +02:00
7add585c03 Fix end_column being off by one in cancheck 2020-04-07 00:16:39 +02:00
03516abb45
Add SublimeLinter-contrib-cancheck to README 2020-04-07 00:05:50 +02:00
6edc682b21 Candran 0.12.0 2020-04-06 23:30:48 +02:00
33ac4c5d7f Discard warnings from external files in cancheck 2020-04-06 23:11:17 +02:00
1de0aafa5b Added cancheck; candran.compile, .make and .preprocess returns nil, err instead of throwing an error; can and canc error output should now be similar to Lua 2020-04-06 21:30:57 +02:00
19 changed files with 8227 additions and 4681 deletions

View file

@ -1,4 +1,4 @@
Copyright (c) 2017-2019 Étienne "Reuh" Fildadut Copyright (c) 2017-2021 Étienne "Reuh" Fildadut
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: 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:

194
README.md
View file

@ -1,24 +1,31 @@
Candran Candran
======= =======
Candran is a dialect of the [Lua 5.3](http://www.lua.org) programming language which compiles to Lua 5.3, LuaJIT and Lua 5.1 compatible code. It adds several useful syntax additions which aims to make Lua faster and easier to write, and a simple preprocessor. Candran is a dialect of the [Lua 5.4](http://www.lua.org) programming language which compiles to Lua 5.4, Lua 5.3, Lua 5.2, LuaJIT and Lua 5.1 compatible code. It adds several useful syntax additions which aims to make Lua faster and easier to write, and a simple preprocessor.
Unlike Moonscript, Candran tries to stay close to the Lua syntax, and existing Lua code should be able to run on Candran unmodified. Unlike Moonscript, Candran tries to stay close to the Lua syntax, and existing Lua code should be able to run on Candran unmodified.
````lua ````lua
#import("lib.thing") -- static import #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 function calculate(toadd=25) -- default parameters
local result = thing.do() local result = thing.do()
result += toadd result += toadd
#if debug then -- preprocessor conditionals #if DEBUG then -- preprocessor conditionals
print("Did something") print("Did something")
#end #end
return result return result
end end
let a = { let a = {
hey = true, hey = 5 // 2, -- Lua 5.3+ syntax, that will be translated to work with the current Lua version
child = nil, child = nil,
@ -31,6 +38,8 @@ let a = {
end end
} }
const five = 5 -- shortcut for Lua 5.4 attributes
a:method(42, (foo) a:method(42, (foo)
return "something " .. foo return "something " .. foo
end) end)
@ -72,25 +81,31 @@ end
Candran is released under the MIT License (see ```LICENSE``` for details). Candran is released under the MIT License (see ```LICENSE``` for details).
#### Quick setup #### Quick setup
Install Candran automatically using LuaRocks: ```sudo luarocks install rockspec/candran-0.11.0-1.rockspec```. 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. The rockspec will install linenoise by default. 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.
You can register the Candran package searcher in your main Lua file (`require("candran").setup()`) and any subsequent `require` call in your project will automatically search for Candran modules. You can register the Candran package searcher in your main Lua file (`require("candran").setup()`) and any subsequent `require` call in your project will automatically search for Candran modules.
If you use LÖVE, some integration with Candran is detailled [here](https://github.com/Reuh/candran/wiki/L%C3%96VE).
#### Editor support #### Editor support
Most editors should be able to use their existing Lua support for Candran code. If you want full support for the additional syntax in your editor: Most editors should be able to use their existing Lua support for Candran code. If you want full support for the additional syntax in your editor:
* **Sublime Text 3**: * **Sublime Text 3**:
* [sublime-candran](https://github.com/Reuh/sublime-candran) support the full Candran syntax * [sublime-candran](https://github.com/Reuh/sublime-candran) support the full Candran syntax
* [SublimeLinter-candran-contrib](https://github.com/Reuh/SublimeLinter-contrib-candran) SublimeLinter plugin for Candran * [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 * **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.
The language The language
------------ ------------
### Syntax additions ### Syntax additions
After the preprocessor is run the Candran code is compiled to Lua. Candran code adds the folowing syntax to Lua: After the [preprocessor](#preprocessor) is run the Candran code is compiled to Lua. Candran code adds the folowing syntax to Lua 5.4 syntax:
##### Assignment operators ##### Assignment operators
* ````var += nb```` * ````var += nb````
@ -169,8 +184,20 @@ let a = {
Similar to ```local```, but the variable will be declared *before* the assignemnt (i.e. it will compile into ```local a; a = value```), so you can access it from functions defined in the value. Similar to ```local```, but the variable will be declared *before* the assignemnt (i.e. it will compile into ```local a; a = value```), so you can access it from functions defined in the value.
This does not support Lua 5.4 attributes.
Can also be used as a shorter name for ```local```. Can also be used as a shorter name for ```local```.
##### `const` and `close` variable declaration
```lua
const a = 5
close b = {}
const x, y, z = 1, 2, 3 -- every variable will be defined using <const>
```
Shortcut to Lua 5.4 variable attribute. Do not behave like `let`, as attributes require the variable to be constant and therefore can't be predeclared. Only compatibel with Lua 5.4 target.
##### `continue` keyword ##### `continue` keyword
```lua ```lua
for i=1, 10 do for i=1, 10 do
@ -422,30 +449,76 @@ Will output ````print("Bonjour")```` or ````print("Hello")```` depending of the
The preprocessor has access to the following variables: The preprocessor has access to the following variables:
* ````candran````: the Candran library table. * ````candran````: the Candran library table.
* ````output````: the current preprocessor output string. Can be redefined at any time. * ````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: ```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). * ````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. * ````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. * ````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. * ```placeholder(name)```: if the variable _name_ is defined in the preprocessor environement, its content will be inserted here.
* ````...````: each arguments passed to the preprocessor is directly available in the environment. * ```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. * 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 Compile targets
--------------- ---------------
Candran is based on the Lua 5.3 syntax, but can be compiled to Lua 5.3, LuaJIT, and Lua 5.1 compatible code. 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.
To chose a compile target, set the ```target``` option to ```lua53```, ```luajit```, or ```lua51``` in the option table when using the library or the command line tools. Candran will try to detect the currently used Lua version and use it as the default target. To chose a compile target, set the ```target``` option to ```lua54```, ```lua53```, ```lua52```, ```luajit```, or ```lua51``` in the option table when using the library or the command line tools. Candran will try to detect the currently used Lua version and use it as the default target.
For the ```luajit``` and ```lua51``` targets, Lua 5.3 specific syntax (bitwise operators, integer division) will automatically be translated to valid Lua 5.1 syntax, using LuaJIT's ```bit``` library if necessary. Unless LuaJIT's bit library is installed, you won't be able to use bitwise operators with vanilla Lua 5.1 ("PUC Lua"). Candran will try to translate Lua 5.4 syntax into something usable with the current target if possible. Here is what is currently supported:
The ```lua51``` target does not support gotos and labels. | Lua version | Candran target | Integer division operator // | Bitwise operators | Goto/Labels | Variable attributes |
| --- | --- | --- | --- | --- | --- |
| Lua 5.4 | lua54 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Lua 5.3 | lua53 | :white_check_mark: | :white_check_mark: | :white_check_mark: | X |
| Lua 5.2 | lua52 | :white_check_mark: | :white_check_mark: (32bit) | :white_check_mark: | X |
| LuaJIT | luajit | :white_check_mark: | :white_check_mark: (32bit) | :white_check_mark: | X |
| Lua 5.1 | lua51 | :white_check_mark: | :white_check_mark: if LuaJIT bit library is available (32bit) | X | X |
**Please note** that Candran only translates syntax, and will not try to do anything about changes in the Lua standard library (for example, the new utf8 module). If you need this, you should be able to use [lua-compat-5.3](https://github.com/keplerproject/lua-compat-5.3) along with Candran. **Please note** that Candran only translates syntax, and will not try to do anything about changes in the Lua standard library (for example, the new utf8 module). If you need this, you should be able to use [lua-compat-5.3](https://github.com/keplerproject/lua-compat-5.3) along with Candran.
Usage Usage
----- -----
### Command-line usage ### Command-line usage
The library can be used standalone through the ```canc``` and ```can``` utility: The library can be used standalone through the ```canc``` (for compiling Candran files) and ```can``` (for running Candran files directly) utilities:
* ````canc```` * ````canc````
@ -455,23 +528,41 @@ The library can be used standalone through the ```canc``` and ```can``` utility:
Preprocess and compile each _filename_ Candran files, and creates the assiociated ```.lua``` files in the same directories. 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. 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: Example uses:
@ -479,35 +570,43 @@ The library can be used standalone through the ```canc``` and ```can``` utility:
preprocess and compile _foo.can_ and write the result in _foo.lua_. 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_. 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. checks foo.can for syntaxic errors.
* ```can``` * ```can```
Start a simplisitic Candran REPL. Start a simplisitic Candran REPL. Will automatically call `candran.setup()`.
If you want a better REPL (autocompletion, history, ability to move the cursor), install lua-linenoise: ```luarocks install linenoise```. If you want a better REPL (autocompletion, history, ability to move the cursor), install lua-linenoise: ```luarocks install linenoise``` (automatically installed if Candran was installed using LuaRocks).
* ````can [options] filename```` * ````can [options] filename````
Preprocess, compile and run _filename_ using the options provided. Preprocess, compile and run _filename_ using the options provided.
This will automatically register the Candran package searcher, so required Candran modules will be compiled as they are needed. This will automatically register the Candran package searcher using `candran.setup()`, so required Candran modules will be compiled as they are needed.
This command will use error rewriting unless explicitely enabled (by setting the `rewriteErrors=false` option). This command will use error rewriting unless explicitely enabled (by setting the `rewriteErrors=false` option).
Instead of providing a filename, you can use ```-``` to read from standard input. 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```
Provides a linter and static analyzer with the exact same interface as [luacheck](https://github.com/luarocks/luacheck).
This requires luacheck: ```luarocks install luacheck``` (automatically installed if Candran was installed through LuaRocks).
### Library usage ### Library usage
Candran can also be used as a Lua library: Candran can also be used as a Lua library:
@ -518,7 +617,7 @@ local f = io.open("foo.can") -- read the file foo.can
local contents = f:read("*a") local contents = f:read("*a")
f:close() 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! load(compiled)() -- execute!
@ -534,14 +633,14 @@ The table returned by _require("candran")_ gives you access to:
##### Compiler & preprocessor ##### Compiler & preprocessor
* ````candran.VERSION````: Candran's version string (e.g. `"0.10.0"`). * ````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. * ````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])````: return the Candran code compiled to Lua with the _options_ option table. * ````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. * ````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 ##### Code loading helpers
* ```candran.loadfile(filepath, env, options)```: Candran equivalent to the Lua 5.3's loadfile funtion. Will rewrite errors by default. * ```candran.loadfile(filepath, env, options)```: Candran equivalent to the Lua 5.4's loadfile funtion. Will rewrite errors by default.
* ```candran.load(chunk, chunkname, env, options)```: Candran equivalent to the Lua 5.3's load funtion. Will rewrite errors by default. * ```candran.load(chunk, chunkname, env, options)```: Candran equivalent to the Lua 5.4's load funtion. Will rewrite errors by default.
* ```candran.dofile(filepath, options)```: Candran equivalent to the Lua 5.3's dofile funtion. Will rewrite errors by default. * ```candran.dofile(filepath, options)```: Candran equivalent to the Lua 5.4's dofile funtion. Will rewrite errors by default.
#### Error rewriting #### Error rewriting
When using the command-line tools or the code loading helpers, Candran will automatically setup error rewriting: because the code is reformated when When using the command-line tools or the code loading helpers, Candran will automatically setup error rewriting: because the code is reformated when
@ -560,9 +659,13 @@ If you are using the preprocessor ```import()``` function, the source Candran fi
example.can:12(final.lua:5): attempt to call a nil value (global 'iWantAnError') example.can:12(final.lua:5): attempt to call a nil value (global 'iWantAnError')
``` ```
You can perform error rewriting manually using: Please note that Candran can only wrap code directly called from Candran; if an error is raised from Lua, there will be no rewriting of Candran lines the stacktrace. These lines are indicated using `(compiled candran)` before the line number.
* ```candran.messageHandler(message)```: the error message handler used by Candran. Given `message` the Lua error string, returns full Candran traceback where soure files and lines are rewritten to their Candran source. You can use it as is in xpcall as a message handler. If you want Candran to always wrap errors, you will need to wrap your whole code in a `xpcall`: `xpcall(func, candran.messageHandler)`.
* ```candran.messageHandler(message[, noTraceback])```: the error message handler used by Candran. Given `message` the Lua error string, returns full Candran traceback where soure files and lines are rewritten to their Candran source. You can use it as is in xpcall as a message handler. If `noTraceback` is `true`, Candran will only rewrite `message` and not add a new traceback.
Also note that the Candran message handler will add a new, rewritten, stacktrace to the error message; it can't replace the default Lua one. You will therefore see two stacktraces when raising an error, the last one being the Lua one and can be ignored.
##### Package searching helpers ##### Package searching helpers
Candran comes with a custom package searcher which will automatically find, preprocesses and compile ```.can``` files. Candran comes with a custom package searcher which will automatically find, preprocesses and compile ```.can``` files.
@ -576,19 +679,22 @@ require("candran").setup()
at the top of your main Lua file. If a Candran file is found when you call ```require()```, it will be automatically compiled and loaded. If both a Lua and Candran file match a module name, the Candran file will be loaded. at the top of your main Lua file. If a Candran file is found when you call ```require()```, it will be automatically compiled and loaded. If both a Lua and Candran file match a module name, the Candran file will be loaded.
* ```candran.searcher(modpath)```: Candran package searcher function. Use the existing package.path. * ```candran.searcher(modpath)```: Candran package searcher function. Use the existing package.path.
* ```candran.setup()```: register the Candran package searcher, and return the `candran` table. * ```candran.setup()```: register the Candran package searcher (if not already done), and return the `candran` table.
##### Available compiler & preprocessor options ##### Available compiler & preprocessor options
You can give arbitrary options to the compiler and preprocessor, but Candran already provide and uses these with their associated default values: You can give arbitrary options to the compiler and preprocessor, but Candran already provide and uses these with their associated default values:
```lua ```lua
target = "lua53" -- compiler target. "lua53", "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. indentation = "" -- character(s) used for indentation in the compiled file.
newline = "\n" -- character(s) used for newlines 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). 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. 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. 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(). 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`. You can change the defaults used for these variables in the table `candran.default`.

92
bin/can
View file

@ -1,37 +1,57 @@
#!/bin/lua #!/usr/bin/env lua
local candran = require("candran")
local cmdline = require("candran.cmdline")
local args = cmdline(arg) local candran = require("candran").setup()
local util = require("candran.util")
local argparse = require("argparse")
local unpack = unpack or table.unpack
if args.help or args.h then -- Parse args --
print("Candran "..candran.VERSION.." interpreter by Reuh")
print("Usage: "..arg[0].." [options] filename") local parser = argparse()
print("Specify no options to start the REPL.") :name "can"
print("Use - instead of a filename to read from the standard input.") :description("Candran "..candran.VERSION.." interpreter by Reuh.")
print("Interpreter options:") :epilog "For more info, see https://github.com/Reuh/candran"
print(" -help or -h print this text")
print("Default options:") parser:argument("filename", "Candran file to run. Use - to read from standard input. Start the REPL if no filename given.")
for opt, val in pairs(candran.default) do :args "?"
if type(val) == "string" then val = val:gsub("\n", "\\n") end
print((" %s=%q"):format(opt, val)) util.cli.addCandranOptions(parser)
end
return local args = parser:parse()
end
local options = util.cli.makeCandranOptions(args)
-- Run --
-- stdin -- stdin
if arg[#arg] == "-" then if args.filename == "-" then
local f, err = candran.load(io.read("*a"), "stdin", nil, args) local f, err = candran.load(io.read("*a"), "stdin", nil, options)
if not f then if not f then
io.stderr:write("can: "..err.."\n") io.stderr:write("can: "..err.."\n")
os.exit(1) os.exit(1)
end end
f() local r, e = xpcall(f, candran.messageHandler)
if not r then
io.stderr:write(e.."\n")
os.exit(1)
end
-- file -- file
elseif #args >= 1 then elseif args.filename then
candran.dofile(args[1], args) local f, err = candran.loadfile(args.filename, nil, options)
if not f then
io.stderr:write("can: "..err.."\n")
os.exit(1)
else
local r, e = xpcall(f, candran.messageHandler)
if not r then
io.stderr:write(e.."\n")
os.exit(1)
end
end
-- REPL -- REPL
else else
candran.default = util.merge(candran.default, options)
-- Setup linenoise -- Setup linenoise
local s, l = pcall(require, "linenoise") local s, l = pcall(require, "linenoise")
if not s then -- pure Lua compatibility thingy if not s then -- pure Lua compatibility thingy
@ -98,6 +118,22 @@ else
print("Candran " .. candran.VERSION .. ", targeting " .. candran.default.target) print("Candran " .. candran.VERSION .. ", targeting " .. candran.default.target)
candran.setup() 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 -- REPL loop
local multiline = false -- true if wait for another line local multiline = false -- true if wait for another line
local buffer local buffer
@ -137,15 +173,15 @@ else
end end
-- exec -- exec
local t = { pcall(candran.load, buffer, "stdin") } local r, e = candran.load(buffer, "stdin")
if t[1] == false then if not r then
if t[2]:match("expected '[end})]+' to close") then if e:match("expected '[end})]+' to close") then
multiline = true multiline = true
else else
print(t[2]) print(e)
end end
else else
t = { pcall(t[2]) } local t = { pcall(r) }
if t[1] == false then if t[1] == false then
print(t[2]) print(t[2])
elseif #t > 1 then elseif #t > 1 then

105
bin/canc
View file

@ -1,51 +1,67 @@
#!/bin/lua #!/usr/bin/env lua
local candran = require("candran") local candran = require("candran")
local cmdline = require("candran.cmdline")
local parse = require("candran.can-parser.parser").parse local parse = require("candran.can-parser.parser").parse
local pp = require("candran.can-parser.pp") 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 local parser = argparse()
print("Candran "..candran.VERSION.." compiler by Reuh") :name "canc"
print("Usage: "..arg[0].." [options] filenames...") :description("Candran "..candran.VERSION.." compiler by Reuh.")
print("Use - instead of filenames to read from the standard input. The output file will be named stdin.lua by default.") :epilog "For more info, see https://github.com/Reuh/candran"
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
if arg[#arg] == "-" then parser:argument("filename", "Candran files to compile. Use - to read from standard input; the output file will then be named stdin.lua by default.")
table.insert(args, io.stdin) :args "+"
end
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 -- Read
local dest, input local dest, input
if file == io.stdin then if file == "-" then
dest = args.out or "stdin.lua" dest = args.output or "stdin.lua"
input = io.read("*a") input = io.read("*a")
args.chunkname = "stdin" args.chunkname = "stdin"
else else
dest = args.out or (file:gsub("%.can$", "")..".lua") dest = args.output or (file:gsub("%.can$", "")..".lua")
local inputFile, err = io.open(file, "r") local inputFile, err = io.open(file, "r")
if not inputFile then if not inputFile then
io.stderr:write("canc: cannot open "..file..": "..err) io.stderr:write("canc: cannot open "..file..": "..err.."\n")
os.exit(1) os.exit(1)
end end
input = inputFile:read("*a") input = inputFile:read("*a")
@ -68,8 +84,10 @@ for _, file in ipairs(args) do
end end
-- Compile and output -- Compile and output
if args.dest then local options = util.cli.makeCandranOptions(args)
dest = args.dest .. "/" .. dest
if args.destination then
dest = args.destination .. "/" .. dest
end end
if not args.print then if not args.print then
@ -78,13 +96,28 @@ for _, file in ipairs(args) do
local out = input local out = input
if args.preprocess then if args.preprocess then
out = candran.preprocess(out, args) local r, err = candran.preprocess(out, options)
if not r then
io.stderr:write("canc: "..err.."\n")
os.exit(1)
end
out = r
end end
if args.compile then if args.compile then
out = candran.compile(out, args) local r, err = candran.compile(out, options)
if not r then
io.stderr:write("canc: "..err.."\n")
os.exit(1)
end
out = r
end end
if args.compile == nil and args.preprocess == nil then if args.compile == nil and args.preprocess == nil then
out = candran.make(input, args) local r, err = candran.make(input, options)
if not r then
io.stderr:write("canc: "..err.."\n")
os.exit(1)
end
out = r
end end
if args.print then if args.print then

215
bin/cancheck Normal file
View file

@ -0,0 +1,215 @@
#!/usr/bin/env lua
--[[
Based on luacheck: https://github.com/luarocks/luacheck
The MIT License (MIT)
Copyright (c) 2014 - 2018 Peter Melnichenko
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.
]]
-- Monkey patch Luacheck (tested against version 0.23.0) to support Candran files
local candran = require("candran")
local util = require("candran.util")
-- set a function upvalues (if several names are given, will go up the upvalue chain)
local function setupvalue(fn, val, name, ...)
for i=1, debug.getinfo(fn, "u").nups do
local n, v = debug.getupvalue(fn, i)
if n == name then
if not ... then
debug.setupvalue(fn, i, val)
else
setupvalue(v, val, ...)
end
end
end
end
-- escape a string to be used as a pattern
local function escape(str)
return str:gsub("[^%w]", "%%%0")
end
-- returns a pattern that find start and stop position of a token
local function pattern(token)
return "()"..escape(token).."()"
end
-- token aliases
local tokenAlias = {
["self"] = { "@", ":" }
}
-- Patch checker
local oldCheck = require("luacheck.check")
local function check(can)
local lua, err = candran.make(can, {chunkname="_luacheck_source"})
-- Warnings
if lua then
local r = oldCheck(lua)
-- Calculate Candran file position.
if #r.warnings > 0 then
local lua_lines = {}
for l in (lua.."\n"):gmatch("([^\n]*)\n") do
table.insert(lua_lines, l)
end
local can_lines = {}
for l in (can.."\n"):gmatch("([^\n]*)\n") do
table.insert(can_lines, l)
end
for i=#r.warnings, 1, -1 do
local warning = r.warnings[i]
-- calculating candran line
local lua_line = lua_lines[warning.line]
local source, line = lua_line:match(".*%-%- (.-)%:(%d+)$")
if source ~= "_luacheck_source" then -- line is from another file, discard
table.remove(r.warnings, i)
elseif source then
warning.can_line = tonumber(line)
-- do the same for prev_line
if warning.prev_line then
local s, l = lua_lines[warning.prev_line]:match(".*%-%- (.-)%:(%d+)$")
if s ~= "_luacheck_source" then
warning.prev_line = s..":"..l -- luacheck seems to do no validation on this, so we can redefine it to anything
elseif l then
warning.prev_line = l
end
end
-- Errors codes highlighting a identifier (alphanumeric+underscore not starting with a number):
-- 1xx: global variables
-- 2xx: unused variables
-- 3xx: unused values
-- 4xx: shadowing delarations
local isIdentifier = tonumber(warning.code) >= 100 and tonumber(warning.code) < 500
-- calculating candran column
local can_line = can_lines[warning.can_line]
local lua_token = lua_line:sub(warning.column, warning.end_column)
local token_pattern = pattern(lua_token) -- token finding pattern
-- the warning happens on the n-th instance of lua_token on this line
local lua_n = 1
for start in lua_line:gmatch(token_pattern) do
if start >= warning.column then
break
end
lua_n = lua_n + 1
end
-- Find associated candran token. If lua_n > can_nmax, the last found lua_token is used.
-- This approximation should work in like, 90% of cases.
local can_n = 1
local pos = 1
while can_n <= lua_n do
-- find first token or alias of this token
local start, stop = can_line:match(token_pattern, pos)
if tokenAlias[lua_token] then
for _, token in ipairs(tokenAlias[lua_token]) do
local nstart, nstop = can_line:match(pattern(token), pos)
if nstart and (not start or nstart < start) then
start, stop = nstart, nstop
end
end
-- for non aliases token that are identifier, check if match is a full identifier
-- (avoid things like `let e` matching the e in let)
elseif start and isIdentifier then
local prev = can_line:sub(start-1, start-1)
local next = can_line:sub(stop, stop)
if prev:match("[%w_]") or next:match("[%w_]") then
can_n = can_n - 1 -- skip this match
end
end
-- found
if start then
pos = stop
warning.can_column, warning.can_end_column = start, stop-1
can_n = can_n + 1
else
break
end
end
-- AFAIK, prev_column and prev_end_column are not displayed in any warning so we don't need to recalculate them for Candran.
end
end
end
return r
-- Syntax error
else
local line, column, msg = err:match(":(%d+):(%d+):%s*(.*)$")
local syntax_error = {
code = "011",
line = line,
column = column,
end_column = column,
msg = msg
}
return {
warnings = {syntax_error},
inline_options = {},
line_lengths = {},
line_endings = {}
}
end
end
package.loaded["luacheck.check"] = check
local runner = require("luacheck.runner")
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
-- Patch formatter
local format = require("luacheck.format")
local function format_location(file, location, opts)
local res = ("%s:%d:%d"):format(file, location.can_line or location.line, location.can_column or location.column)
if opts.ranges then
res = ("%s-%d"):format(res, location.can_end_column or location.end_column)
end
return res
end
setupvalue(format.builtin_formatters.plain, format_location, "format_event", "format_location")
-- Fix some Luacheck messages and run
local path = util.search("luacheck.main", {"lua"})
if path then
local f = io.open(path, "r")
local code = f:read("*a")
f:close()
code = code:gsub(escape(" bug (please report at https://github.com/mpeterv/luacheck/issues)"), ", patched for Candran "..candran.VERSION.." bug. Please DO NOT report this bug to Luacheck") -- error text
:gsub(escape("\"luacheck\","), "\"cancheck\",") -- command name
:gsub("a linter and a static analyzer for Lua%.", "a linter and a static analyzer for Lua, patched for Candran "..candran.VERSION..".") -- help text
-- run
return load(code)()
else
io.stderr:write("can't find luacheck.main\n")
os.exit(1)
end

View file

@ -1,7 +1,14 @@
#import("candran.util") local candran = {
#import("candran.cmdline") VERSION = "1.0.0"
}
package.loaded["candran"] = candran
#import("candran.util")
#import("candran.serpent")
#import("compiler.lua54")
#import("compiler.lua53") #import("compiler.lua53")
#import("compiler.lua52")
#import("compiler.luajit") #import("compiler.luajit")
#import("compiler.lua51") #import("compiler.lua51")
@ -10,19 +17,20 @@
#import("candran.can-parser.pp") #import("candran.can-parser.pp")
#import("candran.can-parser.parser") #import("candran.can-parser.parser")
local candran = { local unpack = unpack or table.unpack
VERSION = "0.11.0"
}
--- Default options. --- Default options.
candran.default = { candran.default = {
target = "lua53", target = "lua54",
indentation = "", indentation = "",
newline = "\n", newline = "\n",
variablePrefix = "__CAN_", variablePrefix = "__CAN_",
mapLines = true, mapLines = true,
chunkname = "nil", chunkname = "nil",
rewriteErrors = true rewriteErrors = true,
builtInMacros = true,
preprocessorEnv = {},
import = {}
} }
-- Autodetect version -- Autodetect version
@ -32,14 +40,30 @@ if _VERSION == "Lua 5.1" then
else else
candran.default.target = "lua51" candran.default.target = "lua51"
end end
elseif _VERSION == "Lua 5.2" then
candran.default.target = "lua52"
elseif _VERSION == "Lua 5.3" then
candran.default.target = "lua53"
end end
--- Run the preprocessor --- Run the preprocessor
-- @tparam input string input code -- @tparam input string input code
-- @tparam options table arguments for the preprocessor. They will be inserted into the preprocessor environement. -- @tparam options table arguments for the preprocessor. They will be inserted into the preprocessor environement.
-- @treturn output string output code -- @treturn[1] output string output code
function candran.preprocess(input, options={}) -- @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) 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 -- generate preprocessor code
local preprocessor = "" local preprocessor = ""
@ -74,7 +98,8 @@ function candran.preprocess(input, options={})
preprocessor ..= "return output" preprocessor ..= "return output"
-- make preprocessor environement -- make preprocessor environement
local env = util.merge(_G, options) local exportenv = {}
local env = util.merge(_G, options.preprocessorEnv)
--- Candran library table --- Candran library table
env.candran = candran env.candran = candran
--- Current preprocessor output --- Current preprocessor output
@ -90,10 +115,13 @@ function candran.preprocess(input, options={})
-- open module file -- open module file
local f = io.open(filepath) local f = io.open(filepath)
if not f then error("Can't open the module file to import") end 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 = util.merge(options, { chunkname = filepath, loadLocal = true, loadPackage = true }, margs)
local modcontent = 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() f:close()
-- get module name (ex: module name of path.to.module is module) -- get module name (ex: module name of path.to.module is module)
@ -113,7 +141,7 @@ function candran.preprocess(input, options={})
-- @tparam file string filepath -- @tparam file string filepath
env.include = function(file) env.include = function(file)
local f = io.open(file) local f = io.open(file)
if not f then error("Can't open the file "..file.." to include") end if not f then error("can't open the file "..file.." to include") end
env.write(f:read("*a")) env.write(f:read("*a"))
f:close() f:close()
end end
@ -129,40 +157,103 @@ function candran.preprocess(input, options={})
env.write(env[name]) env.write(env[name])
end end
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 -- compile & load preprocessor
local preprocess, err = util.load(candran.compile(preprocessor, args), "candran preprocessor", env) local preprocess, err = candran.compile(preprocessor, options)
if not preprocess then error("Error while creating Candran preprocessor: " .. err) end 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 -- execute preprocessor
local success, output = pcall(preprocess) local success, output = pcall(preprocess)
if not success then error("Error while preprocessing file: " .. output) end if not success then
return nil, "in preprocessor: "..output
end
return output return output, macros, exportenv
end end
--- Run the compiler --- Run the compiler
-- @tparam input string input code -- @tparam input string input code
-- @tparam options table options for the compiler -- @tparam options table options for the compiler
-- @treturn output string output code -- @tparam macros table defined macros, as returned by the preprocessor
function candran.compile(input, options={}) -- @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) options = util.merge(candran.default, options)
local ast, errmsg = parser.parse(input, options.chunkname) local ast, errmsg = parser.parse(input, options.chunkname)
if not ast then if not ast then
error("Compiler: error while parsing file: "..errmsg) return nil, errmsg
end end
return require("compiler."..options.target)(input, ast, options) return require("compiler."..options.target)(input, ast, options, macros)
end end
--- Preprocess & compile code --- Preprocess & compile code
-- @tparam code string input code -- @tparam code string input code
-- @tparam options table arguments for the preprocessor and compiler -- @tparam options table arguments for the preprocessor and compiler
-- @treturn output string output code -- @treturn[1] output string output code
-- @treturn[2] nil nil if error
-- @treturn[2] error string error message
function candran.make(code, options) function candran.make(code, options)
return candran.compile(candran.preprocess(code, options), 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 end
local errorRewritingActive = false local errorRewritingActive = false
@ -171,7 +262,9 @@ local codeCache = {}
-- Will rewrite errors by default. -- Will rewrite errors by default.
function candran.loadfile(filepath, env, options) function candran.loadfile(filepath, env, options)
local f, err = io.open(filepath) local f, err = io.open(filepath)
if not f then error("can't open the file: "..err) end if not f then
return nil, "cannot open %s":format(tostring(err))
end
local content = f:read("*a") local content = f:read("*a")
f:close() f:close()
@ -183,24 +276,29 @@ end
function candran.load(chunk, chunkname, env, options={}) function candran.load(chunk, chunkname, env, options={})
options = util.merge({ chunkname = tostring(chunkname or chunk) }, options) options = util.merge({ chunkname = tostring(chunkname or chunk) }, options)
codeCache[options.chunkname] = candran.make(chunk, options) local code, err = candran.make(chunk, options)
local f, err = util.load(codeCache[options.chunkname], options.chunkname, env) 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. -- 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. -- 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. -- But the error message will likely be useless unless you know how Candran works.
if f == nil then if f == nil then
return f, "Candran unexpectedly generated invalid code: "..err return f, "candran unexpectedly generated invalid code: "..err
end end
if options.rewriteErrors == false then if options.rewriteErrors == false then
return f return f
else else
return function(...) return function(...)
local params = {...}
if not errorRewritingActive then if not errorRewritingActive then
errorRewritingActive = true errorRewritingActive = true
local t = { xpcall(() return f(unpack(params)) end, candran.messageHandler) } local t = { xpcall(f, candran.messageHandler, ...) }
errorRewritingActive = false errorRewritingActive = false
if t[1] == false then if t[1] == false then
error(t[2], 0) error(t[2], 0)
@ -227,20 +325,23 @@ end
--- Candran error message handler. --- Candran error message handler.
-- Use it in xpcall to rewrite stacktraces to display Candran source file lines instead of compiled Lua lines. -- Use it in xpcall to rewrite stacktraces to display Candran source file lines instead of compiled Lua lines.
function candran.messageHandler(message) function candran.messageHandler(message, noTraceback)
return debug.traceback(message, 2):gsub("(\n?%s*)([^\n]-)%:(%d+)%:", function(indentation, source, line) 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) line = tonumber(line)
local originalFile local originalFile
local strName = source:match("%[string \"(.-)\"%]") local strName = source:match("^(.-)%(compiled candran%)$")
if strName then if strName then
if codeCache[strName] then if codeCache[strName] then
originalFile = codeCache[strName] originalFile = codeCache[strName]
source = strName source = strName
end end
else else
local fi = io.open(source, "r") if fi = io.open(source, "r") then
if fi then
originalFile = fi:read("*a") originalFile = fi:read("*a")
fi:close() fi:close()
end end
@ -248,7 +349,7 @@ function candran.messageHandler(message)
if originalFile then if originalFile then
local i = 0 local i = 0
for l in originalFile:gmatch("([^\n]*)\n") do for l in (originalFile.."\n"):gmatch("([^\n]*)\n") do
i = i +1 i = i +1
if i == line then if i == line then
local extSource, lineMap = l:match(".*%-%- (.-)%:(%d+)$") local extSource, lineMap = l:match(".*%-%- (.-)%:(%d+)$")
@ -270,18 +371,37 @@ end
function candran.searcher(modpath) function candran.searcher(modpath)
local filepath = util.search(modpath, {"can"}) local filepath = util.search(modpath, {"can"})
if not filepath then if not filepath then
return "\n\tno candran file in package.path" if _VERSION == "Lua 5.4" then
return "no candran file in package.path"
else
return "\n\tno candran file in package.path"
end
end end
return candran.loadfile(filepath) 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 end
--- Register the Candran package searcher. --- Register the Candran package searcher.
function candran.setup() function candran.setup()
if _VERSION == "Lua 5.1" then local searchers = if _VERSION == "Lua 5.1" then
table.insert(package.loaders, 2, candran.searcher) package.loaders
else else
table.insert(package.searchers, 2, candran.searcher) package.searchers
end 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 return candran
end end

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ stat:
| `If{ (lexpr block)+ block? } -- if e1 then b1 [elseif e2 then b2] ... [else bn] end | `If{ (lexpr block)+ block? } -- if e1 then b1 [elseif e2 then b2] ... [else bn] end
| `Fornum{ ident expr expr expr? block } -- for ident = e, e[, e] do b end | `Fornum{ ident expr expr expr? block } -- for ident = e, e[, e] do b end
| `Forin{ {ident+} {expr+} block } -- for i1, i2... in e1, e2... do b end | `Forin{ {ident+} {expr+} block } -- for i1, i2... in e1, e2... do b end
| `Local{ {ident+} {expr+}? } -- local i1, i2... = e1, e2... | `Local{ {attributeident+} {expr+}? } -- local i1, i2... = e1, e2...
| `Let{ {ident+} {expr+}? } -- let i1, i2... = e1, e2... | `Let{ {ident+} {expr+}? } -- let i1, i2... = e1, e2...
| `Localrec{ {ident} {expr} } -- only used for 'local function' | `Localrec{ {ident} {expr} } -- only used for 'local function'
| `Goto{ <string> } -- goto str | `Goto{ <string> } -- goto str
@ -59,7 +59,7 @@ apply:
`Call{ expr expr* } `Call{ expr expr* }
| `SafeCall{ expr expr* } | `SafeCall{ expr expr* }
lhs: `Id{ <string> } | `Index{ expr expr } | ˇDestructuringId{ Id | Pair+ } lhs: `Id{ <string> } | AttributeId{ <string> <string>? } | `Index{ expr expr } | ˇDestructuringId{ Id | Pair+ }
opid: -- includes additional operators from Lua 5.3 and all relational operators opid: -- includes additional operators from Lua 5.3 and all relational operators
'add' | 'sub' | 'mul' | 'div' 'add' | 'sub' | 'mul' | 'div'
@ -113,7 +113,9 @@ local labels = {
{ "ErrDoFor", "expected 'do' after the range of the for loop" }, { "ErrDoFor", "expected 'do' after the range of the for loop" },
{ "ErrDefLocal", "expected a function definition or assignment after local" }, { "ErrDefLocal", "expected a function definition or assignment after local" },
{ "ErrDefLet", "expected a function definition or assignment after let" }, { "ErrDefLet", "expected an assignment after let" },
{ "ErrDefClose", "expected an assignment after close" },
{ "ErrDefConst", "expected an assignment after const" },
{ "ErrNameLFunc", "expected a function name after 'function'" }, { "ErrNameLFunc", "expected a function name after 'function'" },
{ "ErrEListLAssign", "expected one or more expressions after '='" }, { "ErrEListLAssign", "expected one or more expressions after '='" },
{ "ErrEListAssign", "expected one or more expressions after '='" }, { "ErrEListAssign", "expected one or more expressions after '='" },
@ -181,6 +183,9 @@ local labels = {
{ "ErrCBraceUEsc", "expected '}' after the code point" }, { "ErrCBraceUEsc", "expected '}' after the code point" },
{ "ErrEscSeq", "invalid escape sequence" }, { "ErrEscSeq", "invalid escape sequence" },
{ "ErrCloseLStr", "unclosed long string" }, { "ErrCloseLStr", "unclosed long string" },
{ "ErrUnknownAttribute", "unknown variable attribute" },
{ "ErrCBracketAttribute", "expected '>' to close the variable attribute" },
} }
local function throw(label) local function throw(label)
@ -301,6 +306,11 @@ local function fixShortFunc (t)
return t return t
end end
local function markImplicit (t)
t.implicit = true
return t
end
local function statToExpr (t) -- tag a StatExpr local function statToExpr (t) -- tag a StatExpr
t.tag = t.tag .. "Expr" t.tag = t.tag .. "Expr"
return t return t
@ -456,6 +466,24 @@ local function maybe (patt) -- fail pattern instead of propagating errors
return #patt/0 * patt return #patt/0 * patt
end end
local function setAttribute(attribute)
return function(assign)
assign[1].tag = "AttributeNameList"
for _, id in ipairs(assign[1]) do
if id.tag == "Id" then
id.tag = "AttributeId"
id[2] = attribute
elseif id.tag == "DestructuringId" then
for _, did in ipairs(id) do
did.tag = "AttributeId"
did[2] = attribute
end
end
end
return assign
end
end
local stacks = { local stacks = {
lexpr = {} lexpr = {}
} }
@ -488,7 +516,7 @@ local G = { V"Lua",
Block = tagC("Block", (V"Stat" + -V"BlockEnd" * throw("InvalidStat"))^0 * ((V"RetStat" + V"ImplicitPushStat") * sym(";")^-1)^-1); Block = tagC("Block", (V"Stat" + -V"BlockEnd" * throw("InvalidStat"))^0 * ((V"RetStat" + V"ImplicitPushStat") * sym(";")^-1)^-1);
Stat = V"IfStat" + V"DoStat" + V"WhileStat" + V"RepeatStat" + V"ForStat" Stat = V"IfStat" + V"DoStat" + V"WhileStat" + V"RepeatStat" + V"ForStat"
+ V"LocalStat" + V"FuncStat" + V"BreakStat" + V"LabelStat" + V"GoToStat" + V"LocalStat" + V"FuncStat" + V"BreakStat" + V"LabelStat" + V"GoToStat"
+ V"LetStat" + V"LetStat" + V"ConstStat" + V"CloseStat"
+ V"FuncCall" + V"Assignment" + V"FuncCall" + V"Assignment"
+ V"ContinueStat" + V"PushStat" + V"ContinueStat" + V"PushStat"
+ sym(";"); + sym(";");
@ -516,13 +544,18 @@ local G = { V"Lua",
LocalStat = kw("local") * expect(V"LocalFunc" + V"LocalAssign", "DefLocal"); LocalStat = kw("local") * expect(V"LocalFunc" + V"LocalAssign", "DefLocal");
LocalFunc = tagC("Localrec", kw("function") * expect(V"Id", "NameLFunc") * V"FuncBody") / fixFuncStat; LocalFunc = tagC("Localrec", kw("function") * expect(V"Id", "NameLFunc") * V"FuncBody") / fixFuncStat;
LocalAssign = tagC("Local", V"NameList" * (sym("=") * expect(V"ExprList", "EListLAssign") + Ct(Cc()))) LocalAssign = tagC("Local", V"AttributeNameList" * (sym("=") * expect(V"ExprList", "EListLAssign") + Ct(Cc())))
+ tagC("Local", V"DestructuringNameList" * sym("=") * expect(V"ExprList", "EListLAssign")); + tagC("Local", V"DestructuringNameList" * sym("=") * expect(V"ExprList", "EListLAssign"));
LetStat = kw("let") * expect(V"LetAssign", "DefLet"); LetStat = kw("let") * expect(V"LetAssign", "DefLet");
LetAssign = tagC("Let", V"NameList" * (sym("=") * expect(V"ExprList", "EListLAssign") + Ct(Cc()))) LetAssign = tagC("Let", V"NameList" * (sym("=") * expect(V"ExprList", "EListLAssign") + Ct(Cc())))
+ tagC("Let", V"DestructuringNameList" * sym("=") * expect(V"ExprList", "EListLAssign")); + tagC("Let", V"DestructuringNameList" * sym("=") * expect(V"ExprList", "EListLAssign"));
ConstStat = kw("const") * expect(V"AttributeAssign" / setAttribute("const"), "DefConst");
CloseStat = kw("close") * expect(V"AttributeAssign" / setAttribute("close"), "DefClose");
AttributeAssign = tagC("Local", V"NameList" * (sym("=") * expect(V"ExprList", "EListLAssign") + Ct(Cc())))
+ tagC("Local", V"DestructuringNameList" * sym("=") * expect(V"ExprList", "EListLAssign"));
Assignment = tagC("Set", (V"VarList" + V"DestructuringNameList") * V"BinOp"^-1 * (P"=" / "=") * ((V"BinOp" - P"-") + #(P"-" * V"Space") * V"BinOp")^-1 * V"Skip" * expect(V"ExprList", "EListAssign")); Assignment = tagC("Set", (V"VarList" + V"DestructuringNameList") * V"BinOp"^-1 * (P"=" / "=") * ((V"BinOp" - P"-") + #(P"-" * V"Space") * V"BinOp")^-1 * V"Skip" * expect(V"ExprList", "EListAssign"));
FuncStat = tagC("Set", kw("function") * expect(V"FuncName", "FuncName") * V"FuncBody") / fixFuncStat; FuncStat = tagC("Set", kw("function") * expect(V"FuncName", "FuncName") * V"FuncBody") / fixFuncStat;
@ -549,10 +582,11 @@ local G = { V"Lua",
RetStat = tagC("Return", kw("return") * commaSep(V"Expr", "RetList")^-1); RetStat = tagC("Return", kw("return") * commaSep(V"Expr", "RetList")^-1);
PushStat = tagC("Push", kw("push") * 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")); NameList = tagC("NameList", commaSep(V"Id"));
DestructuringNameList = tagC("NameList", commaSep(V"DestructuringId")), DestructuringNameList = tagC("NameList", commaSep(V"DestructuringId")),
AttributeNameList = tagC("AttributeNameList", commaSep(V"AttributeId"));
VarList = tagC("VarList", commaSep(V"VarExpr")); VarList = tagC("VarList", commaSep(V"VarExpr"));
ExprList = tagC("ExpList", commaSep(V"Expr", "ExprList")); ExprList = tagC("ExpList", commaSep(V"Expr", "ExprList"));
@ -626,8 +660,12 @@ local G = { V"Lua",
SelfId = tagC("Id", sym"@" / "self"); SelfId = tagC("Id", sym"@" / "self");
Id = tagC("Id", V"Name") + V"SelfId"; Id = tagC("Id", V"Name") + V"SelfId";
AttributeSelfId = tagC("AttributeId", sym"@" / "self" * V"Attribute"^-1);
AttributeId = tagC("AttributeId", V"Name" * V"Attribute"^-1) + V"AttributeSelfId";
StrId = tagC("String", V"Name"); StrId = tagC("String", V"Name");
Attribute = sym("<") * expect(kw"const" / "const" + kw"close" / "close", "UnknownAttribute") * expect(sym(">"), "CBracketAttribute");
-- lexer -- lexer
Skip = (V"Space" + V"Comment")^0; Skip = (V"Space" + V"Comment")^0;
Space = space^1; Space = space^1;
@ -724,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"; 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 parser = {}
local validator = require("candran.can-parser.validator") local validator = require("candran.can-parser.validator")
@ -741,4 +793,15 @@ function parser.parse (subject, filename)
return validate(ast, errorinfo) return validate(ast, errorinfo)
end 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 return parser

View file

@ -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
View 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 }

View file

@ -1,3 +1,4 @@
local candran = require("candran")
local util = {} local util = {}
function util.search(modpath, exts={}) function util.search(modpath, exts={})
@ -27,6 +28,20 @@ function util.load(str, name, env)
end end
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(...) function util.merge(...)
local r = {} local r = {}
for _, t in ipairs({...}) do for _, t in ipairs({...}) do
@ -37,4 +52,74 @@ function util.merge(...)
return r return r
end 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 return util

View file

@ -22,11 +22,17 @@ tags.Break = ()
end end
-- Unsuported features -- Unsuported features
tags.Goto = nil tags.Goto = ()
tags.Label = nil error("target "..targetName.." does not support gotos")
end
tags.Label = ()
error("target "..targetName.." does not support goto labels")
end
#placeholder("patch")
#local patch = output #local patch = output
#output = "" #output = ""
#import("compiler.luajit", { patch = patch, loadPackage = false }) #import("compiler.luajit", { preprocessorEnv = { patch = patch }, loadPackage = false })
return luajit return luajit

35
compiler/lua52.can Normal file
View file

@ -0,0 +1,35 @@
targetName = "Lua 5.2"
APPEND = (t, toAppend)
return "do" .. indent() .. "local "..var("a")..", "..var("p").." = { " .. toAppend .. " }, #" .. t .. "+1" .. newline() .. "for i=1, #"..var("a").." do" .. indent() .. t .. "["..var("p").."] = "..var("a").."[i]" .. newline() .. ""..var("p").." = "..var("p").." + 1" .. unindent() .. "end" .. unindent() .. "end"
end
tags._opid.idiv = (left, right)
return "math.floor(" .. lua(left) .. " / " .. lua(right) .. ")"
end
tags._opid.band = (left, right)
return "bit32.band(" .. lua(left) .. ", " .. lua(right) .. ")"
end
tags._opid.bor = (left, right)
return "bit32.bor(" .. lua(left) .. ", " .. lua(right) .. ")"
end
tags._opid.bxor = (left, right)
return "bit32.bxor(" .. lua(left) .. ", " .. lua(right) .. ")"
end
tags._opid.shl = (left, right)
return "bit32.lshift(" .. lua(left) .. ", " .. lua(right) .. ")"
end
tags._opid.shr = (left, right)
return "bit32.rshift(" .. lua(left) .. ", " .. lua(right) .. ")"
end
tags._opid.bnot = (right)
return "bit32.bnot(" .. lua(right) .. ")"
end
#placeholder("patch")
#local patch = output
#output = ""
#import("compiler.lua53", { preprocessorEnv = { patch = patch }, loadPackage = false })
return lua53

View file

@ -1,827 +1,18 @@
local targetName = "Lua 5.3" targetName = "Lua 5.3"
return function(code, ast, options) -- Unsuported features
--- Line mapping tags.AttributeId = (t)
local lastInputPos = 1 -- last token position in the input code if t[2] then
local prevLinePos = 1 -- last token position in the previous line of code in the input code error("target "..targetName.." does not support variable attributes")
local lastSource = options.chunkname or "nil" -- last found code source name (from the original file) else
local lastLine = 1 -- last found line number (from the original file) return t[1]
--- Newline management
local indentLevel = 0
-- Returns a newline.
local function newline()
local r = options.newline..string.rep(options.indentation, indentLevel)
if options.mapLines then
local sub = code:sub(lastInputPos)
local source, line = sub:sub(1, sub:find("\n")):match(".*%-%- (.-)%:(%d+)\n")
if source and line then
lastSource = source
lastLine = tonumber(line)
else
for _ in code:sub(prevLinePos, lastInputPos):gmatch("\n") do
lastLine += 1
end
end
prevLinePos = lastInputPos
r = " -- "..lastSource..":"..lastLine..r
end
return r
end end
-- Returns a newline and add one level of indentation.
local function indent()
indentLevel += 1
return newline()
end
-- Returns a newline and remove one level of indentation.
local function unindent()
indentLevel -= 1
return newline()
end
--- State stacks
-- Used for context-sensitive syntax.
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
}
-- Push a new value on top of the stack "name". Returns an empty string for chaining.
local function push(name, state)
table.insert(states[name], state)
return ""
end
-- Remove the value on top of the stack "name". Returns an empty string for chaining.
local function pop(name)
table.remove(states[name])
return ""
end
-- Set the value on top of the stack "name". Returns an empty string for chaining.
local function set(name, state)
states[name][#states[name]] = state
return ""
end
-- Returns the value on top of the stack "name".
local function peek(name)
return states[name][#states[name]]
end
--- Variable management
-- Returns the prefixed variable name.
local function var(name)
return options.variablePrefix..name
end
-- Returns the prefixed temporary variable name.
local function tmp()
local scope = peek("scope")
local var = "%s_%s":format(options.variablePrefix, #scope)
table.insert(scope, var)
return var
end
--- Module management
local required = {} -- { ["full require expression"] = true, ... }
local requireStr = ""
-- Add the module "mod" to the list of modules to require, and load its field "field" (or the whole module if nil) into the variable "name".
local function addRequire(mod, name, field)
local req = "require(%q)%s":format(mod, field and "."..field or "")
if not required[req] then
requireStr ..= "local %s = %s%s":format(var(name), req, options.newline)
required[req] = true
end
end
--- AST traversal helpers
local loop = { "While", "Repeat", "Fornum", "Forin", "WhileExpr", "RepeatExpr", "FornumExpr", "ForinExpr" } -- loops tags (can contain continue)
local func = { "Function", "TableCompr", "DoExpr", "WhileExpr", "RepeatExpr", "IfExpr", "FornumExpr", "ForinExpr" } -- function scope tags (can contain push)
-- Returns the first node or subnode from the list "list" which tag is in the list "tags", or nil if there were none.
-- Won't recursively follow nodes which have a tag in "nofollow".
local function any(list, tags, nofollow={})
local tagsCheck = {}
for _, tag in ipairs(tags) do
tagsCheck[tag] = true
end
local nofollowCheck = {}
for _, tag in ipairs(nofollow) do
nofollowCheck[tag] = true
end
for _, node in ipairs(list) do
if type(node) == "table" then
if tagsCheck[node.tag] then
return node
end
if not nofollowCheck[node.tag] then
local r = any(node, tags, nofollow)
if r then return r end
end
end
end
return nil
end
-- Like any, but returns a list of every node found.
-- Order: in the order of the list, from the deepest to the nearest
local function search(list, tags, nofollow={})
local tagsCheck = {}
for _, tag in ipairs(tags) do
tagsCheck[tag] = true
end
local nofollowCheck = {}
for _, tag in ipairs(nofollow) do
nofollowCheck[tag] = true
end
local found = {}
for _, node in ipairs(list) do
if type(node) == "table" then
if not nofollowCheck[node.tag] then
for _, n in ipairs(search(node, tags, nofollow)) do
table.insert(found, n)
end
end
if tagsCheck[node.tag] then
table.insert(found, node)
end
end
end
return found
end
-- Returns true if the all the nodes in list have their type in tags.
local function all(list, tags)
for _, node in ipairs(list) do
local ok = false
for _, tag in ipairs(tags) do
if node.tag == tag then
ok = true
break
end
end
if not ok then
return false
end
end
return true
end
--- Lua compiler
local tags
-- Recursively returns the compiled AST Lua code, set "forceTag" to override the tag type and pass additional arguments to the tag constructor if needed.
local function lua(ast, forceTag, ...)
if options.mapLines and ast.pos then
lastInputPos = ast.pos
end
return tags[forceTag or ast.tag](ast, ...)
end
--- Lua function calls writer
local UNPACK = (list, i, j) -- table.unpack
return "table.unpack("..list..(i and (", "..i..(j and (", "..j) or "")) or "")..")"
end
local APPEND = (t, toAppend) -- append values "toAppend" (multiple values possible) to t
return "do"..indent().."local a = table.pack("..toAppend..")"..newline().."table.move(a, 1, a.n, #"..t.."+1, "..t..")"..unindent().."end"
end
local CONTINUE_START = () -- at the start of loops using continue
return "do"..indent()
end
local CONTINUE_STOP = () -- at the start of loops using continue
return unindent().."end"..newline().."::"..var"continue".."::"
end
local DESTRUCTURING_ASSIGN = (destructured, newlineAfter=false, noLocal=false) -- to define values from a destructuring assignement
local vars = {}
local values = {}
for _, list in ipairs(destructured) do
for _, v in ipairs(list) do
local var, val
if v.tag == "Id" then
var = v
val = { tag = "Index", { tag = "Id", list.id }, { tag = "String", v[1] } }
elseif v.tag == "Pair" then
var = v[2]
val = { tag = "Index", { tag = "Id", list.id }, v[1] }
else
error("unknown destructuring element type: "..tostring(v.tag))
end
if destructured.rightOp and destructured.leftOp then
val = { tag = "Op", destructured.rightOp, var, { tag = "Op", destructured.leftOp, val, var } }
elseif destructured.rightOp then
val = { tag = "Op", destructured.rightOp, var, val }
elseif destructured.leftOp then
val = { tag = "Op", destructured.leftOp, val, var }
end
table.insert(vars, lua(var))
table.insert(values, lua(val))
end
end
if #vars > 0 then
local decl = noLocal and "" or "local "
if newlineAfter then
return decl..table.concat(vars, ", ").." = "..table.concat(values, ", ")..newline()
else
return newline()..decl..table.concat(vars, ", ").." = "..table.concat(values, ", ")
end
else
return ""
end
end
--- Tag constructors
tags = setmetatable({
-- block: { stat* } --
Block = (t)
local hasPush = peek("push") == nil and any(t, { "Push" }, func) -- push in block and push context not yet defined
if hasPush and hasPush == t[#t] then -- if the first push is the last statement, it's just a return
hasPush.tag = "Return"
hasPush = false
end
local r = push("scope", {})
if hasPush then
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
end
for i=1, #t-1, 1 do
r ..= lua(t[i])..newline()
end
if t[#t] then
r ..= lua(t[#t])
end
if hasPush and (t[#t] and t[#t].tag ~= "Return") then -- add return only if needed
r ..= newline().."return "..UNPACK(var"push")..pop("push")
end
return r..pop("scope")
end,
-- stat --
-- Do{ stat* }
Do = (t)
return "do"..indent()..lua(t, "Block")..unindent().."end"
end,
-- Set{ {lhs+} (opid? = opid?)? {expr+} }
Set = (t)
-- extract vars and values
local expr = t[#t]
local vars, values = {}, {}
local destructuringVars, destructuringValues = {}, {}
for i, n in ipairs(t[1]) do
if n.tag == "DestructuringId" then
table.insert(destructuringVars, n)
table.insert(destructuringValues, expr[i])
else
table.insert(vars, n)
table.insert(values, expr[i])
end
end
--
if #t == 2 or #t == 3 then
local r = ""
if #vars > 0 then
r = lua(vars, "_lhs").." = "..lua(values, "_lhs")
end
if #destructuringVars > 0 then
local destructured = {}
r ..= "local "..push("destructuring", destructured)..lua(destructuringVars, "_lhs")..pop("destructuring").." = "..lua(destructuringValues, "_lhs")
return r..DESTRUCTURING_ASSIGN(destructured, nil, true)
end
return r
elseif #t == 4 then
if t[3] == "=" then
local r = ""
if #vars > 0 then
r ..= lua(vars, "_lhs").." = "..lua({ t[2], vars[1], { tag = "Paren", values[1] } }, "Op")
for i=2, math.min(#t[4], #vars), 1 do
r ..= ", "..lua({ t[2], vars[i], { tag = "Paren", values[i] } }, "Op")
end
end
if #destructuringVars > 0 then
local destructured = { rightOp = t[2] }
r ..= "local "..push("destructuring", destructured)..lua(destructuringVars, "_lhs")..pop("destructuring").." = "..lua(destructuringValues, "_lhs")
return r..DESTRUCTURING_ASSIGN(destructured, nil, true)
end
return r
else
local r = ""
if #vars > 0 then
r ..= lua(vars, "_lhs").." = "..lua({ t[3], { tag = "Paren", values[1] }, vars[1] }, "Op")
for i=2, math.min(#t[4], #t[1]), 1 do
r ..= ", "..lua({ t[3], { tag = "Paren", values[i] }, vars[i] }, "Op")
end
end
if #destructuringVars > 0 then
local destructured = { leftOp = t[3] }
r ..= "local "..push("destructuring", destructured)..lua(destructuringVars, "_lhs")..pop("destructuring").." = "..lua(destructuringValues, "_lhs")
return r..DESTRUCTURING_ASSIGN(destructured, nil, true)
end
return r
end
else -- You are mad.
local r = ""
if #vars > 0 then
r ..= lua(vars, "_lhs").." = "..lua({ t[2], vars[1], { tag = "Op", t[4], { tag = "Paren", values[1] }, vars[1] } }, "Op")
for i=2, math.min(#t[5], #t[1]), 1 do
r ..= ", "..lua({ t[2], vars[i], { tag = "Op", t[4], { tag = "Paren", values[i] }, vars[i] } }, "Op")
end
end
if #destructuringVars > 0 then
local destructured = { rightOp = t[2], leftOp = t[4] }
r ..= "local "..push("destructuring", destructured)..lua(destructuringVars, "_lhs")..pop("destructuring").." = "..lua(destructuringValues, "_lhs")
return r..DESTRUCTURING_ASSIGN(destructured, nil, true)
end
return r
end
end,
-- While{ expr block }
While = (t)
local r = ""
local hasContinue = any(t[2], { "Continue" }, loop)
local lets = search({ t[1] }, { "LetExpr" })
if #lets > 0 then
r ..= "do"..indent()
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
end
r ..= "while "..lua(t[1]).." do"..indent()
if #lets > 0 then
r ..= "do"..indent()
end
if hasContinue then
r ..= CONTINUE_START()
end
r ..= lua(t[2])
if hasContinue then
r ..= CONTINUE_STOP()
end
r ..= unindent().."end"
if #lets > 0 then
for _, l in ipairs(lets) do
r ..= newline()..lua(l, "Set")
end
r ..= unindent().."end"..unindent().."end"
end
return r
end,
-- Repeat{ block expr }
Repeat = (t)
local hasContinue = any(t[1], { "Continue" }, loop)
local r = "repeat"..indent()
if hasContinue then
r ..= CONTINUE_START()
end
r ..= lua(t[1])
if hasContinue then
r ..= CONTINUE_STOP()
end
r ..= unindent().."until "..lua(t[2])
return r
end,
-- If{ (lexpr block)+ block? }
If = (t)
local r = ""
local toClose = 0 -- blocks that need to be closed at the end of the if
local lets = search({ t[1] }, { "LetExpr" })
if #lets > 0 then
r ..= "do"..indent()
toClose += 1
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
end
r ..= "if "..lua(t[1]).." then"..indent()..lua(t[2])..unindent()
for i=3, #t-1, 2 do
lets = search({ t[i] }, { "LetExpr" })
if #lets > 0 then
r ..= "else"..indent()
toClose += 1
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
else
r ..= "else"
end
r ..= "if "..lua(t[i]).." then"..indent()..lua(t[i+1])..unindent()
end
if #t % 2 == 1 then
r ..= "else"..indent()..lua(t[#t])..unindent()
end
r ..= "end"
for i=1, toClose do
r ..= unindent().."end"
end
return r
end,
-- Fornum{ ident expr expr expr? block }
Fornum = (t)
local r = "for "..lua(t[1]).." = "..lua(t[2])..", "..lua(t[3])
if #t == 5 then
local hasContinue = any(t[5], { "Continue" }, loop)
r ..= ", "..lua(t[4]).." do"..indent()
if hasContinue then
r ..= CONTINUE_START()
end
r ..= lua(t[5])
if hasContinue then
r ..= CONTINUE_STOP()
end
return r..unindent().."end"
else
local hasContinue = any(t[4], { "Continue" }, loop)
r ..= " do"..indent()
if hasContinue then
r ..= CONTINUE_START()
end
r ..= lua(t[4])
if hasContinue then
r ..= CONTINUE_STOP()
end
return r..unindent().."end"
end
end,
-- Forin{ {ident+} {expr+} block }
Forin = (t)
local destructured = {}
local hasContinue = any(t[3], { "Continue" }, loop)
local r = "for "..push("destructuring", destructured)..lua(t[1], "_lhs")..pop("destructuring").." in "..lua(t[2], "_lhs").." do"..indent()
if hasContinue then
r ..= CONTINUE_START()
end
r ..= DESTRUCTURING_ASSIGN(destructured, true)..lua(t[3])
if hasContinue then
r ..= CONTINUE_STOP()
end
return r..unindent().."end"
end,
-- Local{ {ident+} {expr+}? }
Local = (t)
local destructured = {}
local r = "local "..push("destructuring", destructured)..lua(t[1], "_lhs")..pop("destructuring")
if t[2][1] then
r ..= " = "..lua(t[2], "_lhs")
end
return r..DESTRUCTURING_ASSIGN(destructured)
end,
-- Let{ {ident+} {expr+}? }
Let = (t)
local destructured = {}
local nameList = push("destructuring", destructured)..lua(t[1], "_lhs")..pop("destructuring")
local r = "local "..nameList
if t[2][1] then
if all(t[2], { "Nil", "Dots", "Boolean", "Number", "String" }) then -- predeclaration doesn't matter here
r ..= " = "..lua(t[2], "_lhs")
else
r ..= newline()..nameList.." = "..lua(t[2], "_lhs")
end
end
return r..DESTRUCTURING_ASSIGN(destructured)
end,
-- Localrec{ {ident} {expr} }
Localrec = (t)
return "local function "..lua(t[1][1])..lua(t[2][1], "_functionWithoutKeyword")
end,
-- Goto{ <string> }
Goto = (t)
return "goto "..lua(t, "Id")
end,
-- Label{ <string> }
Label = (t)
return "::"..lua(t, "Id").."::"
end,
-- Return{ <expr*> }
Return = (t)
local push = peek("push")
if push then
local r = ""
for _, val in ipairs(t) do
r ..= push.."[#"..push.."+1] = "..lua(val)..newline()
end
return r.."return "..UNPACK(push)
else
return "return "..lua(t, "_lhs")
end
end,
-- Push{ <expr*> }
Push = (t)
local var = assert(peek("push"), "no context given for push")
r = ""
for i=1, #t-1, 1 do
r ..= var.."[#"..var.."+1] = "..lua(t[i])..newline()
end
if t[#t] then
if t[#t].tag == "Call" then
r ..= APPEND(var, lua(t[#t]))
else
r ..= var.."[#"..var.."+1] = "..lua(t[#t])
end
end
return r
end,
-- Break
Break = ()
return "break"
end,
-- Continue
Continue = ()
return "goto "..var"continue"
end,
-- apply (below)
-- expr --
-- Nil
Nil = ()
return "nil"
end,
-- Dots
Dots = ()
return "..."
end,
-- Boolean{ <boolean> }
Boolean = (t)
return tostring(t[1])
end,
-- Number{ <string> }
Number = (t)
return tostring(t[1])
end,
-- String{ <string> }
String = (t)
return "%q":format(t[1])
end,
-- Function{ { ( `ParPair{ Id expr } | `Id{ <string> } )* `Dots? } block }
_functionWithoutKeyword = (t)
local r = "("
local decl = {}
if t[1][1] then
if t[1][1].tag == "ParPair" then
local id = lua(t[1][1][1])
indentLevel += 1
table.insert(decl, "if "..id.." == nil then "..id.." = "..lua(t[1][1][2]).." end")
indentLevel -= 1
r ..= id
else
r ..= lua(t[1][1])
end
for i=2, #t[1], 1 do
if t[1][i].tag == "ParPair" then
local id = lua(t[1][i][1])
indentLevel += 1
table.insert(decl, "if "..id.." == nil then "..id.." = "..lua(t[1][i][2]).." end")
indentLevel -= 1
r ..= ", " ..id
else
r ..= ", "..lua(t[1][i])
end
end
end
r ..= ")"..indent()
for _, d in ipairs(decl) do
r ..= d..newline()
end
if t[2][#t[2]] and t[2][#t[2]].tag == "Push" then -- convert final push to return
t[2][#t[2]].tag = "Return"
end
local hasPush = any(t[2], { "Push" }, func)
if hasPush then
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
else
push("push", false) -- no push here (make sure higher push doesn't affect us)
end
r ..= lua(t[2])
if hasPush and (t[2][#t[2]] and t[2][#t[2]].tag ~= "Return") then -- add return only if needed
r ..= newline().."return "..UNPACK(var"push")
end
pop("push")
return r..unindent().."end"
end,
Function = (t)
return "function"..lua(t, "_functionWithoutKeyword")
end,
-- Table{ ( `Pair{ expr expr } | expr )* }
Pair = (t)
return "["..lua(t[1]).."] = "..lua(t[2])
end,
Table = (t)
if #t == 0 then
return "{}"
elseif #t == 1 then
return "{ "..lua(t, "_lhs").." }"
else
return "{"..indent()..lua(t, "_lhs", nil, true)..unindent().."}"
end
end,
-- TableCompr{ block }
TableCompr = (t)
return push("push", "self").."(function()"..indent().."local self = {}"..newline()..lua(t[1])..newline().."return self"..unindent().."end)()"..pop("push")
end,
-- Op{ opid expr expr? }
Op = (t)
local r
if #t == 2 then
if type(tags._opid[t[1]]) == "string" then
r = tags._opid[t[1]].." "..lua(t[2])
else
r = tags._opid[t[1]](t[2])
end
else
if type(tags._opid[t[1]]) == "string" then
r = lua(t[2]).." "..tags._opid[t[1]].." "..lua(t[3])
else
r = tags._opid[t[1]](t[2], t[3])
end
end
return r
end,
-- Paren{ expr }
Paren = (t)
return "("..lua(t[1])..")"
end,
-- MethodStub{ expr expr }
MethodStub = (t)
return "(function()"..indent() ..
"local "..var"object".." = "..lua(t[1])..newline()..
"local "..var"method".." = "..var"object".."."..lua(t[2], "Id")..newline() ..
"if "..var"method".." == nil then return nil end"..newline()..
"return function(...) return "..var"method".."("..var"object"..", ...) end"..unindent()..
"end)()"
end,
-- SafeMethodStub{ expr expr }
SafeMethodStub = (t)
return "(function()"..indent() ..
"local "..var"object".." = "..lua(t[1])..newline()..
"if "..var"object".." == nil then return nil end"..newline()..
"local "..var"method".." = "..var"object".."."..lua(t[2], "Id")..newline() ..
"if "..var"method".." == nil then return nil end"..newline()..
"return function(...) return "..var"method".."("..var"object"..", ...) end"..unindent()..
"end)()"
end,
-- statexpr (below)
-- apply (below)
-- lhs (below)
-- lexpr --
LetExpr = (t)
return lua(t[1][1])
end,
-- statexpr --
_statexpr = (t, stat)
local hasPush = any(t, { "Push" }, func)
local r = "(function()"..indent()
if hasPush then
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
else
push("push", false) -- no push here (make sure higher push don't affect us)
end
r ..= lua(t, stat)
if hasPush then
r ..= newline().."return "..UNPACK(var"push")
end
pop("push")
r ..= unindent().."end)()"
return r
end,
-- DoExpr{ stat* }
DoExpr = (t)
if t[#t].tag == "Push" then -- convert final push to return
t[#t].tag = "Return"
end
return lua(t, "_statexpr", "Do")
end,
-- WhileExpr{ expr block }
WhileExpr = (t)
return lua(t, "_statexpr", "While")
end,
-- RepeatExpr{ block expr }
RepeatExpr = (t)
return lua(t, "_statexpr", "Repeat")
end,
-- IfExpr{ (expr block)+ block? }
IfExpr = (t)
for i=2, #t do -- convert final pushes to returns
local block = t[i]
if block[#block] and block[#block].tag == "Push" then
block[#block].tag = "Return"
end
end
return lua(t, "_statexpr", "If")
end,
-- FornumExpr{ ident expr expr expr? block }
FornumExpr = (t)
return lua(t, "_statexpr", "Fornum")
end,
-- ForinExpr{ {ident+} {expr+} block }
ForinExpr = (t)
return lua(t, "_statexpr", "Forin")
end,
-- apply --
-- Call{ expr expr* }
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 == "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)..")"
else
return lua(t[1][1])..":"..lua(t[1][2], "Id").."("..lua(t, "_lhs", 2)..")"
end
else
return lua(t[1]).."("..lua(t, "_lhs", 2)..")"
end
end,
-- SafeCall{ expr expr* }
SafeCall = (t)
if t[1].tag ~= "Id" then -- side effect possible, only evaluate each expr once (or already in a safe context)
return lua(t, "SafeIndex")
else -- no side effects possible
return "("..lua(t[1]).." ~= nil and "..lua(t[1]).."("..lua(t, "_lhs", 2)..") or nil)"
end
end,
-- lhs --
_lhs = (t, start=1, newlines)
local r
if t[start] then
r = lua(t[start])
for i=start+1, #t, 1 do
r ..= ","..(newlines and newline() or " ")..lua(t[i])
end
else
r = ""
end
return r
end,
-- Id{ <string> }
Id = (t)
return t[1]
end,
-- DestructuringId{ Id | Pair+ }
DestructuringId = (t)
if t.id then -- destructing already done before, use parent variable as id
return t.id
else
local d = assert(peek("destructuring"), "DestructuringId not in a destructurable assignement")
local vars = { id = tmp() }
for j=1, #t, 1 do
table.insert(vars, t[j])
end
table.insert(d, vars)
t.id = vars.id
return vars.id
end
end,
-- Index{ expr expr }
Index = (t)
if t[1].tag == "String" or t[1].tag == "Table" then
return "("..lua(t[1])..")["..lua(t[2]).."]"
else
return lua(t[1]).."["..lua(t[2]).."]"
end
end,
-- SafeIndex{ expr expr }
SafeIndex = (t)
if t[1].tag ~= "Id" then -- side effect possible, only evaluate each expr once (or already in a safe context)
local l = {} -- list of immediately chained safeindex, from deepest to nearest (to simply generated code)
while t.tag == "SafeIndex" or t.tag == "SafeCall" do
table.insert(l, 1, t)
t = t[1]
end
local r = "(function()"..indent().."local "..var"safe".." = "..lua(l[1][1])..newline() -- base expr
for _, e in ipairs(l) do
r ..= "if "..var"safe".." == nil then return nil end"..newline()
if e.tag == "SafeIndex" then
r ..= var"safe".." = "..var"safe".."["..lua(e[2]).."]"..newline()
else
r ..= var"safe".." = "..var"safe".."("..lua(e, "_lhs", 2)..")"..newline()
end
end
r ..= "return "..var"safe"..unindent().."end)()"
return r
else -- no side effects possible
return "("..lua(t[1]).." ~= nil and "..lua(t[1]).."["..lua(t[2]).."] or nil)"
end
end,
-- opid --
_opid = {
add = "+", sub = "-", mul = "*", div = "/",
idiv = "//", mod = "%", pow = "^", concat = "..",
band = "&", bor = "|", bxor = "~", shl = "<<", shr = ">>",
eq = "==", ne = "~=", lt = "<", gt = ">", le = "<=", ge = ">=",
["and"] = "and", ["or"] = "or", unm = "-", len = "#", bnot = "~", ["not"] = "not"
}
}, {
__index = (self, key)
error("don't know how to compile a "..tostring(key).." to "..targetName)
end
})
#placeholder("patch")
local code = lua(ast)..newline()
return requireStr..code
end end
#placeholder("patch")
#local patch = output
#output = ""
#import("compiler.lua54", { preprocessorEnv = { patch = patch }, loadPackage = false })
return lua54

898
compiler/lua54.can Normal file
View file

@ -0,0 +1,898 @@
local util = require("candran.util")
local targetName = "Lua 5.4"
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
local lastSource = options.chunkname or "nil" -- last found code source name (from the original file)
local lastLine = 1 -- last found line number (from the original file)
--- Newline management
local indentLevel = 0
-- Returns a newline.
local function newline()
local r = options.newline..string.rep(options.indentation, indentLevel)
if options.mapLines then
local sub = code:sub(lastInputPos)
local source, line = sub:sub(1, sub:find("\n")):match(".*%-%- (.-)%:(%d+)\n")
if source and line then
lastSource = source
lastLine = tonumber(line)
else
for _ in code:sub(prevLinePos, lastInputPos):gmatch("\n") do
lastLine += 1
end
end
prevLinePos = lastInputPos
r = " -- "..lastSource..":"..lastLine..r
end
return r
end
-- Returns a newline and add one level of indentation.
local function indent()
indentLevel += 1
return newline()
end
-- Returns a newline and remove one level of indentation.
local function unindent()
indentLevel -= 1
return newline()
end
--- State stacks
-- Used for context-sensitive syntax.
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
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)
table.insert(states[name], state)
return ""
end
-- Remove the value on top of the stack "name". Returns an empty string for chaining.
local function pop(name)
table.remove(states[name])
return ""
end
-- Set the value on top of the stack "name". Returns an empty string for chaining.
local function set(name, state)
states[name][#states[name]] = state
return ""
end
-- Returns the value on top of the stack "name".
local function peek(name)
return states[name][#states[name]]
end
--- Variable management
-- Returns the prefixed variable name.
local function var(name)
return options.variablePrefix..name
end
-- Returns the prefixed temporary variable name.
local function tmp()
local scope = peek("scope")
local var = "%s_%s":format(options.variablePrefix, #scope)
table.insert(scope, var)
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 = ""
-- Add the module "mod" to the list of modules to require, and load its field "field" (or the whole module if nil) into the variable "name".
local function addRequire(mod, name, field)
local req = "require(%q)%s":format(mod, field and "."..field or "")
if not required[req] then
requireStr ..= "local %s = %s%s":format(var(name), req, options.newline)
required[req] = true
end
end
--- AST traversal helpers
local loop = { "While", "Repeat", "Fornum", "Forin", "WhileExpr", "RepeatExpr", "FornumExpr", "ForinExpr" } -- loops tags (can contain continue)
local func = { "Function", "TableCompr", "DoExpr", "WhileExpr", "RepeatExpr", "IfExpr", "FornumExpr", "ForinExpr" } -- function scope tags (can contain push)
-- Returns the first node or subnode from the list "list" which tag is in the list "tags", or nil if there were none.
-- Won't recursively follow nodes which have a tag in "nofollow".
local function any(list, tags, nofollow={})
local tagsCheck = {}
for _, tag in ipairs(tags) do
tagsCheck[tag] = true
end
local nofollowCheck = {}
for _, tag in ipairs(nofollow) do
nofollowCheck[tag] = true
end
for _, node in ipairs(list) do
if type(node) == "table" then
if tagsCheck[node.tag] then
return node
end
if not nofollowCheck[node.tag] then
local r = any(node, tags, nofollow)
if r then return r end
end
end
end
return nil
end
-- Like any, but returns a list of every node found.
-- Order: in the order of the list, from the deepest to the nearest
local function search(list, tags, nofollow={})
local tagsCheck = {}
for _, tag in ipairs(tags) do
tagsCheck[tag] = true
end
local nofollowCheck = {}
for _, tag in ipairs(nofollow) do
nofollowCheck[tag] = true
end
local found = {}
for _, node in ipairs(list) do
if type(node) == "table" then
if not nofollowCheck[node.tag] then
for _, n in ipairs(search(node, tags, nofollow)) do
table.insert(found, n)
end
end
if tagsCheck[node.tag] then
table.insert(found, node)
end
end
end
return found
end
-- Returns true if the all the nodes in list have their type in tags.
local function all(list, tags)
for _, node in ipairs(list) do
local ok = false
for _, tag in ipairs(tags) do
if node.tag == tag then
ok = true
break
end
end
if not ok then
return false
end
end
return true
end
--- Lua compiler
local tags
-- Recursively returns the compiled AST Lua code, set "forceTag" to override the tag type and pass additional arguments to the tag constructor if needed.
local function lua(ast, forceTag, ...)
if options.mapLines and ast.pos then
lastInputPos = ast.pos
end
return tags[forceTag or ast.tag](ast, ...)
end
--- Lua function calls writer
local UNPACK = (list, i, j) -- table.unpack
return "table.unpack("..list..(i and (", "..i..(j and (", "..j) or "")) or "")..")"
end
local APPEND = (t, toAppend) -- append values "toAppend" (multiple values possible) to t
return "do"..indent().."local "..var("a").." = table.pack("..toAppend..")"..newline().."table.move("..var("a")..", 1, "..var("a")..".n, #"..t.."+1, "..t..")"..unindent().."end"
end
local CONTINUE_START = () -- at the start of loops using continue
return "do"..indent()
end
local CONTINUE_STOP = () -- at the start of loops using continue
return unindent().."end"..newline().."::"..var"continue".."::"
end
local DESTRUCTURING_ASSIGN = (destructured, newlineAfter=false, noLocal=false) -- to define values from a destructuring assignement
local vars = {}
local values = {}
for _, list in ipairs(destructured) do
for _, v in ipairs(list) do
local var, val
if v.tag == "Id" or v.tag == "AttributeId" then
var = v
val = { tag = "Index", { tag = "Id", list.id }, { tag = "String", v[1] } }
elseif v.tag == "Pair" then
var = v[2]
val = { tag = "Index", { tag = "Id", list.id }, v[1] }
else
error("unknown destructuring element type: "..tostring(v.tag))
end
if destructured.rightOp and destructured.leftOp then
val = { tag = "Op", destructured.rightOp, var, { tag = "Op", destructured.leftOp, val, var } }
elseif destructured.rightOp then
val = { tag = "Op", destructured.rightOp, var, val }
elseif destructured.leftOp then
val = { tag = "Op", destructured.leftOp, val, var }
end
table.insert(vars, lua(var))
table.insert(values, lua(val))
end
end
if #vars > 0 then
local decl = noLocal and "" or "local "
if newlineAfter then
return decl..table.concat(vars, ", ").." = "..table.concat(values, ", ")..newline()
else
return newline()..decl..table.concat(vars, ", ").." = "..table.concat(values, ", ")
end
else
return ""
end
end
--- Tag constructors
tags = setmetatable({
-- block: { stat* } --
Block = (t)
local hasPush = peek("push") == nil and any(t, { "Push" }, func) -- push in block and push context not yet defined
if hasPush and hasPush == t[#t] then -- if the first push is the last statement, it's just a return
hasPush.tag = "Return"
hasPush = false
end
local r = push("scope", {})
if hasPush then
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
end
for i=1, #t-1, 1 do
r ..= lua(t[i])..newline()
end
if t[#t] then
r ..= lua(t[#t])
end
if hasPush and (t[#t] and t[#t].tag ~= "Return") then -- add return only if needed
r ..= newline().."return "..UNPACK(var"push")..pop("push")
end
return r..pop("scope")
end,
-- stat --
-- Do{ stat* }
Do = (t)
return "do"..indent()..lua(t, "Block")..unindent().."end"
end,
-- Set{ {lhs+} (opid? = opid?)? {expr+} }
Set = (t)
-- extract vars and values
local expr = t[#t]
local vars, values = {}, {}
local destructuringVars, destructuringValues = {}, {}
for i, n in ipairs(t[1]) do
if n.tag == "DestructuringId" then
table.insert(destructuringVars, n)
table.insert(destructuringValues, expr[i])
else
table.insert(vars, n)
table.insert(values, expr[i])
end
end
--
if #t == 2 or #t == 3 then
local r = ""
if #vars > 0 then
r = lua(vars, "_lhs").." = "..lua(values, "_lhs")
end
if #destructuringVars > 0 then
local destructured = {}
r ..= "local "..push("destructuring", destructured)..lua(destructuringVars, "_lhs")..pop("destructuring").." = "..lua(destructuringValues, "_lhs")
return r..DESTRUCTURING_ASSIGN(destructured, nil, true)
end
return r
elseif #t == 4 then
if t[3] == "=" then
local r = ""
if #vars > 0 then
r ..= lua(vars, "_lhs").." = "..lua({ t[2], vars[1], { tag = "Paren", values[1] } }, "Op")
for i=2, math.min(#t[4], #vars), 1 do
r ..= ", "..lua({ t[2], vars[i], { tag = "Paren", values[i] } }, "Op")
end
end
if #destructuringVars > 0 then
local destructured = { rightOp = t[2] }
r ..= "local "..push("destructuring", destructured)..lua(destructuringVars, "_lhs")..pop("destructuring").." = "..lua(destructuringValues, "_lhs")
return r..DESTRUCTURING_ASSIGN(destructured, nil, true)
end
return r
else
local r = ""
if #vars > 0 then
r ..= lua(vars, "_lhs").." = "..lua({ t[3], { tag = "Paren", values[1] }, vars[1] }, "Op")
for i=2, math.min(#t[4], #t[1]), 1 do
r ..= ", "..lua({ t[3], { tag = "Paren", values[i] }, vars[i] }, "Op")
end
end
if #destructuringVars > 0 then
local destructured = { leftOp = t[3] }
r ..= "local "..push("destructuring", destructured)..lua(destructuringVars, "_lhs")..pop("destructuring").." = "..lua(destructuringValues, "_lhs")
return r..DESTRUCTURING_ASSIGN(destructured, nil, true)
end
return r
end
else -- You are mad.
local r = ""
if #vars > 0 then
r ..= lua(vars, "_lhs").." = "..lua({ t[2], vars[1], { tag = "Op", t[4], { tag = "Paren", values[1] }, vars[1] } }, "Op")
for i=2, math.min(#t[5], #t[1]), 1 do
r ..= ", "..lua({ t[2], vars[i], { tag = "Op", t[4], { tag = "Paren", values[i] }, vars[i] } }, "Op")
end
end
if #destructuringVars > 0 then
local destructured = { rightOp = t[2], leftOp = t[4] }
r ..= "local "..push("destructuring", destructured)..lua(destructuringVars, "_lhs")..pop("destructuring").." = "..lua(destructuringValues, "_lhs")
return r..DESTRUCTURING_ASSIGN(destructured, nil, true)
end
return r
end
end,
-- While{ expr block }
While = (t)
local r = ""
local hasContinue = any(t[2], { "Continue" }, loop)
local lets = search({ t[1] }, { "LetExpr" })
if #lets > 0 then
r ..= "do"..indent()
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
end
r ..= "while "..lua(t[1]).." do"..indent()
if #lets > 0 then
r ..= "do"..indent()
end
if hasContinue then
r ..= CONTINUE_START()
end
r ..= lua(t[2])
if hasContinue then
r ..= CONTINUE_STOP()
end
r ..= unindent().."end"
if #lets > 0 then
for _, l in ipairs(lets) do
r ..= newline()..lua(l, "Set")
end
r ..= unindent().."end"..unindent().."end"
end
return r
end,
-- Repeat{ block expr }
Repeat = (t)
local hasContinue = any(t[1], { "Continue" }, loop)
local r = "repeat"..indent()
if hasContinue then
r ..= CONTINUE_START()
end
r ..= lua(t[1])
if hasContinue then
r ..= CONTINUE_STOP()
end
r ..= unindent().."until "..lua(t[2])
return r
end,
-- If{ (lexpr block)+ block? }
If = (t)
local r = ""
local toClose = 0 -- blocks that need to be closed at the end of the if
local lets = search({ t[1] }, { "LetExpr" })
if #lets > 0 then
r ..= "do"..indent()
toClose += 1
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
end
r ..= "if "..lua(t[1]).." then"..indent()..lua(t[2])..unindent()
for i=3, #t-1, 2 do
lets = search({ t[i] }, { "LetExpr" })
if #lets > 0 then
r ..= "else"..indent()
toClose += 1
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
else
r ..= "else"
end
r ..= "if "..lua(t[i]).." then"..indent()..lua(t[i+1])..unindent()
end
if #t % 2 == 1 then
r ..= "else"..indent()..lua(t[#t])..unindent()
end
r ..= "end"
for i=1, toClose do
r ..= unindent().."end"
end
return r
end,
-- Fornum{ ident expr expr expr? block }
Fornum = (t)
local r = "for "..lua(t[1]).." = "..lua(t[2])..", "..lua(t[3])
if #t == 5 then
local hasContinue = any(t[5], { "Continue" }, loop)
r ..= ", "..lua(t[4]).." do"..indent()
if hasContinue then
r ..= CONTINUE_START()
end
r ..= lua(t[5])
if hasContinue then
r ..= CONTINUE_STOP()
end
return r..unindent().."end"
else
local hasContinue = any(t[4], { "Continue" }, loop)
r ..= " do"..indent()
if hasContinue then
r ..= CONTINUE_START()
end
r ..= lua(t[4])
if hasContinue then
r ..= CONTINUE_STOP()
end
return r..unindent().."end"
end
end,
-- Forin{ {ident+} {expr+} block }
Forin = (t)
local destructured = {}
local hasContinue = any(t[3], { "Continue" }, loop)
local r = "for "..push("destructuring", destructured)..lua(t[1], "_lhs")..pop("destructuring").." in "..lua(t[2], "_lhs").." do"..indent()
if hasContinue then
r ..= CONTINUE_START()
end
r ..= DESTRUCTURING_ASSIGN(destructured, true)..lua(t[3])
if hasContinue then
r ..= CONTINUE_STOP()
end
return r..unindent().."end"
end,
-- Local{ {attributeident+} {expr+}? }
Local = (t)
local destructured = {}
local r = "local "..push("destructuring", destructured)..lua(t[1], "_lhs")..pop("destructuring")
if t[2][1] then
r ..= " = "..lua(t[2], "_lhs")
end
return r..DESTRUCTURING_ASSIGN(destructured)
end,
-- Let{ {ident+} {expr+}? }
Let = (t)
local destructured = {}
local nameList = push("destructuring", destructured)..lua(t[1], "_lhs")..pop("destructuring")
local r = "local "..nameList
if t[2][1] then
if all(t[2], { "Nil", "Dots", "Boolean", "Number", "String" }) then -- predeclaration doesn't matter here
r ..= " = "..lua(t[2], "_lhs")
else
r ..= newline()..nameList.." = "..lua(t[2], "_lhs")
end
end
return r..DESTRUCTURING_ASSIGN(destructured)
end,
-- Localrec{ {ident} {expr} }
Localrec = (t)
return "local function "..lua(t[1][1])..lua(t[2][1], "_functionWithoutKeyword")
end,
-- Goto{ <string> }
Goto = (t)
return "goto "..lua(t, "Id")
end,
-- Label{ <string> }
Label = (t)
return "::"..lua(t, "Id").."::"
end,
-- Return{ <expr*> }
Return = (t)
local push = peek("push")
if push then
local r = ""
for _, val in ipairs(t) do
r ..= push.."[#"..push.."+1] = "..lua(val)..newline()
end
return r.."return "..UNPACK(push)
else
return "return "..lua(t, "_lhs")
end
end,
-- Push{ <expr*> }
Push = (t)
local var = assert(peek("push"), "no context given for push")
r = ""
for i=1, #t-1, 1 do
r ..= var.."[#"..var.."+1] = "..lua(t[i])..newline()
end
if t[#t] then
if t[#t].tag == "Call" then
r ..= APPEND(var, lua(t[#t]))
else
r ..= var.."[#"..var.."+1] = "..lua(t[#t])
end
end
return r
end,
-- Break
Break = ()
return "break"
end,
-- Continue
Continue = ()
return "goto "..var"continue"
end,
-- apply (below)
-- expr --
-- Nil
Nil = ()
return "nil"
end,
-- Dots
Dots = ()
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)
return tostring(t[1])
end,
-- Number{ <string> }
Number = (t)
return tostring(t[1])
end,
-- String{ <string> }
String = (t)
return "%q":format(t[1])
end,
-- Function{ { ( `ParPair{ Id expr } | `Id{ <string> } )* `Dots? } block }
_functionWithoutKeyword = (t)
local r = "("
local decl = {}
if t[1][1] then
if t[1][1].tag == "ParPair" then
local id = lua(t[1][1][1])
indentLevel += 1
table.insert(decl, "if "..id.." == nil then "..id.." = "..lua(t[1][1][2]).." end")
indentLevel -= 1
r ..= id
else
r ..= lua(t[1][1])
end
for i=2, #t[1], 1 do
if t[1][i].tag == "ParPair" then
local id = lua(t[1][i][1])
indentLevel += 1
table.insert(decl, "if "..id.." == nil then "..id.." = "..lua(t[1][i][2]).." end")
indentLevel -= 1
r ..= ", " ..id
else
r ..= ", "..lua(t[1][i])
end
end
end
r ..= ")"..indent()
for _, d in ipairs(decl) do
r ..= d..newline()
end
if t[2][#t[2]] and t[2][#t[2]].tag == "Push" then -- convert final push to return
t[2][#t[2]].tag = "Return"
end
local hasPush = any(t[2], { "Push" }, func)
if hasPush then
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
else
push("push", false) -- no push here (make sure higher push doesn't affect us)
end
r ..= lua(t[2])
if hasPush and (t[2][#t[2]] and t[2][#t[2]].tag ~= "Return") then -- add return only if needed
r ..= newline().."return "..UNPACK(var"push")
end
pop("push")
return r..unindent().."end"
end,
Function = (t)
return "function"..lua(t, "_functionWithoutKeyword")
end,
-- Table{ ( `Pair{ expr expr } | expr )* }
Pair = (t)
return "["..lua(t[1]).."] = "..lua(t[2])
end,
Table = (t)
if #t == 0 then
return "{}"
elseif #t == 1 then
return "{ "..lua(t, "_lhs").." }"
else
return "{"..indent()..lua(t, "_lhs", nil, true)..unindent().."}"
end
end,
-- TableCompr{ block }
TableCompr = (t)
return push("push", "self").."(function()"..indent().."local self = {}"..newline()..lua(t[1])..newline().."return self"..unindent().."end)()"..pop("push")
end,
-- Op{ opid expr expr? }
Op = (t)
local r
if #t == 2 then
if type(tags._opid[t[1]]) == "string" then
r = tags._opid[t[1]].." "..lua(t[2])
else
r = tags._opid[t[1]](t[2])
end
else
if type(tags._opid[t[1]]) == "string" then
r = lua(t[2]).." "..tags._opid[t[1]].." "..lua(t[3])
else
r = tags._opid[t[1]](t[2], t[3])
end
end
return r
end,
-- Paren{ expr }
Paren = (t)
return "("..lua(t[1])..")"
end,
-- MethodStub{ expr expr }
MethodStub = (t)
return "(function()"..indent() ..
"local "..var"object".." = "..lua(t[1])..newline()..
"local "..var"method".." = "..var"object".."."..lua(t[2], "Id")..newline() ..
"if "..var"method".." == nil then return nil end"..newline()..
"return function(...) return "..var"method".."("..var"object"..", ...) end"..unindent()..
"end)()"
end,
-- SafeMethodStub{ expr expr }
SafeMethodStub = (t)
return "(function()"..indent() ..
"local "..var"object".." = "..lua(t[1])..newline()..
"if "..var"object".." == nil then return nil end"..newline()..
"local "..var"method".." = "..var"object".."."..lua(t[2], "Id")..newline() ..
"if "..var"method".." == nil then return nil end"..newline()..
"return function(...) return "..var"method".."("..var"object"..", ...) end"..unindent()..
"end)()"
end,
-- statexpr (below)
-- apply (below)
-- lhs (below)
-- lexpr --
LetExpr = (t)
return lua(t[1][1])
end,
-- statexpr --
_statexpr = (t, stat)
local hasPush = any(t, { "Push" }, func)
local r = "(function()"..indent()
if hasPush then
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
else
push("push", false) -- no push here (make sure higher push don't affect us)
end
r ..= lua(t, stat)
if hasPush then
r ..= newline().."return "..UNPACK(var"push")
end
pop("push")
r ..= unindent().."end)()"
return r
end,
-- DoExpr{ stat* }
DoExpr = (t)
if t[#t].tag == "Push" then -- convert final push to return
t[#t].tag = "Return"
end
return lua(t, "_statexpr", "Do")
end,
-- WhileExpr{ expr block }
WhileExpr = (t)
return lua(t, "_statexpr", "While")
end,
-- RepeatExpr{ block expr }
RepeatExpr = (t)
return lua(t, "_statexpr", "Repeat")
end,
-- IfExpr{ (expr block)+ block? }
IfExpr = (t)
for i=2, #t do -- convert final pushes to returns
local block = t[i]
if block[#block] and block[#block].tag == "Push" then
block[#block].tag = "Return"
end
end
return lua(t, "_statexpr", "If")
end,
-- FornumExpr{ ident expr expr expr? block }
FornumExpr = (t)
return lua(t, "_statexpr", "Fornum")
end,
-- ForinExpr{ {ident+} {expr+} block }
ForinExpr = (t)
return lua(t, "_statexpr", "Forin")
end,
-- apply --
-- Call{ expr expr* }
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)..")"
else
return lua(t[1][1])..":"..lua(t[1][2], "Id").."("..lua(t, "_lhs", 2)..")"
end
else
return lua(t[1]).."("..lua(t, "_lhs", 2)..")"
end
end,
-- SafeCall{ expr expr* }
SafeCall = (t)
if t[1].tag ~= "Id" then -- side effect possible, only evaluate each expr once (or already in a safe context)
return lua(t, "SafeIndex")
else -- no side effects possible
return "("..lua(t[1]).." ~= nil and "..lua(t[1]).."("..lua(t, "_lhs", 2)..") or nil)"
end
end,
-- lhs --
_lhs = (t, start=1, newlines)
local r
if t[start] then
r = lua(t[start])
for i=start+1, #t, 1 do
r ..= ","..(newlines and newline() or " ")..lua(t[i])
end
else
r = ""
end
return r
end,
-- Id{ <string> }
Id = (t)
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)
if t[2] then
return t[1] .. " <" .. t[2] .. ">"
else
return t[1]
end
end,
-- DestructuringId{ Id | Pair+ }
DestructuringId = (t)
if t.id then -- destructing already done before, use parent variable as id
return t.id
else
local d = assert(peek("destructuring"), "DestructuringId not in a destructurable assignement")
local vars = { id = tmp() }
for j=1, #t, 1 do
table.insert(vars, t[j])
end
table.insert(d, vars)
t.id = vars.id
return vars.id
end
end,
-- Index{ expr expr }
Index = (t)
if t[1].tag == "String" or t[1].tag == "Table" then
return "("..lua(t[1])..")["..lua(t[2]).."]"
else
return lua(t[1]).."["..lua(t[2]).."]"
end
end,
-- SafeIndex{ expr expr }
SafeIndex = (t)
if t[1].tag ~= "Id" then -- side effect possible, only evaluate each expr once (or already in a safe context)
local l = {} -- list of immediately chained safeindex, from deepest to nearest (to simply generated code)
while t.tag == "SafeIndex" or t.tag == "SafeCall" do
table.insert(l, 1, t)
t = t[1]
end
local r = "(function()"..indent().."local "..var"safe".." = "..lua(l[1][1])..newline() -- base expr
for _, e in ipairs(l) do
r ..= "if "..var"safe".." == nil then return nil end"..newline()
if e.tag == "SafeIndex" then
r ..= var"safe".." = "..var"safe".."["..lua(e[2]).."]"..newline()
else
r ..= var"safe".." = "..var"safe".."("..lua(e, "_lhs", 2)..")"..newline()
end
end
r ..= "return "..var"safe"..unindent().."end)()"
return r
else -- no side effects possible
return "("..lua(t[1]).." ~= nil and "..lua(t[1]).."["..lua(t[2]).."] or nil)"
end
end,
-- opid --
_opid = {
add = "+", sub = "-", mul = "*", div = "/",
idiv = "//", mod = "%", pow = "^", concat = "..",
band = "&", bor = "|", bxor = "~", shl = "<<", shr = ">>",
eq = "==", ne = "~=", lt = "<", gt = ">", le = "<=", ge = ">=",
["and"] = "and", ["or"] = "or", unm = "-", len = "#", bnot = "~", ["not"] = "not"
}
}, {
__index = (self, key)
error("don't know how to compile a "..tostring(key).." to "..targetName)
end
})
#placeholder("patch")
local code = lua(ast)..newline()
return requireStr..code
end

View file

@ -3,13 +3,7 @@ targetName = "LuaJIT"
UNPACK = (list, i, j) UNPACK = (list, i, j)
return "unpack(" .. list .. (i and (", " .. i .. (j and (", " .. j) or "")) or "") .. ")" return "unpack(" .. list .. (i and (", " .. i .. (j and (", " .. j) or "")) or "") .. ")"
end end
APPEND = (t, toAppend)
return "do" .. indent() .. "local a, p = { " .. toAppend .. " }, #" .. t .. "+1" .. newline() .. "for i=1, #a do" .. indent() .. t .. "[p] = a[i]" .. newline() .. "p = p + 1" .. unindent() .. "end" .. unindent() .. "end"
end
tags._opid.idiv = (left, right)
return "math.floor(" .. lua(left) .. " / " .. lua(right) .. ")"
end
tags._opid.band = (left, right) tags._opid.band = (left, right)
addRequire("bit", "band", "band") addRequire("bit", "band", "band")
return var("band") .. "(" .. lua(left) .. ", " .. lua(right) .. ")" return var("band") .. "(" .. lua(left) .. ", " .. lua(right) .. ")"
@ -39,6 +33,6 @@ end
#local patch = output #local patch = output
#output = "" #output = ""
#import("compiler.lua53", { patch = patch, loadPackage = false }) #import("compiler.lua52", { preprocessorEnv = { patch = patch }, loadPackage = false })
return lua53 return lua52

View file

@ -2,12 +2,12 @@ rockspec_format = "3.0"
package = "candran" package = "candran"
version = "0.11.0-1" version = "1.0.0-1"
description = { description = {
summary = "A simple Lua dialect and preprocessor.", summary = "A simple Lua dialect and preprocessor.",
detailed = [[ detailed = [[
Candran is a dialect of the Lua 5.3 programming language which compiles to Lua 5.3, LuaJIT and Lua 5.1 compatible code. It adds several useful syntax additions which aims to make Lua faster and easier to write, and a simple preprocessor. Candran is a dialect of the Lua 5.4 programming language which compiles to Lua 5.4, Lua 5.3, Lua 5.2, LuaJIT and Lua 5.1 compatible code. It adds several useful syntax additions which aims to make Lua faster and easier to write, and a simple preprocessor.
Unlike Moonscript, Candran tries to stay close to the Lua syntax, and existing Lua code should be able to run on Candran unmodified. Unlike Moonscript, Candran tries to stay close to the Lua syntax, and existing Lua code should be able to run on Candran unmodified.
]], ]],
license = "MIT", license = "MIT",
@ -19,13 +19,15 @@ description = {
source = { source = {
url = "git://github.com/Reuh/candran", url = "git://github.com/Reuh/candran",
tag = "v0.11.0" tag = "v1.0.0"
} }
dependencies = { dependencies = {
"lua >= 5.1", "lua >= 5.1",
"lpeglabel >= 1.5.0", "lpeglabel >= 1.5.0",
"linenoise >= 0.9" "linenoise >= 0.9",
"luacheck >= 0.23.0",
"argparse >= 0.7.0"
} }
build = { build = {
@ -34,6 +36,6 @@ build = {
candran = "candran.lua" candran = "candran.lua"
}, },
install = { install = {
bin = { "bin/can", "bin/canc" } bin = { "bin/can", "bin/canc", "bin/cancheck" }
} }
} }

View file

@ -23,7 +23,10 @@ source = {
dependencies = { dependencies = {
"lua >= 5.1", "lua >= 5.1",
"lpeglabel >= 1.5.0" "lpeglabel >= 1.5.0",
"linenoise >= 0.9",
"luacheck >= 0.23.0",
"argparse >= 0.7.0"
} }
build = { build = {
@ -32,6 +35,6 @@ build = {
candran = "candran.lua" candran = "candran.lua"
}, },
install = { install = {
bin = { "bin/can", "bin/canc" } bin = { "bin/can", "bin/canc", "bin/cancheck" }
} }
} }

View file

@ -4,6 +4,37 @@ candran.default.mapLines = false
local load = require("candran.util").load local load = require("candran.util").load
-- Text formatting
local colors = {
black = 30,
red = 31,
green = 32,
yellow = 33,
blue = 34,
purple = 35,
cyan = 36,
white = 37,
bgBlack = 40,
bgRed = 41,
bgGreen = 42,
bgYellow = 43,
bgBlue = 44,
bgPurple = 45,
bgCyan = 46,
bgWhite = 47,
bold = 1,
underline = 4
}
local function c(text, ...)
local codes = {}
for _, color in ipairs{...} do
table.insert(codes, colors[color])
end
return ("\027[%sm%s\027[0m"):format(table.concat(codes, ";"), text)
end
-- test helper -- test helper
local results = {} -- tests result local results = {} -- tests result
local function test(name, candranCode, expectedResult, options) local function test(name, candranCode, expectedResult, options)
@ -15,10 +46,10 @@ local function test(name, candranCode, expectedResult, options)
options.chunkname = name options.chunkname = name
-- make code -- make code
local success, code = pcall(candran.make, candranCode, options) local success, code = pcall(function() return assert(candran.make(candranCode, options)) end)
if not success then if not success then
self.result = "error" self.result = "error"
self.message = "/!\\ error while making code:\n"..code self.message = c("/!\\ error while making code:\n", "bold", "red")..c(code, "red")
return return
end end
@ -28,7 +59,7 @@ local function test(name, candranCode, expectedResult, options)
local success, func = pcall(load, code, nil, env) local success, func = pcall(load, code, nil, env)
if not success then if not success then
self.result = "error" self.result = "error"
self.message = "/!\\ error while loading code:\n"..func.."\ngenerated code:\n"..code self.message = c("/!\\ error while loading code:\n"..func.."\ngenerated code:\n", "bold", "red")..c(code, "red")
return return
end end
@ -36,14 +67,14 @@ local function test(name, candranCode, expectedResult, options)
local success, output = pcall(func) local success, output = pcall(func)
if not success then if not success then
self.result = "error" self.result = "error"
self.message = "/!\\ error while running code:\n"..output.."\ngenerated code:\n"..code self.message = c("/!\\ error while running code:\n"..output.."\ngenerated code:\n", "bold", "red")..c(code, "red")
return return
end end
-- check result -- check result
if output ~= expectedResult then if output ~= expectedResult then
self.result = "fail" self.result = "fail"
self.message = "/!\\ invalid result from the code; it returned "..tostring(output).." instead of "..tostring(expectedResult).."; generated code:\n"..code self.message = c("/!\\ invalid result from the code; it returned "..tostring(output).." instead of "..tostring(expectedResult).."; generated code:\n", "bold", "purple")..c(code, "purple")
return return
else else
self.result = "success" self.result = "success"
@ -96,7 +127,7 @@ return a
test("preprocessor placeholder function", [[ test("preprocessor placeholder function", [[
#placeholder('foo') #placeholder('foo')
]], 5, { foo = "return 5" }) ]], 5, { preprocessorEnv = { foo = "return 5" } })
test("preprocessor options", [[ test("preprocessor options", [[
#if not foo == "sky" then #if not foo == "sky" then
@ -105,24 +136,80 @@ test("preprocessor options", [[
return true return true
]], true, { foo = "sky" }) ]], true, { foo = "sky" })
test("preprocessor long comment", [[ test("preprocessor long comment", "--[[\n"..[[
--[[
#error("preprocessor should ignore long comments") #error("preprocessor should ignore long comments")
]].."]]"..[[ ]].."]]"..[[
return true return true
]], true) ]], true)
test("preprocessor long comment in long string", [[ test("preprocessor long comment in long string", [[
a=]].."[["..[[ a=]].."[[--[[\n"..[[
--[[
#error("preprocessor should ignore long strings") #error("preprocessor should ignore long strings")
]].."]]"..[[ ]].."]]"..[[
return a return a
]], [[ ]], "--[[\n"..[[
--[[
#error("preprocessor should ignore long strings") #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 -- -- SYNTAX ADDITIONS --
---------------------- ----------------------
@ -375,9 +462,9 @@ test("short anonymous method parsing edge cases", [[
if a() then h() end if a() then h() end
local function f (...) local function f (...)
if select('#', ...) == 1 then if select('#', ...) == 1 then
return (...) return (...)
else else
return "***" return "***"
end end
end end
return f(x) return f(x)
@ -585,6 +672,7 @@ test("table comprehension associative/self", [[
return a[1] and a[10] return a[1] and a[10]
]], true) ]], true)
test("table comprehension variable length", [[ test("table comprehension variable length", [[
local unpack = table.unpack or unpack
t1 = {"hey", "hop"} t1 = {"hey", "hop"}
t2 = {"foo", "bar"} t2 = {"foo", "bar"}
return table.concat([push unpack(t1); push unpack(t2)]) return table.concat([push unpack(t1); push unpack(t2)])
@ -1024,7 +1112,8 @@ for name, t in pairs(results) do
end end
-- print final results -- print final results
for name, count in pairs(resultCounter) do for _, name in ipairs{{"error", "red"}, {"fail", "purple"}, {"success", "green"}} do
print(count.." "..name.." (" .. math.floor((count / testCounter * 100)*100)/100 .. "%)") local count = resultCounter[name[1]] or 0
print(c(count, "bold", name[2])..c(" "..name[1].." (" .. math.floor((count / testCounter * 100)*100)/100 .. "%)", name[2]))
end end
print(testCounter.." total") print(c(testCounter.." total", "bold"))