├── .busted ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── erde-1.0.0-1.rockspec ├── erde ├── cli.erde ├── cli.lua ├── compile.erde ├── compile.lua ├── config.erde ├── config.lua ├── constants.erde ├── constants.lua ├── init.erde ├── init.lua ├── lib.erde ├── lib.lua ├── stdlib.erde ├── stdlib.lua ├── tokenize.erde ├── tokenize.lua ├── utils.erde └── utils.lua └── spec ├── api_spec.lua ├── compile_assignments_spec.lua ├── compile_blocks_spec.lua ├── compile_declarations_spec.lua ├── compile_destructure_spec.lua ├── compile_expressions_spec.lua ├── compile_functions_spec.lua ├── compile_index_chain_spec.lua ├── compile_jumps_spec.lua ├── compile_loops_spec.lua ├── compile_misc_spec.lua ├── compile_return_spec.lua ├── compile_strings_spec.lua ├── compile_tables_spec.lua ├── setup_spec.lua └── tokenize_spec.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | ROOT = { 'spec' }, 4 | lpath = './?/init.lua', 5 | helper = 'spec/setup_spec.lua', 6 | }, 7 | } 8 | 9 | -- vim: filetype=lua: 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *.rock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 4 | 5 | Versioning based on [Semantic Versioning](https://semver.org/), with suffix 6 | based on [rockspec versioning](https://github.com/luarocks/luarocks/wiki/Rockspec-format). 7 | 8 | ## [1.0.0-1] - August 11, 2023 9 | 10 | ### Changed 11 | - No longer allow keywords as index fields (generated invalid Lua code) 12 | - Loaded compiled Lua chunks now have their source names embedded in their chunkname 13 | - https://github.com/erde-lang/erde/issues/27 14 | - Operator assignments no longer cause unnecessary duplicate indexing 15 | - https://github.com/erde-lang/erde/issues/29 16 | - ex) `my_expensive_function().x += 1` will now only call `my_expensive_function` once. 17 | - ex) `a.b.c += 1` will only cause `a` to be indexed once. 18 | - Erde now transforms variable names based on the scope they were declared with. 19 | - The most recently declared scope will always take precedence 20 | - ex) `local x = 0; global x = 1; assert(x == 1)` 21 | - Erde now throws a parsing error when declaring a destructured variable with no value (ex. `local { a };`) 22 | - Erde now throws a parsing error when using `goto` with an undefined label (same behavior as Lua) 23 | 24 | ### Fixed 25 | - Fixed allowing keywords as variable names 26 | - Fixed compilation error for interpolation strings with only an escaped brace 27 | - https://github.com/erde-lang/erde/issues/24 28 | - Fixed return list failing to parse surround chars in strings 29 | - https://github.com/erde-lang/erde/issues/21 30 | - Fixed not being able to assign to the index of a function call result 31 | - https://github.com/erde-lang/erde/issues/26 32 | - Fixed parsing table string entry with only `=` 33 | - https://github.com/erde-lang/erde/issues/28 34 | - Fixed compilation of function declarations with Lua keywords 35 | - https://github.com/erde-lang/erde/issues/25 36 | - Fixed operator precedence fighting in operator assignments 37 | - https://github.com/erde-lang/erde/issues/20 38 | - Fixed `erde compile` failing when parent directories are not present 39 | - parent directories are now created recursively as needed 40 | - Fixed mismatched names for Lua keywords when accessing `_G` or `_MODULE` 41 | - https://github.com/erde-lang/erde/issues/18 42 | - Fixed `module` declarations not updating `_MODULE` 43 | - https://github.com/erde-lang/erde/issues/22 44 | - Fixed `continue` generating invalid code when targeting 5.1(+) 45 | - https://github.com/erde-lang/erde/issues/34 46 | - Fixed return lists incorrectly interpreting expression commas as list commas 47 | - For example, returning an iife with a comma in the function body 48 | - Fixed mangled stacktrace for `bit` library tail calls 49 | - https://www.freelists.org/post/luajit/Bad-stack-trace-from-lua-getstack-and-lua-getinfo,1 50 | 51 | ### Added 52 | - Erde will now throw a compiler error when targeting `5.1`, `5.1+`, or `jit`, and there are additional statements following a `break`. 53 | - Lua 5.1 / LuaJIT do not support statements following `break` (parsing error, similar to `return`) 54 | - https://github.com/erde-lang/erde/issues/34 55 | - Compiled files now include the Lua target at the bottom of the compiled file. 56 | - Added specific error checks for common mistakes 57 | - `~=` vs `!=` 58 | - `elseif` vs `else if` 59 | - The Erde version is available via the API using `erde.version` 60 | 61 | ## [0.6.0-1] - June 16, 2023 62 | 63 | ### Changed 64 | - BREAKING CHANGE: Functions no longer default to `local` 65 | - Differs too much from vanilla Lua 66 | - Unexpected behavior, especially for newer users 67 | - More confusing than helpful 68 | - Allow excluding target from `load` (i.e. allow options table as only argument) 69 | - When using the `module` keyword, users can now access the module table via `_MODULE`. 70 | - `_MODULE` is declared at the top of the module (accessible anywhere) 71 | - `module` declarations are immediately added to the `_MODULE` table 72 | 73 | ### Fixed 74 | - Fixed throwing object when failing to load compiled code 75 | - Fixed erde loaders not handling multiple returns (package loader && `erde.run`) 76 | - Fixed incorrect source map when code starts with newlines 77 | - Fixed incorrect source map for function call statements 78 | - `erde.traceback` now properly handles non-string messages 79 | - message is returned immediately without further processing 80 | - same behavior as Lua's native `debug.traceback` 81 | - The bitwise unary `~` operator now properly uses native Lua for 5.3+ (previously always used bitlib) 82 | - Erde now properly reports error lines as compiled error lines when there is no sourcemap 83 | - Erde now properly injects the compile footer for empty files 84 | - Fixed not throwing error when using both `return` and `module` 85 | - Fixed `erde` (REPL) and `erde run` not respecting `--target` (https://github.com/erde-lang/erde/issues/11) 86 | - Fixed global destructuring (https://github.com/erde-lang/erde/issues/14) 87 | - Global declarations now directly mutate `_G` (https://github.com/erde-lang/erde/issues/15) 88 | - Fixed compilation of Lua (but not Erde) keyword (https://github.com/erde-lang/erde/issues/8) 89 | - Fixed greedy tokenization of `\` in block strings (error when tokenizing `[[\]]`) 90 | 91 | ### Added 92 | - Allow passing in options in `erde.compile` 93 | - Allow disabling caching source maps 94 | - Added `erde sourcemap` command to CLI help 95 | 96 | ## [0.5.0-1] - April 01, 2023 97 | 98 | ### Removed 99 | - BREAKING CHANGE: Removed `try...catch` statements 100 | - `pcall` paradigm suffices, `try...catch` statement in practice was not _that_ useful 101 | - Do not want to split code styles on `pcall` vs `try...catch` 102 | - Removed `erdeXXX` executables (`erdejit`, `erde5.1`, etc). 103 | - Did not realize luarocks wraps these Lua scripts and forces their running Lua version 104 | - Not as useful now that `erde.load` automatically infers the target 105 | - Polluted path a little bit (lots of executables, especially when installing for multiple versions) 106 | - Removed `-d`, `--debug` cli option 107 | 108 | ### Changed 109 | - Erde now allows all number forms for all Lua targets and will transpile numbers to decimal format (including newly added binary literals). 110 | - `erde` cli no longer requires the subcommand to appear directly after `erde` (flags in between are accepted) 111 | - `erde` cli defaults `compile` and `clean` args to the current directory. 112 | - Erde now throws an error when trying to call `load` with an invalid Lua target. 113 | - Erde now overrides Lua's `debug.traceback` on `load`. 114 | - Can be disabled in `load` options using `keep_traceback` 115 | - Lua's native `debug.traceback` is restored when calling `unload` 116 | - Erde now infers a version for `load` automatically based on `_VERSION` when one is not specified. 117 | - Erde no longer catches and rethrows errors internally. 118 | - Previously used custom rethrown data type w/ `__tostring` metamethod, (overcomplicated, not user friendly) 119 | - Catching and rethrowing _changes the call stack_. Want to avoid this, as it causes confusion when debugging. 120 | - Still cannot handle the case when _callbacks_ written in Erde but run in Lua throw errors. 121 | - Easier / simplest to have user handle error rewriting themselves at top level 122 | - Often times this will be automatically handled since we override `debug.traceback` on `load` 123 | - `erde` cli will still rewrite errors (repl and `erde run`) 124 | 125 | ### Fixed 126 | - Fixed compiling of chained function calls (ex. `myfunc()()`) 127 | - Erde now properly ignores escape sequences (except interpolation) in block strings (`[[ ... ]]`) 128 | - Fixed `erde` compile / clean checks for empty Lua files (`is_compiled_file`) 129 | - Erde now properly rewrites source _references_ in error messages (ex. `xxx: in function `) 130 | - Erde now properly checks for invalid varargs for arrow functions with implicit params 131 | - Erde now properly injects the erde package loader when using the repl. 132 | 133 | ### Added 134 | - Allow binary literals (ex. `print(0b100)`) for all Lua targets. 135 | - Allow strings to act as index chain bases without parentheses. 136 | - ex) `'mystring':find('my')` 137 | - Compiled files now include the version of Erde used at the bottom of the compiled file. 138 | - Allow specifying bitlib in `load` options 139 | - Added `sourcemap` subcommand for debugging 140 | 141 | ## [0.4.0-1] - January 7, 2023 142 | 143 | ### Changed 144 | - CLI options now use all lowercase (more standard) 145 | - `--outDir` is now `--outdir` 146 | - `--bitLib` is now `--bitlib` 147 | - Readline history no longer saves empty lines 148 | - Parser now properly checks varargs scope (cannot be used outside vararg function) 149 | 150 | ### Fixed 151 | - Fixed `module` declarations with destructuring 152 | - Long strings no longer throw errors for unrecognized escape chars (same behavior as Lua) 153 | - Fixed determining ReturnList variants 154 | - Fixed compile error when return arrow function `return () -> { ... }` 155 | - Fixed readline history not saving 156 | - Fixed ambiguous iife syntax in compiled Lua (inject semicolon) 157 | - CLI now checks one extra character when looking for the compiled footer comment (handle trailing newlines injected by editor) 158 | 159 | ### Added 160 | - Added versioned executables to allow specifying the underlying lua executable version. Each additionally sets `--target` appropriately. 161 | - `erde5.1` - sets `--target 5.1` and uses `lua5.1` executable 162 | - `erde5.2` - sets `--target 5.2` and uses `lua5.2` executable 163 | - `erde5.3` - sets `--target 5.3` and uses `lua5.3` executable 164 | - `erde5.4` - sets `--target 5.4` and uses `lua5.4` executable 165 | - `erdejit` - sets `--target jit` and uses `luajit` executable 166 | - Any arguments following a script to be run are now accessible in the script via the `arg` global (same behavior as Lua) 167 | - ex) `erde myscript.erde "myscript_arg1"` 168 | - CLI now supports overwriting existing Lua files when compiling with `-f, --force` 169 | - CLI now supports printing compiled code instead of writing to files with `-p, --print` (useful for debugging) 170 | 171 | ## [0.3.0-2] - November 7, 2022 172 | 173 | ### Removed 174 | - Erde no longer depends on [argparse](https://luarocks.org/modules/argparse/argparse) 175 | 176 | ### Changed 177 | - `erde run` no longer uses a subcommand 178 | - `erde compile` and `erde clean` no longer default to cwd 179 | - Improved error messages and error handling 180 | 181 | ### Fixed 182 | - Fix assignment operator compiled operation 183 | - Fixed compilation error rewriting for asserts 184 | - Fixed compilation of exponentiation operator for pre Lua5.3 185 | - Fixed empty message when error does not have stacktrace 186 | - Fixed `C.BITLIB` vs `C.BIT_LIB` inconsistencies 187 | - Fixed empty file compilation 188 | - Erde now substitutes variable names that are keywords in Lua but not in Erde (previously produced invalid Lua code) 189 | - Erde now reports error lines at the start of the error (previously reported _after_) 190 | - Erde now correctly preserves newlines following interpolations in long strings. 191 | - Erde now correctly rewrites top level errors (previously always used main file source map) 192 | 193 | ### Added 194 | - Added __tostring metamethod for thrown table errors (especially useful for sandboxed, embedded lua such as neovim) 195 | - Added line numbers for errors when using `erde compile` 196 | - Added REPL support 197 | - Throw error when `return` statement is not the last statement in a block 198 | 199 | ## [0.3.0-1] - August 26, 2022 200 | 201 | ### Removed 202 | - Removed `do` expressions. 203 | - Removed spread operator. 204 | - Removed optional chaining. 205 | - Removed `erde.loader` (replaced by `require('erde').load` api) 206 | 207 | ### Changed 208 | - Reverted split of `erde` and `erdec` in favor of more `pacman` like "main flags". 209 | - Improved `erde --help` output. 210 | - `erde` now runs with the regular lua shebang (`#!/usr/bin/env lua` instead of `#!/usr/bin/env luajit`) 211 | 212 | ### Added 213 | - Erde now supports running on Lua 5.1+ 214 | - `erde` now accepts Lua targets to compile to, with specific Lua version compatabilities 215 | - `erde` now accepts specifying a bit library to compile bit operations to 216 | - Erde now generates souce maps and will rewrite errors when running scripts via the CLI or using `erde.load`. 217 | - Several new apis have been added both to replace `erde.loader` and to allow for better error handling 218 | - `erde.rewrite` - rewrite Lua errors using a source map. Does a best-effort 219 | lookup for cached source map when one is not provided 220 | - `erde.traceback` - erde version of `debug.traceback` w/ line rewrites 221 | - `erde.load` - replacement for `erde.loader`, with an optional lua target 222 | as a parameter. 223 | - `erde.unload` - api to remove the injected erde loader (from a previous call 224 | to `erde.load`). 225 | 226 | ## [0.2.0-1] - June 03, 2022 227 | 228 | ### Removed 229 | - Removed `self` shorthand `$`. Completely unnecessary and confusing. 230 | - Removed `main` keyword. Completely unnecessary and confusing. 231 | - Removed ternary / null coalescing operator 232 | - Ternary created ambiguous syntax (`a ? b:c() : d()` vs `a ? b : c():d()`) 233 | - Both difficult to optimize (requires iife) 234 | 235 | ### Changed 236 | - Refactored internal structure (now cleaner / faster) 237 | - Erde now uses a newline to differentiate syntax ambiguity 238 | - Erde no longer parses number destruct aliases as valid syntax 239 | - Varargs now spreads when used as a table or param expression. 240 | - Erde no longer allows trailing commas in Return expressions unless parentheses are present 241 | - `erde.loader` no longer mutates the global require function, but uses `package.loaders` (as it should) 242 | - `catch` var no longer uses parentheses (more lua like) 243 | - `catch` var can now be a destructure 244 | - Array destructures can no longer be nested inside map destructures 245 | - String interpolation is no longer supported for single quote strings 246 | - The `erde` executable has been split into `erde` (interpreter) and `erdec` (compiler) 247 | - `argparse` does not allow arguments if subcommands are used, which means we 248 | could not do: `erde myfile.erde` to run a file. This was very unfriendly to 249 | scripts that may want to use a shebang with `erde`. 250 | - Use a similar structure as moonscript for familiarity. 251 | 252 | ### Fixed 253 | - Tokenizer now correctly consumes whitespace in string interpolation. 254 | - String interpolation w/ names now compiles correctly. 255 | - Parenthesized Return now parses correctly. 256 | - Keywords are now allowed as named index chains (ex: x.if, y:else()). 257 | - `!=` operator is now _actually_ compiled. 258 | - OptChain now correctly parses optional _method_ calls 259 | - Fixed void `return` (i.e. no expressions) 260 | - Parser / compiler no longer crash when the ast is empty 261 | 262 | ## [0.1.0-1] - March 3, 2022 263 | 264 | Initial Release 265 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Brian Sutherland 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LUA_TARGET_TAGS_JIT := "jit,5.1%+" 2 | LUA_TARGET_TAGS_5.1+ := "5.1%+" 3 | LUA_TARGET_TAGS_5.1 := "5.1,$(LUA_TARGET_TAGS_5.1+)" 4 | LUA_TARGET_TAGS_5.2+ := "5.2%+,$(LUA_TARGET_TAGS_5.1+)" 5 | LUA_TARGET_TAGS_5.2 := "5.2,$(LUA_TARGET_TAGS_5.2+)" 6 | LUA_TARGET_TAGS_5.3+ := "5.3%+,$(LUA_TARGET_TAGS_5.2+)" 7 | LUA_TARGET_TAGS_5.3 := "5.3,$(LUA_TARGET_TAGS_5.3+)" 8 | LUA_TARGET_TAGS_5.4+ := "5.4%+,$(LUA_TARGET_TAGS_5.3+)" 9 | LUA_TARGET_TAGS_5.4 := "5.4,$(LUA_TARGET_TAGS_5.4+)" 10 | 11 | .PHONY: default build test release 12 | 13 | define runtest 14 | $(eval LUA_EXECUTABLE = $(1)) 15 | $(eval LUA_TARGET = $(2)) 16 | $(eval LUA_TARGET_TAGS = $(LUA_TARGET_TAGS_$(shell echo $(LUA_TARGET) | tr a-z A-Z))) 17 | @echo "targeting $(LUA_TARGET) on $(LUA_EXECUTABLE)" 18 | @LUA_TARGET="$(LUA_TARGET)" busted --lua="/usr/bin/$(LUA_EXECUTABLE)" --tags="$(LUA_TARGET_TAGS)" 19 | @echo 20 | endef 21 | 22 | default: | build test 23 | 24 | build: 25 | erde compile erde 26 | stylua erde 27 | 28 | test: 29 | @echo 30 | $(call runtest,luajit,jit) 31 | $(call runtest,luajit,5.1+) 32 | $(call runtest,lua5.1,5.1) 33 | $(call runtest,lua5.1,5.1+) 34 | $(call runtest,lua5.2,5.2) 35 | $(call runtest,lua5.2,5.1+) 36 | $(call runtest,lua5.2,5.2+) 37 | $(call runtest,lua5.3,5.3) 38 | $(call runtest,lua5.3,5.1+) 39 | $(call runtest,lua5.3,5.2+) 40 | $(call runtest,lua5.3,5.3+) 41 | $(call runtest,lua5.4,5.4) 42 | $(call runtest,lua5.4,5.1+) 43 | $(call runtest,lua5.4,5.2+) 44 | $(call runtest,lua5.4,5.3+) 45 | $(call runtest,lua5.4,5.4+) 46 | 47 | release: 48 | @echo '- update version:' 49 | @echo ' `mv erde-a.b-c.rockspec erde-x.y-z.rockspec`' 50 | @echo ' `sed -iE "s/a.b-c/x.y-z/" erde/constants.lua erde-a.b-c.rockspec`' 51 | @echo '- update changelog UNRELEASED => today' 52 | @echo '- commit version update changes' 53 | @echo '- create git tag:' 54 | @echo ' `git tag -a x.y-z -m x.y-z`' 55 | @echo ' `git push origin x.y-z`' 56 | @echo '- create rock: `luarocks pack erde-x.y-z.rockspec`' 57 | @echo '- upload rock: `luarocks upload erde-x.y-z.rockspec --api-key=`' 58 | @echo '- create github release' 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erde 2 | 3 | [https://erde-lang.github.io/](https://erde-lang.github.io/) 4 | 5 | Erde is a programming language that compiles to Lua. It uses a more symbol 6 | favored syntax (similar to languages such as Rust, Golang, JavaScript, etc) and 7 | has been designed to map very closely to Lua. 8 | 9 | ## Installation 10 | 11 | The recommended way to install is through [luarocks](https://luarocks.org/modules/bsuth/erde): 12 | 13 | ```bash 14 | luarocks install erde 15 | ``` 16 | 17 | Alternatively you can clone this repo and update your 18 | [LUA_PATH](https://www.lua.org/pil/8.1.html) accordingly: 19 | 20 | ```bash 21 | git clone https://github.com/erde-lang/erde.git 22 | ERDE_ROOT="$(pwd)/erde" 23 | export LUA_PATH="$ERDE_ROOT/?.lua;$ERDE_ROOT/?/init.lua;$LUA_PATH" 24 | 25 | # To use the CLI: 26 | alias erde="lua $ERDE_ROOT/cli.lua" 27 | ``` 28 | 29 | You can check whether Erde is installed correctly by running: 30 | 31 | ```bash 32 | erde -h 33 | ``` 34 | 35 | ## Similar Projects 36 | 37 | There is a comprehensive list of [languages that compile to Lua](https://github.com/hengestone/lua-languages), 38 | but the following are some languages that Erde took inspiration from: 39 | 40 | - [moonscript](https://moonscript.org): A programmer friendly language that compiles into Lua 41 | - [fennel](https://fennel-lang.org): A lisp that compiles to Lua 42 | - [teal](https://github.com/teal-language/tl): A typed dialect of Lua 43 | 44 | ## License 45 | 46 | Copyright 2023 Brian Sutherland 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining a copy of 49 | this software and associated documentation files (the "Software"), to deal in 50 | the Software without restriction, including without limitation the rights to 51 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 52 | of the Software, and to permit persons to whom the Software is furnished to do 53 | so, subject to the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be included in all 56 | copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 59 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 60 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 61 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 62 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 63 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 64 | SOFTWARE. 65 | -------------------------------------------------------------------------------- /erde-1.0.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'erde' 2 | version = '1.0.0-1' 3 | rockspec_format = '3.0' 4 | 5 | description = { 6 | summary = 'A modern Lua dialect', 7 | detailed = [[ 8 | Erde is an programming language that compiles to Lua. It uses a more symbol 9 | favored syntax (similar to languages such as Rust, Golang, JavaScript, etc) 10 | and has been designed to map very closely to Lua. 11 | ]], 12 | homepage = 'https://erde-lang.github.io/', 13 | license = 'MIT', 14 | } 15 | 16 | dependencies = { 17 | 'lua >= 5.1, <= 5.4', 18 | 'luafilesystem', 19 | } 20 | 21 | source = { 22 | url = 'git://github.com/erde-lang/erde', 23 | tag = '1.0.0-1', 24 | } 25 | 26 | build = { 27 | type = 'builtin', 28 | install = { 29 | bin = { 30 | ['erde'] = 'erde/cli.lua', 31 | } 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /erde/cli.erde: -------------------------------------------------------------------------------- 1 | local lfs = require('lfs') 2 | local compile = require('erde.compile') 3 | local config = require('erde.config') 4 | local { COMPILED_FOOTER_COMMENT, VALID_LUA_TARGETS, VERSION } = require('erde.constants') 5 | local lib = require('erde.lib') 6 | local { io, string } = require('erde.stdlib') 7 | local { ensure_path_parents, join_paths } = require('erde.utils') 8 | 9 | local unpack = table.unpack || unpack 10 | local pack = table.pack || (...) -> ({ n = select("#", ...), ... }) 11 | 12 | local REPL_PROMPT = '> ' 13 | local REPL_SUB_PROMPT = '>> ' 14 | local HAS_READLINE, RL = pcall(() -> require('readline')) 15 | 16 | local SUBCOMMANDS = { 17 | compile = true, 18 | clean = true, 19 | sourcemap = true, 20 | } 21 | 22 | -- ----------------------------------------------------------------------------- 23 | -- State 24 | -- ----------------------------------------------------------------------------- 25 | 26 | -- The index of the current arg we are processing 27 | local current_arg_index = 1 28 | 29 | -- The total number of arguments passed to this script. Stored solely as an 30 | -- optimization. 31 | local num_args = #arg 32 | 33 | -- A table of processed flags / args. 34 | local cli = {} 35 | 36 | -- A modified version of Lua's native `arg` to mimic the arg positions when 37 | -- running the `erde` executable. This overrides `arg` when we are running an 38 | -- `.erde` script in case the script tries to access `arg` itself. 39 | local script_args = {} 40 | 41 | -- ----------------------------------------------------------------------------- 42 | -- Helpers 43 | -- ----------------------------------------------------------------------------- 44 | 45 | local function terminate(message, status = 1) { 46 | print(message) 47 | os.exit(status) 48 | } 49 | 50 | local function parse_option(label) { 51 | current_arg_index = current_arg_index + 1 52 | local arg_value = arg[current_arg_index] 53 | 54 | if !arg_value { 55 | terminate("Missing argument for { label }") 56 | } 57 | 58 | return arg_value 59 | } 60 | 61 | local function ensure_path_parents(path) { 62 | local path_parts = string.split(path, PATH_SEPARATOR) 63 | 64 | for i = 1, #path_parts - 1 { 65 | local parent_path = table.concat(path_parts, PATH_SEPARATOR, 1, i) 66 | 67 | if !io.exists(parent_path) { 68 | lfs.mkdir(parent_path) 69 | } 70 | } 71 | } 72 | 73 | local function traverse(paths, pattern, callback) { 74 | for _, path in ipairs(paths) { 75 | local attributes = lfs.attributes(path) 76 | 77 | if attributes == nil { 78 | continue 79 | } 80 | 81 | if attributes.mode == 'file' { 82 | if path:match(pattern) { 83 | callback(path, attributes) 84 | } 85 | } elseif attributes.mode == 'directory' { 86 | local subpaths = {} 87 | 88 | for filename in lfs.dir(path) { 89 | if filename != '.' && filename != '..' { 90 | table.insert(subpaths, join_paths(path, filename)) 91 | } 92 | } 93 | 94 | traverse(subpaths, pattern, callback) 95 | } 96 | } 97 | } 98 | 99 | -- Check whether a file has been generated from `erde` by checking if the file 100 | -- ends with `COMPILED_FOOTER_COMMENT`. 101 | local function is_compiled_file(path) { 102 | local file = io.open(path, 'r') 103 | 104 | if file == nil { 105 | return false 106 | } 107 | 108 | -- Some editors save an invisible trailing newline, so read an extra char just 109 | -- in case. 110 | local read_len = #COMPILED_FOOTER_COMMENT + 1 111 | 112 | file:seek('end', -read_len) 113 | local footer = file:read(read_len) 114 | file:close() 115 | 116 | return !!(footer && footer:find(COMPILED_FOOTER_COMMENT)) 117 | } 118 | 119 | -- ----------------------------------------------------------------------------- 120 | -- Help 121 | -- ----------------------------------------------------------------------------- 122 | 123 | local HELP = [[ 124 | Usage: erde [command] [args] 125 | 126 | Commands: 127 | compile Compile Erde files into Lua. 128 | clean Remove generated Lua files. 129 | sourcemap Map a compiled (Lua) line to a source (Erde) line. 130 | 131 | Options: 132 | -h, --help Show this help message and exit. 133 | -v, --version Show version and exit. 134 | --bitexpr Expression to use for bit operations. 135 | -b, --bitlib Library to use for bit operations. 136 | DEPRECATED: Use --bitexpr instead. 137 | -t, --target Lua target for version compatability. 138 | Must be one of: { table.concat(VALID_LUA_TARGETS, ', ') } 139 | 140 | Compile Options: 141 | -o, --outdir Output directory for compiled files. 142 | -w, --watch Watch files and recompile on change. 143 | -f, --force Force rewrite existing Lua files with compiled files. 144 | -p, --print Print compiled code instead of writing to files. 145 | 146 | Examples: 147 | erde 148 | Launch the REPL. 149 | 150 | erde my_script.erde 151 | Run my_script.erde. 152 | 153 | erde compile my_script.erde 154 | Compile my_script.erde (into my_script.lua). 155 | 156 | erde compile . 157 | Compile all *.erde files under the current directory. 158 | 159 | erde compile src -o dest 160 | Compile all *.erde files in src and place the *.lua files under dest. 161 | 162 | erde clean my_script.lua 163 | Remove my_script.lua if and only if it has been generated by `erde compile`. 164 | 165 | erde clean . 166 | Remove all generated *.lua files under the current directory. 167 | 168 | erde sourcemap my_script.erde 114 169 | Lookup which line in my_script.erde generated line 114 in my_script.lua. 170 | ]] 171 | 172 | -- ----------------------------------------------------------------------------- 173 | -- Run Command 174 | -- ----------------------------------------------------------------------------- 175 | 176 | -- IMPORTANT: THIS IS AN ERDE SOURCE LOADER AND MUST ADHERE TO THE USAGE SPEC OF 177 | -- `__erde_internal_load_source__`! 178 | local function run_command() { 179 | lib.load(cli.target) 180 | 181 | -- Replace Lua's global args with what the script expects as if it were run 182 | -- from the Lua VM directly. 183 | arg = script_args 184 | 185 | local ok, result = xpcall(() -> { 186 | local source = io.readfile(cli.script) 187 | local result = lib.__erde_internal_load_source__(source, { alias = cli.script }) 188 | return result 189 | }, lib.traceback) 190 | 191 | if !ok { 192 | terminate('erde: ' .. result) 193 | } 194 | } 195 | 196 | -- ----------------------------------------------------------------------------- 197 | -- Compile Command 198 | -- ----------------------------------------------------------------------------- 199 | 200 | local function compile_file(path) { 201 | local compile_path = path:gsub('%.erde$', '.lua') 202 | 203 | if cli.outdir { 204 | compile_path = cli.outdir .. '/' .. compile_path 205 | } 206 | 207 | if !cli.print_compiled && !cli.force { 208 | if io.exists(compile_path) && !is_compiled_file(compile_path) { 209 | print("{ path } => ERROR") 210 | print("Cannot write to { compile_path }: file already exists") 211 | return false 212 | } 213 | } 214 | 215 | local ok, result = pcall(() -> compile(io.readfile(path), { alias = path })) 216 | 217 | if !ok { 218 | print("{ path } => ERROR") 219 | 220 | if type(result == 'table') && result.line { 221 | print("erde:{ result.line }: { result.message }") 222 | } else { 223 | print("erde: { result }") 224 | } 225 | 226 | return false 227 | } 228 | 229 | if cli.print_compiled { 230 | print(path) 231 | print(('-'):rep(#path)) 232 | print(result) 233 | } else { 234 | ensure_path_parents(compile_path) 235 | io.writefile(compile_path, result) 236 | 237 | if cli.watch { 238 | print("[{ os.date('%X') }] { path } => { compile_path }") 239 | } else { 240 | print("{ path } => { compile_path }") 241 | } 242 | } 243 | 244 | return true 245 | } 246 | 247 | local function watch_files(cli) { 248 | local modifications = {} 249 | local poll_interval = 1 -- seconds 250 | 251 | local has_socket, socket = pcall(() -> require('socket')) 252 | local has_posix, posix = pcall(() -> require('posix.unistd')) 253 | 254 | if !has_socket && !has_posix { 255 | print(table.concat({ 256 | 'WARNING: No libraries with sleep functionality found. This may ', 257 | 'cause high CPU usage while watching. To fix this, you can install ', 258 | 'either LuaSocket (https://luarocks.org/modules/luasocket/luasocket) ', 259 | 'or luaposix (https://luarocks.org/modules/gvvaughan/luaposix)\n', 260 | })) 261 | } 262 | 263 | while true { 264 | traverse(cli, '%.erde$', (path, attributes) -> { 265 | if !modifications[path] || modifications[path] != attributes.modification { 266 | modifications[path] = attributes.modification 267 | compile_file(path, cli) 268 | } 269 | }) 270 | 271 | if has_socket { 272 | socket.sleep(poll_interval) 273 | } elseif has_posix { 274 | posix.sleep(poll_interval) 275 | } else { 276 | local last_timeout = os.time() 277 | repeat {} until os.time() - last_timeout > poll_interval 278 | } 279 | } 280 | } 281 | 282 | local function compile_command() { 283 | if #cli == 0 { 284 | table.insert(cli, '.') 285 | } 286 | 287 | if cli.watch { 288 | -- Use pcall to catch SIGINT 289 | pcall(() -> watch_files(cli)) 290 | } else { 291 | traverse(cli, '%.erde$', path -> { 292 | if !compile_file(path, cli) { 293 | os.exit(1) 294 | } 295 | }) 296 | } 297 | } 298 | 299 | -- ----------------------------------------------------------------------------- 300 | -- Clean Command 301 | -- ----------------------------------------------------------------------------- 302 | 303 | local function clean_command() { 304 | if #cli == 0 { 305 | table.insert(cli, '.') 306 | } 307 | 308 | traverse(cli, '%.lua$', path -> { 309 | if is_compiled_file(path) { 310 | os.remove(path) 311 | print("{ path } => DELETED") 312 | } 313 | }) 314 | } 315 | 316 | -- ----------------------------------------------------------------------------- 317 | -- Sourcemap Command 318 | -- ----------------------------------------------------------------------------- 319 | 320 | local function sourcemap_command() { 321 | local path, line = cli[1], cli[2] 322 | 323 | if path == nil { 324 | terminate('Missing erde file to map') 325 | } elseif line == nil { 326 | terminate('Missing line number to map') 327 | } 328 | 329 | local ok, result, source_map = pcall(() -> compile(io.readfile(path), { alias = path })) 330 | 331 | if ok { 332 | print("{ line } => { source_map[tonumber(line)] }") 333 | } else { 334 | print("Failed to compile { path }") 335 | 336 | if type(result == 'table') && result.line { 337 | print("erde:{ result.line }: { result.message}") 338 | } else { 339 | print("erde: { result }") 340 | } 341 | } 342 | } 343 | 344 | -- ----------------------------------------------------------------------------- 345 | -- REPL Command 346 | -- ----------------------------------------------------------------------------- 347 | 348 | local function readline(prompt) { 349 | if HAS_READLINE { 350 | return RL.readline(prompt) 351 | } else { 352 | io.write(prompt) 353 | return io.read() 354 | } 355 | } 356 | 357 | local function repl() { 358 | print("Erde { VERSION } on { _VERSION } -- Copyright (C) 2021-2023 bsuth") 359 | 360 | if !HAS_READLINE { 361 | print('Install the `readline` Lua library to get support for arrow keys, keyboard shortcuts, history, etc.') 362 | } 363 | 364 | while true { 365 | local ok, result 366 | local source = readline(REPL_PROMPT) 367 | 368 | -- Readline returns the string '(null)' on for some reason. 369 | if !source || (HAS_READLINE && source == '(null)') { 370 | break 371 | } 372 | 373 | repeat { 374 | -- Try input as an expression first! This way we can still print the value 375 | -- in the case that the expression is also a valid block (i.e. function calls). 376 | -- 377 | -- Pack results so we know how many were actually returned, even if there 378 | -- are nils among them. 379 | ok, result = pcall(() -> pack(lib.run("return { source }", { alias = 'stdin' }))) 380 | 381 | if !ok && type(result) == 'string' && !result:find('unexpected eof') { 382 | -- Try input as a block 383 | ok, result = pcall(() -> pack(lib.run(source, { alias = 'stdin' }))) 384 | } 385 | 386 | if !ok && type(result) == 'string' && result:find('unexpected eof') { 387 | repeat { 388 | local subsource = readline(REPL_SUB_PROMPT) 389 | source ..= subsource || '' 390 | } until subsource 391 | } 392 | } until ok || type(result) != 'string' || !result:find('unexpected eof') 393 | 394 | if !ok { 395 | print(lib.rewrite(result)) 396 | } elseif result.n > 0 { 397 | for i = 1, result.n { 398 | -- explicitly call `tostring()` so nils are also stringified 399 | result[i] = tostring(result[i]) 400 | } 401 | print(unpack(result)) 402 | } 403 | 404 | if HAS_READLINE && string.trim(source) != '' { 405 | RL.add_history(source) 406 | } 407 | } 408 | } 409 | 410 | local function repl_command() { 411 | lib.load(cli.target) 412 | 413 | if HAS_READLINE { 414 | RL.set_readline_name('erde') 415 | RL.set_options({ 416 | keeplines = 1000, 417 | histfile = '~/.erde_history', 418 | completion = false, 419 | auto_add = false, 420 | }) 421 | } 422 | 423 | -- Protect repl so we don't show stacktraces when the user uses Control+c 424 | -- without readline. 425 | pcall(repl) 426 | 427 | if HAS_READLINE { 428 | RL.save_history() 429 | } 430 | } 431 | 432 | -- ----------------------------------------------------------------------------- 433 | -- Main 434 | -- ----------------------------------------------------------------------------- 435 | 436 | config.is_cli_runtime = true 437 | 438 | while current_arg_index <= num_args { 439 | local arg_value = arg[current_arg_index] 440 | 441 | if cli.script { 442 | -- Proxy all arguments after the script to the script itself 443 | -- (same as Lua interpreter behavior) 444 | table.insert(script_args, arg_value) 445 | } elseif !cli.subcommand && SUBCOMMANDS[arg_value] { 446 | cli.subcommand = arg_value 447 | } elseif arg_value == '-h' || arg_value == '--help' { 448 | terminate(HELP, 0) 449 | } elseif arg_value == '-v' || arg_value == '--version' { 450 | terminate(VERSION, 0) 451 | } elseif arg_value == '-w' || arg_value == '--watch' { 452 | cli.watch = true 453 | } elseif arg_value == '-f' || arg_value == '--force' { 454 | cli.force = true 455 | } elseif arg_value == '-p' || arg_value == '--print' { 456 | cli.print_compiled = true 457 | } elseif arg_value == '-t' || arg_value == '--target' { 458 | cli.target = parse_option(arg_value) 459 | config.lua_target = cli.target 460 | if !VALID_LUA_TARGETS[config.lua_target] { 461 | terminate(table.concat({ 462 | "Invalid Lua target: { config.lua_target }", 463 | "Must be one of: { table.concat(VALID_LUA_TARGETS, ', ') }", 464 | }, '\n')) 465 | } 466 | } elseif arg_value == '-o' || arg_value == '--outdir' { 467 | cli.outdir = parse_option(arg_value) 468 | } elseif arg_value == '--bitexpr' { 469 | config.bitexpr = parse_option(arg_value) 470 | } elseif arg_value == '-b' || arg_value == '--bitlib' { 471 | print("WARNING: `--bitlib` has been deprecated in favor of `--bitexpr`.") 472 | config.bitlib = parse_option(arg_value) 473 | } elseif arg_value:sub(1, 1) == '-' { 474 | terminate("Unrecognized option: { arg_value }") 475 | } elseif !cli.subcommand && arg_value:match('%.erde$') { 476 | cli.script = arg_value 477 | script_args[-current_arg_index] = 'erde' 478 | for i = 1, current_arg_index { 479 | script_args[-current_arg_index + i] = arg[i] 480 | } 481 | } else { 482 | table.insert(cli, arg_value) 483 | } 484 | 485 | current_arg_index = current_arg_index + 1 486 | } 487 | 488 | if cli.subcommand == 'compile' { 489 | compile_command() 490 | } elseif cli.subcommand == 'clean' { 491 | clean_command() 492 | } elseif cli.subcommand == 'sourcemap' { 493 | sourcemap_command() 494 | } elseif !cli.script { 495 | repl_command() 496 | } elseif !io.exists(cli.script) { 497 | terminate("File does not exist: { cli.script }") 498 | } else { 499 | run_command() 500 | } 501 | -------------------------------------------------------------------------------- /erde/cli.lua: -------------------------------------------------------------------------------- 1 | local lfs = require("lfs") 2 | local compile = require("erde.compile") 3 | local config = require("erde.config") 4 | local COMPILED_FOOTER_COMMENT, VALID_LUA_TARGETS, VERSION 5 | local __ERDE_TMP_8__ = require("erde.constants") 6 | COMPILED_FOOTER_COMMENT = __ERDE_TMP_8__.COMPILED_FOOTER_COMMENT 7 | VALID_LUA_TARGETS = __ERDE_TMP_8__.VALID_LUA_TARGETS 8 | VERSION = __ERDE_TMP_8__.VERSION 9 | local lib = require("erde.lib") 10 | local io, string 11 | local __ERDE_TMP_13__ = require("erde.stdlib") 12 | io = __ERDE_TMP_13__.io 13 | string = __ERDE_TMP_13__.string 14 | local ensure_path_parents, join_paths 15 | local __ERDE_TMP_16__ = require("erde.utils") 16 | ensure_path_parents = __ERDE_TMP_16__.ensure_path_parents 17 | join_paths = __ERDE_TMP_16__.join_paths 18 | local unpack = table.unpack or unpack 19 | local pack = table.pack or function(...) 20 | return { 21 | n = select("#", ...), 22 | ..., 23 | } 24 | end 25 | local REPL_PROMPT = "> " 26 | local REPL_SUB_PROMPT = ">> " 27 | local HAS_READLINE, RL = pcall(function() 28 | return require("readline") 29 | end) 30 | local SUBCOMMANDS = { 31 | compile = true, 32 | clean = true, 33 | sourcemap = true, 34 | } 35 | local current_arg_index = 1 36 | local num_args = #arg 37 | local cli = {} 38 | local script_args = {} 39 | local function terminate(message, status) 40 | if status == nil then 41 | status = 1 42 | end 43 | print(message) 44 | os.exit(status) 45 | end 46 | local function parse_option(label) 47 | current_arg_index = current_arg_index + 1 48 | local arg_value = arg[current_arg_index] 49 | if not arg_value then 50 | terminate("Missing argument for " .. tostring(label)) 51 | end 52 | return arg_value 53 | end 54 | local function ensure_path_parents(path) 55 | local path_parts = string.split(path, PATH_SEPARATOR) 56 | for i = 1, #path_parts - 1 do 57 | local parent_path = table.concat(path_parts, PATH_SEPARATOR, 1, i) 58 | if not io.exists(parent_path) then 59 | lfs.mkdir(parent_path) 60 | end 61 | end 62 | end 63 | local function traverse(paths, pattern, callback) 64 | for _, path in ipairs(paths) do 65 | local __ERDE_TMP_59__ = true 66 | repeat 67 | local attributes = lfs.attributes(path) 68 | if attributes == nil then 69 | __ERDE_TMP_59__ = false 70 | do 71 | break 72 | end 73 | end 74 | if attributes.mode == "file" then 75 | if path:match(pattern) then 76 | callback(path, attributes) 77 | end 78 | elseif attributes.mode == "directory" then 79 | local subpaths = {} 80 | for filename in lfs.dir(path) do 81 | if filename ~= "." and filename ~= ".." then 82 | table.insert(subpaths, join_paths(path, filename)) 83 | end 84 | end 85 | traverse(subpaths, pattern, callback) 86 | end 87 | __ERDE_TMP_59__ = false 88 | until true 89 | if __ERDE_TMP_59__ then 90 | break 91 | end 92 | end 93 | end 94 | local function is_compiled_file(path) 95 | local file = io.open(path, "r") 96 | if file == nil then 97 | return false 98 | end 99 | local read_len = #COMPILED_FOOTER_COMMENT + 1 100 | file:seek("end", -read_len) 101 | local footer = file:read(read_len) 102 | file:close() 103 | return not not (footer and footer:find(COMPILED_FOOTER_COMMENT)) 104 | end 105 | local HELP = [[ 106 | Usage: erde [command] [args] 107 | 108 | Commands: 109 | compile Compile Erde files into Lua. 110 | clean Remove generated Lua files. 111 | sourcemap Map a compiled (Lua) line to a source (Erde) line. 112 | 113 | Options: 114 | -h, --help Show this help message and exit. 115 | -v, --version Show version and exit. 116 | --bitexpr Expression to use for bit operations. 117 | -b, --bitlib Library to use for bit operations. 118 | DEPRECATED: Use --bitexpr instead. 119 | -t, --target Lua target for version compatability. 120 | Must be one of: ]] .. tostring(table.concat(VALID_LUA_TARGETS, ", ")) .. [[ 121 | 122 | 123 | Compile Options: 124 | -o, --outdir Output directory for compiled files. 125 | -w, --watch Watch files and recompile on change. 126 | -f, --force Force rewrite existing Lua files with compiled files. 127 | -p, --print Print compiled code instead of writing to files. 128 | 129 | Examples: 130 | erde 131 | Launch the REPL. 132 | 133 | erde my_script.erde 134 | Run my_script.erde. 135 | 136 | erde compile my_script.erde 137 | Compile my_script.erde (into my_script.lua). 138 | 139 | erde compile . 140 | Compile all *.erde files under the current directory. 141 | 142 | erde compile src -o dest 143 | Compile all *.erde files in src and place the *.lua files under dest. 144 | 145 | erde clean my_script.lua 146 | Remove my_script.lua if and only if it has been generated by `erde compile`. 147 | 148 | erde clean . 149 | Remove all generated *.lua files under the current directory. 150 | 151 | erde sourcemap my_script.erde 114 152 | Lookup which line in my_script.erde generated line 114 in my_script.lua. 153 | ]] 154 | local function run_command() 155 | lib.load(cli.target) 156 | arg = script_args 157 | local ok, result = xpcall(function() 158 | local source = io.readfile(cli.script) 159 | local result = lib.__erde_internal_load_source__(source, { 160 | alias = cli.script, 161 | }) 162 | return result 163 | end, lib.traceback) 164 | if not ok then 165 | terminate("erde: " .. result) 166 | end 167 | end 168 | local function compile_file(path) 169 | local compile_path = path:gsub("%.erde$", ".lua") 170 | if cli.outdir then 171 | compile_path = cli.outdir .. "/" .. compile_path 172 | end 173 | if not cli.print_compiled and not cli.force then 174 | if io.exists(compile_path) and not is_compiled_file(compile_path) then 175 | print(tostring(path) .. " => ERROR") 176 | print("Cannot write to " .. tostring(compile_path) .. ": file already exists") 177 | return false 178 | end 179 | end 180 | local ok, result = pcall(function() 181 | return compile(io.readfile(path), { 182 | alias = path, 183 | }) 184 | end) 185 | if not ok then 186 | print(tostring(path) .. " => ERROR") 187 | if type(result == "table") and result.line then 188 | print("erde:" .. tostring(result.line) .. ": " .. tostring(result.message)) 189 | else 190 | print("erde: " .. tostring(result)) 191 | end 192 | return false 193 | end 194 | if cli.print_compiled then 195 | print(path) 196 | print(("-"):rep(#path)) 197 | print(result) 198 | else 199 | ensure_path_parents(compile_path) 200 | io.writefile(compile_path, result) 201 | if cli.watch then 202 | print("[" .. tostring(os.date("%X")) .. "] " .. tostring(path) .. " => " .. tostring(compile_path)) 203 | else 204 | print(tostring(path) .. " => " .. tostring(compile_path)) 205 | end 206 | end 207 | return true 208 | end 209 | local function watch_files(cli) 210 | local modifications = {} 211 | local poll_interval = 1 212 | local has_socket, socket = pcall(function() 213 | return require("socket") 214 | end) 215 | local has_posix, posix = pcall(function() 216 | return require("posix.unistd") 217 | end) 218 | if not has_socket and not has_posix then 219 | print(table.concat({ 220 | "WARNING: No libraries with sleep functionality found. This may ", 221 | "cause high CPU usage while watching. To fix this, you can install ", 222 | "either LuaSocket (https://luarocks.org/modules/luasocket/luasocket) ", 223 | "or luaposix (https://luarocks.org/modules/gvvaughan/luaposix)\n", 224 | })) 225 | end 226 | while true do 227 | traverse(cli, "%.erde$", function(path, attributes) 228 | if not modifications[path] or modifications[path] ~= attributes.modification then 229 | modifications[path] = attributes.modification 230 | compile_file(path, cli) 231 | end 232 | end) 233 | if has_socket then 234 | socket.sleep(poll_interval) 235 | elseif has_posix then 236 | posix.sleep(poll_interval) 237 | else 238 | local last_timeout = os.time() 239 | repeat 240 | until os.time() - last_timeout > poll_interval 241 | end 242 | end 243 | end 244 | local function compile_command() 245 | if #cli == 0 then 246 | table.insert(cli, ".") 247 | end 248 | if cli.watch then 249 | pcall(function() 250 | return watch_files(cli) 251 | end) 252 | else 253 | traverse(cli, "%.erde$", function(path) 254 | if not compile_file(path, cli) then 255 | os.exit(1) 256 | end 257 | end) 258 | end 259 | end 260 | local function clean_command() 261 | if #cli == 0 then 262 | table.insert(cli, ".") 263 | end 264 | traverse(cli, "%.lua$", function(path) 265 | if is_compiled_file(path) then 266 | os.remove(path) 267 | print(tostring(path) .. " => DELETED") 268 | end 269 | end) 270 | end 271 | local function sourcemap_command() 272 | local path, line = cli[1], cli[2] 273 | if path == nil then 274 | terminate("Missing erde file to map") 275 | elseif line == nil then 276 | terminate("Missing line number to map") 277 | end 278 | local ok, result, source_map = pcall(function() 279 | return compile(io.readfile(path), { 280 | alias = path, 281 | }) 282 | end) 283 | if ok then 284 | print(tostring(line) .. " => " .. tostring(source_map[tonumber(line)])) 285 | else 286 | print("Failed to compile " .. tostring(path)) 287 | if type(result == "table") and result.line then 288 | print("erde:" .. tostring(result.line) .. ": " .. tostring(result.message)) 289 | else 290 | print("erde: " .. tostring(result)) 291 | end 292 | end 293 | end 294 | local function readline(prompt) 295 | if HAS_READLINE then 296 | return RL.readline(prompt) 297 | else 298 | io.write(prompt) 299 | return io.read() 300 | end 301 | end 302 | local function repl() 303 | print("Erde " .. tostring(VERSION) .. " on " .. tostring(_VERSION) .. " -- Copyright (C) 2021-2023 bsuth") 304 | if not HAS_READLINE then 305 | print("Install the `readline` Lua library to get support for arrow keys, keyboard shortcuts, history, etc.") 306 | end 307 | while true do 308 | local ok, result 309 | local source = readline(REPL_PROMPT) 310 | if not source or (HAS_READLINE and source == "(null)") then 311 | break 312 | end 313 | repeat 314 | ok, result = pcall(function() 315 | return pack(lib.run("return " .. tostring(source), { 316 | alias = "stdin", 317 | })) 318 | end) 319 | if not ok and type(result) == "string" and not result:find("unexpected eof") then 320 | ok, result = pcall(function() 321 | return pack(lib.run(source, { 322 | alias = "stdin", 323 | })) 324 | end) 325 | end 326 | if not ok and type(result) == "string" and result:find("unexpected eof") then 327 | repeat 328 | local subsource = readline(REPL_SUB_PROMPT) 329 | source = source .. (subsource or "") 330 | until subsource 331 | end 332 | until ok or type(result) ~= "string" or not result:find("unexpected eof") 333 | if not ok then 334 | print(lib.rewrite(result)) 335 | elseif result.n > 0 then 336 | for i = 1, result.n do 337 | result[i] = tostring(result[i]) 338 | end 339 | print(unpack(result)) 340 | end 341 | if HAS_READLINE and string.trim(source) ~= "" then 342 | RL.add_history(source) 343 | end 344 | end 345 | end 346 | local function repl_command() 347 | lib.load(cli.target) 348 | if HAS_READLINE then 349 | RL.set_readline_name("erde") 350 | RL.set_options({ 351 | keeplines = 1000, 352 | histfile = "~/.erde_history", 353 | completion = false, 354 | auto_add = false, 355 | }) 356 | end 357 | pcall(repl) 358 | if HAS_READLINE then 359 | RL.save_history() 360 | end 361 | end 362 | config.is_cli_runtime = true 363 | while current_arg_index <= num_args do 364 | local arg_value = arg[current_arg_index] 365 | if cli.script then 366 | table.insert(script_args, arg_value) 367 | elseif not cli.subcommand and SUBCOMMANDS[arg_value] then 368 | cli.subcommand = arg_value 369 | elseif arg_value == "-h" or arg_value == "--help" then 370 | terminate(HELP, 0) 371 | elseif arg_value == "-v" or arg_value == "--version" then 372 | terminate(VERSION, 0) 373 | elseif arg_value == "-w" or arg_value == "--watch" then 374 | cli.watch = true 375 | elseif arg_value == "-f" or arg_value == "--force" then 376 | cli.force = true 377 | elseif arg_value == "-p" or arg_value == "--print" then 378 | cli.print_compiled = true 379 | elseif arg_value == "-t" or arg_value == "--target" then 380 | cli.target = parse_option(arg_value) 381 | config.lua_target = cli.target 382 | if not VALID_LUA_TARGETS[config.lua_target] then 383 | terminate(table.concat({ 384 | "Invalid Lua target: " .. tostring(config.lua_target), 385 | "Must be one of: " .. tostring(table.concat(VALID_LUA_TARGETS, ", ")), 386 | }, "\n")) 387 | end 388 | elseif arg_value == "-o" or arg_value == "--outdir" then 389 | cli.outdir = parse_option(arg_value) 390 | elseif arg_value == "--bitexpr" then 391 | config.bitexpr = parse_option(arg_value) 392 | elseif arg_value == "-b" or arg_value == "--bitlib" then 393 | print("WARNING: `--bitlib` has been deprecated in favor of `--bitexpr`.") 394 | config.bitlib = parse_option(arg_value) 395 | elseif arg_value:sub(1, 1) == "-" then 396 | terminate("Unrecognized option: " .. tostring(arg_value)) 397 | elseif not cli.subcommand and arg_value:match("%.erde$") then 398 | cli.script = arg_value 399 | script_args[-current_arg_index] = "erde" 400 | for i = 1, current_arg_index do 401 | script_args[-current_arg_index + i] = arg[i] 402 | end 403 | else 404 | table.insert(cli, arg_value) 405 | end 406 | current_arg_index = current_arg_index + 1 407 | end 408 | if cli.subcommand == "compile" then 409 | compile_command() 410 | elseif cli.subcommand == "clean" then 411 | clean_command() 412 | elseif cli.subcommand == "sourcemap" then 413 | sourcemap_command() 414 | elseif not cli.script then 415 | repl_command() 416 | elseif not io.exists(cli.script) then 417 | terminate("File does not exist: " .. tostring(cli.script)) 418 | else 419 | run_command() 420 | end 421 | -- Compiled with Erde 1.0.0-1 w/ Lua target 5.1+ 422 | -- __ERDE_COMPILED__ 423 | -------------------------------------------------------------------------------- /erde/config.erde: -------------------------------------------------------------------------------- 1 | -- Lua targets to support when generating compiled code. 2 | module lua_target = '5.1+' 3 | 4 | -- Flag to know whether or not we are running under the cli. Required for more 5 | -- precise error rewriting. 6 | module is_cli_runtime = false 7 | 8 | -- User specified expression to use for bit operations. 9 | module bitexpr = nil 10 | 11 | -- User specified library to use for bit operations. 12 | module bitlib = nil 13 | 14 | -- Flag for whether we should disable caching source maps for loaded sources. 15 | -- See `__erde_internal_load_source__` 16 | module disable_source_maps = false 17 | -------------------------------------------------------------------------------- /erde/config.lua: -------------------------------------------------------------------------------- 1 | local _MODULE = {} 2 | _MODULE.lua_target = "5.1+" 3 | _MODULE.is_cli_runtime = false 4 | _MODULE.bitexpr = nil 5 | _MODULE.bitlib = nil 6 | _MODULE.disable_source_maps = false 7 | return _MODULE 8 | -- Compiled with Erde 1.0.0-1 w/ Lua target 5.1+ 9 | -- __ERDE_COMPILED__ 10 | -------------------------------------------------------------------------------- /erde/constants.erde: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------------------------------- 2 | -- Misc 3 | -- ----------------------------------------------------------------------------- 4 | 5 | module VERSION = '1.0.0-1' 6 | 7 | -- Get the current platform path separator. Note that while this is undocumented 8 | -- in the Lua 5.1 manual, it is indeed supported in 5.1+. 9 | -- 10 | -- https://www.lua.org/manual/5.3/manual.html#pdf-package.config 11 | module PATH_SEPARATOR = package.config:sub(1, 1) 12 | 13 | -- A footer comment we inject into compiled code in order to track which files 14 | -- have been generated by the cli (and thus allows us to also clean them later). 15 | module COMPILED_FOOTER_COMMENT = '-- __ERDE_COMPILED__' 16 | 17 | -- Identifiers for each type of token generated by the tokenizer. 18 | module TOKEN_TYPES = { 19 | EOF = 0, 20 | SHEBANG = 1, 21 | SYMBOL = 2, 22 | WORD = 3, 23 | NUMBER = 4, 24 | SINGLE_QUOTE_STRING = 5, 25 | DOUBLE_QUOTE_STRING = 6, 26 | STRING_CONTENT = 7, 27 | INTERPOLATION = 8, 28 | } 29 | 30 | -- ----------------------------------------------------------------------------- 31 | -- Lua Target 32 | -- ----------------------------------------------------------------------------- 33 | 34 | module VALID_LUA_TARGETS = { 35 | 'jit', 36 | '5.1', 37 | '5.1+', 38 | '5.2', 39 | '5.2+', 40 | '5.3', 41 | '5.3+', 42 | '5.4', 43 | '5.4+', 44 | } 45 | 46 | for i, target in ipairs(VALID_LUA_TARGETS) { 47 | VALID_LUA_TARGETS[target] = true 48 | } 49 | 50 | -- ----------------------------------------------------------------------------- 51 | -- Keywords / Terminals 52 | -- ----------------------------------------------------------------------------- 53 | 54 | module KEYWORDS = { 55 | ['break'] = true, 56 | ['continue'] = true, 57 | ['do'] = true, 58 | ['else'] = true, 59 | ['elseif'] = true, 60 | ['for'] = true, 61 | ['function'] = true, 62 | ['global'] = true, 63 | ['if'] = true, 64 | ['in'] = true, 65 | ['local'] = true, 66 | ['module'] = true, 67 | ['repeat'] = true, 68 | ['return'] = true, 69 | ['until'] = true, 70 | ['while'] = true, 71 | } 72 | 73 | -- Words that are keywords in Lua but NOT in Erde. 74 | module LUA_KEYWORDS = { 75 | ['not'] = true, 76 | ['and'] = true, 77 | ['or'] = true, 78 | ['end'] = true, 79 | ['then'] = true, 80 | } 81 | 82 | module TERMINALS = { 83 | ['true'] = true, 84 | ['false'] = true, 85 | ['nil'] = true, 86 | ['...'] = true, 87 | } 88 | 89 | -- ----------------------------------------------------------------------------- 90 | -- Operators 91 | -- ----------------------------------------------------------------------------- 92 | 93 | module LEFT_ASSOCIATIVE = -1 94 | module RIGHT_ASSOCIATIVE = 1 95 | 96 | module UNOPS = { 97 | ['-'] = { prec = 13 }, 98 | ['#'] = { prec = 13 }, 99 | ['!'] = { prec = 13 }, 100 | ['~'] = { prec = 13 }, 101 | } 102 | 103 | for token, op in pairs(UNOPS) { 104 | op.token = token 105 | } 106 | 107 | module BITOPS = { 108 | ['|'] = { prec = 6, assoc = LEFT_ASSOCIATIVE }, 109 | ['~'] = { prec = 7, assoc = LEFT_ASSOCIATIVE }, 110 | ['&'] = { prec = 8, assoc = LEFT_ASSOCIATIVE }, 111 | ['<<'] = { prec = 9, assoc = LEFT_ASSOCIATIVE }, 112 | ['>>'] = { prec = 9, assoc = LEFT_ASSOCIATIVE }, 113 | } 114 | 115 | module BITLIB_METHODS = { 116 | ['|'] = 'bor', 117 | ['~'] = 'bxor', 118 | ['&'] = 'band', 119 | ['<<'] = 'lshift', 120 | ['>>'] = 'rshift', 121 | } 122 | 123 | module BINOPS = { 124 | ['||'] = { prec = 3, assoc = LEFT_ASSOCIATIVE }, 125 | ['&&'] = { prec = 4, assoc = LEFT_ASSOCIATIVE }, 126 | ['=='] = { prec = 5, assoc = LEFT_ASSOCIATIVE }, 127 | ['!='] = { prec = 5, assoc = LEFT_ASSOCIATIVE }, 128 | ['<='] = { prec = 5, assoc = LEFT_ASSOCIATIVE }, 129 | ['>='] = { prec = 5, assoc = LEFT_ASSOCIATIVE }, 130 | ['<'] = { prec = 5, assoc = LEFT_ASSOCIATIVE }, 131 | ['>'] = { prec = 5, assoc = LEFT_ASSOCIATIVE }, 132 | ['..'] = { prec = 10, assoc = LEFT_ASSOCIATIVE }, 133 | ['+'] = { prec = 11, assoc = LEFT_ASSOCIATIVE }, 134 | ['-'] = { prec = 11, assoc = LEFT_ASSOCIATIVE }, 135 | ['*'] = { prec = 12, assoc = LEFT_ASSOCIATIVE }, 136 | ['/'] = { prec = 12, assoc = LEFT_ASSOCIATIVE }, 137 | ['//'] = { prec = 12, assoc = LEFT_ASSOCIATIVE }, 138 | ['%'] = { prec = 12, assoc = LEFT_ASSOCIATIVE }, 139 | ['^'] = { prec = 14, assoc = RIGHT_ASSOCIATIVE }, 140 | } 141 | 142 | for token, op in pairs(BITOPS) { 143 | BINOPS[token] = op 144 | } 145 | 146 | for token, op in pairs(BINOPS) { 147 | op.token = token 148 | } 149 | 150 | module BINOP_ASSIGNMENT_TOKENS = { 151 | ['||'] = true, 152 | ['&&'] = true, 153 | ['..'] = true, 154 | ['+'] = true, 155 | ['-'] = true, 156 | ['*'] = true, 157 | ['/'] = true, 158 | ['//'] = true, 159 | ['%'] = true, 160 | ['^'] = true, 161 | ['|'] = true, 162 | ['~'] = true, 163 | ['&'] = true, 164 | ['<<'] = true, 165 | ['>>'] = true, 166 | } 167 | 168 | -- ----------------------------------------------------------------------------- 169 | -- Symbol Lookup Tables 170 | -- ----------------------------------------------------------------------------- 171 | 172 | module SURROUND_ENDS = { 173 | ['('] = ')', 174 | ['['] = ']', 175 | ['{'] = '}', 176 | } 177 | 178 | module SYMBOLS = { 179 | ['->'] = true, 180 | ['=>'] = true, 181 | ['...'] = true, 182 | ['::'] = true, 183 | } 184 | 185 | for token, op in pairs(BINOPS) { 186 | if #token > 1 { 187 | SYMBOLS[token] = true 188 | } 189 | } 190 | 191 | -- Valid escape characters for 5.1+ 192 | module STANDARD_ESCAPE_CHARS = { 193 | a = true, 194 | b = true, 195 | f = true, 196 | n = true, 197 | r = true, 198 | t = true, 199 | v = true, 200 | ['\\'] = true, 201 | ['"'] = true, 202 | ["'"] = true, 203 | ['\n'] = true, 204 | } 205 | 206 | -- ----------------------------------------------------------------------------- 207 | -- Alphanumeric Lookup Tables 208 | -- ----------------------------------------------------------------------------- 209 | 210 | module DIGIT = {} 211 | module HEX = {} 212 | module WORD_HEAD = { ['_'] = true } 213 | module WORD_BODY = { ['_'] = true } 214 | 215 | for byte = string.byte('0'), string.byte('9') { 216 | local char = string.char(byte) 217 | DIGIT[char] = true 218 | HEX[char] = true 219 | WORD_BODY[char] = true 220 | } 221 | 222 | for byte = string.byte('A'), string.byte('F') { 223 | local char = string.char(byte) 224 | HEX[char] = true 225 | WORD_HEAD[char] = true 226 | WORD_BODY[char] = true 227 | } 228 | 229 | for byte = string.byte('G'), string.byte('Z') { 230 | local char = string.char(byte) 231 | WORD_HEAD[char] = true 232 | WORD_BODY[char] = true 233 | } 234 | 235 | for byte = string.byte('a'), string.byte('f') { 236 | local char = string.char(byte) 237 | HEX[char] = true 238 | WORD_HEAD[char] = true 239 | WORD_BODY[char] = true 240 | } 241 | 242 | for byte = string.byte('g'), string.byte('z') { 243 | local char = string.char(byte) 244 | WORD_HEAD[char] = true 245 | WORD_BODY[char] = true 246 | } 247 | -------------------------------------------------------------------------------- /erde/constants.lua: -------------------------------------------------------------------------------- 1 | local _MODULE = {} 2 | _MODULE.VERSION = "1.0.0-1" 3 | _MODULE.PATH_SEPARATOR = package.config:sub(1, 1) 4 | _MODULE.COMPILED_FOOTER_COMMENT = "-- __ERDE_COMPILED__" 5 | _MODULE.TOKEN_TYPES = { 6 | EOF = 0, 7 | SHEBANG = 1, 8 | SYMBOL = 2, 9 | WORD = 3, 10 | NUMBER = 4, 11 | SINGLE_QUOTE_STRING = 5, 12 | DOUBLE_QUOTE_STRING = 6, 13 | STRING_CONTENT = 7, 14 | INTERPOLATION = 8, 15 | } 16 | _MODULE.VALID_LUA_TARGETS = { 17 | "jit", 18 | "5.1", 19 | "5.1+", 20 | "5.2", 21 | "5.2+", 22 | "5.3", 23 | "5.3+", 24 | "5.4", 25 | "5.4+", 26 | } 27 | for i, target in ipairs(_MODULE.VALID_LUA_TARGETS) do 28 | _MODULE.VALID_LUA_TARGETS[target] = true 29 | end 30 | _MODULE.KEYWORDS = { 31 | ["break"] = true, 32 | ["continue"] = true, 33 | ["do"] = true, 34 | ["else"] = true, 35 | ["elseif"] = true, 36 | ["for"] = true, 37 | ["function"] = true, 38 | ["global"] = true, 39 | ["if"] = true, 40 | ["in"] = true, 41 | ["local"] = true, 42 | ["module"] = true, 43 | ["repeat"] = true, 44 | ["return"] = true, 45 | ["until"] = true, 46 | ["while"] = true, 47 | } 48 | _MODULE.LUA_KEYWORDS = { 49 | ["not"] = true, 50 | ["and"] = true, 51 | ["or"] = true, 52 | ["end"] = true, 53 | ["then"] = true, 54 | } 55 | _MODULE.TERMINALS = { 56 | ["true"] = true, 57 | ["false"] = true, 58 | ["nil"] = true, 59 | ["..."] = true, 60 | } 61 | _MODULE.LEFT_ASSOCIATIVE = -1 62 | _MODULE.RIGHT_ASSOCIATIVE = 1 63 | _MODULE.UNOPS = { 64 | ["-"] = { 65 | prec = 13, 66 | }, 67 | ["#"] = { 68 | prec = 13, 69 | }, 70 | ["!"] = { 71 | prec = 13, 72 | }, 73 | ["~"] = { 74 | prec = 13, 75 | }, 76 | } 77 | for token, op in pairs(_MODULE.UNOPS) do 78 | op.token = token 79 | end 80 | _MODULE.BITOPS = { 81 | ["|"] = { 82 | prec = 6, 83 | assoc = _MODULE.LEFT_ASSOCIATIVE, 84 | }, 85 | ["~"] = { 86 | prec = 7, 87 | assoc = _MODULE.LEFT_ASSOCIATIVE, 88 | }, 89 | ["&"] = { 90 | prec = 8, 91 | assoc = _MODULE.LEFT_ASSOCIATIVE, 92 | }, 93 | ["<<"] = { 94 | prec = 9, 95 | assoc = _MODULE.LEFT_ASSOCIATIVE, 96 | }, 97 | [">>"] = { 98 | prec = 9, 99 | assoc = _MODULE.LEFT_ASSOCIATIVE, 100 | }, 101 | } 102 | _MODULE.BITLIB_METHODS = { 103 | ["|"] = "bor", 104 | ["~"] = "bxor", 105 | ["&"] = "band", 106 | ["<<"] = "lshift", 107 | [">>"] = "rshift", 108 | } 109 | _MODULE.BINOPS = { 110 | ["||"] = { 111 | prec = 3, 112 | assoc = _MODULE.LEFT_ASSOCIATIVE, 113 | }, 114 | ["&&"] = { 115 | prec = 4, 116 | assoc = _MODULE.LEFT_ASSOCIATIVE, 117 | }, 118 | ["=="] = { 119 | prec = 5, 120 | assoc = _MODULE.LEFT_ASSOCIATIVE, 121 | }, 122 | ["!="] = { 123 | prec = 5, 124 | assoc = _MODULE.LEFT_ASSOCIATIVE, 125 | }, 126 | ["<="] = { 127 | prec = 5, 128 | assoc = _MODULE.LEFT_ASSOCIATIVE, 129 | }, 130 | [">="] = { 131 | prec = 5, 132 | assoc = _MODULE.LEFT_ASSOCIATIVE, 133 | }, 134 | ["<"] = { 135 | prec = 5, 136 | assoc = _MODULE.LEFT_ASSOCIATIVE, 137 | }, 138 | [">"] = { 139 | prec = 5, 140 | assoc = _MODULE.LEFT_ASSOCIATIVE, 141 | }, 142 | [".."] = { 143 | prec = 10, 144 | assoc = _MODULE.LEFT_ASSOCIATIVE, 145 | }, 146 | ["+"] = { 147 | prec = 11, 148 | assoc = _MODULE.LEFT_ASSOCIATIVE, 149 | }, 150 | ["-"] = { 151 | prec = 11, 152 | assoc = _MODULE.LEFT_ASSOCIATIVE, 153 | }, 154 | ["*"] = { 155 | prec = 12, 156 | assoc = _MODULE.LEFT_ASSOCIATIVE, 157 | }, 158 | ["/"] = { 159 | prec = 12, 160 | assoc = _MODULE.LEFT_ASSOCIATIVE, 161 | }, 162 | ["//"] = { 163 | prec = 12, 164 | assoc = _MODULE.LEFT_ASSOCIATIVE, 165 | }, 166 | ["%"] = { 167 | prec = 12, 168 | assoc = _MODULE.LEFT_ASSOCIATIVE, 169 | }, 170 | ["^"] = { 171 | prec = 14, 172 | assoc = _MODULE.RIGHT_ASSOCIATIVE, 173 | }, 174 | } 175 | for token, op in pairs(_MODULE.BITOPS) do 176 | _MODULE.BINOPS[token] = op 177 | end 178 | for token, op in pairs(_MODULE.BINOPS) do 179 | op.token = token 180 | end 181 | _MODULE.BINOP_ASSIGNMENT_TOKENS = { 182 | ["||"] = true, 183 | ["&&"] = true, 184 | [".."] = true, 185 | ["+"] = true, 186 | ["-"] = true, 187 | ["*"] = true, 188 | ["/"] = true, 189 | ["//"] = true, 190 | ["%"] = true, 191 | ["^"] = true, 192 | ["|"] = true, 193 | ["~"] = true, 194 | ["&"] = true, 195 | ["<<"] = true, 196 | [">>"] = true, 197 | } 198 | _MODULE.SURROUND_ENDS = { 199 | ["("] = ")", 200 | ["["] = "]", 201 | ["{"] = "}", 202 | } 203 | _MODULE.SYMBOLS = { 204 | ["->"] = true, 205 | ["=>"] = true, 206 | ["..."] = true, 207 | ["::"] = true, 208 | } 209 | for token, op in pairs(_MODULE.BINOPS) do 210 | if #token > 1 then 211 | _MODULE.SYMBOLS[token] = true 212 | end 213 | end 214 | _MODULE.STANDARD_ESCAPE_CHARS = { 215 | a = true, 216 | b = true, 217 | f = true, 218 | n = true, 219 | r = true, 220 | t = true, 221 | v = true, 222 | ["\\"] = true, 223 | ['"'] = true, 224 | ["'"] = true, 225 | ["\n"] = true, 226 | } 227 | _MODULE.DIGIT = {} 228 | _MODULE.HEX = {} 229 | _MODULE.WORD_HEAD = { 230 | ["_"] = true, 231 | } 232 | _MODULE.WORD_BODY = { 233 | ["_"] = true, 234 | } 235 | for byte = string.byte("0"), string.byte("9") do 236 | local char = string.char(byte) 237 | _MODULE.DIGIT[char] = true 238 | _MODULE.HEX[char] = true 239 | _MODULE.WORD_BODY[char] = true 240 | end 241 | for byte = string.byte("A"), string.byte("F") do 242 | local char = string.char(byte) 243 | _MODULE.HEX[char] = true 244 | _MODULE.WORD_HEAD[char] = true 245 | _MODULE.WORD_BODY[char] = true 246 | end 247 | for byte = string.byte("G"), string.byte("Z") do 248 | local char = string.char(byte) 249 | _MODULE.WORD_HEAD[char] = true 250 | _MODULE.WORD_BODY[char] = true 251 | end 252 | for byte = string.byte("a"), string.byte("f") do 253 | local char = string.char(byte) 254 | _MODULE.HEX[char] = true 255 | _MODULE.WORD_HEAD[char] = true 256 | _MODULE.WORD_BODY[char] = true 257 | end 258 | for byte = string.byte("g"), string.byte("z") do 259 | local char = string.char(byte) 260 | _MODULE.WORD_HEAD[char] = true 261 | _MODULE.WORD_BODY[char] = true 262 | end 263 | return _MODULE 264 | -- Compiled with Erde 1.0.0-1 w/ Lua target 5.1+ 265 | -- __ERDE_COMPILED__ 266 | -------------------------------------------------------------------------------- /erde/init.erde: -------------------------------------------------------------------------------- 1 | local lib = require('erde.lib') 2 | local { VERSION } = require('erde.constants') 3 | 4 | return { 5 | version = VERSION, 6 | compile = require('erde.compile'), 7 | rewrite = lib.rewrite, 8 | traceback = lib.traceback, 9 | run = lib.run, 10 | load = lib.load, 11 | unload = lib.unload, 12 | } 13 | -------------------------------------------------------------------------------- /erde/init.lua: -------------------------------------------------------------------------------- 1 | local lib = require("erde.lib") 2 | local VERSION 3 | local __ERDE_TMP_4__ = require("erde.constants") 4 | VERSION = __ERDE_TMP_4__.VERSION 5 | return { 6 | version = VERSION, 7 | compile = require("erde.compile"), 8 | rewrite = lib.rewrite, 9 | traceback = lib.traceback, 10 | run = lib.run, 11 | load = lib.load, 12 | unload = lib.unload, 13 | } 14 | -- Compiled with Erde 1.0.0-1 w/ Lua target 5.1+ 15 | -- __ERDE_COMPILED__ 16 | -------------------------------------------------------------------------------- /erde/lib.erde: -------------------------------------------------------------------------------- 1 | -- This module contains higher level functions for use either via API or 2 | -- internally (mostly in the CLI). 3 | 4 | local compile = require('erde.compile') 5 | local config = require('erde.config') 6 | local { PATH_SEPARATOR, VALID_LUA_TARGETS } = require('erde.constants') 7 | local { io, string } = require('erde.stdlib') 8 | local { echo, get_source_summary } = require('erde.utils') 9 | 10 | local loadlua = loadstring || load 11 | local unpack = table.unpack || unpack 12 | local native_traceback = debug.traceback 13 | 14 | -- https://www.lua.org/manual/5.1/manual.html#pdf-package.loaders 15 | -- https://www.lua.org/manual/5.2/manual.html#pdf-package.searchers 16 | local searchers = package.loaders || package.searchers 17 | 18 | local erde_source_cache = {} 19 | local erde_source_id_counter = 1 20 | 21 | -- ----------------------------------------------------------------------------- 22 | -- Debug 23 | -- ----------------------------------------------------------------------------- 24 | 25 | module function rewrite(message) { 26 | -- Only rewrite strings! Other thrown values (including nil) do not get source 27 | -- and line number information added. 28 | if type(message) != 'string' { 29 | return message 30 | } 31 | 32 | for erde_source_id, chunkname, compiled_line in message:gmatch('%[string "erde::(%d+)::([^\n]+)"]:(%d+)') { 33 | local cache = erde_source_cache[tonumber(erde_source_id)] || {} 34 | 35 | -- If we have don't have a source map for erde code, we need to indicate that 36 | -- the error line is for the generated Lua. 37 | local source_map = cache.source_map || {} 38 | local source_line = source_map[tonumber(compiled_line)] || "(compiled:{ compiled_line })" 39 | 40 | -- Escape the match in case the chunkname contains parentheses! 41 | local match = string.escape("[string \"erde::{ erde_source_id }::{ chunkname }\"]:{ compiled_line }") 42 | 43 | message = cache.has_alias 44 | && message:gsub(match, chunkname .. ':' .. source_line) 45 | || message:gsub(match, "[string \"{ chunkname }\"]:{ source_line }") 46 | } 47 | 48 | -- When compiling, we translate words that are keywords in Lua but not in 49 | -- Erde. When reporting errors, we need to transform them back. 50 | message = message:gsub('__ERDE_SUBSTITUTE_([a-zA-Z]+)__', '%1') 51 | 52 | return message 53 | } 54 | 55 | module function traceback(arg1, arg2, arg3) { 56 | local stacktrace, level 57 | 58 | -- Follows native_traceback behavior for determining args. 59 | if type(arg1) == 'thread' { 60 | level = arg3 || 1 61 | -- Add an extra level to account for this traceback function itself! 62 | stacktrace = native_traceback(arg1, arg2, level + 1) 63 | } else { 64 | level = arg2 || 1 65 | -- Add an extra level to account for this traceback function itself! 66 | stacktrace = native_traceback(arg1, level + 1) 67 | } 68 | 69 | if type(stacktrace) != 'string' { 70 | return stacktrace 71 | } 72 | 73 | if level > -1 && config.is_cli_runtime { 74 | -- Remove following from stack trace caused by the cli: 75 | -- 76 | -- [C]: in function 77 | -- [C]: in function 'xpcall' 78 | -- erde/cli/run.lua:xxx: in function 'run' 79 | -- erde/cli/init.lua:xxx: in main chunk 80 | -- 81 | -- Note, we do not remove the very last line of the stack, this is the C 82 | -- entry point of the Lua VM. 83 | local stack = string.split(stacktrace, '\n') 84 | local stacklen = #stack 85 | for i = 1, 4 { table.remove(stack, stacklen - i) } 86 | stacktrace = table.concat(stack, '\n') 87 | } 88 | 89 | -- Remove any lines from `__erde_internal_load_source__` calls. 90 | -- See `__erde_internal_load_source__` for more details. 91 | stacktrace = stacktrace:gsub(table.concat({ 92 | '[^\n]*\n', 93 | '[^\n]*__erde_internal_load_source__[^\n]*\n', 94 | '[^\n]*\n', 95 | }), '') 96 | 97 | return rewrite(stacktrace) 98 | } 99 | 100 | -- ----------------------------------------------------------------------------- 101 | -- Source Loaders 102 | -- ----------------------------------------------------------------------------- 103 | 104 | -- Load a chunk of Erde code. This caches the generated source map 105 | -- (see `erde_source_cache`) so we can fetch them later during error rewrites. 106 | -- 107 | -- The alias is _not_ used as the chunkname in the underlying Lua `load` 108 | -- call. Instead, a unique ID is generated and inserted instead. During error 109 | -- rewrites, this ID will be extracted and replaced with the cached alias. 110 | -- 111 | -- This function is also given a unique function name so that it is reliably 112 | -- searchable in stacktraces. During stracetrace rewrites (see `traceback`), the 113 | -- presence of this name dictates which lines we need to remove. Otherwise, the 114 | -- resulting stacktraces will include function calls from this file, which will 115 | -- be quite confusing and noisy for the end user. 116 | -- 117 | -- IMPORTANT: THIS FUNCTION MUST NOT BE TAIL CALLED NOR DIRECTLY CALLED BY THE 118 | -- USER AND IS ASSUMED TO BE CALLED AT THE TOP LEVEL OF LOADING ERDE SOURCE CODE. 119 | -- 120 | -- Although we use a unique name here to find it in stacktraces, the actual 121 | -- rewriting is much trickier. Because Lua will automatically collapse tail 122 | -- calls in stacktraces, its hard to know how many lines of internal code 123 | -- _before_ the call to `__erde_internal_load_source__` we need to remove. 124 | -- 125 | -- Furthermore, finding the name of the function call is also nontrivial and 126 | -- will actually get lost if this is directly called by the user, so it must 127 | -- have at least one function call before it (even the Lua docs seem to mention 128 | -- this in `debug.getinfo`, see https://www.lua.org/pil/23.1.html). 129 | -- 130 | -- Thus, for consistency we always assume that this is never tail called and 131 | -- it is called at the top level of loading erde source code, which ensures that 132 | -- we always have the following 3 lines to remove: 133 | -- 134 | -- 1. The `xpcall` in `__erde_internal_load_source__` 135 | -- 2. The call to `__erde_internal_load_source__` itself 136 | -- 3. The call that invoked `__erde_internal_load_source__` 137 | module function __erde_internal_load_source__(source, options = {}) { 138 | local chunkname = table.concat({ 139 | 'erde', 140 | erde_source_id_counter, 141 | options.alias || get_source_summary(source), 142 | }, '::') 143 | 144 | -- No xpcall here, we want the traceback to start from this stack! 145 | local compiled, source_map = compile(source, { 146 | alias = options.alias, 147 | lua_target = options.lua_target, 148 | bitlib = options.bitlib, 149 | }) 150 | 151 | -- Remove the shebang! Lua's `load` function cannot handle shebangs. 152 | compiled = compiled:gsub('^#![^\n]+', '') 153 | 154 | local loader, load_error = loadlua(compiled, chunkname) 155 | 156 | if load_error != nil { 157 | error(table.concat({ 158 | 'Failed to load compiled code:', 159 | tostring(load_error), 160 | '', 161 | 'This is an internal error that should never happen.', 162 | 'Please report this at: https://github.com/erde-lang/erde/issues', 163 | '', 164 | 'erde', 165 | '----', 166 | source, 167 | '', 168 | 'lua', 169 | '---', 170 | compiled, 171 | }, '\n')) 172 | } 173 | 174 | erde_source_cache[erde_source_id_counter] = { has_alias = options.alias != nil } 175 | 176 | if !config.disable_source_maps && !options.disable_source_maps { 177 | -- Cache source maps. Allow user to specify whether to disallow this, as the 178 | -- source map tables can be potentially large. 179 | erde_source_cache[erde_source_id_counter].source_map = source_map 180 | } 181 | 182 | erde_source_id_counter += 1 183 | 184 | return loader() 185 | } 186 | 187 | -- IMPORTANT: THIS IS AN ERDE SOURCE LOADER AND MUST ADHERE TO THE USAGE SPEC OF 188 | -- `__erde_internal_load_source__`! 189 | module function run(source, options) { 190 | return echo(__erde_internal_load_source__(source, options)) 191 | } 192 | 193 | -- ----------------------------------------------------------------------------- 194 | -- Package Loader 195 | -- ----------------------------------------------------------------------------- 196 | 197 | local function erde_searcher(module_name) { 198 | local module_path = module_name:gsub('%.', PATH_SEPARATOR) 199 | 200 | for path in package.path:gmatch('[^;]+') { 201 | local fullpath = path:gsub('%.lua$', '.erde'):gsub('?', module_path) 202 | 203 | if io.exists(fullpath) { 204 | -- IMPORTANT: THIS IS AN ERDE SOURCE LOADER AND MUST ADHERE TO THE USAGE SPEC OF 205 | -- `__erde_internal_load_source__`! 206 | return () -> { 207 | local source = io.readfile(fullpath) 208 | local result = { __erde_internal_load_source__(source, { alias = fullpath }) } 209 | return unpack(result) 210 | } 211 | } 212 | } 213 | } 214 | 215 | module function load(arg1, arg2) { 216 | local new_lua_target, options = nil, {} 217 | 218 | if type(arg1) == 'string' { 219 | new_lua_target = arg1 220 | } 221 | 222 | if type(arg1) == 'table' { 223 | options = arg1 224 | } elseif type(arg2) == 'table' { 225 | options = arg2 226 | } 227 | 228 | config.bitlib = options.bitlib 229 | config.disable_source_maps = options.disable_source_maps 230 | 231 | -- Always set `debug.traceback`, in case this is called multiple times 232 | -- with different arguments. By default we override Lua's native traceback 233 | -- with our own to rewrite Erde paths. 234 | debug.traceback = options.keep_traceback == true && native_traceback || traceback 235 | 236 | if new_lua_target != nil { 237 | if VALID_LUA_TARGETS[new_lua_target] { 238 | config.lua_target = new_lua_target 239 | } else { 240 | error(table.concat({ 241 | "Invalid Lua target: { new_lua_target }", 242 | "Must be one of: { table.concat(VALID_LUA_TARGETS, ', ') }", 243 | }, '\n')) 244 | } 245 | } elseif jit != nil { 246 | config.lua_target = 'jit' 247 | } else { 248 | new_lua_target = _VERSION:match('Lua (%d%.%d)') 249 | if VALID_LUA_TARGETS[new_lua_target] { 250 | config.lua_target = new_lua_target 251 | } else { 252 | error("Unsupported Lua version: { _VERSION }") 253 | } 254 | } 255 | 256 | for _, searcher in ipairs(searchers) { 257 | if searcher == erde_searcher { 258 | return 259 | } 260 | } 261 | 262 | -- We need to place the searcher before the `.lua` searcher to prioritize Erde 263 | -- modules over Lua modules. If the user has compiled an Erde project before 264 | -- but the compiled files are out of date, we need to avoid loading the 265 | -- outdated modules. 266 | table.insert(searchers, 2, erde_searcher) 267 | } 268 | 269 | module function unload() { 270 | -- Restore Lua's native traceback 271 | debug.traceback = native_traceback 272 | 273 | for i, searcher in ipairs(searchers) { 274 | if searcher == erde_searcher { 275 | table.remove(searchers, i) 276 | return 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /erde/lib.lua: -------------------------------------------------------------------------------- 1 | local _MODULE = {} 2 | local compile = require("erde.compile") 3 | local config = require("erde.config") 4 | local PATH_SEPARATOR, VALID_LUA_TARGETS 5 | local __ERDE_TMP_6__ = require("erde.constants") 6 | PATH_SEPARATOR = __ERDE_TMP_6__.PATH_SEPARATOR 7 | VALID_LUA_TARGETS = __ERDE_TMP_6__.VALID_LUA_TARGETS 8 | local io, string 9 | local __ERDE_TMP_9__ = require("erde.stdlib") 10 | io = __ERDE_TMP_9__.io 11 | string = __ERDE_TMP_9__.string 12 | local echo, get_source_summary 13 | local __ERDE_TMP_12__ = require("erde.utils") 14 | echo = __ERDE_TMP_12__.echo 15 | get_source_summary = __ERDE_TMP_12__.get_source_summary 16 | local loadlua = loadstring or load 17 | local unpack = table.unpack or unpack 18 | local native_traceback = debug.traceback 19 | local searchers = package.loaders or package.searchers 20 | local erde_source_cache = {} 21 | local erde_source_id_counter = 1 22 | function _MODULE.rewrite(message) 23 | if type(message) ~= "string" then 24 | return message 25 | end 26 | for erde_source_id, chunkname, compiled_line in message:gmatch('%[string "erde::(%d+)::([^\n]+)"]:(%d+)') do 27 | local cache = erde_source_cache[tonumber(erde_source_id)] or {} 28 | local source_map = cache.source_map or {} 29 | local source_line = source_map[tonumber(compiled_line)] or "(compiled:" .. tostring(compiled_line) .. ")" 30 | local match = string.escape( 31 | '[string "erde::' 32 | .. tostring(erde_source_id) 33 | .. "::" 34 | .. tostring(chunkname) 35 | .. '"]:' 36 | .. tostring(compiled_line) 37 | ) 38 | message = cache.has_alias and message:gsub(match, chunkname .. ":" .. source_line) 39 | or message:gsub(match, '[string "' .. tostring(chunkname) .. '"]:' .. tostring(source_line)) 40 | end 41 | message = message:gsub("__ERDE_SUBSTITUTE_([a-zA-Z]+)__", "%1") 42 | return message 43 | end 44 | function _MODULE.traceback(arg1, arg2, arg3) 45 | local stacktrace, level 46 | if type(arg1) == "thread" then 47 | level = arg3 or 1 48 | stacktrace = native_traceback(arg1, arg2, level + 1) 49 | else 50 | level = arg2 or 1 51 | stacktrace = native_traceback(arg1, level + 1) 52 | end 53 | if type(stacktrace) ~= "string" then 54 | return stacktrace 55 | end 56 | if level > -1 and config.is_cli_runtime then 57 | local stack = string.split(stacktrace, "\n") 58 | local stacklen = #stack 59 | for i = 1, 4 do 60 | table.remove(stack, stacklen - i) 61 | end 62 | stacktrace = table.concat(stack, "\n") 63 | end 64 | stacktrace = stacktrace:gsub( 65 | table.concat({ 66 | "[^\n]*\n", 67 | "[^\n]*__erde_internal_load_source__[^\n]*\n", 68 | "[^\n]*\n", 69 | }), 70 | "" 71 | ) 72 | return _MODULE.rewrite(stacktrace) 73 | end 74 | function _MODULE.__erde_internal_load_source__(source, options) 75 | if options == nil then 76 | options = {} 77 | end 78 | local chunkname = table.concat({ 79 | "erde", 80 | erde_source_id_counter, 81 | options.alias or get_source_summary(source), 82 | }, "::") 83 | local compiled, source_map = compile(source, { 84 | alias = options.alias, 85 | lua_target = options.lua_target, 86 | bitlib = options.bitlib, 87 | }) 88 | compiled = compiled:gsub("^#![^\n]+", "") 89 | local loader, load_error = loadlua(compiled, chunkname) 90 | if load_error ~= nil then 91 | error(table.concat({ 92 | "Failed to load compiled code:", 93 | tostring(load_error), 94 | "", 95 | "This is an internal error that should never happen.", 96 | "Please report this at: https://github.com/erde-lang/erde/issues", 97 | "", 98 | "erde", 99 | "----", 100 | source, 101 | "", 102 | "lua", 103 | "---", 104 | compiled, 105 | }, "\n")) 106 | end 107 | erde_source_cache[erde_source_id_counter] = { 108 | has_alias = options.alias ~= nil, 109 | } 110 | if not config.disable_source_maps and not options.disable_source_maps then 111 | erde_source_cache[erde_source_id_counter].source_map = source_map 112 | end 113 | erde_source_id_counter = erde_source_id_counter + 1 114 | return loader() 115 | end 116 | function _MODULE.run(source, options) 117 | return echo(_MODULE.__erde_internal_load_source__(source, options)) 118 | end 119 | local function erde_searcher(module_name) 120 | local module_path = module_name:gsub("%.", PATH_SEPARATOR) 121 | for path in package.path:gmatch("[^;]+") do 122 | local fullpath = path:gsub("%.lua$", ".erde"):gsub("?", module_path) 123 | if io.exists(fullpath) then 124 | return function() 125 | local source = io.readfile(fullpath) 126 | local result = { 127 | _MODULE.__erde_internal_load_source__(source, { 128 | alias = fullpath, 129 | }), 130 | } 131 | return unpack(result) 132 | end 133 | end 134 | end 135 | end 136 | function _MODULE.load(arg1, arg2) 137 | local new_lua_target, options = nil, {} 138 | if type(arg1) == "string" then 139 | new_lua_target = arg1 140 | end 141 | if type(arg1) == "table" then 142 | options = arg1 143 | elseif type(arg2) == "table" then 144 | options = arg2 145 | end 146 | config.bitlib = options.bitlib 147 | config.disable_source_maps = options.disable_source_maps 148 | debug.traceback = options.keep_traceback == true and native_traceback or _MODULE.traceback 149 | if new_lua_target ~= nil then 150 | if VALID_LUA_TARGETS[new_lua_target] then 151 | config.lua_target = new_lua_target 152 | else 153 | error(table.concat({ 154 | "Invalid Lua target: " .. tostring(new_lua_target), 155 | "Must be one of: " .. tostring(table.concat(VALID_LUA_TARGETS, ", ")), 156 | }, "\n")) 157 | end 158 | elseif jit ~= nil then 159 | config.lua_target = "jit" 160 | else 161 | new_lua_target = _VERSION:match("Lua (%d%.%d)") 162 | if VALID_LUA_TARGETS[new_lua_target] then 163 | config.lua_target = new_lua_target 164 | else 165 | error("Unsupported Lua version: " .. tostring(_VERSION)) 166 | end 167 | end 168 | for _, searcher in ipairs(searchers) do 169 | if searcher == erde_searcher then 170 | return 171 | end 172 | end 173 | table.insert(searchers, 2, erde_searcher) 174 | end 175 | function _MODULE.unload() 176 | debug.traceback = native_traceback 177 | for i, searcher in ipairs(searchers) do 178 | if searcher == erde_searcher then 179 | table.remove(searchers, i) 180 | return 181 | end 182 | end 183 | end 184 | return _MODULE 185 | -- Compiled with Erde 1.0.0-1 w/ Lua target 5.1+ 186 | -- __ERDE_COMPILED__ 187 | -------------------------------------------------------------------------------- /erde/stdlib.erde: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------------------------------- 2 | -- Libraries 3 | -- 4 | -- Forward declared to be accessible anywhere. 5 | -- ----------------------------------------------------------------------------- 6 | 7 | local _native_coroutine = coroutine 8 | module coroutine = {} 9 | 10 | local _native_debug = debug 11 | module debug = {} 12 | 13 | local _native_io = io 14 | module io = {} 15 | 16 | local _native_math = math 17 | module math = {} 18 | 19 | local _native_os = os 20 | module os = {} 21 | 22 | local _native_package = package 23 | module package = {} 24 | 25 | local _native_string = string 26 | module string = {} 27 | 28 | local _native_table = table 29 | module table = {} 30 | 31 | -- ----------------------------------------------------------------------------- 32 | -- Load / Unload 33 | -- ----------------------------------------------------------------------------- 34 | 35 | module function load() { 36 | for key, value in pairs(_MODULE) { 37 | local value_type = type(value) 38 | 39 | if value_type == 'function' { 40 | if key != 'load' && key != 'unload' { 41 | _G[key] = value 42 | } 43 | } elseif value_type == 'table' { 44 | local library = _G[key] 45 | 46 | if type(library) == 'table' { 47 | for subkey, subvalue in pairs(value) { 48 | library[subkey] = subvalue 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | module function unload() { 56 | for key, value in pairs(_MODULE) { 57 | local value_type = type(value) 58 | 59 | if value_type == 'function' { 60 | if _G[key] == value { -- only remove values we injected 61 | _G[key] = nil 62 | } 63 | } elseif value_type == 'table' { 64 | local library = _G[key] 65 | 66 | if type(library) == 'table' { 67 | for subkey, subvalue in pairs(value) { 68 | if library[subkey] == subvalue { -- only remove values we injected 69 | library[subkey] = nil 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | -- ----------------------------------------------------------------------------- 78 | -- Globals 79 | -- ----------------------------------------------------------------------------- 80 | 81 | local function _kpairs_iter(a, i) { 82 | local key, value = i, nil 83 | 84 | repeat { 85 | key, value = next(a, key) 86 | } until type(key) != 'number' 87 | 88 | return key, value 89 | } 90 | 91 | module function kpairs(t) { 92 | return _kpairs_iter, t, nil 93 | } 94 | 95 | -- ----------------------------------------------------------------------------- 96 | -- Coroutine 97 | -- ----------------------------------------------------------------------------- 98 | 99 | -- EMPTY 100 | 101 | -- ----------------------------------------------------------------------------- 102 | -- Debug 103 | -- ----------------------------------------------------------------------------- 104 | 105 | -- EMPTY 106 | 107 | -- ----------------------------------------------------------------------------- 108 | -- IO 109 | -- ----------------------------------------------------------------------------- 110 | 111 | function io.exists(path) { 112 | local file = io.open(path, 'r') 113 | 114 | if file == nil { 115 | return false 116 | } 117 | 118 | file:close() 119 | 120 | return true 121 | } 122 | 123 | function io.readfile(path) { 124 | local file = assert(io.open(path, 'r')) 125 | local content = assert(file:read('*a')) 126 | file:close() 127 | return content 128 | } 129 | 130 | function io.writefile(path, content) { 131 | local file = assert(io.open(path, 'w')) 132 | assert(file:write(content)) 133 | file:close() 134 | } 135 | 136 | -- ----------------------------------------------------------------------------- 137 | -- Math 138 | -- ----------------------------------------------------------------------------- 139 | 140 | function math.clamp(x, min, max) { 141 | return math.min(math.max(x, min), max) 142 | } 143 | 144 | function math.round(x) { 145 | if x < 0 { 146 | return math.ceil(x - 0.5) 147 | } else { 148 | return math.floor(x + 0.5) 149 | } 150 | } 151 | 152 | function math.sign(x) { 153 | if x < 0 { 154 | return -1 155 | } elseif x > 0 { 156 | return 1 157 | } else { 158 | return 0 159 | } 160 | } 161 | 162 | -- ----------------------------------------------------------------------------- 163 | -- OS 164 | -- ----------------------------------------------------------------------------- 165 | 166 | function os.capture(cmd) { 167 | local file = assert(io.popen(cmd, 'r')) 168 | local stdout = assert(file:read('*a')) 169 | file:close() 170 | return stdout 171 | } 172 | 173 | -- ----------------------------------------------------------------------------- 174 | -- Package 175 | -- ----------------------------------------------------------------------------- 176 | 177 | function package.cinsert(...) { 178 | local templates = package.split(package.cpath) 179 | table.insert(templates, ...) 180 | package.cpath = package.concat(templates) 181 | } 182 | 183 | function package.concat(templates, i, j) { 184 | local template_separator = string.split(package.config, '\n')[2] 185 | return table.concat(templates, template_separator, i, j) 186 | } 187 | 188 | function package.cremove(position) { 189 | local templates = package.split(package.cpath) 190 | local removed = table.remove(templates, position) 191 | package.cpath = package.concat(templates) 192 | return removed 193 | } 194 | 195 | function package.insert(...) { 196 | local templates = package.split(package.path) 197 | table.insert(templates, ...) 198 | package.path = package.concat(templates) 199 | } 200 | 201 | function package.remove(position) { 202 | local templates = package.split(package.path) 203 | local removed = table.remove(templates, position) 204 | package.path = package.concat(templates) 205 | return removed 206 | } 207 | 208 | function package.split(path) { 209 | local template_separator = string.split(package.config, '\n')[2] 210 | return string.split(path, template_separator) 211 | } 212 | 213 | -- ----------------------------------------------------------------------------- 214 | -- String 215 | -- ----------------------------------------------------------------------------- 216 | 217 | local function _string_chars_iter(a, i) { 218 | i += 1 219 | local char = a:sub(i, i) 220 | if char != '' { 221 | return i, char 222 | } 223 | } 224 | 225 | function string.chars(s) { 226 | return _string_chars_iter, s, 0 227 | } 228 | 229 | function string.escape(s) { 230 | -- Store in local variable to ensure we return only 1 (stylua will remove wrapping parens) 231 | local escaped = s:gsub('[().%%+%-*?[^$]', '%%%1') 232 | return escaped 233 | } 234 | 235 | function string.lpad(s, length, padding = ' ') { 236 | return padding:rep(math.ceil((length - #s) / #padding)) .. s 237 | } 238 | 239 | function string.ltrim(s, pattern = '%s+') { 240 | -- Store in local variable to ensure we return only 1 (stylua will remove wrapping parens) 241 | local trimmed = s:gsub("^{pattern}", '') 242 | return trimmed 243 | } 244 | 245 | function string.pad(s, length, padding = ' ') { 246 | local num_pads = math.ceil(((length - #s) / #padding) / 2) 247 | return padding:rep(num_pads) .. s .. padding:rep(num_pads) 248 | } 249 | 250 | function string.rpad(s, length, padding = ' ') { 251 | return s .. padding:rep(math.ceil((length - #s) / #padding)) 252 | } 253 | 254 | function string.rtrim(s, pattern = '%s+') { 255 | -- Store in local variable to ensure we return only 1 (stylua will remove wrapping parens) 256 | local trimmed = s:gsub("{pattern}$", '') 257 | return trimmed 258 | } 259 | 260 | function string.split(s, separator = '%s+') { 261 | local result = {} 262 | local i, j = s:find(separator) 263 | 264 | while i != nil { 265 | table.insert(result, s:sub(1, i - 1)) 266 | s = s:sub(j + 1) || '' 267 | i, j = s:find(separator) 268 | } 269 | 270 | table.insert(result, s) 271 | return result 272 | } 273 | 274 | function string.trim(s, pattern = '%s+') { 275 | return string.ltrim(string.rtrim(s, pattern), pattern) 276 | } 277 | 278 | -- ----------------------------------------------------------------------------- 279 | -- Table 280 | -- ----------------------------------------------------------------------------- 281 | 282 | -- Polyfill `table.pack` and `table.unpack` 283 | if _VERSION == 'Lua 5.1' { 284 | table.pack = (...) -> ({ n = select('#', ...), ... }) 285 | table.unpack = unpack 286 | } 287 | 288 | function table.clear(t, callback) { 289 | if type(callback) == 'function' { 290 | for key, value in kpairs(t) { 291 | if callback(value, key) { 292 | t[key] = nil 293 | } 294 | } 295 | 296 | for i = #t, 1, -1 { 297 | if callback(t[i], i) { 298 | table.remove(t, i) 299 | } 300 | } 301 | } else { 302 | for key, value in kpairs(t) { 303 | if value == callback { 304 | t[key] = nil 305 | } 306 | } 307 | 308 | for i = #t, 1, -1 { 309 | if t[i] == callback { 310 | table.remove(t, i) 311 | } 312 | } 313 | } 314 | } 315 | 316 | function table.collect(...) { 317 | local result = {} 318 | 319 | for key, value in ... { 320 | if value == nil { 321 | table.insert(result, key) 322 | } else { 323 | result[key] = value 324 | } 325 | } 326 | 327 | return result 328 | } 329 | 330 | function table.deepcopy(t) { 331 | local result = {} 332 | 333 | for key, value in pairs(t) { 334 | if type(value) == 'table' { 335 | result[key] = table.deepcopy(value) 336 | } else { 337 | result[key] = value 338 | } 339 | } 340 | 341 | return result 342 | } 343 | 344 | function table.empty(t) { 345 | return next(t) == nil 346 | } 347 | 348 | function table.filter(t, callback) { 349 | local result = {} 350 | 351 | for key, value in pairs(t) { 352 | if callback(value, key) { 353 | if type(key) == 'number' { 354 | table.insert(result, value) 355 | } else { 356 | result[key] = value 357 | } 358 | } 359 | } 360 | 361 | return result 362 | } 363 | 364 | function table.find(t, callback) { 365 | if type(callback) == 'function' { 366 | for key, value in pairs(t) { 367 | if callback(value, key) { 368 | return value, key 369 | } 370 | } 371 | } else { 372 | for key, value in pairs(t) { 373 | if value == callback { 374 | return value, key 375 | } 376 | } 377 | } 378 | } 379 | 380 | function table.has(t, callback) { 381 | local _, key = table.find(t, callback) 382 | return key != nil 383 | } 384 | 385 | function table.keys(t) { 386 | local result = {} 387 | 388 | for key, value in pairs(t) { 389 | table.insert(result, key) 390 | } 391 | 392 | return result 393 | } 394 | 395 | function table.map(t, callback) { 396 | local result = {} 397 | 398 | for key, value in pairs(t) { 399 | local newValue, newKey = callback(value, key) 400 | 401 | if newKey != nil { 402 | result[newKey] = newValue 403 | } elseif type(key) == 'number' { 404 | table.insert(result, newValue) 405 | } else { 406 | result[key] = newValue 407 | } 408 | } 409 | 410 | return result 411 | } 412 | 413 | function table.merge(t, ...) { 414 | for _, _t in pairs({ ... }) { 415 | for key, value in pairs(_t) { 416 | if type(key) == 'number' { 417 | table.insert(t, value) 418 | } else { 419 | t[key] = value 420 | } 421 | } 422 | } 423 | } 424 | 425 | function table.reduce(t, initial, callback) { 426 | local result = initial 427 | 428 | for key, value in pairs(t) { 429 | result = callback(result, value, key) 430 | } 431 | 432 | return result 433 | } 434 | 435 | function table.reverse(t) { 436 | local len = #t 437 | 438 | for i = 1, math.floor(len / 2) { 439 | t[i], t[len - i + 1] = t[len - i + 1], t[i] 440 | } 441 | } 442 | 443 | function table.shallowcopy(t) { 444 | local result = {} 445 | 446 | for key, value in pairs(t) { 447 | result[key] = value 448 | } 449 | 450 | return result 451 | } 452 | 453 | function table.slice(t, i = 1, j = #t) { 454 | local result, len = {}, #t 455 | 456 | if i < 0 { i = i + len + 1 } 457 | if j < 0 { j = j + len + 1 } 458 | 459 | for i = math.max(i, 0), math.min(j, len) { 460 | table.insert(result, t[i]) 461 | } 462 | 463 | return result 464 | } 465 | 466 | function table.values(t) { 467 | local result = {} 468 | 469 | for key, value in pairs(t) { 470 | table.insert(result, value) 471 | } 472 | 473 | return result 474 | } 475 | 476 | -- ----------------------------------------------------------------------------- 477 | -- Library Metatables 478 | -- 479 | -- Set library metatables. We must do this at the end, since our libraries will 480 | -- effectively be frozen once the `__newindex` metamethod is set. 481 | -- ----------------------------------------------------------------------------- 482 | 483 | setmetatable(coroutine, { __index = _native_coroutine, __newindex = _native_coroutine }) 484 | setmetatable(debug, { __index = _native_debug, __newindex = _native_debug }) 485 | setmetatable(io, { __index = _native_io, __newindex = _native_io }) 486 | setmetatable(math, { __index = _native_math, __newindex = _native_math }) 487 | setmetatable(os, { __index = _native_os, __newindex = _native_os }) 488 | setmetatable(package, { __index = _native_package, __newindex = _native_package }) 489 | setmetatable(string, { __index = _native_string, __newindex = _native_string }) 490 | setmetatable(table, { __index = _native_table, __newindex = _native_table }) 491 | -------------------------------------------------------------------------------- /erde/stdlib.lua: -------------------------------------------------------------------------------- 1 | local _MODULE = {} 2 | local _native_coroutine = coroutine 3 | _MODULE.coroutine = {} 4 | local _native_debug = debug 5 | _MODULE.debug = {} 6 | local _native_io = io 7 | _MODULE.io = {} 8 | local _native_math = math 9 | _MODULE.math = {} 10 | local _native_os = os 11 | _MODULE.os = {} 12 | local _native_package = package 13 | _MODULE.package = {} 14 | local _native_string = string 15 | _MODULE.string = {} 16 | local _native_table = table 17 | _MODULE.table = {} 18 | function _MODULE.load() 19 | for key, value in pairs(_MODULE) do 20 | local value_type = type(value) 21 | if value_type == "function" then 22 | if key ~= "load" and key ~= "unload" then 23 | _G[key] = value 24 | end 25 | elseif value_type == "table" then 26 | local library = _G[key] 27 | if type(library) == "table" then 28 | for subkey, subvalue in pairs(value) do 29 | library[subkey] = subvalue 30 | end 31 | end 32 | end 33 | end 34 | end 35 | function _MODULE.unload() 36 | for key, value in pairs(_MODULE) do 37 | local value_type = type(value) 38 | if value_type == "function" then 39 | if _G[key] == value then 40 | _G[key] = nil 41 | end 42 | elseif value_type == "table" then 43 | local library = _G[key] 44 | if type(library) == "table" then 45 | for subkey, subvalue in pairs(value) do 46 | if library[subkey] == subvalue then 47 | library[subkey] = nil 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | local function _kpairs_iter(a, i) 55 | local key, value = i, nil 56 | repeat 57 | key, value = next(a, key) 58 | until type(key) ~= "number" 59 | return key, value 60 | end 61 | function _MODULE.kpairs(t) 62 | return _kpairs_iter, t, nil 63 | end 64 | function _MODULE.io.exists(path) 65 | local file = _MODULE.io.open(path, "r") 66 | if file == nil then 67 | return false 68 | end 69 | file:close() 70 | return true 71 | end 72 | function _MODULE.io.readfile(path) 73 | local file = assert(_MODULE.io.open(path, "r")) 74 | local content = assert(file:read("*a")) 75 | file:close() 76 | return content 77 | end 78 | function _MODULE.io.writefile(path, content) 79 | local file = assert(_MODULE.io.open(path, "w")) 80 | assert(file:write(content)) 81 | file:close() 82 | end 83 | function _MODULE.math.clamp(x, min, max) 84 | return _MODULE.math.min(_MODULE.math.max(x, min), max) 85 | end 86 | function _MODULE.math.round(x) 87 | if x < 0 then 88 | return _MODULE.math.ceil(x - 0.5) 89 | else 90 | return _MODULE.math.floor(x + 0.5) 91 | end 92 | end 93 | function _MODULE.math.sign(x) 94 | if x < 0 then 95 | return -1 96 | elseif x > 0 then 97 | return 1 98 | else 99 | return 0 100 | end 101 | end 102 | function _MODULE.os.capture(cmd) 103 | local file = assert(_MODULE.io.popen(cmd, "r")) 104 | local stdout = assert(file:read("*a")) 105 | file:close() 106 | return stdout 107 | end 108 | function _MODULE.package.cinsert(...) 109 | local templates = _MODULE.package.split(_MODULE.package.cpath) 110 | _MODULE.table.insert(templates, ...) 111 | _MODULE.package.cpath = _MODULE.package.concat(templates) 112 | end 113 | function _MODULE.package.concat(templates, i, j) 114 | local template_separator = _MODULE.string.split(_MODULE.package.config, "\n")[2] 115 | return _MODULE.table.concat(templates, template_separator, i, j) 116 | end 117 | function _MODULE.package.cremove(position) 118 | local templates = _MODULE.package.split(_MODULE.package.cpath) 119 | local removed = _MODULE.table.remove(templates, position) 120 | _MODULE.package.cpath = _MODULE.package.concat(templates) 121 | return removed 122 | end 123 | function _MODULE.package.insert(...) 124 | local templates = _MODULE.package.split(_MODULE.package.path) 125 | _MODULE.table.insert(templates, ...) 126 | _MODULE.package.path = _MODULE.package.concat(templates) 127 | end 128 | function _MODULE.package.remove(position) 129 | local templates = _MODULE.package.split(_MODULE.package.path) 130 | local removed = _MODULE.table.remove(templates, position) 131 | _MODULE.package.path = _MODULE.package.concat(templates) 132 | return removed 133 | end 134 | function _MODULE.package.split(path) 135 | local template_separator = _MODULE.string.split(_MODULE.package.config, "\n")[2] 136 | return _MODULE.string.split(path, template_separator) 137 | end 138 | local function _string_chars_iter(a, i) 139 | i = i + 1 140 | local char = a:sub(i, i) 141 | if char ~= "" then 142 | return i, char 143 | end 144 | end 145 | function _MODULE.string.chars(s) 146 | return _string_chars_iter, s, 0 147 | end 148 | function _MODULE.string.escape(s) 149 | local escaped = s:gsub("[().%%+%-*?[^$]", "%%%1") 150 | return escaped 151 | end 152 | function _MODULE.string.lpad(s, length, padding) 153 | if padding == nil then 154 | padding = " " 155 | end 156 | return padding:rep(_MODULE.math.ceil((length - #s) / #padding)) .. s 157 | end 158 | function _MODULE.string.ltrim(s, pattern) 159 | if pattern == nil then 160 | pattern = "%s+" 161 | end 162 | local trimmed = s:gsub("^" .. tostring(pattern), "") 163 | return trimmed 164 | end 165 | function _MODULE.string.pad(s, length, padding) 166 | if padding == nil then 167 | padding = " " 168 | end 169 | local num_pads = _MODULE.math.ceil(((length - #s) / #padding) / 2) 170 | return padding:rep(num_pads) .. s .. padding:rep(num_pads) 171 | end 172 | function _MODULE.string.rpad(s, length, padding) 173 | if padding == nil then 174 | padding = " " 175 | end 176 | return s .. padding:rep(_MODULE.math.ceil((length - #s) / #padding)) 177 | end 178 | function _MODULE.string.rtrim(s, pattern) 179 | if pattern == nil then 180 | pattern = "%s+" 181 | end 182 | local trimmed = s:gsub(tostring(pattern) .. "$", "") 183 | return trimmed 184 | end 185 | function _MODULE.string.split(s, separator) 186 | if separator == nil then 187 | separator = "%s+" 188 | end 189 | local result = {} 190 | local i, j = s:find(separator) 191 | while i ~= nil do 192 | _MODULE.table.insert(result, s:sub(1, i - 1)) 193 | s = s:sub(j + 1) or "" 194 | i, j = s:find(separator) 195 | end 196 | _MODULE.table.insert(result, s) 197 | return result 198 | end 199 | function _MODULE.string.trim(s, pattern) 200 | if pattern == nil then 201 | pattern = "%s+" 202 | end 203 | return _MODULE.string.ltrim(_MODULE.string.rtrim(s, pattern), pattern) 204 | end 205 | if _VERSION == "Lua 5.1" then 206 | _MODULE.table.pack = function(...) 207 | return { 208 | n = select("#", ...), 209 | ..., 210 | } 211 | end 212 | _MODULE.table.unpack = unpack 213 | end 214 | function _MODULE.table.clear(t, callback) 215 | if type(callback) == "function" then 216 | for key, value in _MODULE.kpairs(t) do 217 | if callback(value, key) then 218 | t[key] = nil 219 | end 220 | end 221 | for i = #t, 1, -1 do 222 | if callback(t[i], i) then 223 | _MODULE.table.remove(t, i) 224 | end 225 | end 226 | else 227 | for key, value in _MODULE.kpairs(t) do 228 | if value == callback then 229 | t[key] = nil 230 | end 231 | end 232 | for i = #t, 1, -1 do 233 | if t[i] == callback then 234 | _MODULE.table.remove(t, i) 235 | end 236 | end 237 | end 238 | end 239 | function _MODULE.table.collect(...) 240 | local result = {} 241 | for key, value in ... do 242 | if value == nil then 243 | _MODULE.table.insert(result, key) 244 | else 245 | result[key] = value 246 | end 247 | end 248 | return result 249 | end 250 | function _MODULE.table.deepcopy(t) 251 | local result = {} 252 | for key, value in pairs(t) do 253 | if type(value) == "table" then 254 | result[key] = _MODULE.table.deepcopy(value) 255 | else 256 | result[key] = value 257 | end 258 | end 259 | return result 260 | end 261 | function _MODULE.table.empty(t) 262 | return next(t) == nil 263 | end 264 | function _MODULE.table.filter(t, callback) 265 | local result = {} 266 | for key, value in pairs(t) do 267 | if callback(value, key) then 268 | if type(key) == "number" then 269 | _MODULE.table.insert(result, value) 270 | else 271 | result[key] = value 272 | end 273 | end 274 | end 275 | return result 276 | end 277 | function _MODULE.table.find(t, callback) 278 | if type(callback) == "function" then 279 | for key, value in pairs(t) do 280 | if callback(value, key) then 281 | return value, key 282 | end 283 | end 284 | else 285 | for key, value in pairs(t) do 286 | if value == callback then 287 | return value, key 288 | end 289 | end 290 | end 291 | end 292 | function _MODULE.table.has(t, callback) 293 | local _, key = _MODULE.table.find(t, callback) 294 | return key ~= nil 295 | end 296 | function _MODULE.table.keys(t) 297 | local result = {} 298 | for key, value in pairs(t) do 299 | _MODULE.table.insert(result, key) 300 | end 301 | return result 302 | end 303 | function _MODULE.table.map(t, callback) 304 | local result = {} 305 | for key, value in pairs(t) do 306 | local newValue, newKey = callback(value, key) 307 | if newKey ~= nil then 308 | result[newKey] = newValue 309 | elseif type(key) == "number" then 310 | _MODULE.table.insert(result, newValue) 311 | else 312 | result[key] = newValue 313 | end 314 | end 315 | return result 316 | end 317 | function _MODULE.table.merge(t, ...) 318 | for _, _t in pairs({ 319 | ..., 320 | }) do 321 | for key, value in pairs(_t) do 322 | if type(key) == "number" then 323 | _MODULE.table.insert(t, value) 324 | else 325 | t[key] = value 326 | end 327 | end 328 | end 329 | end 330 | function _MODULE.table.reduce(t, initial, callback) 331 | local result = initial 332 | for key, value in pairs(t) do 333 | result = callback(result, value, key) 334 | end 335 | return result 336 | end 337 | function _MODULE.table.reverse(t) 338 | local len = #t 339 | for i = 1, _MODULE.math.floor(len / 2) do 340 | t[i], t[len - i + 1] = t[len - i + 1], t[i] 341 | end 342 | end 343 | function _MODULE.table.shallowcopy(t) 344 | local result = {} 345 | for key, value in pairs(t) do 346 | result[key] = value 347 | end 348 | return result 349 | end 350 | function _MODULE.table.slice(t, i, j) 351 | if i == nil then 352 | i = 1 353 | end 354 | if j == nil then 355 | j = #t 356 | end 357 | local result, len = {}, #t 358 | if i < 0 then 359 | i = i + len + 1 360 | end 361 | if j < 0 then 362 | j = j + len + 1 363 | end 364 | for i = _MODULE.math.max(i, 0), _MODULE.math.min(j, len) do 365 | _MODULE.table.insert(result, t[i]) 366 | end 367 | return result 368 | end 369 | function _MODULE.table.values(t) 370 | local result = {} 371 | for key, value in pairs(t) do 372 | _MODULE.table.insert(result, value) 373 | end 374 | return result 375 | end 376 | setmetatable(_MODULE.coroutine, { 377 | __index = _native_coroutine, 378 | __newindex = _native_coroutine, 379 | }) 380 | setmetatable(_MODULE.debug, { 381 | __index = _native_debug, 382 | __newindex = _native_debug, 383 | }) 384 | setmetatable(_MODULE.io, { 385 | __index = _native_io, 386 | __newindex = _native_io, 387 | }) 388 | setmetatable(_MODULE.math, { 389 | __index = _native_math, 390 | __newindex = _native_math, 391 | }) 392 | setmetatable(_MODULE.os, { 393 | __index = _native_os, 394 | __newindex = _native_os, 395 | }) 396 | setmetatable(_MODULE.package, { 397 | __index = _native_package, 398 | __newindex = _native_package, 399 | }) 400 | setmetatable(_MODULE.string, { 401 | __index = _native_string, 402 | __newindex = _native_string, 403 | }) 404 | setmetatable(_MODULE.table, { 405 | __index = _native_table, 406 | __newindex = _native_table, 407 | }) 408 | return _MODULE 409 | -- Compiled with Erde 1.0.0-1 w/ Lua target 5.1+ 410 | -- __ERDE_COMPILED__ 411 | -------------------------------------------------------------------------------- /erde/tokenize.erde: -------------------------------------------------------------------------------- 1 | local config = require('erde.config') 2 | local { 3 | DIGIT, 4 | HEX, 5 | STANDARD_ESCAPE_CHARS, 6 | SYMBOLS, 7 | TOKEN_TYPES, 8 | WORD_BODY, 9 | WORD_HEAD, 10 | } = require('erde.constants') 11 | local { get_source_alias } = require('erde.utils') 12 | 13 | -- Foward declare 14 | local tokenize_token 15 | 16 | -- ----------------------------------------------------------------------------- 17 | -- State 18 | -- ----------------------------------------------------------------------------- 19 | 20 | -- Table of tokens 21 | local tokens = {} 22 | 23 | -- The source text to tokenize 24 | local text = '' 25 | 26 | -- The current character we are processing 27 | local current_char = '' 28 | 29 | -- The index of `current_char` in `text` 30 | local current_char_index = 1 31 | 32 | -- The line number of `current_char` in `text` 33 | local current_line = 1 34 | 35 | -- The name to use when referencing `text`. Used for error reporting. 36 | local source_name = '' 37 | 38 | -- ----------------------------------------------------------------------------- 39 | -- Helpers 40 | -- ----------------------------------------------------------------------------- 41 | 42 | local function peek(n) { 43 | return text:sub(current_char_index, current_char_index + n - 1) 44 | } 45 | 46 | local function look_ahead(n) { 47 | return text:sub(current_char_index + n, current_char_index + n) 48 | } 49 | 50 | local function throw(message, line = current_line) { 51 | -- Use error level 0 since we already include `source_name` 52 | error("{ source_name }:{ line }: { message }", 0) 53 | } 54 | 55 | local function consume(n = 1) { 56 | local consumed = n == 1 && current_char || peek(n) 57 | current_char_index += n 58 | current_char = text:sub(current_char_index, current_char_index) 59 | return consumed 60 | } 61 | 62 | local function newline() { 63 | current_line += 1 64 | return consume() -- '\n' 65 | } 66 | 67 | -- ----------------------------------------------------------------------------- 68 | -- Numbers 69 | -- ----------------------------------------------------------------------------- 70 | 71 | local function tokenize_binary() { 72 | consume(2) -- 0[bB] 73 | 74 | if current_char != '0' && current_char != '1' { 75 | throw('malformed binary') 76 | } 77 | 78 | local value = 0 79 | 80 | repeat { 81 | value = 2 * value + tonumber(consume()) 82 | } until current_char != '0' && current_char != '1' 83 | 84 | table.insert(tokens, { 85 | type = TOKEN_TYPES.NUMBER, 86 | line = current_line, 87 | value = tostring(value), 88 | }) 89 | } 90 | 91 | local function tokenize_decimal() { 92 | local value = '' 93 | 94 | while DIGIT[current_char] { 95 | value ..= consume() 96 | } 97 | 98 | if current_char == '.' && DIGIT[look_ahead(1)] { 99 | value ..= consume(2) 100 | 101 | while DIGIT[current_char] { 102 | value ..= consume() 103 | } 104 | } 105 | 106 | if current_char == 'e' || current_char == 'E' { 107 | value ..= consume() 108 | 109 | if current_char == '+' || current_char == '-' { 110 | value ..= consume() 111 | } 112 | 113 | if !DIGIT[current_char] { 114 | throw('missing exponent value') 115 | } 116 | 117 | while DIGIT[current_char] { 118 | value ..= consume() 119 | } 120 | } 121 | 122 | table.insert(tokens, { 123 | type = TOKEN_TYPES.NUMBER, 124 | line = current_line, 125 | value = value, 126 | }) 127 | } 128 | 129 | local function tokenize_hex() { 130 | consume(2) -- 0[xX] 131 | 132 | if !HEX[current_char] && !(current_char == '.' && HEX[look_ahead(1)]) { 133 | throw('malformed hex') 134 | } 135 | 136 | local value = 0 137 | 138 | while HEX[current_char] { 139 | value = 16 * value + tonumber(consume(), 16) 140 | } 141 | 142 | if current_char == '.' && HEX[look_ahead(1)] { 143 | consume() 144 | 145 | local counter = 1 146 | 147 | repeat { 148 | value += tonumber(consume(), 16) / (16 ^ counter) 149 | counter += 1 150 | } until !HEX[current_char] 151 | } 152 | 153 | if current_char == 'p' || current_char == 'P' { 154 | consume() 155 | 156 | local exponent, sign = 0, 1 157 | 158 | if current_char == '+' || current_char == '-' { 159 | sign *= tonumber(consume() .. '1') 160 | } 161 | 162 | if !DIGIT[current_char] { 163 | throw('missing exponent value') 164 | } 165 | 166 | repeat { 167 | exponent = 10 * exponent + tonumber(consume()) 168 | } until !DIGIT[current_char] 169 | 170 | value = value * 2 ^ (sign * exponent) 171 | } 172 | 173 | table.insert(tokens, { 174 | type = TOKEN_TYPES.NUMBER, 175 | line = current_line, 176 | value = tostring(value), 177 | }) 178 | } 179 | 180 | -- ----------------------------------------------------------------------------- 181 | -- Strings 182 | -- ----------------------------------------------------------------------------- 183 | 184 | local function escape_sequence() { 185 | if STANDARD_ESCAPE_CHARS[current_char] { 186 | return consume() 187 | } elseif DIGIT[current_char] { 188 | return consume() 189 | } elseif current_char == 'z' { 190 | if config.lua_target == '5.1' || config.lua_target == '5.1+' { 191 | throw('escape sequence \\z not compatible w/ lua targets 5.1, 5.1+') 192 | } 193 | 194 | return consume() 195 | } elseif current_char == 'x' { 196 | if config.lua_target == '5.1' || config.lua_target == '5.1+' { 197 | throw('escape sequence \\xXX not compatible w/ lua targets 5.1, 5.1+') 198 | } 199 | 200 | if !HEX[look_ahead(1)] || !HEX[look_ahead(2)] { 201 | throw('escape sequence \\xXX must use exactly 2 hex characters') 202 | } 203 | 204 | return consume(3) 205 | } elseif current_char == 'u' { 206 | if config.lua_target == '5.1' || config.lua_target == '5.1+' || config.lua_target == '5.2' || config.lua_target == '5.2+' { 207 | throw('escape sequence \\u{XXX} not compatible w/ lua targets 5.1, 5.1+, 5.2, 5.2+') 208 | } 209 | 210 | local sequence = consume() -- u 211 | 212 | if current_char != '{' { 213 | throw('missing { in escape sequence \\u{XXX}') 214 | } 215 | 216 | sequence ..= consume() 217 | 218 | if !HEX[current_char] { 219 | throw('missing hex in escape sequence \\u{XXX}') 220 | } 221 | 222 | while HEX[current_char] { 223 | sequence ..= consume() 224 | } 225 | 226 | if current_char != '}' { 227 | throw('missing } in escape sequence \\u{XXX}') 228 | } 229 | 230 | return sequence .. consume() 231 | } else { 232 | throw("invalid escape sequence \\{ current_char }") 233 | } 234 | } 235 | 236 | local function tokenize_interpolation() { 237 | table.insert(tokens, { 238 | type = TOKEN_TYPES.INTERPOLATION, 239 | line = current_line, 240 | value = consume(), -- '{' 241 | }) 242 | 243 | local interpolation_line = current_line 244 | local brace_depth = 0 -- Keep track of brace depth in case of nested braces 245 | 246 | while current_char != '}' || brace_depth > 0 { 247 | if current_char == '{' { 248 | brace_depth += 1 249 | 250 | table.insert(tokens, { 251 | type = TOKEN_TYPES.SYMBOL, 252 | line = current_line, 253 | value = consume(), 254 | }) 255 | } elseif current_char == '}' { 256 | brace_depth -= 1 257 | 258 | table.insert(tokens, { 259 | type = TOKEN_TYPES.SYMBOL, 260 | line = current_line, 261 | value = consume(), 262 | }) 263 | } elseif current_char == '' { 264 | throw('unterminated interpolation', interpolation_line) 265 | } else { 266 | tokenize_token() 267 | } 268 | } 269 | 270 | table.insert(tokens, { 271 | type = TOKEN_TYPES.INTERPOLATION, 272 | line = current_line, 273 | value = consume(), -- '}' 274 | }) 275 | } 276 | 277 | local function tokenize_single_quote_string() { 278 | table.insert(tokens, { 279 | type = TOKEN_TYPES.SINGLE_QUOTE_STRING, 280 | line = current_line, 281 | value = consume(), -- "'" 282 | }) 283 | 284 | local content = '' 285 | 286 | while current_char != "'" { 287 | if current_char == '' || current_char == '\n' { 288 | throw('unterminated string') 289 | } elseif current_char == '\\' { 290 | content ..= consume() .. escape_sequence() 291 | } else { 292 | content ..= consume() 293 | } 294 | } 295 | 296 | if content != '' { 297 | table.insert(tokens, { 298 | type = TOKEN_TYPES.STRING_CONTENT, 299 | line = current_line, 300 | value = content, 301 | }) 302 | } 303 | 304 | table.insert(tokens, { 305 | type = TOKEN_TYPES.SINGLE_QUOTE_STRING, 306 | line = current_line, 307 | value = consume(), -- "'" 308 | }) 309 | } 310 | 311 | local function tokenize_double_quote_string() { 312 | table.insert(tokens, { 313 | type = TOKEN_TYPES.DOUBLE_QUOTE_STRING, 314 | line = current_line, 315 | value = consume(), -- '"' 316 | }) 317 | 318 | local content, content_line = '', current_line 319 | 320 | while current_char != '"' { 321 | if current_char == '' || current_char == '\n' { 322 | throw('unterminated string') 323 | } elseif current_char == '\\' { 324 | consume() 325 | 326 | if current_char == '{' || current_char == '}' { 327 | content ..= consume() 328 | } else { 329 | content ..= '\\' .. escape_sequence() 330 | } 331 | } elseif current_char == '{' { 332 | if content != '' { 333 | table.insert(tokens, { 334 | type = TOKEN_TYPES.STRING_CONTENT, 335 | line = content_line, 336 | value = content, 337 | }) 338 | 339 | content, content_line = '', current_line 340 | } 341 | 342 | tokenize_interpolation() 343 | } else { 344 | content ..= consume() 345 | } 346 | } 347 | 348 | if content != '' { 349 | table.insert(tokens, { 350 | type = TOKEN_TYPES.STRING_CONTENT, 351 | line = content_line, 352 | value = content, 353 | }) 354 | } 355 | 356 | table.insert(tokens, { 357 | type = TOKEN_TYPES.DOUBLE_QUOTE_STRING, 358 | line = current_line, 359 | value = consume(), -- '"' 360 | }) 361 | } 362 | 363 | local function tokenize_block_string() { 364 | consume() -- '[' 365 | 366 | local equals = '' 367 | while current_char == '=' { 368 | equals ..= consume() 369 | } 370 | 371 | if current_char != '[' { 372 | throw('unterminated block string opening', current_line) 373 | } 374 | 375 | consume() -- '[' 376 | 377 | table.insert(tokens, { 378 | type = TOKEN_TYPES.BLOCK_STRING, 379 | line = current_line, 380 | value = '[' .. equals .. '[', 381 | equals = equals, 382 | }) 383 | 384 | local close_quote = ']' .. equals .. ']' 385 | local close_quote_len = #close_quote 386 | local block_string_line = current_line 387 | local content, content_line = '', current_line 388 | 389 | -- Check `current_char ~= ']'` first as slight optimization 390 | while current_char != ']' || peek(close_quote_len) != close_quote { 391 | if current_char == '' { 392 | throw('unterminated block string', block_string_line) 393 | } elseif current_char == '\n' { 394 | content ..= newline() 395 | } elseif current_char == '\\' { 396 | consume() 397 | 398 | if current_char == '{' || current_char == '}' { 399 | content ..= consume() 400 | } else { 401 | content ..= '\\' 402 | } 403 | } elseif current_char == '{' { 404 | if content != '' { 405 | table.insert(tokens, { 406 | type = TOKEN_TYPES.STRING_CONTENT, 407 | line = content_line, 408 | value = content, 409 | }) 410 | 411 | content, content_line = '', current_line 412 | } 413 | 414 | tokenize_interpolation() 415 | } else { 416 | content ..= consume() 417 | } 418 | } 419 | 420 | if content != '' { 421 | table.insert(tokens, { 422 | type = TOKEN_TYPES.STRING_CONTENT, 423 | line = content_line, 424 | value = content, 425 | }) 426 | } 427 | 428 | table.insert(tokens, { 429 | type = TOKEN_TYPES.BLOCK_STRING, 430 | line = current_line, 431 | value = consume(close_quote_len), 432 | }) 433 | } 434 | 435 | -- ----------------------------------------------------------------------------- 436 | -- Misc Tokenizers 437 | -- ----------------------------------------------------------------------------- 438 | 439 | local function tokenize_word() { 440 | local word = consume() 441 | 442 | while WORD_BODY[current_char] { 443 | word ..= consume() 444 | } 445 | 446 | table.insert(tokens, { 447 | type = TOKEN_TYPES.WORD, 448 | line = current_line, 449 | value = word, 450 | }) 451 | } 452 | 453 | local function tokenize_comment() { 454 | consume(2) -- '--' 455 | 456 | local is_block_comment, equals = false, '' 457 | 458 | if current_char == '[' { 459 | consume() 460 | 461 | while current_char == '=' { 462 | equals ..= consume() 463 | } 464 | 465 | if current_char == '[' { 466 | consume() 467 | is_block_comment = true 468 | } 469 | } 470 | 471 | if !is_block_comment { 472 | while current_char != '' && current_char != '\n' { 473 | consume() 474 | } 475 | } else { 476 | local close_quote = ']' .. equals .. ']' 477 | local close_quote_len = #close_quote 478 | local comment_line = current_line 479 | 480 | -- Check `current_char ~= ']'` first as slight optimization 481 | while current_char != ']' || peek(close_quote_len) != close_quote { 482 | if current_char == '' { 483 | throw('unterminated comment', comment_line) 484 | } elseif current_char == '\n' { 485 | newline() 486 | } else { 487 | consume() 488 | } 489 | } 490 | 491 | consume(close_quote_len) 492 | } 493 | } 494 | 495 | function tokenize_token() { 496 | if current_char == '\n' { 497 | newline() 498 | } elseif current_char == ' ' || current_char == '\t' { 499 | consume() 500 | } elseif WORD_HEAD[current_char] { 501 | tokenize_word() 502 | } elseif current_char == "'" { 503 | tokenize_single_quote_string() 504 | } elseif current_char == '"' { 505 | tokenize_double_quote_string() 506 | } else { 507 | local peek_two = peek(2) 508 | 509 | if peek_two == '--' { 510 | tokenize_comment() 511 | } elseif peek_two == '0x' || peek_two == '0X' { 512 | tokenize_hex() 513 | } elseif peek_two == '0b' || peek_two == '0B' { 514 | tokenize_binary() 515 | } elseif DIGIT[current_char] || (current_char == '.' && DIGIT[look_ahead(1)]) { 516 | tokenize_decimal() 517 | } elseif peek_two == '[[' || peek_two == '[=' { 518 | tokenize_block_string() 519 | } elseif SYMBOLS[peek(3)] { 520 | table.insert(tokens, { 521 | type = TOKEN_TYPES.SYMBOL, 522 | line = current_line, 523 | value = consume(3), 524 | }) 525 | } elseif SYMBOLS[peek_two] { 526 | table.insert(tokens, { 527 | type = TOKEN_TYPES.SYMBOL, 528 | line = current_line, 529 | value = consume(2), 530 | }) 531 | } else { 532 | table.insert(tokens, { 533 | type = TOKEN_TYPES.SYMBOL, 534 | line = current_line, 535 | value = consume(1), 536 | }) 537 | } 538 | } 539 | } 540 | 541 | -- ----------------------------------------------------------------------------- 542 | -- Main 543 | -- ----------------------------------------------------------------------------- 544 | 545 | return (new_text, new_source_name) -> { 546 | tokens = {} 547 | text = new_text 548 | current_char = text:sub(1, 1) 549 | current_char_index = 1 550 | current_line = 1 551 | source_name = new_source_name || get_source_alias(text) 552 | 553 | if peek(2) == '#!' { 554 | local shebang = consume(2) 555 | 556 | while current_char != '' && current_char != '\n' { 557 | shebang ..= consume() 558 | } 559 | 560 | table.insert(tokens, { 561 | type = TOKEN_TYPES.SHEBANG, 562 | line = current_line, 563 | value = shebang, 564 | }) 565 | } 566 | 567 | while current_char != '' { 568 | tokenize_token() 569 | } 570 | 571 | table.insert(tokens, { 572 | type = TOKEN_TYPES.EOF, 573 | line = current_line, 574 | value = nil, 575 | }) 576 | 577 | return tokens 578 | } 579 | -------------------------------------------------------------------------------- /erde/tokenize.lua: -------------------------------------------------------------------------------- 1 | local config = require("erde.config") 2 | local DIGIT, HEX, STANDARD_ESCAPE_CHARS, SYMBOLS, TOKEN_TYPES, WORD_BODY, WORD_HEAD 3 | local __ERDE_TMP_4__ = require("erde.constants") 4 | DIGIT = __ERDE_TMP_4__.DIGIT 5 | HEX = __ERDE_TMP_4__.HEX 6 | STANDARD_ESCAPE_CHARS = __ERDE_TMP_4__.STANDARD_ESCAPE_CHARS 7 | SYMBOLS = __ERDE_TMP_4__.SYMBOLS 8 | TOKEN_TYPES = __ERDE_TMP_4__.TOKEN_TYPES 9 | WORD_BODY = __ERDE_TMP_4__.WORD_BODY 10 | WORD_HEAD = __ERDE_TMP_4__.WORD_HEAD 11 | local get_source_alias 12 | local __ERDE_TMP_7__ = require("erde.utils") 13 | get_source_alias = __ERDE_TMP_7__.get_source_alias 14 | local tokenize_token 15 | local tokens = {} 16 | local text = "" 17 | local current_char = "" 18 | local current_char_index = 1 19 | local current_line = 1 20 | local source_name = "" 21 | local function peek(n) 22 | return text:sub(current_char_index, current_char_index + n - 1) 23 | end 24 | local function look_ahead(n) 25 | return text:sub(current_char_index + n, current_char_index + n) 26 | end 27 | local function throw(message, line) 28 | if line == nil then 29 | line = current_line 30 | end 31 | error(tostring(source_name) .. ":" .. tostring(line) .. ": " .. tostring(message), 0) 32 | end 33 | local function consume(n) 34 | if n == nil then 35 | n = 1 36 | end 37 | local consumed = n == 1 and current_char or peek(n) 38 | current_char_index = current_char_index + n 39 | current_char = text:sub(current_char_index, current_char_index) 40 | return consumed 41 | end 42 | local function newline() 43 | current_line = current_line + 1 44 | return consume() 45 | end 46 | local function tokenize_binary() 47 | consume(2) 48 | if current_char ~= "0" and current_char ~= "1" then 49 | throw("malformed binary") 50 | end 51 | local value = 0 52 | repeat 53 | value = 2 * value + tonumber(consume()) 54 | until current_char ~= "0" and current_char ~= "1" 55 | table.insert(tokens, { 56 | type = TOKEN_TYPES.NUMBER, 57 | line = current_line, 58 | value = tostring(value), 59 | }) 60 | end 61 | local function tokenize_decimal() 62 | local value = "" 63 | while DIGIT[current_char] do 64 | value = value .. (consume()) 65 | end 66 | if current_char == "." and DIGIT[look_ahead(1)] then 67 | value = value .. (consume(2)) 68 | while DIGIT[current_char] do 69 | value = value .. (consume()) 70 | end 71 | end 72 | if current_char == "e" or current_char == "E" then 73 | value = value .. (consume()) 74 | if current_char == "+" or current_char == "-" then 75 | value = value .. (consume()) 76 | end 77 | if not DIGIT[current_char] then 78 | throw("missing exponent value") 79 | end 80 | while DIGIT[current_char] do 81 | value = value .. (consume()) 82 | end 83 | end 84 | table.insert(tokens, { 85 | type = TOKEN_TYPES.NUMBER, 86 | line = current_line, 87 | value = value, 88 | }) 89 | end 90 | local function tokenize_hex() 91 | consume(2) 92 | if not HEX[current_char] and not (current_char == "." and HEX[look_ahead(1)]) then 93 | throw("malformed hex") 94 | end 95 | local value = 0 96 | while HEX[current_char] do 97 | value = 16 * value + tonumber(consume(), 16) 98 | end 99 | if current_char == "." and HEX[look_ahead(1)] then 100 | consume() 101 | local counter = 1 102 | repeat 103 | value = value + (tonumber(consume(), 16) / (16 ^ counter)) 104 | counter = counter + 1 105 | until not HEX[current_char] 106 | end 107 | if current_char == "p" or current_char == "P" then 108 | consume() 109 | local exponent, sign = 0, 1 110 | if current_char == "+" or current_char == "-" then 111 | sign = sign * (tonumber(consume() .. "1")) 112 | end 113 | if not DIGIT[current_char] then 114 | throw("missing exponent value") 115 | end 116 | repeat 117 | exponent = 10 * exponent + tonumber(consume()) 118 | until not DIGIT[current_char] 119 | value = value * 2 ^ (sign * exponent) 120 | end 121 | table.insert(tokens, { 122 | type = TOKEN_TYPES.NUMBER, 123 | line = current_line, 124 | value = tostring(value), 125 | }) 126 | end 127 | local function escape_sequence() 128 | if STANDARD_ESCAPE_CHARS[current_char] then 129 | return consume() 130 | elseif DIGIT[current_char] then 131 | return consume() 132 | elseif current_char == "z" then 133 | if config.lua_target == "5.1" or config.lua_target == "5.1+" then 134 | throw("escape sequence \\z not compatible w/ lua targets 5.1, 5.1+") 135 | end 136 | return consume() 137 | elseif current_char == "x" then 138 | if config.lua_target == "5.1" or config.lua_target == "5.1+" then 139 | throw("escape sequence \\xXX not compatible w/ lua targets 5.1, 5.1+") 140 | end 141 | if not HEX[look_ahead(1)] or not HEX[look_ahead(2)] then 142 | throw("escape sequence \\xXX must use exactly 2 hex characters") 143 | end 144 | return consume(3) 145 | elseif current_char == "u" then 146 | if 147 | config.lua_target == "5.1" 148 | or config.lua_target == "5.1+" 149 | or config.lua_target == "5.2" 150 | or config.lua_target == "5.2+" 151 | then 152 | throw("escape sequence \\u{XXX} not compatible w/ lua targets 5.1, 5.1+, 5.2, 5.2+") 153 | end 154 | local sequence = consume() 155 | if current_char ~= "{" then 156 | throw("missing { in escape sequence \\u{XXX}") 157 | end 158 | sequence = sequence .. (consume()) 159 | if not HEX[current_char] then 160 | throw("missing hex in escape sequence \\u{XXX}") 161 | end 162 | while HEX[current_char] do 163 | sequence = sequence .. (consume()) 164 | end 165 | if current_char ~= "}" then 166 | throw("missing } in escape sequence \\u{XXX}") 167 | end 168 | return sequence .. consume() 169 | else 170 | throw("invalid escape sequence \\" .. tostring(current_char)) 171 | end 172 | end 173 | local function tokenize_interpolation() 174 | table.insert(tokens, { 175 | type = TOKEN_TYPES.INTERPOLATION, 176 | line = current_line, 177 | value = consume(), 178 | }) 179 | local interpolation_line = current_line 180 | local brace_depth = 0 181 | while current_char ~= "}" or brace_depth > 0 do 182 | if current_char == "{" then 183 | brace_depth = brace_depth + 1 184 | table.insert(tokens, { 185 | type = TOKEN_TYPES.SYMBOL, 186 | line = current_line, 187 | value = consume(), 188 | }) 189 | elseif current_char == "}" then 190 | brace_depth = brace_depth - 1 191 | table.insert(tokens, { 192 | type = TOKEN_TYPES.SYMBOL, 193 | line = current_line, 194 | value = consume(), 195 | }) 196 | elseif current_char == "" then 197 | throw("unterminated interpolation", interpolation_line) 198 | else 199 | tokenize_token() 200 | end 201 | end 202 | table.insert(tokens, { 203 | type = TOKEN_TYPES.INTERPOLATION, 204 | line = current_line, 205 | value = consume(), 206 | }) 207 | end 208 | local function tokenize_single_quote_string() 209 | table.insert(tokens, { 210 | type = TOKEN_TYPES.SINGLE_QUOTE_STRING, 211 | line = current_line, 212 | value = consume(), 213 | }) 214 | local content = "" 215 | while current_char ~= "'" do 216 | if current_char == "" or current_char == "\n" then 217 | throw("unterminated string") 218 | elseif current_char == "\\" then 219 | content = content .. (consume() .. escape_sequence()) 220 | else 221 | content = content .. (consume()) 222 | end 223 | end 224 | if content ~= "" then 225 | table.insert(tokens, { 226 | type = TOKEN_TYPES.STRING_CONTENT, 227 | line = current_line, 228 | value = content, 229 | }) 230 | end 231 | table.insert(tokens, { 232 | type = TOKEN_TYPES.SINGLE_QUOTE_STRING, 233 | line = current_line, 234 | value = consume(), 235 | }) 236 | end 237 | local function tokenize_double_quote_string() 238 | table.insert(tokens, { 239 | type = TOKEN_TYPES.DOUBLE_QUOTE_STRING, 240 | line = current_line, 241 | value = consume(), 242 | }) 243 | local content, content_line = "", current_line 244 | while current_char ~= '"' do 245 | if current_char == "" or current_char == "\n" then 246 | throw("unterminated string") 247 | elseif current_char == "\\" then 248 | consume() 249 | if current_char == "{" or current_char == "}" then 250 | content = content .. (consume()) 251 | else 252 | content = content .. ("\\" .. escape_sequence()) 253 | end 254 | elseif current_char == "{" then 255 | if content ~= "" then 256 | table.insert(tokens, { 257 | type = TOKEN_TYPES.STRING_CONTENT, 258 | line = content_line, 259 | value = content, 260 | }) 261 | content, content_line = "", current_line 262 | end 263 | tokenize_interpolation() 264 | else 265 | content = content .. (consume()) 266 | end 267 | end 268 | if content ~= "" then 269 | table.insert(tokens, { 270 | type = TOKEN_TYPES.STRING_CONTENT, 271 | line = content_line, 272 | value = content, 273 | }) 274 | end 275 | table.insert(tokens, { 276 | type = TOKEN_TYPES.DOUBLE_QUOTE_STRING, 277 | line = current_line, 278 | value = consume(), 279 | }) 280 | end 281 | local function tokenize_block_string() 282 | consume() 283 | local equals = "" 284 | while current_char == "=" do 285 | equals = equals .. (consume()) 286 | end 287 | if current_char ~= "[" then 288 | throw("unterminated block string opening", current_line) 289 | end 290 | consume() 291 | table.insert(tokens, { 292 | type = TOKEN_TYPES.BLOCK_STRING, 293 | line = current_line, 294 | value = "[" .. equals .. "[", 295 | equals = equals, 296 | }) 297 | local close_quote = "]" .. equals .. "]" 298 | local close_quote_len = #close_quote 299 | local block_string_line = current_line 300 | local content, content_line = "", current_line 301 | while current_char ~= "]" or peek(close_quote_len) ~= close_quote do 302 | if current_char == "" then 303 | throw("unterminated block string", block_string_line) 304 | elseif current_char == "\n" then 305 | content = content .. (newline()) 306 | elseif current_char == "\\" then 307 | consume() 308 | if current_char == "{" or current_char == "}" then 309 | content = content .. (consume()) 310 | else 311 | content = content .. "\\" 312 | end 313 | elseif current_char == "{" then 314 | if content ~= "" then 315 | table.insert(tokens, { 316 | type = TOKEN_TYPES.STRING_CONTENT, 317 | line = content_line, 318 | value = content, 319 | }) 320 | content, content_line = "", current_line 321 | end 322 | tokenize_interpolation() 323 | else 324 | content = content .. (consume()) 325 | end 326 | end 327 | if content ~= "" then 328 | table.insert(tokens, { 329 | type = TOKEN_TYPES.STRING_CONTENT, 330 | line = content_line, 331 | value = content, 332 | }) 333 | end 334 | table.insert(tokens, { 335 | type = TOKEN_TYPES.BLOCK_STRING, 336 | line = current_line, 337 | value = consume(close_quote_len), 338 | }) 339 | end 340 | local function tokenize_word() 341 | local word = consume() 342 | while WORD_BODY[current_char] do 343 | word = word .. (consume()) 344 | end 345 | table.insert(tokens, { 346 | type = TOKEN_TYPES.WORD, 347 | line = current_line, 348 | value = word, 349 | }) 350 | end 351 | local function tokenize_comment() 352 | consume(2) 353 | local is_block_comment, equals = false, "" 354 | if current_char == "[" then 355 | consume() 356 | while current_char == "=" do 357 | equals = equals .. (consume()) 358 | end 359 | if current_char == "[" then 360 | consume() 361 | is_block_comment = true 362 | end 363 | end 364 | if not is_block_comment then 365 | while current_char ~= "" and current_char ~= "\n" do 366 | consume() 367 | end 368 | else 369 | local close_quote = "]" .. equals .. "]" 370 | local close_quote_len = #close_quote 371 | local comment_line = current_line 372 | while current_char ~= "]" or peek(close_quote_len) ~= close_quote do 373 | if current_char == "" then 374 | throw("unterminated comment", comment_line) 375 | elseif current_char == "\n" then 376 | newline() 377 | else 378 | consume() 379 | end 380 | end 381 | consume(close_quote_len) 382 | end 383 | end 384 | function tokenize_token() 385 | if current_char == "\n" then 386 | newline() 387 | elseif current_char == " " or current_char == "\t" then 388 | consume() 389 | elseif WORD_HEAD[current_char] then 390 | tokenize_word() 391 | elseif current_char == "'" then 392 | tokenize_single_quote_string() 393 | elseif current_char == '"' then 394 | tokenize_double_quote_string() 395 | else 396 | local peek_two = peek(2) 397 | if peek_two == "--" then 398 | tokenize_comment() 399 | elseif peek_two == "0x" or peek_two == "0X" then 400 | tokenize_hex() 401 | elseif peek_two == "0b" or peek_two == "0B" then 402 | tokenize_binary() 403 | elseif DIGIT[current_char] or (current_char == "." and DIGIT[look_ahead(1)]) then 404 | tokenize_decimal() 405 | elseif peek_two == "[[" or peek_two == "[=" then 406 | tokenize_block_string() 407 | elseif SYMBOLS[peek(3)] then 408 | table.insert(tokens, { 409 | type = TOKEN_TYPES.SYMBOL, 410 | line = current_line, 411 | value = consume(3), 412 | }) 413 | elseif SYMBOLS[peek_two] then 414 | table.insert(tokens, { 415 | type = TOKEN_TYPES.SYMBOL, 416 | line = current_line, 417 | value = consume(2), 418 | }) 419 | else 420 | table.insert(tokens, { 421 | type = TOKEN_TYPES.SYMBOL, 422 | line = current_line, 423 | value = consume(1), 424 | }) 425 | end 426 | end 427 | end 428 | return function(new_text, new_source_name) 429 | tokens = {} 430 | text = new_text 431 | current_char = text:sub(1, 1) 432 | current_char_index = 1 433 | current_line = 1 434 | source_name = new_source_name or get_source_alias(text) 435 | if peek(2) == "#!" then 436 | local shebang = consume(2) 437 | while current_char ~= "" and current_char ~= "\n" do 438 | shebang = shebang .. (consume()) 439 | end 440 | table.insert(tokens, { 441 | type = TOKEN_TYPES.SHEBANG, 442 | line = current_line, 443 | value = shebang, 444 | }) 445 | end 446 | while current_char ~= "" do 447 | tokenize_token() 448 | end 449 | table.insert(tokens, { 450 | type = TOKEN_TYPES.EOF, 451 | line = current_line, 452 | value = nil, 453 | }) 454 | return tokens 455 | end 456 | -- Compiled with Erde 1.0.0-1 w/ Lua target 5.1+ 457 | -- __ERDE_COMPILED__ 458 | -------------------------------------------------------------------------------- /erde/utils.erde: -------------------------------------------------------------------------------- 1 | local { PATH_SEPARATOR } = require('erde.constants') 2 | local { io, string } = require('erde.stdlib') 3 | 4 | module function echo(...) { 5 | return ... 6 | } 7 | 8 | module function join_paths(...) { 9 | -- Store in local variable to ensure we return only 1 (stylua will remove wrapping parens) 10 | local joined = table.concat({ ... }, PATH_SEPARATOR):gsub(PATH_SEPARATOR .. '+', PATH_SEPARATOR) 11 | return joined 12 | } 13 | 14 | module function get_source_summary(source) { 15 | local summary = string.trim(source):sub(1, 5) 16 | 17 | if #source > 5 { 18 | summary ..= '...' 19 | } 20 | 21 | return summary 22 | } 23 | 24 | module function get_source_alias(source) { 25 | return "[string \"{ get_source_summary(source) }\"]" 26 | } 27 | -------------------------------------------------------------------------------- /erde/utils.lua: -------------------------------------------------------------------------------- 1 | local _MODULE = {} 2 | local PATH_SEPARATOR 3 | local __ERDE_TMP_2__ = require("erde.constants") 4 | PATH_SEPARATOR = __ERDE_TMP_2__.PATH_SEPARATOR 5 | local io, string 6 | local __ERDE_TMP_5__ = require("erde.stdlib") 7 | io = __ERDE_TMP_5__.io 8 | string = __ERDE_TMP_5__.string 9 | function _MODULE.echo(...) 10 | return ... 11 | end 12 | function _MODULE.join_paths(...) 13 | local joined = table.concat({ 14 | ..., 15 | }, PATH_SEPARATOR):gsub(PATH_SEPARATOR .. "+", PATH_SEPARATOR) 16 | return joined 17 | end 18 | function _MODULE.get_source_summary(source) 19 | local summary = string.trim(source):sub(1, 5) 20 | if #source > 5 then 21 | summary = summary .. "..." 22 | end 23 | return summary 24 | end 25 | function _MODULE.get_source_alias(source) 26 | return '[string "' .. tostring(_MODULE.get_source_summary(source)) .. '"]' 27 | end 28 | return _MODULE 29 | -- Compiled with Erde 1.0.0-1 w/ Lua target 5.1+ 30 | -- __ERDE_COMPILED__ 31 | -------------------------------------------------------------------------------- /spec/api_spec.lua: -------------------------------------------------------------------------------- 1 | local erde = require('erde') 2 | local config = require('erde.config') 3 | 4 | -- ----------------------------------------------------------------------------- 5 | -- Helpers 6 | -- ----------------------------------------------------------------------------- 7 | 8 | local function make_load_spec(callback) 9 | return function() 10 | local old_lua_target = config.lua_target 11 | local old_disable_source_maps = config.disable_source_maps 12 | local old_bitlib = config.bitlib 13 | 14 | callback() 15 | 16 | config.lua_target = old_lua_target 17 | config.disable_source_maps = old_disable_source_maps 18 | config.bitlib = old_bitlib 19 | end 20 | end 21 | 22 | local function assert_rewrite(source, expected) 23 | local ok, result = xpcall(function() erde.run(source) end, erde.rewrite) 24 | assert.are.equal(false, ok) 25 | assert.are.equal(expected, result) 26 | end 27 | 28 | -- ----------------------------------------------------------------------------- 29 | -- API 30 | -- ----------------------------------------------------------------------------- 31 | 32 | spec('api #5.1+', function() 33 | assert.are.equal(erde.version, '1.0.0-1') 34 | assert.are.equal(erde.compile, require('erde.compile')) 35 | assert.are.equal(erde.rewrite, require('erde.lib').rewrite) 36 | assert.are.equal(erde.traceback, require('erde.lib').traceback) 37 | assert.are.equal(erde.run, require('erde.lib').run) 38 | assert.are.equal(erde.load, require('erde.lib').load) 39 | assert.are.equal(erde.unload, require('erde.lib').unload) 40 | end) 41 | 42 | -- ----------------------------------------------------------------------------- 43 | -- Rewrite 44 | -- ----------------------------------------------------------------------------- 45 | 46 | describe('erde.rewrite', function() 47 | spec('#jit #5.1 #5.2 #5.3', function() 48 | assert_rewrite( 49 | [[print('a' + 1)]], 50 | [[[string "print..."]:1: attempt to perform arithmetic on a string value]] 51 | ) 52 | 53 | assert_rewrite( 54 | [[ 55 | 56 | print('a' + 1) 57 | ]], 58 | [[[string "print..."]:2: attempt to perform arithmetic on a string value]] 59 | ) 60 | 61 | assert_rewrite( 62 | [[print( 63 | 'a' + 1)]], 64 | [[[string "print..."]:2: attempt to perform arithmetic on a string value]] 65 | ) 66 | 67 | assert_rewrite( 68 | [[ 69 | 70 | 71 | 72 | error('myerror') 73 | ]], 74 | [[[string "error..."]:4: myerror]] 75 | ) 76 | end) 77 | 78 | spec('#5.4', function() 79 | assert_rewrite( 80 | [[print('a' + 1)]], 81 | [[[string "print..."]:1: attempt to add a 'string' with a 'number']] 82 | ) 83 | 84 | assert_rewrite( 85 | [[ 86 | 87 | print('a' + 1) 88 | ]], 89 | [[[string "print..."]:2: attempt to add a 'string' with a 'number']] 90 | ) 91 | 92 | assert_rewrite( 93 | [[print( 94 | 'a' + 1)]], 95 | [[[string "print..."]:2: attempt to add a 'string' with a 'number']] 96 | ) 97 | 98 | assert_rewrite( 99 | [[ 100 | 101 | 102 | 103 | error('myerror') 104 | ]], 105 | [[[string "error..."]:4: myerror]] 106 | ) 107 | end) 108 | end) 109 | 110 | -- ----------------------------------------------------------------------------- 111 | -- Compile 112 | -- ----------------------------------------------------------------------------- 113 | 114 | spec('erde.compile #5.1+', function() 115 | assert.has_no.errors(function() 116 | erde.compile('') 117 | erde.compile('', {}) 118 | erde.compile('return') 119 | erde.compile('return', {}) 120 | end) 121 | end) 122 | 123 | spec('erde.compile lua target #5.1+', function() 124 | assert.has.errors(function() 125 | erde.compile('::test::', { lua_target = '5.1' }) 126 | end) 127 | assert.has_no.errors(function() 128 | erde.compile('::test::', { lua_target = 'jit' }) 129 | end) 130 | end) 131 | 132 | spec('erde.compile bitlib #5.1+', function() 133 | local compiled = erde.compile('print(1 & 1)', { bitlib = 'mybitlib' }) 134 | assert.is_not.falsy(compiled:find('mybitlib')) 135 | end) 136 | 137 | spec('erde.compile alias #5.1+', function() 138 | local ok, result = pcall(function() 139 | erde.compile('print(', { alias = 'myalias' }) 140 | end) 141 | 142 | assert.are.equal(false, ok) 143 | assert.are.equal('myalias:1: unexpected eof (expected expression)', result) 144 | end) 145 | 146 | spec('erde.compile returns #5.1+', function() 147 | local compiled, sourcemap = erde.compile('') 148 | assert.are.equal(type(compiled), 'string') 149 | assert.are.equal(type(sourcemap), 'table') 150 | 151 | local compiled, sourcemap = erde.compile('print("")') 152 | assert.are.equal(type(compiled), 'string') 153 | assert.are.equal(type(sourcemap), 'table') 154 | end) 155 | 156 | -- ----------------------------------------------------------------------------- 157 | -- Run 158 | -- ----------------------------------------------------------------------------- 159 | 160 | spec('erde.run #5.1+', function() 161 | assert.has_no.errors(function() 162 | erde.run('') 163 | erde.run('', {}) 164 | erde.run('return') 165 | erde.run('return', {}) 166 | end) 167 | end) 168 | 169 | spec('erde.run bitlib #5.1+', function() 170 | local ok, result = pcall(function() 171 | return erde.run('print(1 & 1)', { bitlib = 'mybitlib' }) 172 | end) 173 | 174 | assert.are.equal(false, ok) 175 | assert.is_not.falsy(result:find("module 'mybitlib' not found")) 176 | end) 177 | 178 | spec('erde.run alias #5.1+', function() 179 | local ok, result = pcall(function() 180 | erde.run('print(', { alias = 'myalias' }) 181 | end) 182 | 183 | assert.are.equal(false, ok) 184 | assert.are.equal('myalias:1: unexpected eof (expected expression)', result) 185 | 186 | local ok, result = xpcall(function() 187 | erde.run('error("myerror")', { alias = 'myalias' }) 188 | end, erde.rewrite) 189 | 190 | assert.are.equal(false, ok) 191 | assert.are.equal('myalias:1: myerror', result) 192 | end) 193 | 194 | spec('erde.run disable source maps #5.1+', function() 195 | local ok, result = xpcall(function() 196 | erde.run('error("myerror")', { 197 | alias = 'myalias', 198 | disable_source_maps = true, 199 | }) 200 | end, erde.rewrite) 201 | 202 | assert.are.equal(false, ok) 203 | assert.are.equal('myalias:(compiled:1): myerror', result) 204 | end) 205 | 206 | spec('erde.run multiple returns #5.1+', function() 207 | local a, b, c = erde.run('return 1, 2, 3') 208 | assert.are.equal(a, 1) 209 | assert.are.equal(b, 2) 210 | assert.are.equal(c, 3) 211 | end) 212 | 213 | -- ----------------------------------------------------------------------------- 214 | -- Load / Unload 215 | -- ----------------------------------------------------------------------------- 216 | 217 | local searchers = package.loaders or package.searchers 218 | local native_num_searchers = #searchers 219 | local native_traceback = debug.traceback 220 | 221 | spec('erde.load #5.1+', make_load_spec(function() 222 | erde.load() 223 | assert.are.equal(native_num_searchers + 1, #searchers) 224 | end)) 225 | 226 | spec('erde.load repeated calls #5.1+', make_load_spec(function() 227 | erde.load() 228 | assert.are.equal(native_num_searchers + 1, #searchers) 229 | erde.load() 230 | assert.are.equal(native_num_searchers + 1, #searchers) 231 | end)) 232 | 233 | spec('erde.load parameters #5.1+', make_load_spec(function() 234 | assert.has_no.errors(function() 235 | erde.load() 236 | erde.load('5.1') 237 | erde.load('5.1', {}) 238 | erde.load({}) 239 | end) 240 | end)) 241 | 242 | spec('erde.load Lua target #5.1+', make_load_spec(function() 243 | erde.load('5.1') 244 | assert.are.equal('5.1', config.lua_target) 245 | 246 | assert.has.errors(function() 247 | erde.load('5.5') -- invalid target 248 | end) 249 | end)) 250 | 251 | spec('erde.load keep_traceback #5.1+', make_load_spec(function() 252 | erde.load({ keep_traceback = true }) 253 | assert.are.equal(native_traceback, debug.traceback) 254 | 255 | erde.load({ keep_traceback = false }) 256 | assert.are_not.equal(native_traceback, debug.traceback) 257 | end)) 258 | 259 | spec('erde.load bitlib #5.1+', make_load_spec(function() 260 | erde.load({ bitlib = 'mybitlib' }) 261 | assert.are.equal(config.bitlib, 'mybitlib') 262 | end)) 263 | 264 | spec('erde.load disable_source_maps #5.1+', make_load_spec(function() 265 | erde.load({ disable_source_maps = true }) 266 | assert.are.equal(true, config.disable_source_maps) 267 | 268 | local ok, result = xpcall(function() 269 | erde.run('error("myerror")', { alias = 'myalias' }) 270 | end, erde.rewrite) 271 | 272 | assert.are.equal(false, ok) 273 | assert.are.equal('myalias:(compiled:1): myerror', result) 274 | end)) 275 | 276 | spec('erde.unload #5.1+', make_load_spec(function() 277 | erde.load() -- reset any flags 278 | erde.unload() 279 | assert.are.equal(native_num_searchers, #searchers) 280 | end)) 281 | -------------------------------------------------------------------------------- /spec/compile_assignments_spec.lua: -------------------------------------------------------------------------------- 1 | local compile = require('erde.compile') 2 | 3 | -- ----------------------------------------------------------------------------- 4 | -- Non-Operator Assignments 5 | -- ----------------------------------------------------------------------------- 6 | 7 | spec('single assignment #5.1+', function() 8 | assert_run(1, [[ 9 | local a 10 | a = 1 11 | return a 12 | ]]) 13 | 14 | assert_run(2, [[ 15 | local a = {} 16 | a.b = 2 17 | return a.b 18 | ]]) 19 | 20 | assert_run(3, [[ 21 | local a = {} 22 | (() -> a)().b = 3 23 | return a.b 24 | ]]) 25 | 26 | assert_run(4, [[ 27 | local a = { b = 0 } 28 | local c = { end = () -> a } 29 | c:end().b = 4 30 | return a.b 31 | ]]) 32 | end) 33 | 34 | spec('multi assignment #5.1+', function() 35 | assert_run({ 1, 2 }, [[ 36 | local a, b 37 | a, b = 1, 2 38 | return { a, b } 39 | ]]) 40 | 41 | assert_run({ 3, 4 }, [[ 42 | local a, b = {}, {} 43 | a.c, b.d = 3, 4 44 | return { a.c, b.d } 45 | ]]) 46 | 47 | assert_run({ 5, 6 }, [[ 48 | local a = { b = 0 } 49 | local c = { end = () -> a } 50 | c.d, c:end().b = 5, 6 51 | return { c.d, a.b } 52 | ]]) 53 | end) 54 | 55 | describe('assignment transform Lua keywords #5.1+', function() 56 | assert_run(1, [[ 57 | local end = 0 58 | end = 1 59 | return end 60 | ]]) 61 | 62 | assert_run(2, [[ 63 | local a = {} 64 | a.end = 2 65 | return a.end 66 | ]]) 67 | 68 | assert_run(3, [[ 69 | local end = {} 70 | end.a = 3 71 | return end.a 72 | ]]) 73 | end) 74 | 75 | spec('assignment use tracked scopes #5.1+', function() 76 | assert_run(1, [[ 77 | local a = 0 78 | global a = 0 79 | a = 1 80 | local result = _G.a 81 | _G.a = nil 82 | return result 83 | ]]) 84 | 85 | assert_run({ a = 2 }, [[ 86 | module a = 0 87 | a = 2 88 | ]]) 89 | end) 90 | 91 | spec('assignment function call index #5.1+', function() 92 | assert_run(1, [[ 93 | local a = {} 94 | a.b = () -> a 95 | a.b().c = 1 96 | return a.c 97 | ]]) 98 | 99 | assert_run(2, [[ 100 | local a = { b = () => self } 101 | a:b().c = 2 102 | return a.c 103 | ]]) 104 | 105 | assert.has_error(function() 106 | compile('a() = 0') 107 | end) 108 | 109 | assert.has_error(function() 110 | compile('a, b() = 0') 111 | end) 112 | end) 113 | 114 | -- ----------------------------------------------------------------------------- 115 | -- Operator Assignments 116 | -- ----------------------------------------------------------------------------- 117 | 118 | spec('single operator assignment #5.1+', function() 119 | assert_run(1, [[ 120 | local a = -1 121 | a += 2 122 | return a 123 | ]]) 124 | 125 | assert_run(2, [[ 126 | local a = { b = -1 } 127 | a.b += 3 128 | return a.b 129 | ]]) 130 | 131 | assert_run(3, [[ 132 | local a = { b = -1 } 133 | (() -> a)().b += 4 134 | return a.b 135 | ]]) 136 | 137 | assert_run(4, [[ 138 | local a = { b = -1 } 139 | local c = { end = () -> a } 140 | c:end().b += 5 141 | return a.b 142 | ]]) 143 | 144 | assert.has_error(function() 145 | compile('a() += 0') 146 | end) 147 | 148 | assert.has_error(function() 149 | compile('a, b() += 0') 150 | end) 151 | end) 152 | 153 | spec('multi operator assignment #5.1+', function() 154 | assert_run({ 1, 2 }, [[ 155 | local a, b = -1, -1 156 | a, b += 2, 3 157 | return { a, b } 158 | ]]) 159 | 160 | assert_run({ 3, 4 }, [[ 161 | local a, b = { c = -1 }, { d = -1 } 162 | a.c, b.d += 4, 5 163 | return { a.c, b.d } 164 | ]]) 165 | 166 | assert_run({ 5, 6 }, [[ 167 | local a = { b = -1 } 168 | local c = { d = -1, end = () -> a } 169 | c.d, c:end().b += 6, 7 170 | return { c.d, a.b } 171 | ]]) 172 | 173 | assert_run({ 7, 8 }, [[ 174 | local a = () -> (8, 9) 175 | local b, c = -1, -1 176 | b, c += a() 177 | return { b, c } 178 | ]]) 179 | end) 180 | 181 | spec('operator assignment transform Lua keywords #5.1+', function() 182 | assert_run(1, [[ 183 | local end = -1 184 | end += 2 185 | return end 186 | ]]) 187 | 188 | assert_run(2, [[ 189 | local a = { end = -1 } 190 | a.end += 3 191 | return a.end 192 | ]]) 193 | 194 | assert_run(3, [[ 195 | local end = { a = -1 } 196 | end.a += 4 197 | return end.a 198 | ]]) 199 | end) 200 | 201 | spec('operator assignment use tracked scopes #5.1+', function() 202 | assert_run(1, [[ 203 | local a = -1 204 | global a = -1 205 | a += 2 206 | local result = _G.a 207 | _G.a = nil 208 | return result 209 | ]]) 210 | 211 | assert_run({ a = 2 }, [[ 212 | module a = -1 213 | a += 3 214 | ]]) 215 | end) 216 | 217 | spec('operator assignment source map #5.1+', function() 218 | assert_source_map(1, [[ 219 | local { a } = b 220 | ]]) 221 | 222 | assert_source_map(2, [[ 223 | 224 | local { a } 225 | = b 226 | ]]) 227 | end) 228 | 229 | describe('operator assignment source map', function() 230 | spec('#5.1+', function() 231 | assert_source_map(1, [[ 232 | local a; a += 1 233 | ]]) 234 | 235 | assert_source_map(2, [[ 236 | local a = 'a' 237 | a += 1 238 | ]]) 239 | 240 | assert_source_map(3, [[ 241 | local a = 0 242 | a 243 | += 'a' 244 | ]]) 245 | end) 246 | 247 | spec('#5.1 jit', function() 248 | assert_source_map(4, [[ 249 | local a = 0 250 | a 251 | += 252 | 'a' 253 | ]]) 254 | end) 255 | 256 | spec('#5.2+', function() 257 | assert_source_map(3, [[ 258 | local a = 0 259 | a 260 | += 261 | 'a' 262 | ]]) 263 | end) 264 | end) 265 | 266 | spec('operator assignment mixed precedence #5.1+', function() 267 | assert_run(1, [[ 268 | local a = -1 269 | a += true && 2 || 0 270 | return a 271 | ]]) 272 | end) 273 | 274 | spec('operator assignment minimize repeated index chains #5.1+', function() 275 | assert_run({ 1, 1 }, [[ 276 | local a = { b = -1 } 277 | local call_counter = 0 278 | 279 | local function c() { 280 | call_counter += 1 281 | return a 282 | } 283 | 284 | c().b += 2 285 | return { call_counter, a.b } 286 | ]]) 287 | 288 | assert_run({ 1, 2 }, [[ 289 | local a, b = {}, { c = -1 } 290 | local index_counter = 0 291 | 292 | setmetatable(a, { 293 | __index = key => { 294 | index_counter += 1 295 | return b[key] 296 | }, 297 | }) 298 | 299 | a.c += 3 300 | return { index_counter, a.c } 301 | ]]) 302 | end) 303 | -------------------------------------------------------------------------------- /spec/compile_blocks_spec.lua: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------------------------------- 2 | -- Do Block 3 | -- ----------------------------------------------------------------------------- 4 | 5 | spec('do block #5.1+', function() 6 | assert_run(1, [[ 7 | local a 8 | do { a = 1 } 9 | return a 10 | ]]) 11 | 12 | assert_run(2, [[ 13 | local a = 2 14 | do { local a = 0 } 15 | return a 16 | ]]) 17 | end) 18 | 19 | -- ----------------------------------------------------------------------------- 20 | -- Block Declarations 21 | -- ----------------------------------------------------------------------------- 22 | 23 | spec('update block declarations #5.1+', function() 24 | assert_run(1, [[ 25 | local a = 1 26 | do { global a = 0 } 27 | local result = a 28 | _G.a = nil 29 | return result 30 | ]]) 31 | 32 | assert_run(2, [[ 33 | local a = 0 34 | local result 35 | 36 | do { 37 | global a = 2 38 | result = a 39 | _G.a = nil 40 | } 41 | 42 | return result 43 | ]]) 44 | 45 | assert_run({ a = 3 }, [[ 46 | module a = 0 47 | do { a = 3 } 48 | ]]) 49 | end) 50 | -------------------------------------------------------------------------------- /spec/compile_declarations_spec.lua: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------------------------------- 2 | -- Local Declarations 3 | -- ----------------------------------------------------------------------------- 4 | 5 | spec('local declarations #5.1+', function() 6 | assert_run(1, [[ 7 | local a = 1 8 | return a 9 | ]]) 10 | 11 | assert_run(2, [[ 12 | local a = { b = 2 } 13 | local { b } = a 14 | return b 15 | ]]) 16 | 17 | assert_run(3, [[ 18 | local a = { 3 } 19 | local [ b ] = a 20 | return b 21 | ]]) 22 | 23 | assert_run(4, [[ 24 | local a, b = 1, 3 25 | return a + b 26 | ]]) 27 | end) 28 | 29 | -- ----------------------------------------------------------------------------- 30 | -- Module Declarations 31 | -- ----------------------------------------------------------------------------- 32 | 33 | spec('module declarations #5.1+', function() 34 | assert_run({ a = 1 }, 'module a = 1') 35 | 36 | assert_run({ b = 2 }, [[ 37 | local a = { b = 2 } 38 | module { b } = a 39 | ]]) 40 | 41 | assert_run({ b = 3 }, [[ 42 | local a = { b = 3 } 43 | module { b } = a 44 | ]]) 45 | 46 | assert_run({ b = 4 }, [[ 47 | local a = { 4 } 48 | module [ b ] = a 49 | ]]) 50 | 51 | assert_run({ a = 5, b = 6 }, [[ 52 | module a = 5 53 | _MODULE.b = 6 54 | ]]) 55 | 56 | assert_run(nil, [[ return _MODULE ]]) 57 | end) 58 | 59 | -- ----------------------------------------------------------------------------- 60 | -- Global Declarations 61 | -- ----------------------------------------------------------------------------- 62 | 63 | spec('global declarations #5.1+', function() 64 | assert_run(1, [[ 65 | global a = 1 66 | local result = a 67 | _G.a = nil 68 | return result 69 | ]]) 70 | 71 | assert_run(2, [[ 72 | global a = 2 73 | local result = _G.a 74 | _G.a = nil 75 | return result 76 | ]]) 77 | 78 | assert_run(3, [[ 79 | local a = { b = 3 } 80 | global { b } = a 81 | local result = _G.b 82 | _G.b = nil 83 | return result 84 | ]]) 85 | 86 | assert_run(4, [[ 87 | local a = { 4 } 88 | global [ b ] = a 89 | local result = _G.b 90 | _G.b = nil 91 | return result 92 | ]]) 93 | 94 | assert_run({ 5, 6 }, [[ 95 | global a, b = 5, 6 96 | local result = { _G.a, _G.b } 97 | _G.a = nil 98 | _G.b = nil 99 | return result 100 | ]]) 101 | end) 102 | 103 | -- ----------------------------------------------------------------------------- 104 | -- Misc 105 | -- ----------------------------------------------------------------------------- 106 | 107 | spec('transform Lua keywords #5.1+', function() 108 | assert_run(1, [[ 109 | local end = 1 110 | return end 111 | ]]) 112 | end) 113 | 114 | spec('use tracked scopes #5.1+', function() 115 | assert_run(1, [[ 116 | local a = 0 117 | global a = 1 118 | local result = _G.a 119 | _G.a = nil 120 | return result 121 | ]]) 122 | 123 | assert_run({ a = 2 }, [[ 124 | local a = 0 125 | module a = 2 126 | ]]) 127 | end) 128 | 129 | spec('update block declarations #5.1+', function() 130 | assert_run(1, [[ 131 | local a = 0 132 | global a = 1 133 | local result = a 134 | _G.a = nil 135 | return result 136 | ]]) 137 | 138 | assert_run({ a = 2 }, [[ 139 | local a = 0 140 | module a = 0 141 | a = 2 142 | ]]) 143 | end) 144 | -------------------------------------------------------------------------------- /spec/compile_destructure_spec.lua: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------------------------------- 2 | -- Array Destructure 3 | -- ----------------------------------------------------------------------------- 4 | 5 | spec('array destructure #5.1+', function() 6 | assert_run(1, [[ 7 | local [ a ] = { 1 } 8 | return a 9 | ]]) 10 | 11 | assert_run({ 2, 3 }, [[ 12 | local [ a, b ] = { 2, 3 } 13 | return { a, b } 14 | ]]) 15 | 16 | assert_run(4, [[ 17 | local [ a ] = { 4, 5 } 18 | return a 19 | ]]) 20 | end) 21 | 22 | spec('array destructure defaults #5.1+', function() 23 | assert_run(1, [[ 24 | local [ a = 1 ] = {} 25 | return a 26 | ]]) 27 | 28 | assert_run(2, [[ 29 | local [ a = 0 ] = { 2 } 30 | return a 31 | ]]) 32 | 33 | assert_run(3, [[ 34 | local [ a = 3 ] = { nil } 35 | return a 36 | ]]) 37 | end) 38 | 39 | spec('array destructure transform Lua keywords #5.1+', function() 40 | assert_run(1, [[ 41 | local [ end ] = { 1 } 42 | return end 43 | ]]) 44 | 45 | assert_run(2, [[ 46 | local [ end = 2 ] = {} 47 | return end 48 | ]]) 49 | end) 50 | 51 | spec('array destructure use tracked scopes #5.1+', function() 52 | assert_run(1, [[ 53 | global [ a ] = { 1 } 54 | local result = _G.a 55 | _G.a = nil 56 | return result 57 | ]]) 58 | 59 | assert_run(2, [[ 60 | global [ a = 2 ] = {} 61 | local result = _G.a 62 | _G.a = nil 63 | return result 64 | ]]) 65 | 66 | assert_run({ a = 3 }, [[ 67 | module [ a ] = { 3 } 68 | ]]) 69 | 70 | assert_run({ a = 4 }, [[ 71 | module [ a = 4 ] = {} 72 | ]]) 73 | end) 74 | 75 | spec('array destructure update block declarations #5.1+', function() 76 | assert_run(1, [[ 77 | global a = 0 78 | local [ a ] = { 1 } 79 | local result = a 80 | _G.a = nil 81 | return result 82 | ]]) 83 | 84 | assert_run(2, [[ 85 | local a = 0 86 | global [ a ] = { 2 } 87 | local result = a 88 | _G.a = nil 89 | return result 90 | ]]) 91 | 92 | assert_run({ a = 3 }, [[ 93 | local a = 0 94 | module [ a ] = {} 95 | a = 3 96 | ]]) 97 | end) 98 | 99 | spec('array destructure source map #5.1+', function() 100 | assert_source_map(1, [[ 101 | local [ a ] = b 102 | ]]) 103 | 104 | assert_source_map(2, [[ 105 | 106 | local [ a ] 107 | = b 108 | ]]) 109 | end) 110 | 111 | -- ----------------------------------------------------------------------------- 112 | -- Map Destructure 113 | -- ----------------------------------------------------------------------------- 114 | 115 | spec('map destructure #5.1+', function() 116 | assert_run(1, [[ 117 | local { a } = { a = 1 } 118 | return a 119 | ]]) 120 | 121 | assert_run({ 2, 3 }, [[ 122 | local { a, b } = { a = 2, b = 3 } 123 | return { a, b } 124 | ]]) 125 | 126 | assert_run(4, [[ 127 | local { a } = { a = 4, b = 5 } 128 | return a 129 | ]]) 130 | end) 131 | 132 | spec('map destructure defaults #5.1+', function() 133 | assert_run(1, [[ 134 | local { a = 1 } = {} 135 | return a 136 | ]]) 137 | 138 | assert_run(2, [[ 139 | local { a = 0 } = { a = 2 } 140 | return a 141 | ]]) 142 | 143 | assert_run(3, [[ 144 | local { a = 3 } = { a = nil } 145 | return a 146 | ]]) 147 | end) 148 | 149 | spec('map destructure alias #5.1+', function() 150 | assert_run(nil, [[ 151 | local { a: b } = { a = 1 } 152 | return a 153 | ]]) 154 | 155 | assert_run(1, [[ 156 | local { a: b } = { a = 1 } 157 | return b 158 | ]]) 159 | 160 | assert_run(2, [[ 161 | local { a: b } = { a = 2, b = 0 } 162 | return b 163 | ]]) 164 | end) 165 | 166 | spec('map destructure transform Lua keywords #5.1+', function() 167 | assert_run(1, [[ 168 | local { end } = { end = 1 } 169 | return end 170 | ]]) 171 | 172 | assert_run(2, [[ 173 | local { end = 2 } = {} 174 | return end 175 | ]]) 176 | 177 | assert_run(3, [[ 178 | local { a: end } = { a = 3 } 179 | return end 180 | ]]) 181 | end) 182 | 183 | spec('map destructure use tracked scopes #5.1+', function() 184 | assert_run(1, [[ 185 | global { a } = { a = 1 } 186 | local result = _G.a 187 | _G.a = nil 188 | return result 189 | ]]) 190 | 191 | assert_run(2, [[ 192 | global { a = 2 } = {} 193 | local result = _G.a 194 | _G.a = nil 195 | return result 196 | ]]) 197 | 198 | assert_run({ nil, 3 }, [[ 199 | global { a: b } = { a = 3 } 200 | local result = _G.b 201 | _G.b = nil 202 | return { _G.a, result } 203 | ]]) 204 | 205 | assert_run({ a = 4 }, [[ 206 | module { a } = { a = 4 } 207 | ]]) 208 | 209 | assert_run({ a = 5 }, [[ 210 | module { a = 5 } = {} 211 | ]]) 212 | 213 | assert_run({ b = 6 }, [[ 214 | module { a: b } = { a = 6 } 215 | ]]) 216 | end) 217 | 218 | spec('map destructure update block declarations #5.1+', function() 219 | assert_run(1, [[ 220 | global a = 0 221 | local { a } = { a = 1 } 222 | local result = a 223 | _G.a = nil 224 | return result 225 | ]]) 226 | 227 | assert_run(2, [[ 228 | local a = 0 229 | global { a } = { a = 2 } 230 | local result = a 231 | _G.a = nil 232 | return result 233 | ]]) 234 | 235 | assert_run({ a = 3 }, [[ 236 | local a = 0 237 | module { a } = {} 238 | a = 3 239 | ]]) 240 | end) 241 | 242 | spec('map destructure source map #5.1+', function() 243 | assert_source_map(1, [[ 244 | local { a } = b 245 | ]]) 246 | 247 | assert_source_map(2, [[ 248 | 249 | local { a } 250 | = b 251 | ]]) 252 | end) 253 | -------------------------------------------------------------------------------- /spec/compile_expressions_spec.lua: -------------------------------------------------------------------------------- 1 | local config = require('erde.config') 2 | local lib = require('erde.lib') 3 | 4 | -- ----------------------------------------------------------------------------- 5 | -- Terminal Expressions 6 | -- ----------------------------------------------------------------------------- 7 | 8 | spec('terminal expression transform Lua keywords #5.1+', function() 9 | assert_run(1, [[ 10 | local end = 1 11 | return end 12 | ]]) 13 | 14 | assert_run(2, [[ 15 | local a = { end = 2 } 16 | return a.end 17 | ]]) 18 | 19 | assert_run(3, [[ 20 | local end = { a = 3 } 21 | return end.a 22 | ]]) 23 | 24 | assert_run(4, [[ 25 | local t = { end = 4 } 26 | local key = 'end' 27 | return t[key] 28 | ]]) 29 | end) 30 | 31 | spec('terminal expression use tracked scopes #5.1+', function() 32 | assert_run(1, [[ 33 | global a = 0 34 | _G.a = 1 35 | local result = a 36 | _G.a = nil 37 | return result 38 | ]]) 39 | 40 | assert_run({ a = 2, b = 2 }, [[ 41 | module a = 2 42 | module b = a 43 | ]]) 44 | end) 45 | 46 | -- ----------------------------------------------------------------------------- 47 | -- Unary Operators 48 | -- ----------------------------------------------------------------------------- 49 | 50 | spec('unops #5.1+', function() 51 | assert_eval(-1, '-1') 52 | assert_eval(1, '#("a")') 53 | assert_eval(false, '!true') 54 | 55 | if config.lua_target == '5.1+' or config.lua_target == '5.2+' then 56 | assert.has_error(function() compile('print(~1)') end) 57 | elseif config.lua_target == '5.2' then 58 | assert_eval(4294967294, '~1') 59 | else 60 | assert_eval(-2, '~1') 61 | end 62 | end) 63 | 64 | describe('unop precedence #5.1+', function() 65 | spec('below', function() 66 | assert_eval(1, '-1 + 2') 67 | assert_run(2, 'local a = "a"; return #a * 2') 68 | assert_eval(true, '!true || true') 69 | 70 | if config.lua_target == '5.2' then 71 | assert_eval(4294967299, '~1 + 5') 72 | elseif config.lua_target ~= '5.1+' and config.lua_target ~= '5.2+' then 73 | assert_eval(3, '~1 + 5') 74 | end 75 | end) 76 | 77 | spec('above', function() 78 | assert_eval(-1, '-1^2') 79 | assert.has_error(function() lib.run('local a = "a"; return #a^2') end) 80 | assert_eval(false, '!2^2') 81 | 82 | if config.lua_target == '5.2' then 83 | assert_eval(4294967294, '~1^2') 84 | elseif config.lua_target ~= '5.1+' and config.lua_target ~= '5.2+' then 85 | assert_eval(-2, '~1^2') 86 | end 87 | end) 88 | end) 89 | 90 | describe('unop source map', function() 91 | spec('#5.1+', function() 92 | assert_source_map(2, [[ 93 | math.floor(0) -- disalign compiled and source lines 94 | return -('a') 95 | ]]) 96 | 97 | assert_source_map(3, [[ 98 | 99 | math.floor(0) -- disalign compiled and source lines 100 | return ~('a') 101 | ]]) 102 | 103 | assert_source_map(4, [[ 104 | 105 | 106 | math.floor(0) -- disalign compiled and source lines 107 | return ~ 108 | ('a') 109 | ]]) 110 | 111 | end) 112 | 113 | spec('#5.1 jit', function() 114 | assert_source_map(3, [[ 115 | math.floor(0) -- disalign compiled and source lines 116 | return - 117 | ('a') 118 | ]]) 119 | end) 120 | 121 | spec('#5.2+', function() 122 | assert_source_map(2, [[ 123 | math.floor(0) -- disalign compiled and source lines 124 | return - 125 | ('a') 126 | ]]) 127 | end) 128 | end) 129 | 130 | -- ----------------------------------------------------------------------------- 131 | -- Bit Operators 132 | -- ----------------------------------------------------------------------------- 133 | 134 | spec('bitops #5.1+', function() 135 | if config.lua_target == '5.1+' or config.lua_target == '5.2+' then 136 | assert.has_error(function() compile('print(2 >> 1)') end) 137 | assert.has_error(function() compile('print(1 << 1)') end) 138 | assert.has_error(function() compile('print(2 | 1)') end) 139 | assert.has_error(function() compile('print(6 ~ 2)') end) 140 | assert.has_error(function() compile('print(7 & 5)') end) 141 | else 142 | assert_eval(1, '2 >> 1') 143 | assert_eval(2, '1 << 1') 144 | assert_eval(3, '2 | 1') 145 | assert_eval(4, '6 ~ 2') 146 | assert_eval(5, '7 & 5') 147 | end 148 | end) 149 | 150 | if config.lua_target ~= '5.1+' and config.lua_target ~= '5.2+' then 151 | describe('bitop precedence #5.1+', function() 152 | spec('below', function() 153 | assert_eval(true, '2 >> 1 == 1') 154 | assert_eval(true, '1 << 1 == 2') 155 | assert_eval(true, '2 | 1 == 3') 156 | assert_eval(true, '6 ~ 2 == 4') 157 | assert_eval(true, '7 & 5 == 5') 158 | end) 159 | 160 | spec('above', function() 161 | assert_eval(1, '4 >> 1 + 1') 162 | assert_eval(2, '1 << 2 - 1') 163 | assert_eval(3, '1 | 4 / 2') 164 | assert_eval(4, '2 ~ 3 * 2') 165 | assert_eval(5, '7 & 3 + 10') 166 | end) 167 | end) 168 | 169 | spec('bitop associativity #5.1+', function() 170 | assert_eval(1, '15 >> 2 >> 1') 171 | assert_eval(7, '15 >> (2 >> 1)') 172 | 173 | assert_eval(24, '3 << 2 << 1') 174 | assert_eval(48, '3 << (2 << 1)') 175 | end) 176 | end 177 | 178 | spec('bitop source map #5.1+', function() 179 | assert_source_map(2, [[ 180 | math.floor(0) -- disalign compiled and source lines 181 | return 1 << 'a' 182 | ]]) 183 | 184 | assert_source_map(3, [[ 185 | math.floor(0) -- disalign compiled and source lines 186 | return 'a' 187 | << 1 188 | ]]) 189 | 190 | assert_source_map(4, [[ 191 | 192 | math.floor(0) -- disalign compiled and source lines 193 | return 1 194 | << 195 | 'a' 196 | ]]) 197 | end) 198 | 199 | -- ----------------------------------------------------------------------------- 200 | -- Binary Operators 201 | -- ----------------------------------------------------------------------------- 202 | 203 | spec('binops #5.1+', function() 204 | assert_eval(true, 'true || true') 205 | assert_eval(true, 'true || false') 206 | assert_eval(true, 'false || true') 207 | assert_eval(false, 'false || false') 208 | 209 | assert_eval(true, 'true && true') 210 | assert_eval(false, 'true && false') 211 | assert_eval(false, 'false && true') 212 | assert_eval(false, 'false && false') 213 | 214 | assert_eval(true, '0 == 0') 215 | assert_eval(false, '0 == 1') 216 | 217 | assert_eval(false, '0 != 0') 218 | assert_eval(true, '0 != 1') 219 | 220 | assert_eval(true, '0 <= 0') 221 | assert_eval(true, '0 <= 1') 222 | assert_eval(false, '0 <= -1') 223 | 224 | assert_eval(true, '0 >= 0') 225 | assert_eval(false, '0 >= 1') 226 | assert_eval(true, '0 >= -1') 227 | 228 | assert_eval(false, '0 < 0') 229 | assert_eval(true, '0 < 1') 230 | assert_eval(false, '0 < -1') 231 | 232 | assert_eval(false, '0 > 0') 233 | assert_eval(false, '0 > 1') 234 | assert_eval(true, '0 > -1') 235 | 236 | assert_eval('ab', '"a" .. "b"') 237 | 238 | assert_eval(1, '-1 + 2') 239 | assert_eval(2, '5 - 3') 240 | assert_eval(3, '1.5 * 2') 241 | assert_eval(4, '8 / 2') 242 | assert_eval(5, '11 // 2') 243 | assert_eval(6, '13 % 7') 244 | assert_eval(8, '2^3') 245 | end) 246 | 247 | describe('binop precedence #5.1+', function() 248 | assert_eval(true, 'true || true && false') 249 | assert_eval(true, 'true || false && false') 250 | assert_eval(7, '1 + 2 * 3') 251 | end) 252 | 253 | spec('binop associativity #5.1+', function() 254 | assert_eval(1, '3 - 1 - 1') 255 | assert_eval(3, '3 - (1 - 1)') 256 | 257 | assert_eval(2, '8 / 2 / 2') 258 | assert_eval(8, '8 / (2 / 2)') 259 | 260 | assert_eval(3, '14 // 2 // 2') 261 | assert_eval(14, '14 // (2 // 2)') 262 | 263 | assert_eval(4, '24 % 13 % 7') 264 | assert_eval(0, '24 % (13 % 7)') 265 | 266 | assert_eval(512, '2^3^2') 267 | assert_eval(64, '(2^3)^2') 268 | end) 269 | 270 | describe('binop source map', function() 271 | spec('#5.1+', function() 272 | assert_source_map(2, [[ 273 | math.floor(0) -- disalign compiled and source lines 274 | return 1 + 'a' 275 | ]]) 276 | 277 | assert_source_map(3, [[ 278 | math.floor(0) -- disalign compiled and source lines 279 | return 'a' 280 | + 1 281 | ]]) 282 | end) 283 | 284 | spec('#5.1 jit', function() 285 | assert_source_map(4, [[ 286 | math.floor(0) -- disalign compiled and source lines 287 | return 1 288 | + 289 | 'a' 290 | ]]) 291 | end) 292 | 293 | spec('#5.2+', function() 294 | assert_source_map(3, [[ 295 | math.floor(0) -- disalign compiled and source lines 296 | return 1 297 | + 298 | 'a' 299 | ]]) 300 | end) 301 | end) 302 | 303 | -- ----------------------------------------------------------------------------- 304 | -- Complex Expressions 305 | -- ----------------------------------------------------------------------------- 306 | 307 | spec('complex expressions #5.1+', function() 308 | assert_eval(11, '1 + 2 * 3 + 4') 309 | assert_eval(13, '(5 * 2) + 3') 310 | assert_eval(25, '5 * (2 + 3)') 311 | end) 312 | 313 | describe('numbers source map #5.1 jit', function() 314 | assert_source_map(1, [[ 315 | local a = 'a' > 1 316 | ]]) 317 | 318 | assert_source_map(2, [[ 319 | local a = 'a' > 320 | 1 321 | ]]) 322 | end) 323 | 324 | spec('terminals source map #5.1 jit', function() 325 | assert_source_map(1, [[ 326 | local a = 1 + nil 327 | ]]) 328 | 329 | assert_source_map(2, [[ 330 | local a = 1 + 331 | nil 332 | ]]) 333 | end) 334 | -------------------------------------------------------------------------------- /spec/compile_functions_spec.lua: -------------------------------------------------------------------------------- 1 | local compile = require('erde.compile') 2 | local lib = require('erde.lib') 3 | 4 | -- ----------------------------------------------------------------------------- 5 | -- Parameters 6 | -- ----------------------------------------------------------------------------- 7 | 8 | spec('parameters #5.1+', function() 9 | assert_run(1, [[ 10 | local a = (b) -> b 11 | return a(1) 12 | ]]) 13 | 14 | assert_run(2, [[ 15 | local a = b -> b + 1 16 | return a(1) 17 | ]]) 18 | 19 | assert_run(3, [[ 20 | local a = [ b ] -> b + 1 21 | return a({ 2 }) 22 | ]]) 23 | 24 | assert_run(4, [[ 25 | local a = { b } -> b + 1 26 | return a({ b = 3 }) 27 | ]]) 28 | end) 29 | 30 | spec('parameter defaults #5.1+', function() 31 | assert_run(1, [[ 32 | local a = (b, c = 2) -> b + c 33 | return a(-1) 34 | ]]) 35 | 36 | assert_run(2, [[ 37 | local a = (b = 1, c) -> b + c 38 | return a(nil, 1) 39 | ]]) 40 | 41 | assert_run(3, [[ 42 | local a = (b = 1, c) -> b + c 43 | return a(2, 1) 44 | ]]) 45 | 46 | assert.has_error(function() 47 | compile('local a = b = 1 -> 1') 48 | end) 49 | end) 50 | 51 | spec('parameter varargs #5.1+', function() 52 | assert_run({ 1, 2 }, [[ 53 | local a = (...) -> ({ ... }) 54 | return a(1, 2) 55 | ]]) 56 | 57 | assert_run({ 3, 4 }, [[ 58 | local a = (b, ...) -> ({ ... }) 59 | return a(0, 3, 4) 60 | ]]) 61 | 62 | assert_run({ 5, 6 }, [[ 63 | local a = (...b) -> b 64 | return a(5, 6) 65 | ]]) 66 | 67 | assert_run({ 7, 8 }, [[ 68 | local a = (b, ...c) -> c 69 | return a(0, 7, 8) 70 | ]]) 71 | 72 | assert.has_error(function() 73 | compile('local a = ... -> 1') 74 | end) 75 | 76 | assert.has_error(function() 77 | compile('local a = (..., b) -> 1') 78 | end) 79 | end) 80 | 81 | spec('parameters transform Lua keywords #5.1+', function() 82 | assert_run(1, [[ 83 | local f = end -> end 84 | return f(1) 85 | ]]) 86 | 87 | assert_run(2, [[ 88 | local f = (end) -> end 89 | return f(2) 90 | ]]) 91 | 92 | assert_run({ 3 }, [[ 93 | local f = (...end) -> end 94 | return f(3) 95 | ]]) 96 | end) 97 | 98 | spec('parameters update block declarations #5.1+', function() 99 | assert_run({ a = 1 }, [[ 100 | module a = 1 101 | local f = a -> { a = 0 } 102 | f() 103 | ]]) 104 | 105 | assert_run({ a = 2 }, [[ 106 | module a = 2 107 | local f = (a) -> { a = 0 } 108 | f() 109 | ]]) 110 | 111 | assert_run({ a = 3 }, [[ 112 | module a = 3 113 | local f = (a = 0) -> {} 114 | f() 115 | ]]) 116 | 117 | assert_run({ a = 4 }, [[ 118 | module a = 4 119 | local f = (...a) -> { a = 0 } 120 | f() 121 | ]]) 122 | end) 123 | 124 | -- ----------------------------------------------------------------------------- 125 | -- Arrow Functions 126 | -- ----------------------------------------------------------------------------- 127 | 128 | spec('skinny arrow function #5.1+', function() 129 | assert_eval('function', 'type(() -> {})') 130 | 131 | assert_run(1, [[ 132 | local a = () -> { return 1 } 133 | return a() 134 | ]]) 135 | 136 | assert_run(2, [[ 137 | local a = b -> { return b } 138 | return a(2) 139 | ]]) 140 | 141 | assert_run(3, [[ 142 | local a = b -> { return b } 143 | return a(3) 144 | ]]) 145 | 146 | local sum = lib.run('return (a, b) -> a + b') 147 | assert.are.equal(4, sum(1, 3)) 148 | end) 149 | 150 | spec('fat arrow function #5.1+', function() 151 | assert_eval('function', 'type(() => {})') 152 | 153 | assert_run(1, [[ 154 | local a = { b = 1 } 155 | a.c = () => { return self.b } 156 | return a:c() 157 | ]]) 158 | 159 | assert_run(2, [[ 160 | local a = { b = 1 } 161 | a.c = d => { return self.b + d } 162 | return a:c(1) 163 | ]]) 164 | 165 | local sum = lib.run('return (a, b) => a + b + self.c') 166 | assert.are.equal(3, sum({ c = -1 }, 3, 1)) 167 | end) 168 | 169 | spec('arrow function implicit return #5.1+', function() 170 | assert_run(1, [[ 171 | local a = () -> 1 172 | return a() 173 | ]]) 174 | 175 | assert_run(2, [[ 176 | local a = (b) -> b 177 | return a(2) 178 | ]]) 179 | 180 | assert_run(3, [[ 181 | local a = (b, c) -> b + c 182 | return a(1, 2) 183 | ]]) 184 | 185 | assert_run({ 4, 5 }, [[ 186 | local a = (b, c) -> ({ b, c }) 187 | return a(4, 5) 188 | ]]) 189 | 190 | assert_run(6, [[ 191 | local a = () -> (2, 4) 192 | local b, c = a() 193 | return b + c 194 | ]]) 195 | 196 | assert_run(7, [[ 197 | local a = b -> ({ b = b }).b 198 | return a(7) 199 | ]]) 200 | 201 | assert.has_error(function() 202 | compile('local a = () -> { 1 }') 203 | end) 204 | end) 205 | 206 | spec('iife #5.1+', function() 207 | assert_eval(1, '(() -> { return 1 })()') 208 | assert_eval(2, '(() -> 2)()') 209 | end) 210 | 211 | spec('arrow function source map #5.1 jit', function() 212 | assert_source_map(1, [[ 213 | local a = 1 + () -> 1 214 | ]]) 215 | 216 | assert_source_map(2, [[ 217 | local a = 1 + 218 | () -> 1 219 | ]]) 220 | end) 221 | 222 | -- ----------------------------------------------------------------------------- 223 | -- Function Declarations 224 | -- ----------------------------------------------------------------------------- 225 | 226 | spec('local function declaration #5.1+', function() 227 | assert_run(1, [[ 228 | local function a() { 229 | return 1 230 | } 231 | 232 | do { 233 | local function a() { 234 | return 2 235 | } 236 | } 237 | 238 | return a() 239 | ]]) 240 | 241 | assert_run(2, [[ 242 | local function a() { 243 | return 1 244 | } 245 | 246 | do { 247 | function a() { 248 | return 2 249 | } 250 | } 251 | 252 | return a() 253 | ]]) 254 | 255 | assert_run(nil, [[ 256 | local function a() { 257 | return 1 258 | } 259 | 260 | do { 261 | function a() { 262 | return 2 263 | } 264 | } 265 | 266 | return _G.a 267 | ]]) 268 | 269 | assert.has_error(function() 270 | compile('local function a.b() {}') 271 | end) 272 | end) 273 | 274 | spec('global function declaration #5.1+', function() 275 | assert_run(1, [[ 276 | local function a() { 277 | return 1 278 | } 279 | 280 | do { 281 | global function a() { 282 | return 2 283 | } 284 | } 285 | 286 | local result = a() 287 | _G.a = nil 288 | return result 289 | ]]) 290 | 291 | assert_run(2, [[ 292 | local function a() { 293 | return 1 294 | } 295 | 296 | do { 297 | global function a() { 298 | return 2 299 | } 300 | } 301 | 302 | local result = _G.a() 303 | _G.a = nil 304 | return result 305 | ]]) 306 | 307 | assert_run(3, [[ 308 | function a() { return 3 } 309 | local result = _G.a() 310 | _G.a = nil 311 | return result 312 | ]]) 313 | end) 314 | 315 | spec('module function declaration #5.1+', function() 316 | local testModule = lib.run([[ 317 | module function a() { 318 | return 1 319 | } 320 | ]]) 321 | 322 | assert.are.equal(1, testModule.a()) 323 | 324 | assert.has_error(function() 325 | compile('module function a.b() {}') 326 | end) 327 | end) 328 | 329 | spec('method declaration method #5.1+', function() 330 | assert_run(1, [[ 331 | local a = { b = 1 } 332 | 333 | function a:c() { 334 | return self.b 335 | } 336 | 337 | return a:c() 338 | ]]) 339 | end) 340 | 341 | spec('function declaration transform Lua keywords #5.1+', function() 342 | assert_run(1, [[ 343 | local function end() { return 1 } 344 | return end() 345 | ]]) 346 | 347 | assert_run(2, [[ 348 | local a = {} 349 | function a.end() { return 2 } 350 | return a.end() 351 | ]]) 352 | 353 | assert_run(3, [[ 354 | local a = { b = 3 } 355 | function a:end() { return self.b } 356 | return a:end() 357 | ]]) 358 | end) 359 | 360 | spec('function declaration use tracked scopes #5.1+', function() 361 | assert_run('function', [[ 362 | global function a() {} 363 | local result = type(_G.a) 364 | _G.a = nil 365 | return result 366 | ]]) 367 | 368 | assert.is_function(lib.run([[ 369 | module function a() {} 370 | ]]).a) 371 | end) 372 | 373 | spec('function declaration update block declarations #5.1+', function() 374 | assert_run(1, [[ 375 | local a = 0 376 | global function a() {} 377 | a = 1 378 | local result = _G.a 379 | _G.a = nil 380 | return result 381 | ]]) 382 | 383 | assert_run({ a = 2 }, [[ 384 | local a = 0 385 | module function a() {} 386 | a = 2 387 | ]]) 388 | end) 389 | 390 | spec('function declaration source map #5.1+', function() 391 | assert_source_map(1, [[ 392 | function a.b() {} 393 | ]]) 394 | 395 | assert_source_map(2, [[ 396 | 397 | function a 398 | .b() {} 399 | ]]) 400 | 401 | assert_source_map(3, [[ 402 | 403 | 404 | function 405 | a 406 | :b() {} 407 | ]]) 408 | end) 409 | 410 | -- ----------------------------------------------------------------------------- 411 | -- Misc 412 | -- ----------------------------------------------------------------------------- 413 | 414 | spec('no varargs outside vararg function #5.1+', function() 415 | assert.has_no.errors(function() 416 | compile('print(...)') -- varargs allowed at top level in Lua! 417 | end) 418 | 419 | assert.has_error(function() 420 | compile('local a = () -> ({ ... })') 421 | end) 422 | 423 | assert.has_error(function() 424 | compile('local a = () -> { print(...) }') 425 | end) 426 | 427 | assert.has_error(function() 428 | compile([[ 429 | local a = (...) -> { 430 | local b = () -> { 431 | print(...) 432 | } 433 | } 434 | ]]) 435 | end) 436 | 437 | assert.has_error(function() 438 | compile('function a() { print(...) }') 439 | end) 440 | 441 | assert.has_error(function() 442 | compile([[ 443 | function a(...) { 444 | function b() { 445 | print(...) 446 | } 447 | } 448 | ]]) 449 | end) 450 | end) 451 | -------------------------------------------------------------------------------- /spec/compile_index_chain_spec.lua: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------------------------------- 2 | -- Dot Index 3 | -- ----------------------------------------------------------------------------- 4 | 5 | spec('dot index #5.1+', function() 6 | assert_eval(1, '({ a = 1 }).a') 7 | 8 | assert_run(2, [[ 9 | local a = { b = 2 } 10 | return a.b 11 | ]]) 12 | 13 | assert_run(3, [[ 14 | local a = { end = 3 } 15 | return a.end 16 | ]]) 17 | end) 18 | 19 | spec('dot index source map #5.1+', function() 20 | assert_source_map(1, [[ 21 | math.round(a.b) 22 | ]]) 23 | 24 | assert_source_map(2, [[ 25 | math.round(a 26 | .b) 27 | ]]) 28 | end) 29 | 30 | -- ----------------------------------------------------------------------------- 31 | -- Bracket Index 32 | -- ----------------------------------------------------------------------------- 33 | 34 | spec('bracket index #5.1+', function() 35 | assert_eval(1, '({ 1 })[1]') 36 | 37 | assert_run(2, [[ 38 | local a = { [5] = 2 } 39 | return a[2 + 3] 40 | ]]) 41 | 42 | assert_run(3, [[ 43 | local a = { end = 3 } 44 | local key = 'end' 45 | return a[key] 46 | ]]) 47 | end) 48 | 49 | spec('bracket index source map #5.1+', function() 50 | assert_source_map(1, [[ 51 | math.round(a['b']) 52 | ]]) 53 | 54 | assert_source_map(2, [[ 55 | math.round(a 56 | ['b']) 57 | ]]) 58 | end) 59 | 60 | -- ----------------------------------------------------------------------------- 61 | -- Method Call 62 | -- ----------------------------------------------------------------------------- 63 | 64 | spec('method call #5.1+', function() 65 | assert_run(1, [[ 66 | local a = { b = c => c + self.d, d = 2 } 67 | return a:b(-1) 68 | ]]) 69 | 70 | assert_run(2, [[ 71 | local a = { end = () => 2 } 72 | return a:end() 73 | ]]) 74 | 75 | assert_eval(3, '({ end = () => 3 }):end()') 76 | 77 | assert.has_error(function() 78 | compile('a:b') 79 | end) 80 | end) 81 | 82 | describe('method call source map', function() 83 | spec('#5.1+', function() 84 | assert_source_map(1, [[ 85 | math.round(a:b()) 86 | ]]) 87 | 88 | assert_source_map(2, [[ 89 | math.round(a 90 | :b()) 91 | ]]) 92 | end) 93 | 94 | spec('#5.1 jit #5.4', function() 95 | assert_source_map(3, [[ 96 | local a = {} 97 | a 98 | :b(4) 99 | ]]) 100 | 101 | assert_source_map(4, [[ 102 | 103 | local a = 'a' 104 | a 105 | :gsub(4) 106 | ]]) 107 | end) 108 | 109 | spec('#5.2 #5.3', function() 110 | assert_source_map(2, [[ 111 | local a = {} 112 | a 113 | :b(4) 114 | ]]) 115 | 116 | assert_source_map(3, [[ 117 | 118 | local a = 'a' 119 | a 120 | :gsub(4) 121 | ]]) 122 | end) 123 | end) 124 | 125 | -- ----------------------------------------------------------------------------- 126 | -- Function Call 127 | -- ----------------------------------------------------------------------------- 128 | 129 | spec('function call #5.1+', function() 130 | assert_run(1, [[ 131 | local a = () -> 1 132 | return a() 133 | ]]) 134 | 135 | assert_run(2, [[ 136 | local a = b -> b + 1 137 | return a(1) 138 | ]]) 139 | 140 | assert_run(3, [[ 141 | local a = { b = () -> 3 } 142 | return a.b() 143 | ]]) 144 | end) 145 | 146 | -- ----------------------------------------------------------------------------- 147 | -- Misc 148 | -- ----------------------------------------------------------------------------- 149 | 150 | spec('nested parens base #5.1+', function() 151 | assert_eval(1, '((({ a = 1 }))).a') 152 | end) 153 | 154 | spec('chain #5.1+', function() 155 | assert_run(1, [[ 156 | local a = { b = { 1 } } 157 | return a.b[1] 158 | ]]) 159 | 160 | assert_run(2, [[ 161 | local a = { { b = 2 } } 162 | return a[1].b 163 | ]]) 164 | end) 165 | 166 | spec('string base #5.1+', function() 167 | assert_eval('bbb', '"aaa":gsub("a", "b")') 168 | assert_eval('bbb', "'aaa':gsub('a', 'b')") 169 | assert_eval('bbb', "[[aaa]]:gsub('a', 'b')") 170 | end) 171 | 172 | spec('index chain base source map #5.1+', function() 173 | assert_source_map(1, [[ 174 | (nil)() 175 | ]]) 176 | 177 | assert_source_map(2, [[ 178 | 179 | my_fake_function() 180 | ]]) 181 | 182 | assert_source_map(3, [[ 183 | 184 | local a = 185 | my_fake_function() 186 | ]]) 187 | 188 | assert_source_map(4, [[ 189 | 190 | 191 | local a = 192 | (nil)() 193 | ]]) 194 | end) 195 | -------------------------------------------------------------------------------- /spec/compile_jumps_spec.lua: -------------------------------------------------------------------------------- 1 | local compile = require('erde.compile') 2 | 3 | -- ----------------------------------------------------------------------------- 4 | -- Goto 5 | -- ----------------------------------------------------------------------------- 6 | 7 | spec('goto #jit #5.2+', function() 8 | assert_run(1, [[ 9 | local a 10 | a = 1 11 | goto test 12 | a = 2 13 | ::test:: 14 | return a 15 | ]]) 16 | 17 | -- Lua throws errors at PARSE time if it cannot find `goto` labels 18 | assert.has_error(function() 19 | compile('goto test') 20 | end) 21 | end) 22 | 23 | spec('goto transform Lua keywords #jit #5.2+', function() 24 | assert_run(1, [[ 25 | local x = 1 26 | goto end 27 | x = 0 28 | ::end:: 29 | return x 30 | ]]) 31 | end) 32 | 33 | -- ----------------------------------------------------------------------------- 34 | -- If Else 35 | -- ----------------------------------------------------------------------------- 36 | 37 | spec('if #5.1+', function() 38 | assert_run(1, 'if true { return 1 }') 39 | assert_run(2, 'if false { return 0 }; return 2') 40 | end) 41 | 42 | spec('if + elseif #5.1+', function() 43 | assert_run(1, [[ 44 | if true { 45 | return 1 46 | } elseif true { 47 | return 0 48 | } 49 | ]]) 50 | 51 | assert_run(2, [[ 52 | if false { 53 | return 0 54 | } elseif true { 55 | return 2 56 | } 57 | ]]) 58 | end) 59 | 60 | spec('if + else #5.1+', function() 61 | assert_run(1, [[ 62 | if true { 63 | return 1 64 | } else { 65 | return 0 66 | } 67 | ]]) 68 | 69 | assert_run(2, [[ 70 | if false { 71 | return 0 72 | } else { 73 | return 2 74 | } 75 | ]]) 76 | end) 77 | 78 | spec('if + elseif + else #5.1+', function() 79 | assert_run(1, [[ 80 | if true { 81 | return 1 82 | } elseif true { 83 | return 0 84 | } else { 85 | return 0 86 | } 87 | ]]) 88 | 89 | assert_run(2, [[ 90 | if false { 91 | return 0 92 | } elseif true { 93 | return 2 94 | } else { 95 | return 0 96 | } 97 | ]]) 98 | 99 | assert_run(3, [[ 100 | if false { 101 | return 0 102 | } elseif false { 103 | return 0 104 | } else { 105 | return 3 106 | } 107 | ]]) 108 | end) 109 | -------------------------------------------------------------------------------- /spec/compile_loops_spec.lua: -------------------------------------------------------------------------------- 1 | local compile = require('erde.compile') 2 | 3 | -- ----------------------------------------------------------------------------- 4 | -- Break 5 | -- ----------------------------------------------------------------------------- 6 | 7 | spec('break #5.1+', function() 8 | assert_run(1, [[ 9 | local a = 0 10 | 11 | while true { 12 | a += 1 13 | break 14 | } 15 | 16 | return a 17 | ]]) 18 | 19 | assert_run(2, [[ 20 | local a = 2 21 | 22 | while true { 23 | if true { break } 24 | a += 1 25 | } 26 | 27 | return a 28 | ]]) 29 | 30 | assert_run(3, [[ 31 | local a = 0 32 | 33 | for i = 1, 3 { 34 | for j = 1, 4 { 35 | a += 1 36 | break 37 | } 38 | } 39 | 40 | return a 41 | ]]) 42 | 43 | assert.has_error(function() 44 | compile('if true { break }') 45 | end) 46 | end) 47 | 48 | 49 | describe('code following break', function() 50 | spec('disallow #5.1 #jit', function() 51 | assert.has_error(function() 52 | compile([[ 53 | while true { 54 | break 55 | print() 56 | } 57 | ]]) 58 | end) 59 | end) 60 | 61 | spec('allow #5.2+', function() 62 | assert.has.no_error(function() 63 | compile([[ 64 | while true { 65 | break 66 | print() 67 | } 68 | ]]) 69 | end) 70 | end) 71 | end) 72 | 73 | -- ----------------------------------------------------------------------------- 74 | -- Continue 75 | -- ----------------------------------------------------------------------------- 76 | 77 | spec('continue #5.1+', function() 78 | assert_run({ 1, 2 }, [[ 79 | local a, b = 1, 0 80 | 81 | for i = 1, 2 { 82 | b += 1 83 | continue 84 | a += 1 85 | } 86 | 87 | return { a, b } 88 | ]]) 89 | 90 | assert_run({ 3, 4 }, [[ 91 | local a, b = 3, 0 92 | 93 | for i = 1, 2 { 94 | for j = 1, 2 { 95 | b += 1 96 | continue 97 | a += 1 98 | } 99 | } 100 | 101 | return { a, b } 102 | ]]) 103 | 104 | assert.has_error(function() 105 | compile('if true { continue }') 106 | end) 107 | end) 108 | 109 | -- ----------------------------------------------------------------------------- 110 | -- For Loop 111 | -- ----------------------------------------------------------------------------- 112 | 113 | spec('numeric for loop #5.1+', function() 114 | assert_run({ 1, 2, 3 }, [[ 115 | local a = {} 116 | for i = 1, 3 { table.insert(a, i) } 117 | return a 118 | ]]) 119 | 120 | assert_run({ 2, 4, 6 }, [[ 121 | local a = {} 122 | for i = 2, 6, 2 { table.insert(a, i) } 123 | return a 124 | ]]) 125 | end) 126 | 127 | spec('generic for loop #5.1+', function() 128 | assert_run({ 1, 2, 3 }, [[ 129 | local a = {} 130 | for i, value in ipairs({ 0, 0, 0 }) { table.insert(a, i) } 131 | return a 132 | ]]) 133 | 134 | assert_run({ 4, 5, 6 }, [[ 135 | local a = {} 136 | for i, value in ipairs({ 4, 5, 6 }) { table.insert(a, value) } 137 | return a 138 | ]]) 139 | 140 | assert_run({ 7, 8 }, [[ 141 | local a = {} 142 | for i, [ b ] in ipairs({ { 7 }, { 8 } }) { table.insert(a, b) } 143 | return a 144 | ]]) 145 | end) 146 | 147 | spec('for loop transform Lua keywords #5.1+', function() 148 | assert_run(1, [[ 149 | local a = 0 150 | for end = 1, 1 { a = end } 151 | return a 152 | ]]) 153 | 154 | assert_run(2, [[ 155 | local a = 0 156 | for _, end in ipairs({ 2 }) { a = end } 157 | return a 158 | ]]) 159 | end) 160 | 161 | spec('for loop update block declarations #5.1+', function() 162 | assert_run(1, [[ 163 | global a = 1 164 | for a = 1, 1 { a = 0 } 165 | local result = _G.a 166 | _G.a = nil 167 | return result 168 | ]]) 169 | 170 | assert_run(2, [[ 171 | global a = 2 172 | for _, a in ipairs({ 0 }) { a = 0 } 173 | local result = _G.a 174 | _G.a = nil 175 | return result 176 | ]]) 177 | 178 | assert_run({ a = 3 }, [[ 179 | module a = 3 180 | for a = 1, 1 { a = 0 } 181 | ]]) 182 | 183 | assert_run({ a = 4 }, [[ 184 | module a = 4 185 | for _, a in ipairs({ 0 }) { a = 0 } 186 | ]]) 187 | end) 188 | 189 | -- ----------------------------------------------------------------------------- 190 | -- Repeat Until 191 | -- ----------------------------------------------------------------------------- 192 | 193 | spec('repeat until #5.1+', function() 194 | assert_run(10, [[ 195 | local a = 0 196 | repeat { a += 1 } until a > 9 197 | return a 198 | ]]) 199 | end) 200 | 201 | -- ----------------------------------------------------------------------------- 202 | -- While Loop 203 | -- ----------------------------------------------------------------------------- 204 | 205 | spec('while loop #5.1+', function() 206 | assert_run(10, [[ 207 | local a = 0 208 | while a < 10 { a += 1 } 209 | return a 210 | ]]) 211 | end) 212 | -------------------------------------------------------------------------------- /spec/compile_misc_spec.lua: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------------------------------- 2 | -- Ambiguous Syntax 3 | -- ----------------------------------------------------------------------------- 4 | 5 | spec('ambiguous syntax #5.1+', function() 6 | assert_run(1, [[ 7 | local a = 1 8 | local b = a;(() -> 0)() 9 | return b 10 | ]]) 11 | 12 | assert_run(2, [[ 13 | local a = 2 14 | local b = a 15 | (() -> 0)() 16 | return b 17 | ]]) 18 | 19 | assert_run(3, [[ 20 | local a = f -> f 21 | local b = a(() -> 3)() 22 | return b 23 | ]]) 24 | end) 25 | 26 | -- ----------------------------------------------------------------------------- 27 | -- Throwaway Parentheses 28 | -- 29 | -- Often used to throwaway excessive values, in particular for functions that 30 | -- return multiple values. 31 | -- ----------------------------------------------------------------------------- 32 | 33 | spec('retain throwaway parens #5.1+', function() 34 | assert_run(1, [[ 35 | local a = () -> (1, 2) 36 | local b, c = (a()) 37 | return b 38 | ]]) 39 | 40 | assert_run(nil, [[ 41 | local a = () -> (1, 2) 42 | local b, c = (a()) 43 | return c 44 | ]]) 45 | end) 46 | -------------------------------------------------------------------------------- /spec/compile_return_spec.lua: -------------------------------------------------------------------------------- 1 | local lib = require('erde.lib') 2 | 3 | spec('return #5.1+', function() 4 | assert_run(nil, 'return') 5 | assert_run(1, 'return 1') 6 | assert_run(2, 'return (2)') 7 | assert_run('{', "return ('{')") 8 | assert.are.equal('function', type(lib.run('return () -> 1'))) 9 | 10 | assert_run(3, [[ 11 | return (() -> { 12 | local a, b = 1, 2 13 | return a + b 14 | })() 15 | ]]) 16 | 17 | assert_run({ 1, 2, 3, 4 }, [[ 18 | local function a() { 19 | return ( 20 | 1, 21 | 2, 22 | 3, 23 | 4, 24 | ) 25 | } 26 | 27 | return { a() } 28 | ]]) 29 | 30 | assert.has_error(function() 31 | compile('return 1 print()') 32 | end) 33 | 34 | assert.has_error(function() 35 | compile([[ 36 | if true { 37 | return 1 38 | print() 39 | } 40 | ]]) 41 | end) 42 | end) 43 | -------------------------------------------------------------------------------- /spec/compile_strings_spec.lua: -------------------------------------------------------------------------------- 1 | local compile = require('erde.compile') 2 | 3 | -- ----------------------------------------------------------------------------- 4 | -- Single Quote String 5 | -- ----------------------------------------------------------------------------- 6 | 7 | spec('single quote string #5.1+', function() 8 | assert_eval('', "''") 9 | 10 | assert_eval('hello', "'hello'") 11 | assert_eval('hello\nworld', "'hello\\nworld'") 12 | assert_eval('\\', "'\\\\'") 13 | end) 14 | 15 | spec('single quote string interpolation #5.1+', function() 16 | assert_eval('{', "'{'") 17 | assert_eval('}', "'}'") 18 | 19 | assert_eval('{1}', "'{1}'") 20 | assert_eval('a{1}', "'a{1}'") 21 | assert_eval('{1}b', "'{1}b'") 22 | assert_eval('a{1}b', "'a{1}b'") 23 | assert_eval('a{1 + 2}b', "'a{1 + 2}b'") 24 | 25 | assert.has_error(function() 26 | compile("return '\\{'") 27 | end) 28 | end) 29 | 30 | spec('single quote string source map #5.1 jit', function() 31 | assert_source_map(1, [[ 32 | local a = 1 + 'a' 33 | ]]) 34 | 35 | assert_source_map(2, [[ 36 | local a = 1 + 37 | 'a' 38 | ]]) 39 | end) 40 | 41 | -- ----------------------------------------------------------------------------- 42 | -- Double Quote String 43 | -- ----------------------------------------------------------------------------- 44 | 45 | spec('double quote string #5.1+', function() 46 | assert_eval('', '""') 47 | 48 | assert_eval('hello', '"hello"') 49 | assert_eval('hello\nworld', '"hello\\nworld"') 50 | assert_eval('\\', '"\\\\"') 51 | end) 52 | 53 | spec('double quote string interpolation #5.1+', function() 54 | assert_eval('{', '"\\{"') 55 | assert_eval('}', '"\\}"') 56 | 57 | assert_eval('{1}', '"\\{1}"') 58 | assert_eval('{1}', '"\\{1\\}"') 59 | 60 | assert_eval('1', '"{1}"') 61 | assert_eval('a1', '"a{1}"') 62 | assert_eval('1b', '"{1}b"') 63 | assert_eval('a1b', '"a{1}b"') 64 | assert_eval('a3b', '"a{1 + 2}b"') 65 | end) 66 | 67 | spec('double quote string source map #5.1 jit', function() 68 | assert_source_map(1, [[ 69 | local a = 1 + "a" 70 | ]]) 71 | 72 | assert_source_map(2, [[ 73 | local a = 1 + 74 | "a" 75 | ]]) 76 | end) 77 | 78 | -- ----------------------------------------------------------------------------- 79 | -- Block Quote String 80 | -- ----------------------------------------------------------------------------- 81 | 82 | spec('block string #5.1+', function() 83 | assert_eval('', '[[]]') 84 | assert_eval('', '[[\n]]') 85 | 86 | assert_eval('hello world', '[[hello world]]') 87 | assert_eval(' hello\nworld', '[[ hello\nworld]]') 88 | 89 | assert_eval('a[[b', '[=[a[[b]=]') 90 | end) 91 | 92 | spec('block string interpolation #5.1+', function() 93 | assert_eval('{', '[[\\{]]') 94 | assert_eval('}', '[[\\}]]') 95 | 96 | assert_eval('{1}', '[[\\{1}]]') 97 | assert_eval('{1}', '[[\\{1\\}]]') 98 | 99 | assert_eval('1', '[[{1}]]') 100 | assert_eval('a1', '[[a{1}]]') 101 | assert_eval('1b', '[[{1}b]]') 102 | assert_eval('a1b', '[[a{1}b]]') 103 | assert_eval('a3b', '[[a{1 + 2}b]]') 104 | end) 105 | 106 | spec('block string source map #5.1 jit', function() 107 | assert_source_map(1, [[ 108 | local a = 1 + [=[a]=] 109 | ]]) 110 | 111 | assert_source_map(2, [[ 112 | local a = 1 + 113 | [=[a]=] 114 | ]]) 115 | end) 116 | 117 | spec('block string leading newline #5.1+', function() 118 | assert_eval('a', '[[\na]]') 119 | assert_eval('1\na', '[[{1}\na]]') 120 | end) 121 | -------------------------------------------------------------------------------- /spec/compile_tables_spec.lua: -------------------------------------------------------------------------------- 1 | spec('tables #5.1+', function() 2 | assert_eval({ 10 }, '{ 10 }') 3 | assert_eval({ x = 2 }, '{ x = 2 }') 4 | assert_eval({ [3] = 1 }, '{ [1 + 2] = 1 }') 5 | assert_eval({ x = { y = 1 } }, '{ x = { y = 1 } }') 6 | end) 7 | 8 | spec('tables source map #5.1 jit', function() 9 | assert_source_map(1, [[ 10 | local a = 1 + {} 11 | ]]) 12 | 13 | assert_source_map(2, [[ 14 | local a = 1 + 15 | {} 16 | ]]) 17 | end) 18 | -------------------------------------------------------------------------------- /spec/setup_spec.lua: -------------------------------------------------------------------------------- 1 | local busted = require('busted') 2 | local config = require('erde.config') 3 | local lib = require('erde.lib') 4 | 5 | config.lua_target = os.getenv('LUA_TARGET') or '5.1+' 6 | 7 | function assert_eval(expected, source) 8 | if expected == nil then 9 | busted.assert.is_nil(lib.run('return ' .. source)) 10 | else 11 | busted.assert.are.same(expected, lib.run('return ' .. source)) 12 | end 13 | end 14 | 15 | function assert_run(expected, source) 16 | if expected == nil then 17 | busted.assert.is_nil(lib.run(source)) 18 | else 19 | busted.assert.are.same(expected, lib.run(source)) 20 | end 21 | end 22 | 23 | function assert_source_map(expected_error_line, source) 24 | local ok, result = xpcall(function() lib.run(source) end, lib.rewrite) 25 | 26 | busted.assert.are.equal(false, ok) 27 | 28 | local error_line = result:match('^[^:]+:(%d+)') 29 | busted.assert.are.equal(expected_error_line, tonumber(error_line)) 30 | end 31 | -------------------------------------------------------------------------------- /spec/tokenize_spec.lua: -------------------------------------------------------------------------------- 1 | local C = require('erde.constants') 2 | local tokenize = require('erde.tokenize') 3 | 4 | -- ----------------------------------------------------------------------------- 5 | -- Helpers 6 | -- ----------------------------------------------------------------------------- 7 | 8 | local function assert_token(expected, token) 9 | local tokens = tokenize(token or expected) 10 | assert.are.equal(expected, tokens[1].value) 11 | end 12 | 13 | local function assert_tokens(expected, text) 14 | local token_values = {} 15 | 16 | for _, token in ipairs(tokenize(text)) do 17 | table.insert(token_values, token.value) 18 | end 19 | 20 | assert.are.same(expected, token_values) 21 | end 22 | 23 | local function assert_num_tokens(expected, text) 24 | local tokens = tokenize(text) 25 | assert.are.equal(expected, #tokens) 26 | end 27 | 28 | -- ----------------------------------------------------------------------------- 29 | -- Number Tokenizers 30 | -- ----------------------------------------------------------------------------- 31 | 32 | spec('tokenize_binary #5.1+', function() 33 | assert_token('0', '0b0') 34 | assert_token('0', '0B0') 35 | assert_token('1', '0b1') 36 | assert_token('1', '0B1') 37 | 38 | assert_token('0', '0b00') 39 | assert_token('1', '0b01') 40 | assert_token('2', '0b10') 41 | assert_token('4', '0b0100') 42 | assert_token('12', '0B1100') 43 | 44 | assert.has_error(function() tokenize('0b') end) 45 | assert.has_error(function() tokenize('0B') end) 46 | assert.has_error(function() tokenize('0b3') end) 47 | assert.has_error(function() tokenize('0ba') end) 48 | assert.has_error(function() tokenize('0b.') end) 49 | end) 50 | 51 | spec('tokenize_decimal #5.1+', function() 52 | for i = 1, 9 do 53 | assert_token(tostring(i)) 54 | end 55 | 56 | assert_token('123') 57 | assert_token('12300') 58 | assert_token('00123') 59 | assert_token('0012300') 60 | 61 | assert_token('.0') 62 | assert_token('0.0') 63 | assert_token('.123') 64 | assert_token('0.001') 65 | assert_token('0.100') 66 | assert_token('0.010') 67 | assert_token('9.87') 68 | 69 | assert_tokens({ '0', '.' }, '0.') 70 | assert_tokens({ '0', '.', 'e' }, '0.e') 71 | 72 | assert_token('1e2') 73 | assert_token('1E2') 74 | assert_token('1e+2') 75 | assert_token('1e-2') 76 | assert_token('1E+2') 77 | assert_token('1E-2') 78 | 79 | assert_token('1.23e29') 80 | assert_token('0.11E-39') 81 | 82 | assert.has_error(function() tokenize('9e') end) 83 | assert.has_error(function() tokenize('9e+') end) 84 | assert.has_error(function() tokenize('9e-') end) 85 | end) 86 | 87 | describe('tokenize_hex', function() 88 | spec('#5.1+', function() 89 | assert_token('1', '0x1') 90 | assert_token('1', '0X1') 91 | 92 | assert_token('4886718345', '0x123456789') 93 | assert_token('11259375', '0xabcdef') 94 | assert_token('11259375', '0xABCDEF') 95 | assert_token('41394', '0xa1B2') 96 | 97 | assert_token('0.0625', '0x.1') 98 | assert_token('13.625', '0xd.a') 99 | 100 | assert_token('3.75', '0xfp-2') 101 | assert_tokens({ '15', '.' }, '0xf.') 102 | 103 | assert.has_error(function() tokenize('0x') end) 104 | assert.has_error(function() tokenize('0xg') end) 105 | assert.has_error(function() tokenize('0x.') end) 106 | assert.has_error(function() tokenize('0x.g') end) 107 | assert.has_error(function() tokenize('0x.p1') end) 108 | assert.has_error(function() tokenize('0xfp') end) 109 | assert.has_error(function() tokenize('0xfp+') end) 110 | assert.has_error(function() tokenize('0xfp-') end) 111 | assert.has_error(function() tokenize('0xfpa') end) 112 | end) 113 | 114 | spec('#5.1 #5.2 #jit', function() 115 | assert_token('30', '0xfp1') 116 | assert_token('30', '0xfP1') 117 | assert_token('60', '0xfp+2') 118 | end) 119 | 120 | spec('#5.3+', function() 121 | assert_token('30.0', '0xfp1') 122 | assert_token('30.0', '0xfP1') 123 | assert_token('60.0', '0xfp+2') 124 | end) 125 | end) 126 | 127 | -- ----------------------------------------------------------------------------- 128 | -- String Tokenizers 129 | -- ----------------------------------------------------------------------------- 130 | 131 | describe('tokenize_escape_sequence', function() 132 | spec('#5.1+', function() 133 | for escapeChar in pairs(C.STANDARD_ESCAPE_CHARS) do 134 | assert_tokens({ "'", '\\' .. escapeChar, "'" }, "'\\" .. escapeChar .. "'") 135 | assert_tokens({ '"', '\\' .. escapeChar, '"' }, '"\\' .. escapeChar .. '"') 136 | end 137 | 138 | assert_tokens({ "'", '\\1', "'" }, "'\\1'") 139 | assert_tokens({ "'", '\\123', "'" }, "'\\123'") 140 | assert_tokens({ '"', '\\1', '"' }, '"\\1"') 141 | assert_tokens({ '"', '\\123', '"' }, '"\\123"') 142 | 143 | -- Lua 5.2+ will throw an error for unrecognized escape sequences 144 | assert.has_error(function() tokenize("'\\o'") end) 145 | assert.has_error(function() tokenize('"\\o"') end) 146 | assert.has_no.error(function() tokenize('[[\\o]]') end) 147 | 148 | -- Erde does not allow for interpolation in single quote strings 149 | assert.has_error(function() tokenize("'\\{'") end) 150 | assert.has_no.error(function() tokenize('"\\{"') end) 151 | assert.has_no.error(function() tokenize('[[\\{]]') end) 152 | 153 | -- Lua does not process escape sequences in block strings 154 | assert.has_no.error(function() tokenize('[[\\x]]') end) 155 | assert.has_no.error(function() tokenize('[[\\u]]') end) 156 | end) 157 | 158 | spec('#5.1', function() 159 | assert.has_error(function() tokenize('"\\z"') end) 160 | assert.has_error(function() tokenize('"\\x1f"') end) 161 | end) 162 | 163 | spec('#5.1 #5.2', function() 164 | assert.has_error(function() tokenize('"\\u{a}"') end) 165 | assert.has_error(function() tokenize('"\\u{abc}"') end) 166 | end) 167 | 168 | spec('#jit #5.2+', function() 169 | assert_tokens({ '"', '\\z', '"' }, '"\\z"') 170 | assert_tokens({ '"', '\\x1f', '"' }, '"\\x1f"') 171 | 172 | assert.has_error(function() tokenize('"\\xA"') end) 173 | assert.has_error(function() tokenize('"\\x1G"') end) 174 | end) 175 | 176 | spec('#jit #5.3+', function() 177 | assert_tokens({ '"', '\\u{a}', '"' }, '"\\u{a}"') 178 | assert_tokens({ '"', '\\u{abc}', '"' }, '"\\u{abc}"') 179 | 180 | assert.has_error(function() tokenize('"\\uabc"') end) 181 | assert.has_error(function() tokenize('"\\u{abc"') end) 182 | assert.has_error(function() tokenize('"\\uabc}"') end) 183 | assert.has_error(function() tokenize('"\\u{}"') end) 184 | assert.has_error(function() tokenize('"\\u{g}"') end) 185 | end) 186 | end) 187 | 188 | spec('tokenize_interpolation #5.1+', function() 189 | assert_tokens({ "'", 'a{bc}d', "'" }, "'a{bc}d'") 190 | assert_tokens({ '"', 'a', '{', 'bc', '}', 'd', '"' }, '"a{bc}d"') 191 | assert_tokens({ '[[', 'a', '{', 'bc', '}', 'd', ']]' }, '[[a{bc}d]]') 192 | 193 | assert_tokens({ "'", 'a{ bc }d', "'" }, "'a{ bc }d'") 194 | assert_tokens({ '"', 'a', '{', 'bc', '}', 'd', '"' }, '"a{ bc }d"') 195 | assert_tokens({ '[[', 'a', '{', 'bc', '}', 'd', ']]' }, '[[a{ bc }d]]') 196 | 197 | assert_tokens({ '"', '{', '"', '{', 'a', '}', '"', '}', '"' }, '"{"{a}"}"') 198 | assert_tokens({ '[[', 'a', '{', '{', 'bc', '}', '}', 'd', ']]' }, '[[a{{bc}}d]]') 199 | 200 | assert.has_error(function() tokenize('"{a"') end) 201 | assert.has_error(function() tokenize('"{{a}"') end) 202 | end) 203 | 204 | spec('tokenize_single_quote_string #5.1+', function() 205 | assert_tokens({ "'", "'" }, "''") 206 | assert_tokens({ "'", ' ', "'" }, "' '") 207 | assert_tokens({ "'", '\t', "'" }, "'\t'") 208 | 209 | assert_tokens({ "'", 'a', "'" }, "'a'") 210 | assert_tokens({ "'", ' a b ', "'" }, "' a b '") 211 | 212 | assert_tokens({ "'", "\\'", "'" }, "'\\''") 213 | assert_tokens({ "'", '\\n', "'" }, "'\\n'") 214 | 215 | assert.has_error(function() tokenize("'a") end) 216 | assert.has_error(function() tokenize("'\n'") end) 217 | end) 218 | 219 | spec('tokenize_double_quote_string #5.1+', function() 220 | assert_tokens({ '"', '"' }, '""') 221 | assert_tokens({ '"', ' ', '"' }, '" "') 222 | assert_tokens({ '"', '\t', '"' }, '"\t"') 223 | 224 | assert_tokens({ '"', 'a', '"' }, '"a"') 225 | assert_tokens({ '"', ' a b ', '"' }, '" a b "') 226 | 227 | assert_tokens({ '"', '\\"', '"' }, '"\\""') 228 | assert_tokens({ '"', '\\n', '"' }, '"\\n"') 229 | 230 | assert_tokens({ '"', '{a}', '"' }, '"\\{a}"') 231 | assert_tokens({ '"', '{a}', '"' }, '"\\{a\\}"') 232 | assert_tokens({ '"', '{', 'a', '}', '"' }, '"{a}"') 233 | 234 | assert.has_error(function() tokenize('"a') end) 235 | assert.has_error(function() tokenize('"\n"') end) 236 | end) 237 | 238 | spec('tokenize_block_string #5.1+', function() 239 | assert_tokens({ '[[', ']]' }, '[[]]') 240 | assert_tokens({ '[[', ' ', ']]' }, '[[ ]]') 241 | assert_tokens({ '[[', '\t', ']]' }, '[[\t]]') 242 | assert_tokens({ '[[', '\n', ']]' }, '[[\n]]') 243 | 244 | assert_tokens({ '[[', ' hello world ', ']]' }, '[[ hello world ]]') 245 | assert_tokens({ '[[', 'hello\nworld', ']]' }, '[[hello\nworld]]') 246 | 247 | assert_tokens({ '[[', '[=[', ']]' }, '[[[=[]]') 248 | assert_tokens({ '[[', ']=', ']]' }, '[[]=]]') 249 | assert_tokens({ '[=[', '[[', ']=]' }, '[=[[[]=]') 250 | assert_tokens({ '[=[', ']]', ']=]' }, '[=[]]]=]') 251 | 252 | assert_tokens({ '[[', '\\', ']]' }, '[[\\]]') 253 | assert_tokens({ '[[', '\\u', ']]' }, '[[\\u]]') 254 | 255 | assert_tokens({ '[[', '{a}', ']]' }, '[[\\{a}]]') 256 | assert_tokens({ '[[', '{a}', ']]' }, '[[\\{a\\}]]') 257 | assert_tokens({ '[[', '{', 'a', '}', ']]' }, '[[{a}]]') 258 | 259 | assert.has_error(function() tokenize('[=hello world]=]') end) 260 | assert.has_error(function() tokenize('[[hello world') end) 261 | assert.has_error(function() tokenize('[[hello world]=]') end) 262 | assert.has_error(function() tokenize('[=[hello world]]') end) 263 | end) 264 | 265 | -- ----------------------------------------------------------------------------- 266 | -- Misc Tokenizers 267 | -- ----------------------------------------------------------------------------- 268 | 269 | spec('tokenize symbols #5.1+', function() 270 | for symbol in pairs(C.SYMBOLS) do 271 | assert_token(symbol) 272 | end 273 | end) 274 | 275 | spec('tokenize_word #5.1+', function() 276 | assert_token('lua') 277 | assert_token('Erde') 278 | assert_token('_test') 279 | assert_token('aa1B_') 280 | assert_token('_aa1B') 281 | 282 | assert_tokens({ 'a', '-' }, 'a-') 283 | assert_tokens({ '1', 'abc' }, '1abc') 284 | end) 285 | 286 | spec('tokenize_comment #5.1+', function() 287 | assert_num_tokens(1, '--') 288 | assert_num_tokens(1, '--a') 289 | assert_num_tokens(1, '-- a') 290 | assert_num_tokens(2, '--\na') 291 | assert_num_tokens(2, '--a\nb') 292 | 293 | assert_num_tokens(2, '--[=a\nb') 294 | assert_num_tokens(2, '-- [[a\nb') 295 | 296 | assert_num_tokens(1, '--[[a]]') 297 | assert_num_tokens(1, '--[[ a ]]') 298 | assert_num_tokens(1, '--[[a\nb]]') 299 | assert_num_tokens(2, '--[[a]]b') 300 | 301 | assert_num_tokens(1, '--[[[=[]]') 302 | assert_num_tokens(1, '--[[]=]]') 303 | assert_num_tokens(1, '--[=[[[]=]') 304 | assert_num_tokens(1, '--[=[]]]=]') 305 | 306 | assert_num_tokens(2, '-- [[a\nb') 307 | assert_num_tokens(4, 'x + --[[hi]] 4') 308 | 309 | assert.has_error(function() tokenize('--[[hello world') end) 310 | assert.has_error(function() tokenize('--[[hello world]=]') end) 311 | assert.has_error(function() tokenize('--[=[hello world]]') end) 312 | end) 313 | --------------------------------------------------------------------------------