├── .gitattributes ├── .github └── workflows │ └── makefile.yml ├── Makefile ├── README.md ├── benchmark ├── babbage.eqx ├── babbage.lua ├── brainfuck.eqx ├── brazilian.eqx ├── prime.eqx └── prime.lua ├── doc ├── catalogue.md ├── core.md ├── interop.md ├── modules.md ├── syntax.md ├── table.md └── vars.md ├── equinox ├── ext ├── repl_ext.eqx └── tutorial.eqx ├── imgs ├── Nova001.jpg ├── fib.gif ├── logo.png └── screenshot.png ├── license.txt ├── rockspec └── equinox-template.rockspec ├── src ├── ast.lua ├── ast_matchers.lua ├── ast_optimizer.lua ├── codegen.lua ├── compiler.lua ├── console.lua ├── dict.lua ├── env.lua ├── equinox.lua ├── equinox_bundle.lua ├── interop.lua ├── line_mapping.lua ├── ln_repl_backend.lua ├── macros.lua ├── output.lua ├── parser.lua ├── repl.lua ├── simple_repl_backend.lua ├── source.lua ├── stack.lua ├── stack_aux.lua ├── utils.lua └── version │ ├── version.lua │ └── version.txt └── tests ├── json.lua ├── mymod.eqx ├── mymodule.eqx ├── opt.expected ├── opt.out ├── sout ├── linenum1.eqx ├── linenum1.eqx.expected ├── linenum1.eqx.out ├── linenum2.eqx ├── linenum2.eqx.expected ├── linenum2.eqx.out ├── linenum3.eqx ├── linenum3.eqx.expected ├── linenum3.eqx.out ├── mymod.eqx.out ├── prn.eqx ├── prn.eqx.expected ├── prn.eqx.out ├── see.eqx ├── see.eqx.expected ├── see.eqx.out ├── words.eqx ├── words.eqx.expected └── words.eqx.out ├── test_alias.eqx ├── test_begin_again.eqx ├── test_begin_until.eqx ├── test_begin_while_repeat.eqx ├── test_block.eqx ├── test_case.eqx ├── test_comment.eqx ├── test_compiler.lua ├── test_core.eqx ├── test_do_loop.eqx ├── test_env.lua ├── test_for.eqx ├── test_for_each.eqx ├── test_for_iter.eqx ├── test_hyperstat.eqx ├── test_if.eqx ├── test_interop.lua ├── test_luacall.eqx ├── test_luacallback.eqx ├── test_module.eqx ├── test_nil.eqx ├── test_optimizer.eqx ├── test_override.eqx ├── test_parser.lua ├── test_recurse.eqx ├── test_scope1.eqx ├── test_scope2.eqx ├── test_smoke.lua ├── test_stack.lua ├── test_string.eqx ├── test_table.eqx ├── test_tick.eqx ├── test_unloop_exit.eqx ├── test_utils.lua ├── test_var.eqx └── test_words.eqx /.gitattributes: -------------------------------------------------------------------------------- 1 | *.eqx linguist-language=Forth 2 | -------------------------------------------------------------------------------- /.github/workflows/makefile.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@master 15 | - uses: leafo/gh-actions-lua@v10 16 | with: 17 | luaVersion: "5.1.5" 18 | - uses: leafo/gh-actions-luarocks@v4 19 | 20 | - name: Install dependencies 21 | run: | 22 | luarocks install luacov 23 | luarocks install luacov-coveralls 24 | 25 | - name: Check Lua 26 | run: | 27 | lua -v 28 | which lua 29 | luarocks list 30 | lua -e 'print(require("luacov"))' 31 | ls -al .luarocks 32 | 33 | - name: Run tests 34 | run: | 35 | make coverage 36 | 37 | - name: Publish Coverage 38 | env: 39 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | run: | 41 | luacov-coveralls -i 'src/*.lua' -e 'src/version/*' 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LUA54 = lua5.4 2 | LUA51 = lua5.1 3 | LUA_VERSIONS := $(LUA54) $(LUA51) luajit lua 4 | SRC_DIR = src 5 | TEST_DIR = tests 6 | TEST_LUA_FILES = $(wildcard $(TEST_DIR)/test_*.lua) 7 | TEST_EQX_FILES = $(wildcard $(TEST_DIR)/test_*.eqx) 8 | TEST_SOUT_FILES = $(wildcard $(TEST_DIR)/sout/*.eqx) 9 | EQUINOX = $(SRC_DIR)/equinox.lua 10 | BUNDLE = $(SRC_DIR)/equinox_bundle.lua 11 | AMALG = amalg.lua 12 | luaver ?= lua 13 | opt ?= o1 14 | 15 | GREEN := \033[0;32m 16 | RED := \033[0;31m 17 | NC := \033[0m 18 | 19 | GET_VERSION = version=$$(cat $(SRC_DIR)/version/version.txt) 20 | 21 | LUA_PATH := $(SRC_DIR)/?.lua;$(TEST_DIR)/?.lua;;$(LUA_PATH) 22 | export LUA_PATH 23 | 24 | all: clean test version bundle rockspec 25 | 26 | lua_tests: 27 | @echo "Running Lua tests ($(luaver))"; \ 28 | for file in $(TEST_LUA_FILES); do \ 29 | echo " - $$file..."; \ 30 | $$luaver $$file || exit 1; \ 31 | done 32 | 33 | eqx_tests: 34 | @echo "Running EQX tests ($(luaver)) ($(opt))"; \ 35 | for file in $(TEST_EQX_FILES); do \ 36 | echo " - $$file..."; \ 37 | $$luaver $(EQUINOX) $(opt) $$file || exit 1; \ 38 | done 39 | 40 | out_tests: 41 | @echo "Running OUT tests ($(luaver)) ($(opt))"; \ 42 | for file in $(TEST_SOUT_FILES); do \ 43 | echo " - $$file..."; \ 44 | $$luaver $(EQUINOX) $(opt) $$file 2>&1 | head -n 6 | sed -E 's/\([0-9]+\)//g' > "$$file.out" || exit 1; \ 45 | diff "$$file.out" "$$file.expected" || exit 1; \ 46 | done 47 | 48 | opt_tests: 49 | @echo "Running optimizer tests ($(luaver))"; \ 50 | $$luaver $(EQUINOX) -od "tests/test_optimizer.eqx" > "tests/opt.out" || exit 1; \ 51 | diff "tests/opt.out" "tests/opt.expected" || exit 1; \ 52 | 53 | test: 54 | @for luaver in $(LUA_VERSIONS); do \ 55 | if ! command -v $$luaver > /dev/null 2>&1; then \ 56 | echo "$$luaver is not installed skippping"; \ 57 | else \ 58 | echo "* $$luaver @ $$(which $$luaver)"; \ 59 | luacmd="$$luaver"; \ 60 | if [ "$$coverage" = "true" ]; then \ 61 | if "$$luaver" -e 'require("luacov")' >/dev/null 2>&1; then \ 62 | echo "luacov is installed"; \ 63 | luacmd="$$luaver -lluacov"; \ 64 | else \ 65 | echo "luacov is NOT installed"; \ 66 | fi ; \ 67 | fi ; \ 68 | $(MAKE) -s lua_tests luaver="$$luacmd" || exit 1; \ 69 | $(MAKE) -s eqx_tests luaver="$$luacmd" opt=-o0 || exit 1; \ 70 | $(MAKE) -s eqx_tests luaver="$$luacmd" opt=-o1 || exit 1; \ 71 | $(MAKE) -s out_tests luaver="$$luacmd" opt=-o0 || exit 1; \ 72 | $(MAKE) -s out_tests luaver="$$luacmd" opt=-o1 || exit 1; \ 73 | $(MAKE) -s opt_tests luaver="$$luacmd" || exit 1; \ 74 | fi; \ 75 | done ; 76 | @echo "$(GREEN)All tests passed!$(NC)" 77 | 78 | coverage: 79 | @export ENABLE_COV="true"; \ 80 | $(MAKE) -s test coverage="true" || exit 1; \ 81 | 82 | version: 83 | @echo "Increase patch version" ; \ 84 | lua $(SRC_DIR)/version/version.lua ; \ 85 | 86 | bundle: 87 | @$(GET_VERSION) ; \ 88 | echo "Creating $(BUNDLE) v$${version}" ; \ 89 | $(AMALG) -s $(EQUINOX) compiler utils console ln_repl_backend simple_repl_backend env codegen ast_optimizer ast_matchers stack_aux dict ast line_mapping interop parser macros source output repl stack -o $(BUNDLE); \ 90 | sed -i "s/^__VERSION__=.*$$/__VERSION__=\"$${version}\"/" $(BUNDLE); \ 91 | 92 | repl: 93 | @lua $(shell [ "$(coverage)" = "true" ] && echo "-lluacov" || echo "") $(EQUINOX); \ 94 | 95 | rockspec: 96 | @$(GET_VERSION) ; \ 97 | specfile="equinox-$${version}.rockspec" ; \ 98 | echo "Creating rockspec: $${specfile} for v$${version}.." ; \ 99 | cp rockspec/equinox-template.rockspec $${specfile} ; \ 100 | sed -i "s/^version =.*$$/version = \"$$version\"/" $${specfile} ; \ 101 | sed -i "s/\s*tag =.*$$/ tag = \"v$$version\"/" $${specfile} ; \ 102 | 103 | install: 104 | @$(GET_VERSION) ; \ 105 | specfile="equinox-$${version}.rockspec" ; \ 106 | luarocks make $${specfile} ; \ 107 | 108 | clean: 109 | @echo "Cleaning up" ; \ 110 | rm equinox-*.*-*.rockspec ; \ 111 | rm -f $(BUNDLE) ; \ 112 | 113 | # Add a phony directive to prevent file conflicts 114 | .PHONY: all test clean bundle rockspec repl version lua_tests eqx_tests opt_tests out_tests install coverage 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Equinox - Forth Programming Language That Targets Lua 🌑 2 | 3 | ![{master}](https://github.com/zeroflag/equinox/actions/workflows/makefile.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/zeroflag/equinox/badge.svg?branch=master&kill_cache=2)](https://coveralls.io/github/zeroflag/equinox?branch=master) 4 | 5 | ```forth 6 | _____ _ _____ _ _ 7 | | ____|__ _ _ _(_)_ __ _____ __ | ___|__ _ __| |_| |__ 8 | | _| / _` | | | | | '_ \ / _ \ \/ / | |_ / _ \| '__| __| '_ \ 9 | | |__| (_| | |_| | | | | | (_) > < | _| (_) | | | |_| | | | 10 | |_____\__, |\__,_|_|_| |_|\___/_/\_\ |_| \___/|_| \__|_| |_| 11 | |_| 12 | 13 | : fibonacci ( n -- .. ) 0 1 rot 0 do 2dup + loop ; 14 | 15 | 10 fibonacci .s 16 | ``` 17 | 18 | ## 🌕 Design goals 19 | 20 | * Compiles directly to (optimized) Lua source code. 21 | * Modeless with no interpretation mode, no return stack. 22 | * Lua table and array support. 23 | * Fixes Lua's accidental global problem. 24 | * GameDev support via [Love2D](https://love2d.org/) and [TIC-80](https://tic80.com/) (later). 25 | * Self-hosted compiler (later). 26 | 27 | ## 🚀 Install 28 | 29 | ```bash 30 | $ luarocks install equinox 31 | ``` 32 | 33 | The easiest way to install Equinox is by using Lua's package manager, [Luarocks](https://luarocks.org/). 34 | 35 | Equinox requires Lua 5.1 or later. 36 | 37 | ### 💻 Start the REPL 38 | 39 | ```bash 40 | $ equinox 41 | ``` 42 | 43 | fib 44 | 45 | 46 | If you're a first-time Forth user, I suggest you start with the built-in tutorial. 47 | 48 | In the REPL, type: 49 | 50 | ``` 51 | load-file tutorial 52 | ``` 53 | 54 | ### Compile and execute a file: 55 | 56 | ```bash 57 | $ equinox file.eqx 58 | ``` 59 | 60 | ### Embed Into Lua Project 61 | 62 | * For Love2D sample project see this repository: [VimSnake](https://github.com/zeroflag/vimsnake). 63 | 64 | ## 👍 Why Equinox? 65 | 66 | Popular retro gaming platforms like the [TIC-80](https://tic80.com/) tiny computer and 2D game engines like [Love2D](https://love2d.org/) usually use [Lua](https://www.lua.org/) for scripting. 67 | 68 | While Lua's a cool, lightweight language, it doesn’t quite give you that old-school game dev vibe. Forth, on the other hand, really brings you back to the golden age of gaming with that nostalgic feel. 69 | 70 | Lua has some questionable semantics, like how a simple typo can accidentally create a global variable when you wanted to modify a local one. Equinox fixes this problem by preventing accidental creation of globals. 71 | 72 | Unlike Lua, Equinox syntactically distinguishes between sequential tables `[]` and hash maps `{}`. While the underlying data structure is the same, this differentiation helps make the code easier to read, in my opinion. 73 | 74 | ## ⛔ Why Not Equinox? 75 | 76 | Equinox is a Forth that uses postfix notation and a stack to manage the parameters of words. This is quite different from how mainstream programming languages work and look. Some people might find this style unusual or hard to read. 77 | 78 | While I believe Forth helps make people better programmers by teaching them to value simplicity and break down definitions into small, manageable pieces (which is more of a must than an option), I’m fully aware it’s not for everyone. 79 | 80 | Equinox is generally slower than Lua, mainly due to the stack operations. While the compiler uses various optimization rules to minimize these operations, it's still in its early phase, so the end result is often slower compared to a pure Lua counterpart. 81 | 82 | However, this performance difference is expected to improve in the future. 83 | 84 | ## Differences from Other Forth Languages 85 | 86 | * Equinox can leverage Lua's high-level data structure, the table, which can be used as an array or a dictionary. 87 | * `!` (put) and `@` (at) are used for table access and modification, rather than variable assignment. 88 | * You can use control structures such as `if`, `then`, as well as loops, outside of word definitions because Equinox does not have an interpretation mode. 89 | * String literals are supported at the syntax level and are denoted with double quotes (`"`). 90 | * Equinox doesn't have a dedicated return stack (no `>r`, `r>` words), but it has an auxiliary stack that can be used similarly (`a>`, `>a`). 91 | * `DO` loops use Lua local variables internally instead of the return stack, so no `unloop` is needed for an early exit. 92 | * `DO` loops have safer semantics as they check the condition before entering the loop, so `1 1 do i . loop` won't do anything. 93 | * Equinox doesn't have its own standard library besides the stack manipulation words and a few others for table construction, so you need to use Lua functions. 94 | * The majority of the Equinox words are macros (immediate words), including the arithmetic operators and stack manipulation words. 95 | * In the current version user defined immediate words are not supported. 96 | 97 | ## 📚 Documentation 98 | * [Syntax](doc/syntax.md) 99 | * [Core](doc/core.md) 100 | * [Variables](doc/vars.md) 101 | * [Lua Interop](doc/interop.md) 102 | * [Table Operations](doc/table.md) 103 | * [Modules & Objects](doc/modules.md) 104 | * [Catalogue](doc/catalogue.md) 105 | 106 | ## 🪐 The Name 107 | 108 | The USS Equinox, `NCC-72381`, was a small, Nova class Federation science vessel that stuck in the Delta Quadrant and was (will?) destroyed in 2376. 109 | 110 | RIP Captain Ransom and crew. 111 | 112 | starship 113 | 114 | Source [Wikipedia](https://en.wikipedia.org/wiki/Equinox_(Star_Trek:_Voyager)). 115 | 116 | (If you have good ASCII art or logo of a Nova-class starship, let me know, and I'll replace the Constitution one.) 117 | 118 | 119 | -------------------------------------------------------------------------------- /benchmark/babbage.eqx: -------------------------------------------------------------------------------- 1 | alias: clock #( os.clock ) 2 | 3 | : babbage 4 | 1 5 | begin 6 | 1 + 7 | dup dup * 8 | 1000000 % 9 | 269696 = 10 | until ; 11 | 12 | clock 13 | 1 500 to: i 14 | babbage drop 15 | end 16 | clock swap ( started ) - . cr 17 | 18 | babbage . cr 19 | -------------------------------------------------------------------------------- /benchmark/babbage.lua: -------------------------------------------------------------------------------- 1 | function babbage() 2 | local x = 1 3 | while true do 4 | x = x + 1 5 | local squared = x * x 6 | if squared % 1000000 == 269696 then 7 | break 8 | end 9 | end 10 | return x 11 | end 12 | 13 | local started = os.clock() 14 | for i = 1, 500 do 15 | babbage() 16 | end 17 | print(os.clock() - started) 18 | 19 | print(babbage()) 20 | -------------------------------------------------------------------------------- /benchmark/brainfuck.eqx: -------------------------------------------------------------------------------- 1 | alias: sub #( string.sub 3 1 ) 2 | alias: >chr #( string.char 1 1 ) 3 | alias: write #( io.write 1 0 ) 4 | alias: read #( io.read 0 1 ) 5 | alias: ord #( string.byte 1 1 ) 6 | 7 | var ptr 8 | var mem 9 | var input 10 | var index 11 | 12 | : alloc ( n -- ) 13 | [] -> mem 14 | 0 do mem 0 append loop ; 15 | 16 | : load ( -- n ) mem ptr @ ; 17 | : stor ( n -- ) mem ptr rot ! ; 18 | 19 | : chr ( -- c ) input index index sub ; 20 | : next ( -- ) index 1 + -> index ; 21 | 22 | : jmp ( dir tok -- ) 23 | begin 24 | dup chr != 25 | while 26 | over index + -> index 27 | repeat 28 | drop drop ; 29 | 30 | : print ( -- ) load >chr write ; 31 | 32 | : ?jmp-fwrd load 0 = if +1 $] jmp then ; 33 | : ?jmp-back load 0 != if -1 $[ jmp then ; 34 | 35 | : eval-token ( chr -- ) 36 | case 37 | $> of ptr 1 + -> ptr endof 38 | $< of ptr 1 - -> ptr endof 39 | $+ of load 1 + stor endof 40 | $- of load 1 - stor endof 41 | $. of print endof 42 | $, of read ord stor endof 43 | $[ of ?jmp-fwrd endof 44 | $] of ?jmp-back endof 45 | drop 46 | endcase ; 47 | 48 | : init ( str -- ) 49 | -> input 50 | 1 -> index 51 | 1 -> ptr 52 | 256 alloc ; 53 | 54 | : eval ( str -- ) 55 | init 56 | begin 57 | index input # <= 58 | while 59 | chr eval-token 60 | next 61 | repeat ; 62 | 63 | : test ( -- ) 64 | "++>+++++[<+>-]" eval 65 | mem 1 @ 7 =assert 66 | mem 2 @ 0 =assert ; 67 | 68 | : hello-world ( -- ) 69 | ">++++++++[<+++++++++>-]<.>++++[<+++++++>-]<+.+++++++..+++.>>++++++[<+++++++>-]<++.------------.>++++++[<+++++++++>-]<+.<.+++.------.--------.>>>++++[<++++++++>- ]<+." 70 | eval ; 71 | 72 | : echo ( -- ) ",." eval ; 73 | 74 | test 75 | hello-world 76 | echo 77 | -------------------------------------------------------------------------------- /benchmark/brazilian.eqx: -------------------------------------------------------------------------------- 1 | alias: floor #( math.floor 1 1 ) 2 | alias: clock #( os.clock ) 3 | 4 | : div ( n n -- n ) / floor ; 5 | 6 | : prime? ( n -- flag ) 7 | dup 2 < if drop false exit then 8 | dup 2 % 0 = if 2 = exit then 9 | dup 3 % 0 = if 3 = exit then 10 | 5 11 | begin 12 | 2dup dup * >= 13 | while 14 | 2dup % 0 = if drop drop false exit then 15 | 2 + 16 | 2dup % 0 = if drop drop false exit then 17 | 4 + 18 | repeat 19 | drop drop true ; 20 | 21 | : same_digits? ( n b -- ? ) 22 | 2dup % -> var tmp 23 | begin 24 | tuck div swap 25 | over 0 > 26 | while 27 | 2dup % tmp != if 28 | drop drop false exit 29 | then 30 | repeat 31 | drop drop true ; 32 | 33 | : brazilian? ( n -- ? ) 34 | dup 7 < if drop false exit then 35 | dup 2 % 0 = if drop true exit then 36 | dup 1 - 2 do 37 | dup i same_digits? if 38 | drop true exit 39 | then 40 | loop 41 | drop false ; 42 | 43 | : next_prime ( n -- n ) 44 | begin 2 + dup prime? until ; 45 | 46 | [] -> var result 47 | 48 | : print_brazilian ( n1 n2 -- ) 49 | -> var tmp 50 | 7 51 | begin 52 | tmp 0 > 53 | while 54 | dup brazilian? if 55 | dup . 56 | result over append 57 | tmp 1 - -> tmp 58 | then 59 | over 0 = if 60 | next_prime 61 | else 62 | over + 63 | then 64 | repeat 65 | drop drop cr ; 66 | 67 | : sum ( -- n ) 68 | 0 69 | result ipairs: _ each 70 | each + 71 | end ; 72 | 73 | clock 74 | 1 5000 print_brazilian 75 | sum 14530468 =assert 76 | clock swap ( started ) - . cr 77 | -------------------------------------------------------------------------------- /benchmark/prime.eqx: -------------------------------------------------------------------------------- 1 | alias: sqrt #( math.sqrt 1 1 ) 2 | alias: clock #( os.clock ) 3 | 4 | : sieve_of_eratosthenes ( limit -- tbl ) 5 | var primes 6 | { } -> primes 7 | 2 over ( limit ) to: i 8 | primes i i ! 9 | end 10 | 2 over ( limit ) sqrt to: i 11 | primes i @ if 12 | i i * over ( limit ) i step: j 13 | primes j false ! 14 | end 15 | then 16 | end 17 | [ ] ( result ) 18 | 2 rot ( limit ) to: i 19 | primes i @ if 20 | dup i append 21 | then 22 | end ; 23 | 24 | clock 25 | 1000000 sieve_of_eratosthenes 26 | clock rot ( started ) - . cr 27 | 28 | pairs: k v 29 | k . v . cr 30 | end 31 | -------------------------------------------------------------------------------- /benchmark/prime.lua: -------------------------------------------------------------------------------- 1 | local function sieve_of_eratosthenes(limit) 2 | local primes = {} 3 | for i = 2, limit do primes[i] = true end 4 | for i = 2, math.sqrt(limit) do 5 | if primes[i] then 6 | for j = i * i, limit, i do 7 | primes[j] = false 8 | end 9 | end 10 | end 11 | local result = {} 12 | for i = 2, limit do 13 | if primes[i] then 14 | table.insert(result, i) 15 | end 16 | end 17 | return result 18 | end 19 | 20 | local started = os.clock() 21 | local result = sieve_of_eratosthenes(1000000) 22 | print(os.clock() - started) 23 | 24 | for k,v in pairs(result) do 25 | print(k .. " " .. v .. " ") 26 | end 27 | -------------------------------------------------------------------------------- /doc/catalogue.md: -------------------------------------------------------------------------------- 1 | # Word Catalog 2 | 3 | ## Arithmetic and Logical Operations 4 | 5 | | Word | Description | Stack Effect | Immediate | 6 | |-------|--------------------------|---------------------------|-----------| 7 | | `+` | Addition | `( n1 n2 -- sum )` | Yes | 8 | | `-` | Subtraction | `( n1 n2 -- diff )` | Yes | 9 | | `*` | Multiplication | `( n1 n2 -- prod )` | Yes | 10 | | `/` | Division | `( n1 n2 -- quot )` | Yes | 11 | | `%` | Modulo | `( n1 n2 -- rem )` | Yes | 12 | | `=` | Equals | `( n1 n2 -- bool )` | Yes | 13 | | `!=` | Not equals | `( n1 n2 -- bool )` | Yes | 14 | | `>` | Greater than | `( n1 n2 -- bool )` | Yes | 15 | | `>=` | Greater than or equal to | `( n1 n2 -- bool )` | Yes | 16 | | `<` | Less than | `( n1 n2 -- bool )` | Yes | 17 | | `<=` | Less than or equal to | `( n1 n2 -- bool )` | Yes | 18 | | `not` | Logical NOT | `( bool -- bool' )` | Yes | 19 | | `and` | Logical AND | `( bool1 bool2 -- bool )` | Yes | 20 | | `or` | Logical OR | `( bool1 bool2 -- bool )` | Yes | 21 | | `max` | Maximum of two values | `( n1 n2 -- max )` | No | 22 | | `min` | Minimum of two values | `( n1 n2 -- min )` | No | 23 | | `pow` | A base raised to a power | `( n1 n2 -- n1^n2 )` | No | 24 | 25 | ## Stack Manipulation 26 | 27 | | Word | Description | Stack Effect | Immediate | 28 | |----------|---------------------------------|---------------------------------------|-----------| 29 | | `swap` | Swap top two items on stack | `( x1 x2 -- x2 x1 )` | Yes | 30 | | `over` | Copy second item to top | `( x1 x2 -- x1 x2 x1 )` | Yes | 31 | | `rot` | Rotate top three items | `( x1 x2 x3 -- x2 x3 x1 )` | Yes | 32 | | `-rot` | Reverse rotate top three items | `( x1 x2 x3 -- x3 x1 x2 )` | Yes | 33 | | `nip` | Remove second item from stack | `( x1 x2 -- x2 )` | Yes | 34 | | `drop` | Remove top item from stack | `( x -- )` | Yes | 35 | | `dup` | Duplicate top item | `( x -- x x )` | Yes | 36 | | `2dup` | Duplicate top two items | `( x1 x2 -- x1 x2 x1 x2 )` | Yes | 37 | | `tuck` | Copy top item below second item | `( x1 x2 -- x2 x1 x2 )` | Yes | 38 | | `depth` | Get stack depth | `( -- n )` | Yes | 39 | | `pick` | Copy nth item to top | `( xn ... x1 n -- xn ... x1 xn )` | Yes | 40 | | `roll` | Rotate nth item to top | `( xn ... x1 n -- x(n-1) ... x1 xn )` | Yes | 41 | | `adepth` | Aux stack depth | `( -- n )` | Yes | 42 | | `>a` | Move top item to aux stack | `( n -- )` | Yes | 43 | | `a>` | Move top of aux to data stack | `( -- n )` | Yes | 44 | 45 | ## IO 46 | 47 | | Word | Description | Stack Effect | Immediate | 48 | |--------|---------------------------------------|--------------|-----------| 49 | | `.` | Print out top of the stack | `( x -- )` | Yes | 50 | | `emit` | Display a character by its ASCII code | `( n -- )` | Yes | 51 | | `cr` | Print out a new line | `( -- )` | Yes | 52 | 53 | ## Control Structures 54 | 55 | | Word | Description | Stack Effect | Immediate | 56 | |-----------|-----------------------------------------------------|---------------------------|-----------| 57 | | `if` | Conditional branch start | `( bool -- )` | Yes | 58 | | `then` | End an `if` block | `( -- )` | Yes | 59 | | `else` | Alternate branch in `if` | `( -- )` | Yes | 60 | | `begin` | Start of a loop | `( -- )` | Yes | 61 | | `again` | Infinite loop (for `begin`) | `( -- )` | Yes | 62 | | `until` | Loop condition check (for `begin`) | `( bool -- )` | Yes | 63 | | `while` | Loop condition check (for `begin`) | `( bool -- )` | Yes | 64 | | `repeat` | End a `while` loop | `( -- )` | Yes | 65 | | `case` | Start of a case statement | `( x -- )` | Yes | 66 | | `of` | Case match clause | `( x1 x2 -- )` | Yes | 67 | | `endof` | End of a case clause | `( -- )` | Yes | 68 | | `endcase` | End of case statement | `( -- )` | Yes | 69 | | `do` | Start of a counted loop | `( limit start -- )` | Yes | 70 | | `loop` | End of a counted loop | `( -- )` | Yes | 71 | | `ipairs:` | Iterate over array pairs | `( array -- )` | Yes | 72 | | `pairs:` | Iterate over hash-table pairs | `( hash-tbl -- )` | Yes | 73 | | `iter:` | Iterate over an iterable | `( iterable -- )` | Yes | 74 | | `to:` | Start a counted loop | `( start limit -- )` | Yes | 75 | | `step:` | Start a counted loop with a step | `( start limit step -- )` | Yes | 76 | | `end` | End a `to:` `step:` `pairs:` `ipairs:` `iter:` loop | `( -- )` | Yes | 77 | | `exit` | Exit from a word definition | `( -- )` | Yes | 78 | | `exec` | Run an execution token (function ref.) | `( xt -- )` | Yes | 79 | 80 | ## Defining 81 | 82 | | Word | Description | Stack Effect | Immediate | 83 | |-------------|-------------------------------------------------|--------------|-----------| 84 | | `:` | Define a new word | `( -- )` | Yes | 85 | | `::` | Define a new local word | `( -- )` | Yes | 86 | | `;` | End word definition | `( -- )` | Yes | 87 | | `var` | Define a new variable (preferred over `global`) | `( -- )` | Yes | 88 | | `global` | Define a new global variable | `( -- )` | Yes | 89 | | `->` | Assign value to a variable | `( x -- )` | Yes | 90 | | `alias:` | Define an alias | `( -- )` | Yes | 91 | | `recursive` | Make a word recursive | `( -- )` | Yes | 92 | | `(:` | Define parameters for a Lua callback | `( -- )` | Yes | 93 | | `:)` | End of Lua callback parameter list | `( -- )` | Yes | 94 | | \` | Get the execution token of a word | `( -- xt )` | Yes | 95 | 96 | ## Table Operations 97 | 98 | 99 | | Word | Description | Stack Effect | Immediate | Example | 100 | |----------|-----------------------------------|--------------------------------|-----------|-------------------------------| 101 | | `[]` | Empty sequential table | `( -- table )` | No | | 102 | | `{}` | Empty hash table | `( -- table )` | No | | 103 | | `[` | Start creating a sequential table | `( -- )` | No | `[ 1 2 dup * 3 ]` | 104 | | `]` | End creating a sequential table | `( ... -- table )` | No | | 105 | | `{` | Start creating a hash table | `( -- )` | No | `{ :x 10 :y 20 }` | 106 | | `}` | End creating a hash table | `( ... -- table )` | No | | 107 | | `@` | Look up by index / key | `( table key/index -- value )` | No | `array 3 @` `tbl $x @` | 108 | | `!` | Put/Update element into a table | `( table key/index value -- )` | No | `array 1 30 !` `tbl $x 30 !` | 109 | | `append` | Append to a sequential table | `( table value -- )` | No | | 110 | | `insert` | Insert into a sequential table | `( table index value -- )` | No | | 111 | | `remove` | Remove element from table | `( table key -- )` | No | | 112 | | `#` | Size of a sequential table | `( table -- n )` | Yes | | 113 | 114 | ## Modules 115 | 116 | | Word | Description | Stack Effect | Immediate | Example | 117 | |----------|--------------------------------------------|---------------------|-----------|-----------------------------| 118 | | `need` | Require a module | `( str -- module )` | Yes | `"dkjson" need -> var json` | 119 | | `return` | Export a module / Return from Lua function | `( x -- )` | Yes | | 120 | 121 | ## Misc 122 | 123 | | Word | Description | Stack Effect | Immediate | 124 | |--------|-------------------|----------------|-----------| 125 | | `>str` | Convert to string | `( n -- str )` | No | 126 | | `>num` | Convert to number | `( str -- n )` | No | 127 | 128 | ## Debugging 129 | 130 | | Word | Description | Stack Effect | Immediate | 131 | |----------------|--------------------------------------|----------------|-----------| 132 | | `.s` | Print out the stack (REPL only) | `( -- )` | No | 133 | | `clear` | Clear the data stack (REPL only) | `( .. -- )` | No | 134 | | `inspect` | Print out a table (REPL only) | `( x -- )` | No | 135 | | `see` | Decompiles a word | `( -- )` | Yes | 136 | | `words` | Show availale words | `( -- )` | Yes | 137 | | `=assert` | Check if the top two items are equal | `( x1 x2 -- )` | No | 138 | | `assert-true` | Check if top of the stack is true | `( bool -- )` | No | 139 | | `assert-false` | Check if top of the stack is false | `( bool -- )` | No | 140 | 141 | -------------------------------------------------------------------------------- /doc/core.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | The core of the language is very small, as most of the library functions come from Lua. 4 | 5 | Equinox supports the usual stack manipulation words like `dup`, `drop`, `swap`, `2dup`, `nip`, `rot`, `-rot`, `over`, `tuck`, and `pick`, as well as two additional words for accessing the aux stack: `>a` and `a>`. 6 | 7 | Traditional Forth control structures are supported, as well as some newer constructs. 8 | 9 | ## Conditional 10 | 11 | The words `if`, `else`, and `then` work the same way as in other Forth dialects. 12 | 13 | ```forth 14 | 2 1 > if "ok" else "no" then 15 | 16 | 2 1 > if "ok" then 17 | 18 | ``` 19 | 20 | A switch-case-like construct is also supported: 21 | 22 | ```forth 23 | 5 24 | case 25 | 1 of "Monday" . endof 26 | 2 of "Tuesday" . endof 27 | 3 of "Wednesday" . endof 28 | 4 of "Thursday" . endof 29 | 5 of "Friday" . endof 30 | 6 of "Saturday" . endof 31 | 7 of "Sunday" . endof 32 | "Unknown day: " . 33 | endcase 34 | 35 | \ this will print out Friday 36 | ``` 37 | 38 | If none of the conditions in the branches are met, the value is left on the stack in the default branch. 39 | 40 | ## Loops 41 | 42 | ### Do Loop 43 | 44 | `Do` loops in Equinox look like those in traditional Forth dialects, but they are implemented differently. 45 | 46 | ```forth 47 | 10 1 do i . loop \ prints out 1 2 3 4 5 6 7 8 9 48 | ``` 49 | 50 | Unlike in a traditional Forth dialect, where the loop variable `i` is allocated on the return stack, Equinox translates this loop into a regular for loop in Lua and names the loop variable `i`. 51 | 52 | ```forth 53 | 54 | : tst 10 1 do i . loop ; 55 | 56 | see tst 57 | 58 | function tst() 59 | stack:push(10) 60 | stack:push(1) 61 | for i=stack:pop(),stack:pop() - 1 do 62 | stack:push(i) 63 | io.write(tostring(stack:pop())) 64 | io.write(" ") 65 | end 66 | end 67 | ``` 68 | 69 | This also means that the condition is checked at the beginning of the loop, before entering the body. 70 | Therefore, if the `limit` is smaller than the `start`, the loop won’t be executed. 71 | 72 | Another consequence is that you can freely do an early return (`exit`) from the loop without worrying about `unlooping` the return stack. 73 | 74 | You can nest up to three levels of `DO` loops, with the loop variables named `i`, `j`, and `k`, respectively. 75 | 76 | ```forth 77 | [] 78 | 1000 1 do 79 | 1000 1 do 80 | i j * palindrom? if 81 | dup i j * append 82 | then 83 | loop 84 | loop 85 | ``` 86 | 87 | ### Begin 88 | 89 | The `begin` word supports three different loop types: `begin` - `until`, `begin` - `again`, and `begin` - `while` - `repeat`. 90 | 91 | ``` 92 | begin until 93 | begin again 94 | begin .. while repeat 95 | ``` 96 | 97 | For example: 98 | 99 | ```forth 100 | 5 101 | begin 102 | dup 0 >= 103 | while 104 | dup . 1 - 105 | repeat 106 | drop 107 | \ prints out 5 4 3 2 1 0 108 | ``` 109 | 110 | ```forth 111 | 5 112 | begin 113 | dup . 114 | 1 - dup 115 | 0 < until 116 | drop 117 | \ prints out 5 4 3 2 1 0 118 | ``` 119 | 120 | ```forth 121 | : tst 122 | 0 123 | begin 124 | dup 5 < if 1 + else dup * exit then 125 | again ; 126 | 127 | \ leaves 25 on the stack 128 | 129 | ``` 130 | 131 | ### New-Style Loops 132 | 133 | Equinox is equipped with five new-style loops as well. 134 | 135 | #### To Loop 136 | 137 | `to:` and `step:` are more generalized for loops that allow you to name the loop variable. 138 | 139 | ```forth 140 | to: 141 | 142 | end 143 | ``` 144 | 145 | For example: 146 | 147 | ```forth 148 | 1 10 to: idx 149 | idx . 150 | end 151 | \ prints out 1 2 3 4 5 6 7 8 9 10 152 | ``` 153 | 154 | In the example above, we chose `idx` as the loop variable name. 155 | 156 | Unlike with `do` loops this type of loop expect the parameters in the opposite order therefore it looks more like a for loop in other languges. 157 | 158 | The `limit` is *inclusive*, so the loop will exit when idx is greater than 10. 159 | 160 | #### Step Loop 161 | 162 | `step:` works similarly, but it also allows you to specify the increment. 163 | 164 | ```forth 165 | step: 166 | 167 | end 168 | ``` 169 | 170 | For example: 171 | 172 | ```forth 173 | 10 1 -1 step: idx 174 | idx . 175 | end 176 | \ prints out 10 9 8 7 6 5 4 3 2 1 177 | ``` 178 | 179 | #### Ipairs Loop 180 | 181 | The `ipairs:` loop allows you to iterate over a sequential table. 182 | 183 | ```forth 184 | ipairs: 185 | 186 | end 187 | ``` 188 | 189 | Where `name1` is the loop variable for the current index, and `name2` is the loop variable containing the current element of the table. 190 | 191 | For example: 192 | 193 | ```forth 194 | [ 10 20 30 ] ipairs: idx elem 195 | idx . elem . cr 196 | end 197 | \ prints out 198 | \ 1 10 199 | \ 2 20 200 | \ 3 30 201 | ``` 202 | 203 | #### Pairs Loop 204 | 205 | The `pairs:` loop works similarly to `ipairs:` but it is for traversing a hash table (key value pairs). 206 | 207 | 208 | ```forth 209 |
pairs: 210 | 211 | end 212 | ``` 213 | 214 | For example: 215 | 216 | ```forth 217 | { $k1 10 $k2 20 $k3 30 } pairs: key val 218 | key . val . cr 219 | end 220 | \ prints out 221 | \ k1 10 222 | \ k2 20 223 | \ k3 30 224 | ``` 225 | 226 | #### Iter Loop 227 | 228 | The `iter` loop is a general `for-each` loop used to traverse iterators, such as the result of `gmatch`. 229 | 230 | ```forth 231 | iter: 232 | 233 | end 234 | ``` 235 | 236 | For example: 237 | 238 | ```forth 239 | alias: gmatch #( string.gmatch 2 ) 240 | 241 | "1,2,3,4,5" "[^,]+" gmatch iter: each 242 | each >num + 243 | end 244 | ``` 245 | 246 | The previously mentioned `pairs:` and `ipairs:` loops are special cases of this, as they automatically wrap a table into an iterator. 247 | 248 | ## Word Definition 249 | 250 | The single colon (`:`) word is used to define words, while the double colon (`::`) is used for defining words local to the specific file. 251 | 252 | ```forth 253 | : double ( n -- n ) dup + ; 254 | 255 | :: square ( n -- n ) dup * ; \ local word 256 | 257 | ``` 258 | 259 | Equinox maintains a hyper-static global environment (*yes, that's a real term, not a Star Trek reference*) that allows words to be redefined in terms of their older definitions. 260 | 261 | ```forth 262 | : double ( n -- n ) 263 | "called double" . \ log something out 264 | double ; \ then call the original definition 265 | ``` 266 | 267 | So if you accidentally redefine a word, it will only affect its newer users. 268 | 269 | This also prevents recursion by default, but if you need recursion, you just need to mark the word as `recursive`. 270 | 271 | ```forth 272 | : factorial ( n -- n! ) recursive 273 | dup 2 > if dup 1 - factorial * then ; 274 | ``` 275 | 276 | ## Tick 277 | 278 | The tick word (single quote `'`) is used to get a reference to a word. 279 | 280 | ```forth 281 | ' double . 282 | \ prints out something like function: 0x5578dfda96c0 283 | ``` 284 | 285 | A reference like this can later be called with the `exec` word. 286 | 287 | ```forth 288 | 1 289 | ' double 290 | exec . 291 | \ prints out 2 292 | ``` 293 | 294 | Note that tick works only with words, not with compile-time macros (immediate words). 295 | -------------------------------------------------------------------------------- /doc/interop.md: -------------------------------------------------------------------------------- 1 | # Lua Interop 2 | 3 | Equinox doesn't have its own standard library besides the stack manipulation words and a few others for table construction. 4 | 5 | If you want to use something like `max`, you'll need to use the Lua function `math.max`. 6 | However, Lua functions don't use Forth's data stack to pass parameters, and many Lua functions are variadic. 7 | Even when they aren't, there is no reliable way to check the arity of a Lua function. 8 | 9 | Therefore, you need to tell the compiler how many parameters a function expects. 10 | 11 | ```forth 12 | 3 4 #( math.max 2 ) . \ this will print out 4 13 | ``` 14 | 15 | This will call `math.max` with 2 parameters (3 and 4). 16 | 17 | ## Alias 18 | 19 | Typing this every time would be too verbose and error-prone, so you can define an alias for it. 20 | 21 | ``` 22 | alias: max #( math.max 2 ) 23 | 24 | 3 4 max . \ this will print out 4 25 | ``` 26 | 27 | The word `alias:` can also be used to define compile-time constants. 28 | 29 | ```forth 30 | alias: magic-number 42 31 | 32 | magic-number . \ prints out 42 33 | ``` 34 | 35 | Some of these aliases are already predefined, such as those for common table operations, as [shown here](table.md). 36 | 37 | 38 | ## Return Value 39 | 40 | Sometimes, a Lua function returns a result you're not interested in. For example, `table.remove` removes and returns an item from a table. 41 | Often, you just want to delete it without doing anything with the result. 42 | 43 | ```forth 44 | [ "a" "b" "c" ] 2 #( table.remove 2 ) \ this will remove the 2nd item "b" from the table and return it 45 | ``` 46 | 47 | In this case you would need to keep this is mind and `drop` the item every-time you use `table.remove`. 48 | A better approach is to define an alias with number of return values set to 0. 49 | 50 | 51 | ```forth 52 | [ "a" "b" "c" ] dup 2 #( table.remove 2 0 ) \ just remove "b" don't return it 53 | inspect \ this will print [ "a" "c" ] 54 | ``` 55 | 56 | The built-in alias for this is simply called `remove`. 57 | 58 | Sometimes, a Lua function leaves multiple return values on the stack, and you're only interested in the first one. In such cases, you can set the number of return values to 1 to ignore the others. 59 | 60 | ## General Form 61 | 62 | The general form of this syntax is as follows: 63 | 64 | ```forth 65 | #( lua_func [arity] [#return] ) 66 | ``` 67 | 68 | Where: 69 | * `arity` defines the number of input parameters (should be >= 0). 70 | - The default is 0. 71 | * `#return` defines the number of return values. 72 | - `-1` means any (default), 73 | - `0` means none, and 74 | - `1` means one. 75 | 76 | ## Examples 77 | 78 | | Operation | Syntax | 79 | |----------------------------------------------------|-----------------------------| 80 | | Call Lua function (2 parameters) | 2 8 #( math.pow 2 ) | 81 | | Call nullary Lua function (no parameters) | #( os.time ) | 82 | | Call Lua (binary) function and ignore return value | tbl 2 #( table.remove 2 0 ) | 83 | | Call Lua (unary) method | #( astr:sub 1 ) | 84 | | Property lookup | math.pi | 85 | | | | 86 | 87 | ## Lua Callbacks 88 | 89 | Sometimes, an Equinox word is expected to be called by Lua, like the following Love2D callback. 90 | 91 | Often, these callbacks take parameters, such as the key that has been pressed by the user. 92 | 93 | ```forth 94 | : love.keypressed (: key :) 95 | key case 96 | $escape of quit endof 97 | $space of jump endof 98 | drop ( key ) 99 | endcase ; 100 | ``` 101 | 102 | 103 | Equinox provides a way to declare Lua function parameters using the words `(:` and `:)`. 104 | 105 | Here, the Love2D framework calls our `love.keypressed` callback every time the user presses a key and passes the key's name as a parameter. 106 | -------------------------------------------------------------------------------- /doc/modules.md: -------------------------------------------------------------------------------- 1 | # Modules 2 | 3 | Just like in Lua, you can build modules using tables to store words and variables. 4 | 5 | Contents of `mymodule.eqx`: 6 | 7 | ```forth 8 | { $state 10 } -> var mymodule 9 | 10 | : mymodule:access ( -- n ) self.state ; 11 | 12 | : mymodule:change ( n -- ) -> self.state ; 13 | 14 | mymodule return \ make the module available 15 | ``` 16 | 17 | Contents of `myapp.eqx`: 18 | 19 | ```forth 20 | "mymodule.eqx" need -> var mymod \ require the module 21 | 22 | mymod:access . \ prints out 10 23 | 24 | 6 mymod:change \ change state to 6 25 | 26 | mymod:access . \ prints out 6 27 | 28 | ``` 29 | 30 | Equinox uses the same method call syntax as Lua, denoted by a colon (`:`), where an implicit self parameter is passed to refer to the module itself. 31 | 32 | ## Classes & Objects 33 | 34 | If you need multiple instances of the same type of object, you can simulate classes using Lua's metatable support. 35 | 36 | The `__index` is a metatable field that allows table lookups to fall back to another table when a key is missing. 37 | 38 | This makes it possible for multiple objects to share the same set of "methods", essentially simulating classes. 39 | 40 | Contents of `player.eqx`: 41 | 42 | ```forth 43 | {} -> var Player 44 | 45 | : Player:new ( props -- instance ) 46 | dup { $__index self } #( setmetatable 2 0 ) ; 47 | 48 | : Player:update ( dt -- ) 49 | dup self.vx * self.x + -> self.x 50 | ( dt ) self.vy * self.y + -> self.y ; 51 | 52 | Player return 53 | 54 | ``` 55 | 56 | Contents of `game.eqx`: 57 | 58 | ```forth 59 | "player.eqx" need -> var Player 60 | 61 | { $x 0 $y 0 $vx 10 $vy 5 } Player:new -> var player1 62 | 63 | 0.3 player1:update 64 | 65 | player1.x . \ prints out 3 66 | player1.y . \ prints out 1.5 67 | 68 | ``` 69 | 70 | Note that dot notation is used to access fields in a table. If the field is a method, it will not be called automatically. 71 | 72 | `0.3 player1:update` is essentially the same as `0.3 player1 #( player1.update 1 )`. 73 | -------------------------------------------------------------------------------- /doc/syntax.md: -------------------------------------------------------------------------------- 1 | # Syntax 2 | 3 | Equinox syntax consists of numbers, words, and strings/symbols: 4 | 5 | * Number: `1.23` 6 | * String: `"Hello World\n"` 7 | * Symbol: `$key` 8 | * Word: `emit` 9 | 10 | The language also supports dot (`.`) notation for property lookup (e.g.: `math.pi`) and colon (`:`) notation for method calls (e.g.: `astr:upper`). 11 | 12 | ```forth 13 | 1.25 "aString" $aSymbol .. ( concat ) # ( length ) * 14 | ``` 15 | 16 | Table literals are *not* part of the syntax; they are defined as Forth words. 17 | 18 | ```forth 19 | [ 1 2 ] 20 | { $key 123 } 21 | ``` 22 | 23 | The same is true for comments: 24 | 25 | ``` 26 | \ single line comment 27 | ( comment ) 28 | ``` 29 | 30 | #### Example: 31 | 32 | ```forth 33 | alias: 2^n 2 swap #( math.pow 2 1 ) 34 | 35 | : sum-of-pows ( -- n ) 0 10 0 do i 2^n + loop ; 36 | 37 | var map 38 | { $key [ 1 2 "apple" sums-of-pows ] } -> map 39 | 40 | map.key 3 @ . \ prints out 1023 41 | map.key 1 42 ! 42 | 43 | \ define a Lua callback 44 | : love.keypressed (: key :) 45 | key $escape = if #( os.exit ) then ; 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /doc/table.md: -------------------------------------------------------------------------------- 1 | # Tables 2 | 3 | Lua has a single complex data structure called the Table. It can be used both as an array (sequential table) and as a hash table (dictionary/associative array). 4 | 5 | Lua tables can be constructed and used from Equinox, but unlike in Lua, we differentiate sequential tables from hash tables syntactically. 6 | 7 | ```forth 8 | [] \ create an empty sequential table 9 | {} \ create an empty hash table 10 | 11 | [ 1 2 3 ] \ create a sequential table with 1, 2 and 3 12 | 13 | { "key1" 4 $key2 5 } \ create a hash table with key1=4 and key2=5 14 | 15 | ``` 16 | 17 | Sequential tables or arrays are created using square brackets (`[]`), while hash tables are created with curly braces (`{}`). 18 | 19 | The dollar (`$`) prefix can be used as a shorthand to denote a symbol, which is represented as a string that cannot contain whitespaces. 20 | 21 | ## Access 22 | 23 | The at (`@`) word can be used to index an array or look up a value under a key in a hash table. 24 | 25 | ```forth 26 | [ "a" "b" "c" ] 2 @ . \ prints out "b" 27 | 28 | { "key1" "val" } "key1" @ . \ prints out "val" 29 | ``` 30 | 31 | If the key is a symbol with no whitespaces or special characters, you can also use the dot notation. 32 | 33 | ```forth 34 | var tbl 35 | 36 | { "key1" "val" } -> tbl \ assing table to variable 37 | 38 | tbl.key1 . \ prints out "val" 39 | ``` 40 | 41 | ## Update and Insert 42 | 43 | The put (`!`) word can be used to change or insert an item into the table. 44 | 45 | ```forth 46 | var tbl 47 | 48 | [ "a" "x" ] -> tbl 49 | 50 | tbl 2 "b" ! 51 | 52 | tbl inspect \ prints out [ "a" "b" ] 53 | 54 | 55 | { $key1 "val" } -> tbl 56 | 57 | tbl $key1 "new-val" ! 58 | tbl $key2 "val2" ! 59 | 60 | tbl inspect \ prints out { "key1" "new-val" "key2" "val2" } 61 | 62 | ``` 63 | 64 | For hash tables, you can also use the dot notation to insert or overwrite an item if the keys are strings with no special characters or whitespaces. 65 | 66 | ```forth 67 | { $key1 "val" } -> tbl 68 | 69 | "new-val" -> tbl.key1 70 | "val2" -> tbl.key2 71 | 72 | tbl inspect \ prints out { "key1" "new-val" "key2" "val2" } 73 | 74 | ``` 75 | 76 | ## Key-Value Pair Syntax 77 | 78 | You can construct a table by using names and values of variables with the following syntax: 79 | 80 | ```forth 81 | var vx 82 | var vy 83 | 84 | 10 -> vx 85 | 20 -> vy 86 | 87 | { $ vx $ vy } inspect \ prints out { "vx" 10 "vy" 20 } 88 | 89 | ``` 90 | 91 | 92 | ## Operations 93 | 94 | 95 | | Operation | Array | Table | 96 | |-----------------|-----------------------------|-------------------------------------| 97 | | Create | [ 1 2 3 ] | { key1 val1 } | 98 | | Append | tbl item append | | 99 | | Insert new | tbl idx item insert | value -> tbl.key or tbl key value ! | 100 | | Overwrite | tbl idx item ! | value -> tbl.key or tbl key value ! | 101 | | Lookup | tbl idx @ | tbl.key or tbl key @ | 102 | | Remove | tbl idx remove | tbl key nil ! | 103 | | Remove & Return | tbl idx #( table.remove 2 ) | | 104 | | Size | tbl # | | 105 | -------------------------------------------------------------------------------- /doc/vars.md: -------------------------------------------------------------------------------- 1 | # Variables 2 | 3 | Equinox supports both global and local variables, but unlike Lua, both require declaration. 4 | 5 | Local variables are restricted to the specific block in which they're declared, but that scope can span an entire file. 6 | 7 | ## Vars 8 | 9 | Below, the scope of variable `x` extends across the entire file, while the scope of variable `y` is limited to `my-word`. 10 | 11 | ```forth 12 | \ declared at top level 13 | var x 14 | 15 | ... 16 | 17 | : my-word 18 | \ declared within a word 19 | var y 20 | ... ; 21 | 22 | ``` 23 | 24 | Values can be assigned to variables using the `->` word 25 | 26 | ```forth 27 | 12 -> x 28 | x . \ prints out 12 29 | 30 | ``` 31 | 32 | To fetch the value of a variable, you don't need any special words; simply typing its name is enough. 33 | 34 | A shorthand for declaring and initializing a variable in one step: `value -> var name`. 35 | 36 | 37 | ```forth 38 | 20 -> var v1 39 | ``` 40 | 41 | The above program is translated to the following Lua code: 42 | 43 | ```lua 44 | local v1 = 20 45 | ``` 46 | 47 | If you assign a value to a variable that hasn't been previously declared, you'll get an error. 48 | 49 | You can simulate parameters with locals in the following way: 50 | 51 | ```forth 52 | : length -> var z -> var y -> var x 53 | x square y square + z square + sqrt ; 54 | 55 | ``` 56 | 57 | ## Globals 58 | 59 | Globals are accessible throughout the entire program, but globals in Lua are known for their slowness relative to locals. 60 | 61 | Since top-level locals (`vars`) can be accessed throughout the entire file, globals are rarely needed. 62 | 63 | ```forth 64 | global g1 65 | 66 | 12 -> g1 67 | ``` 68 | 69 | The above program is translated to the following Lua code: 70 | 71 | ```lua 72 | g1 = nil 73 | g1 = 12 74 | ``` 75 | 76 | Try to avoid globals and use vars if possible. 77 | -------------------------------------------------------------------------------- /equinox: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | MODULE="equinox" 3 | 4 | LUA=$(which lua) 5 | if [ -z "$LUA" ]; then 6 | echo "Lua is not installed. Equinox requires Lua." 7 | exit 1 8 | fi 9 | 10 | EXT_DIR="$(luarocks show --rock-dir $MODULE)/ext" 11 | MODULE_PATH=$(luarocks show --porcelain "$MODULE" | grep "bundle" | awk -F' ' '{print $3}') 12 | 13 | if [ -z "$MODULE_PATH" ]; then 14 | MODULE_PATH=$(luarocks show "$MODULE" | grep "bundle" | sed -n 's/.*(\(.*\)).*/\1/p') 15 | fi 16 | 17 | EQUINOX_EXT_DIR="$EXT_DIR" exec "$LUA" "$MODULE_PATH" "$@" 18 | -------------------------------------------------------------------------------- /ext/repl_ext.eqx: -------------------------------------------------------------------------------- 1 | alias: rep #( string.rep 2 ) 2 | alias: sub #( string.sub 3 ) 3 | alias: gsub #( string.gsub 3 ) 4 | 5 | 12 -> var width 6 | 7 | :: line ( -- ) 8 | " +" 9 | "-" width 2 + rep 10 | .. "+" 11 | .. . cr ; 12 | 13 | :: trunc ( s -- s ) 14 | dup size width > if 15 | 1 width 2 - sub ".." .. 16 | then ; 17 | 18 | :: padlen ( s -- n ) size width swap - ; 19 | :: padding ( s -- s ) padlen " " swap rep ; 20 | :: pad ( s -- s ) dup padding swap .. ; 21 | :: .str "\"%s\"" swap #( string.format 2 ) . ; 22 | :: sanitize ( s -- s ) 23 | "\n" "\\n" gsub drop ( count ) ; 24 | 25 | : clear depth 0 do drop loop ; 26 | : table? type "table" = ; 27 | : number? type "number" = ; 28 | 29 | : seq? ( tbl -- bool ) 30 | dup table? not if 31 | drop ( tbl ) 32 | false exit 33 | then 34 | 0 ( count ) 35 | over ( tbl ) pairs: key val 36 | key number? not if 37 | drop ( tbl ) drop ( count ) 38 | false exit 39 | then 40 | 1 + 41 | end 42 | swap ( tbl ) size = ; 43 | 44 | : inspect ( obj -- ) recursive 45 | dup type 46 | case 47 | "string" of .str endof 48 | "number" of . endof 49 | "table" of 50 | dup seq? if 51 | "[" . ipairs: i elem elem inspect end "]" . 52 | else 53 | "{" . pairs: key val key inspect val inspect end "}" . 54 | then 55 | endof 56 | drop >str . ( default ) 57 | endcase ; 58 | 59 | : .s ( -- ) 60 | depth 0 = if exit then 61 | line 62 | depth 0 do 63 | " |" . 64 | i pick >str sanitize trunc pad . 65 | "|" . cr 66 | line 67 | loop ; 68 | -------------------------------------------------------------------------------- /ext/tutorial.eqx: -------------------------------------------------------------------------------- 1 | 0 -> var counter 2 | var steps 3 | 4 | [ 5 | [ 6 | "Welcome to the Equinox Forth programming language tutorial." 7 | "Forth-based languages use postfix notation, so instead of writing '1 + 2' you need to use '1 2 +'." 8 | "" 9 | "Type:" 10 | " 1 2 +" 11 | "Then hit enter." 12 | ] 13 | [ 14 | "Forth uses a stack to pass parameters as well as to store return values." 15 | "When you type 1 2, both numbers are pushed onto the stack." 16 | "Then the word '+' pops those two numbers, adds them together, and leaves the result (3) on the stack." 17 | "You see a prompt OK(1), where the number 1 indicates the depth of the stack." 18 | "" 19 | "Type:" 20 | " ." 21 | "The dot (.) word will pop and print out the top of the stack, and you should see 3." 22 | ] 23 | [ 24 | "You can print out the stack anytime in the REPL by typing '.s'." 25 | "To make things easier, let's enable stack visualization after every command by typing 'stack-on'." 26 | "" 27 | "Type:" 28 | " stack-on" 29 | "" 30 | "You can type 'clear' anytime to remove all items from the stack." 31 | ] 32 | [ 33 | "Now let's type these numbers again, one by one and see how they're arranged on the stack." 34 | "" 35 | "Type:" 36 | " 1" 37 | " 2" 38 | "The number 2 is on top because it was pushed last, and 1 is below." 39 | "" 40 | "Type:" 41 | " +" 42 | "After hitting enter, you should see the result, 3 on the stack." 43 | "" 44 | "Type:" 45 | " ." 46 | ] 47 | [ 48 | "Words like '+' or '*' expect two items on the stack, and their order doesn't matter." 49 | "But the word '-' will subtract the top item from the one below. So, if you type 1 2 -, the result will be negative one." 50 | "There are words in Forth for rearranging the stack. One of the simplest ones is 'swap', which swaps the top two items." 51 | "" 52 | "Type:" 53 | " 1" 54 | " 2" 55 | " swap" 56 | " -" 57 | " ." 58 | "You should see 1 as the result." 59 | ] 60 | [ 61 | "'dup' is another stack manipulation word that makes a copy of the top of the stack." 62 | "" 63 | "Type:" 64 | " 3" 65 | " dup" 66 | " *" 67 | " ." 68 | "You should see 9 as the result." 69 | ] 70 | [ 71 | "'drop' is the opposite of dup, removing the top item from the stack." 72 | "'nip' is similar to drop, but it removes the second item from the top." 73 | "" 74 | "Type:" 75 | " 1" 76 | " 2" 77 | " nip" 78 | " drop" 79 | ] 80 | [ 81 | "'2dup' duplicates the top two items. It works the same as dup but treats two items as one." 82 | "" 83 | "Type:" 84 | " 1" 85 | " 2" 86 | " 2dup" 87 | " clear" 88 | ] 89 | [ 90 | "The colon (:) character is used to extend Forth's dictionary by defining new words." 91 | "Let's define a 'min' word to select the smaller of two numbers." 92 | "If the top of the stack is smaller, we need to nip the second item; otherwise, we need to drop the top." 93 | "Here is how you would write it in Forth." 94 | "" 95 | "Type:" 96 | " : min 2dup > if nip else drop then ;" 97 | "" 98 | "Pay attention to the leading colon (:) and ending semicolon (;), and keep a space between them and the next word." 99 | ] 100 | [ 101 | "You have successfully defined a new word called 'min'. To see all available words you can type 'words'." 102 | "Type:" 103 | " words" 104 | "" 105 | "Type:" 106 | " 5" 107 | " 2" 108 | " min" 109 | " ." 110 | "If you did it correctly, you will see 2." 111 | "" 112 | "Now you can try defining the word 'max' on your own to select the maximum between two numbers." 113 | ] 114 | [ 115 | "Let's get back to stack manipulation words. The 'rot' and '-rot' words rotate the first three items on the stack." 116 | "" 117 | "Type:" 118 | " 1 2 3" 119 | " rot" 120 | " clear" 121 | "You will see that 'rot' brings the third item to the top and shifts the rest." 122 | ] 123 | [ 124 | "The '-rot' works the opposite way by pushing the top item to the third place." 125 | "" 126 | "Type:" 127 | " 1 2 3" 128 | " -rot" 129 | " clear" 130 | "By the way, 'rot rot' is the same as '-rot'." 131 | ] 132 | [ 133 | "The 'over' word works similarly to 'dup', but instead of making a copy of the top item, it makes a copy of the second item." 134 | "1 2 over will result in 1 2 1." 135 | "" 136 | "'tuck' makes a copy of the top item and pushes it below the second item." 137 | "1 2 tuck will result in 2 1 2." 138 | "" 139 | "Many of these words are defined as combinations of others, like nip, which is the same as swap drop, and tuck, which is the same as swap over. 2dup is the same as over over." 140 | "" 141 | "Try these words out and observe how the stack changes." 142 | ] 143 | [ 144 | "Let's write a word for generating the Fibonacci sequence, where each number is the sum of the two preceding ones, starting from 0 and 1." 145 | "Example: 0, 1, 1, 2, 3, 5, 8, 13 ... In Forth, this is very easy thanks to the stack." 146 | "Let's put the first two items of the Fibonacci sequence onto the stack." 147 | "" 148 | "Type:" 149 | " clear" 150 | " 0 1" 151 | "Then use 2dup to make a copy of each." 152 | "" 153 | "Type:" 154 | " 2dup" 155 | "Then simply add these numbers together." 156 | "" 157 | "Type:" 158 | " +" 159 | "You should see 0 1 1. If you continue with '2dup +' you will keep getting the next number from the Fibonacci sequence." 160 | ] 161 | [ 162 | "We just need to put it into a loop and we're done." 163 | "" 164 | "Type:" 165 | " clear" 166 | " : fib 0 1 8 0 do 2dup + loop ;" 167 | " fib" 168 | "Here we use a 'do' loop that goes from 0 to 8-1, so it will be executed 8 times." 169 | ] 170 | [ 171 | "If we want to parameterize the number of iterations, you would need to do something like:" 172 | "" 173 | "Type:" 174 | " clear" 175 | " : fib ( n -- .. ) 0 1 rot ( param ) 0 do 2dup + loop ;" 176 | "I added a comment to the word indicating that it expects a number, as well as a 'rot' word in place of the hardcoded 8 to bring this parameter to the top before entering the loop." 177 | "" 178 | "Type:" 179 | " 10 fib" 180 | ] 181 | [ 182 | "'10 fib' generated 12 items, since we already started with two items on the stack." 183 | "You can check the size of the stack by" 184 | "" 185 | "Type:" 186 | " depth ." 187 | " clear" 188 | " depth ." 189 | ] 190 | [ 191 | "Equinox is a hosted language that doesn't have its own standard library but relies on Lua functions." 192 | "When calling a Lua function, you need to indicate its arity (number of parameters)." 193 | "" 194 | "Type:" 195 | " 2 8 #( math.pow 2 ) ." 196 | "" 197 | "This will print out 256." 198 | "The number 2 at the end instructs Equinox to compile code that consumes two parameters from the stack when calling the Lua function." 199 | "You can omit the final part when calling a function with no parameters." 200 | "" 201 | "Type:" 202 | " #( os.time ) ." 203 | ] 204 | [ 205 | "Some Lua functions don't return anything, or you deliberately want to ignore the return value." 206 | "In such cases, you can use the same synax with an additional parameter indicating the number of return values." 207 | "" 208 | "Type:" 209 | " \"Hello\" #( io.write 1 0 )" 210 | "" 211 | "You can define an alias to reduce verbosity using the following syntax:" 212 | "" 213 | "Type:" 214 | " alias: pow #( math.pow 2 )" 215 | " 2 10 pow ." 216 | ] 217 | [ "Congratulations, you finished the basic Equinox Forth tutorial." ] 218 | ] -> steps 219 | 220 | :: show-instructions ( -- ) 221 | "After you finished, type 'go' to go to the next tutorial step or 'back' to go back." . ; 222 | 223 | :: len ( -- ) steps size ; 224 | 225 | :: show-progress ( -- ) 226 | "Lesson %d/%d:" counter len #( string.format 3 ) . cr 227 | "============" . cr ; 228 | 229 | :: finished? counter len >= ; 230 | :: current ( -- [] ) steps counter @ ; 231 | 232 | :: show-lesson ( -- ) 233 | current ipairs: i line 234 | line . cr 235 | end ; 236 | 237 | :: show ( -- ) 238 | show-progress 239 | show-lesson cr 240 | finished? not if show-instructions cr then ; 241 | 242 | :: clamp ( -- ) 243 | counter 1 max -> counter 244 | counter len min -> counter ; 245 | 246 | :: step-by ( n -- ) 247 | counter swap + -> counter 248 | clamp ; 249 | 250 | : go ( -- ) 251 | 1 step-by 252 | show ; 253 | 254 | : back ( -- ) 255 | -1 step-by 256 | show ; 257 | 258 | go 259 | -------------------------------------------------------------------------------- /imgs/Nova001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroflag/equinox/66b9111597d7172a752198110d9d1b21ece1782f/imgs/Nova001.jpg -------------------------------------------------------------------------------- /imgs/fib.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroflag/equinox/66b9111597d7172a752198110d9d1b21ece1782f/imgs/fib.gif -------------------------------------------------------------------------------- /imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroflag/equinox/66b9111597d7172a752198110d9d1b21ece1782f/imgs/logo.png -------------------------------------------------------------------------------- /imgs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroflag/equinox/66b9111597d7172a752198110d9d1b21ece1782f/imgs/screenshot.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2025 Attila Magyar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /rockspec/equinox-template.rockspec: -------------------------------------------------------------------------------- 1 | package = "equinox" 2 | version = "0.0-0" 3 | 4 | source = { 5 | url = "git+https://github.com/zeroflag/equinox", 6 | tag = "v0.1-5" 7 | } 8 | 9 | description = { 10 | summary = "Forth Dialect", 11 | detailed = [[ 12 | Equinox is a Forth programming language that compiles to Lua. 13 | ]], 14 | homepage = "https://github.com/zeroflag/equinox", 15 | license = "MIT" 16 | } 17 | 18 | dependencies = { 19 | "lua >= 5.1", 20 | "linenoise >= 0.9" 21 | } 22 | 23 | build = { 24 | type = "builtin", 25 | modules = { 26 | ["equinox_bundle"] = "src/equinox_bundle.lua", 27 | }, 28 | install = { 29 | bin = { 30 | "equinox", 31 | }, 32 | }, 33 | copy_directories = { 34 | "ext" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ast.lua: -------------------------------------------------------------------------------- 1 | local ast = {} 2 | 3 | local id_counter = 1 4 | 5 | function ast.gen_id(prefix) 6 | id_counter = id_counter + 1 7 | return prefix .. id_counter 8 | end 9 | 10 | function ast.func_header(func_name, global) 11 | return { 12 | name = "func_header", 13 | func_name = func_name, 14 | params = {}, 15 | global = global 16 | } 17 | end 18 | 19 | function ast.end_func(func_name) 20 | return {name = "end_func", func_name = func_name} 21 | end 22 | 23 | function ast.func_call(func_name, ...) 24 | return { 25 | name = "func_call", 26 | func_name = func_name, 27 | args = {...} 28 | } 29 | end 30 | 31 | function ast.pop() 32 | return {name = "stack_consume", op = "pop"} 33 | end 34 | 35 | function ast.pop2nd() 36 | return {name = "stack_consume", op = "pop2nd"} 37 | end 38 | 39 | function ast.pop3rd() 40 | return {name = "stack_consume", op = "pop3rd"} 41 | end 42 | 43 | function ast.stack_op(operation) 44 | return {name = "stack_op", op = operation} 45 | end 46 | 47 | function ast.aux_op(operation) 48 | return {name = "aux_op", op = operation} 49 | end 50 | 51 | function ast.push(exp) 52 | return {name = "push", exp = exp} 53 | end 54 | 55 | function ast.push_many(func_call) 56 | return {name = "push_many", func_call = func_call} 57 | end 58 | 59 | function ast.aux_push(exp) 60 | return {name = "push_aux", exp = exp} 61 | end 62 | 63 | function ast._while(cond) 64 | return { 65 | name = "while", 66 | cond = cond 67 | } 68 | end 69 | 70 | function ast._until(cond) 71 | return { 72 | name = "until", 73 | cond = cond 74 | } 75 | end 76 | 77 | function ast.literal(kind, value) 78 | return { 79 | name = "literal", 80 | kind = kind, 81 | value = value 82 | } 83 | end 84 | 85 | function ast.str(val) 86 | return ast.literal("string", val) 87 | end 88 | 89 | function ast.bin_op(operator, param1, param2) 90 | return { 91 | name = "bin_op", 92 | op = operator, 93 | p1 = param1, 94 | p2 = param2 95 | } 96 | end 97 | 98 | function ast.unary_op(operator, operand) 99 | return {name = "unary_op", op = operator, exp = operand} 100 | end 101 | 102 | function ast.assignment(var, exp) 103 | return {name = "assignment", var = var, exp = exp} 104 | end 105 | 106 | function ast.init_local(var, exp) 107 | return {name = "init_local", var = var, exp = exp} 108 | end 109 | 110 | function ast.init_global(var, exp) 111 | return {name = "init_global", var = var, exp = exp} 112 | end 113 | 114 | function ast._if(cond, body) 115 | return {name = "if", exp = cond, body = body} 116 | end 117 | 118 | function ast.def_local(var) 119 | return {name = "local", var = var} 120 | end 121 | 122 | function ast.def_global(var) 123 | return {name = "global", var = var} 124 | end 125 | 126 | function ast.new_table() 127 | return {name = "table_new"} 128 | end 129 | 130 | function ast.table_at(tbl, key) 131 | return {name = "table_at", key = key, tbl = tbl} 132 | end 133 | 134 | function ast.table_put(tbl, key, value) 135 | return { 136 | name = "table_put", 137 | tbl = tbl, 138 | key = key, 139 | value = value 140 | } 141 | end 142 | 143 | function ast.keyword(keyword) 144 | return {name = "keyword", keyword = keyword} 145 | end 146 | 147 | function ast.identifier(id) 148 | return {name = "identifier", id = id} 149 | end 150 | 151 | function ast._return(opt_arg) 152 | return {name = "return", arg = opt_arg} 153 | end 154 | 155 | function ast._for(loop_var, start, stop, step) 156 | return { 157 | name = "for", 158 | loop_var = loop_var, 159 | start = start, 160 | stop = stop, 161 | step = step 162 | } 163 | end 164 | 165 | function ast._foreach(loop_var1, loop_var2, iterable) 166 | return { 167 | name = "for_each", 168 | loop_var1 = loop_var1, 169 | loop_var2 = loop_var2, 170 | iterable = iterable 171 | } 172 | end 173 | 174 | function ast._ipairs(iterable) 175 | return {name = "ipairs", iterable = iterable} 176 | end 177 | 178 | function ast._pairs(iterable) 179 | return {name = "pairs", iterable = iterable} 180 | end 181 | 182 | return ast 183 | -------------------------------------------------------------------------------- /src/ast_matchers.lua: -------------------------------------------------------------------------------- 1 | local asts = require("ast") 2 | 3 | local AstMatcher = {} 4 | 5 | local function OR(...) 6 | local fs = {...} 7 | return function(ast) 8 | local result = false 9 | for i, f in ipairs(fs) do 10 | result = result or f(ast) 11 | end 12 | return result 13 | end 14 | end 15 | 16 | local function AND(...) 17 | local fs = {...} 18 | return function(ast) 19 | local result = nil 20 | for i, f in ipairs(fs) do 21 | if result == nil then 22 | result = f(ast) 23 | else 24 | result = result and f(ast) 25 | end 26 | end 27 | return result 28 | end 29 | end 30 | 31 | local function NOT(f) 32 | return function(ast) 33 | return not f(ast) 34 | end 35 | end 36 | 37 | local function is(ast, name) return ast.name == name end 38 | local function any(ast) return ast ~= nil end 39 | 40 | local function has(name, matcher) 41 | return function (ast) 42 | return matcher(ast[name]) 43 | end 44 | end 45 | 46 | local function eq(expected) 47 | return function (val) 48 | return val == expected 49 | end 50 | end 51 | 52 | local function has_name(name) return has("name", eq(name)) end 53 | local function has_op(name) return has("op", eq(name)) end 54 | local function has_exp(matcher) return has("exp", matcher) end 55 | local function has_tbl(matcher) return has("tbl", matcher) end 56 | local function has_key(matcher) return has("key", matcher) end 57 | local function has_p1(matcher) return has("p1", matcher) end 58 | local function has_p2(matcher) return has("p2", matcher) end 59 | local function has_value(val) return has("value", eq(val)) end 60 | 61 | local is_identifier = has_name("identifier") 62 | local is_literal = has_name("literal") 63 | local is_stack_consume = has_name("stack_consume") 64 | local is_assignment = AND(has_name("assignment"), has_exp(is_stack_consume)) 65 | local is_if = AND(has_name("if"), has_exp(is_stack_consume)) 66 | local is_init_local = AND(has_name("init_local"), has_exp(is_stack_consume)) 67 | local is_push_binop = AND(has_name("push"), has_exp(has_name("bin_op"))) 68 | local is_push_unop = AND(has_name("push"), has_exp(has_name("unary_op"))) 69 | local is_pop = AND(is_stack_consume, has_op("pop")) 70 | 71 | local is_literal_tbl_at = AND( 72 | has_name("table_at"), 73 | AND( 74 | OR(has_tbl(is_identifier), has_tbl(is_literal)), 75 | OR(has_key(is_identifier), has_key(is_literal)))) 76 | 77 | local is_const = OR(is_identifier, is_literal, is_literal_tbl_at) 78 | local is_push_const = AND(has_name("push"), has_exp(is_const)) 79 | local is_push_unop_pop = AND(is_push_unop, has_exp(has_exp(is_stack_consume))) 80 | local is_stack_op = has_name("stack_op") 81 | local is_stack_peek = has_name("stack_peek") 82 | local is_dup = AND(is_stack_op, has_op("dup")) 83 | local is_2dup = AND(is_stack_op, has_op("dup2")) 84 | local is_over = AND(is_stack_op, has_op("over")) 85 | local is_tos = AND(is_stack_peek, has_op("tos")) 86 | local has_p1_pop = has_p1(has_op("pop")) 87 | local has_p2_pop = has_p2(has_op("pop")) 88 | local has_lit_value = AND(is_literal, has("value", eq(1))) 89 | 90 | local is_push_binop_pop = AND( 91 | has_name("push"), 92 | has_exp(AND( 93 | has_name("bin_op"), 94 | has_p1(is_stack_consume), 95 | has_p2(is_stack_consume)))) 96 | 97 | local is_exp_binop_pop = AND( 98 | has("exp", any), 99 | has_exp(AND( 100 | has_name("bin_op"), 101 | has_p1(is_stack_consume), 102 | has_p2(is_stack_consume)))) 103 | 104 | local is_wrapped_binop_tos = AND( 105 | has("exp", any), 106 | has_exp(AND( 107 | has_name("bin_op"), 108 | OR( 109 | AND(has_p1(is_tos)), 110 | AND(has_p2(is_tos), NOT(has_p1(is_stack_consume))))))) 111 | 112 | local is_inc = AND( 113 | has_name("push"), 114 | has_exp(AND( 115 | has_name("bin_op"), 116 | has_op("+"), 117 | OR( 118 | AND(has_p1_pop, has_p2(has_value(1))), 119 | AND(has_p2_pop, has_p1(has_value(1))))))) 120 | 121 | local is_neg = AND( 122 | has_name("push"), 123 | has_exp(AND( 124 | has_name("unary_op"), 125 | has_exp(is_pop), 126 | has_op("not")))) 127 | 128 | local is_wrapped_binop_free_operand = AND( 129 | has("exp", any), 130 | has_exp(AND( 131 | has_name("bin_op"), 132 | OR(has_p1_pop, 133 | has_p2_pop)))) 134 | 135 | local inlined_push_unop = AND( 136 | is_push_unop, 137 | NOT(has_exp(has_exp(is_stack_consume)))) 138 | 139 | local inlined_push_binop = AND( 140 | is_push_binop, 141 | OR( 142 | -- fully inlined 143 | AND(NOT(has_exp(has_p1(is_stack_consume)), NOT(has_exp(has_p2(is_stack_consume))))), 144 | -- partially inlined 145 | AND(has_exp(has_p1_pop), NOT(has_exp(has_p2(is_stack_consume)))), 146 | AND(has_exp(has_p2_pop), NOT(has_exp(has_p1(is_stack_consume)))))) 147 | 148 | local is_tbl_at = AND( 149 | has_name("push"), 150 | has_exp( 151 | AND( 152 | has_name("table_at"), 153 | has_tbl(is_stack_consume), 154 | has_key(is_stack_consume)))) 155 | 156 | local is_tbl_put = AND( 157 | has_name("table_put"), 158 | has_tbl(is_stack_consume), 159 | has_key(is_stack_consume)) 160 | 161 | function AstMatcher:new(name, parts) 162 | local obj = {name = name, parts = parts, logging = false} 163 | setmetatable(obj, self) 164 | self.__index = self 165 | return obj 166 | end 167 | 168 | function AstMatcher:matches(ast, start) 169 | for i, matcher in ipairs(self.parts) do 170 | if start + i -1 > #ast then return false end 171 | local node = ast[start + i -1] 172 | if not matcher(node) then 173 | return false 174 | end 175 | end 176 | return true 177 | end 178 | 179 | function AstMatcher:optimize(ast, i, result) 180 | error("not implemented") 181 | end 182 | 183 | function AstMatcher:log(message) 184 | if self.logging then 185 | print("[OPTI] " .. self.name .. ": " .. message) 186 | end 187 | end 188 | 189 | function AstMatcher:size() 190 | return #self.parts 191 | end 192 | 193 | AtParamsInline = AstMatcher:new() 194 | AtParamsInlineP2 = AstMatcher:new() 195 | PutParamsInline = AstMatcher:new() 196 | StackOpBinaryInline = AstMatcher:new() 197 | BinaryInline = AstMatcher:new() 198 | BinaryInlineP2 = AstMatcher:new() 199 | BinaryConstBinaryInline = AstMatcher:new() 200 | InlineGeneralUnary = AstMatcher:new() 201 | TosBinaryInline = AstMatcher:new() 202 | IncInline = AstMatcher:new() 203 | NegInline = AstMatcher:new() 204 | NegBinInline = AstMatcher:new() 205 | 206 | --[[ 207 | Inline table at parameters 208 | 1.) t 4 at => PUSH(t[4]) 209 | ]]-- 210 | function AtParamsInline:optimize(ast, i, result) 211 | self:log("inlining tbl at params") 212 | local tbl, idx, op = ast[i], ast[i + 1], ast[i + 2] 213 | op.exp.tbl = tbl.exp 214 | op.exp.key = idx.exp 215 | table.insert(result, op) 216 | end 217 | 218 | --[[ 219 | Inline table at index parameter 220 | 1.) ... 4 at => PUSH(POP[4]) 221 | ]]-- 222 | function AtParamsInlineP2:optimize(ast, i, result) 223 | self:log("inlining tbl at 2nd param") 224 | local tbl, idx, op = ast[i], ast[i + 1], ast[i + 2] 225 | if op.exp.tbl.name == "stack_consume" and 226 | op.exp.tbl.op == "pop2nd" 227 | then 228 | op.exp.tbl.op = "pop" 229 | end 230 | op.exp.key = idx.exp 231 | table.insert(result, tbl) 232 | table.insert(result, op) 233 | end 234 | 235 | --[[ 236 | Inline table put parameters 237 | 1.) t 4 "abc" put => t[4]="abc" 238 | ]]-- 239 | function PutParamsInline:optimize(ast, i, result) 240 | self:log("inlining tbl put params") 241 | local tbl, key, val, op = ast[i], ast[i + 1], ast[i + 2], ast[i + 3] 242 | op.tbl = tbl.exp 243 | op.key = key.exp 244 | op.value = val.exp 245 | table.insert(result, op) 246 | end 247 | 248 | --[[ 249 | Inline assignment/init-local(used internal) operator's value operand 250 | 251 | 1.) 123 -> v => v = 123 252 | 2.) false not => PUSH(not false) 253 | 3.) false not IF ... THEN => IF not false THEN ... END 254 | 4.) 10 v < IF ... THEN => IF 10 < v THEN ... END 255 | 5.) v IF ... THEN => IF v THEN ... END 256 | 6.) DUP IF .. THEN => TOS IF .. THEN 257 | 7.) OVER IF .. THEN => TOS2 IF .. THEN 258 | 8.) [ 1 2 3 ] DUP size => PUSH(#TOS) 259 | 9.) true false over not => PUSH(NOT TOS2) 260 | ]]-- 261 | function InlineGeneralUnary:optimize(ast, i, result) 262 | local p1, operator = ast[i], ast[i + 1] 263 | local target 264 | if is_push_unop_pop(operator) then 265 | -- unary is embedded into a push 266 | target = operator.exp 267 | else 268 | target = operator 269 | end 270 | 271 | if is_dup(p1) then 272 | self:log(operator.name .. " (dup)") 273 | target.exp.op = "tos" 274 | target.exp.name ="stack_peek" 275 | elseif is_over(p1) then 276 | self:log(operator.name .. " (over)") 277 | target.exp.op = "tos2" 278 | target.exp.name ="stack_peek" 279 | else 280 | self:log(operator.name) 281 | target.exp = p1.exp 282 | end 283 | 284 | table.insert(result, operator) 285 | end 286 | 287 | --[[ 288 | Inline DUP followed by binary operator 289 | 290 | 1.) 3 DUP * => PUSH(TOS * POP) 291 | 2.) DUP DUP * => PUSH(TOS * TOS) 292 | 3.) 3 7 OVER + => PUSH(TOS2 + POP) 293 | 4.) 3 7 OVER - => PUSH(POP - TOS) 294 | 5.) 1 2 2DUP + => PUSH(TOS + TOS) 295 | ]]-- 296 | function StackOpBinaryInline:optimize(ast, i, result) 297 | local p1, p2, op = ast[i], ast[i + 1], ast[i + 2] 298 | self:log("inlining stack op in binary") 299 | if is_dup(p1) and is_dup(p2) then 300 | -- double dup 301 | self:log("dup dup") 302 | op.exp.p1.op = "tos" 303 | op.exp.p2.op = "tos" 304 | op.exp.p1.name = "stack_peek" 305 | op.exp.p2.name = "stack_peek" 306 | table.insert(result, op) 307 | elseif is_2dup(p2) then 308 | self:log("2dup") 309 | op.exp.p1.op = "tos2" 310 | op.exp.p2.op = "tos" 311 | op.exp.p1.name = "stack_peek" 312 | op.exp.p2.name = "stack_peek" 313 | table.insert(result, p1) 314 | table.insert(result, op) 315 | elseif is_dup(p2) then 316 | -- single dup 317 | self:log("single dup") 318 | op.exp.p1.op = "tos" 319 | op.exp.p1.name = "stack_peek" 320 | table.insert(result, p1) 321 | table.insert(result, op) 322 | elseif is_over(p2) then 323 | -- single over 324 | self:log("over") 325 | op.exp.p1.op = "pop" 326 | op.exp.p1.name = "stack_consume" 327 | op.exp.p2.op = "tos" 328 | op.exp.p2.name = "stack_peek" 329 | table.insert(result, p1) 330 | table.insert(result, op) 331 | else 332 | error("Unexpected p2: " .. tostring(p2.name)) 333 | end 334 | end 335 | 336 | --[[ 337 | Inline binary operator's ALL constant operands 338 | 339 | 1.) 12 45 + => PUSH(12 + 45) 340 | 2.) 12 v1 + => PUSH(12 + v1) 341 | 3.) t 1 at 45 + => PUSH(t[1] + 45) 342 | ]]-- 343 | function BinaryInline:optimize(ast, i, result) 344 | self:log("inlining binary operator params") 345 | local p1, p2, op = ast[i], ast[i + 1], ast[i + 2] 346 | op.exp.p1 = p1.exp 347 | op.exp.p2 = p2.exp 348 | table.insert(result, op) 349 | end 350 | 351 | --[[ 352 | Inline binary operator's 2nd, constant operand 353 | Additonally replace DUP with DROP if applicable 354 | 355 | 1.) 123 + => PUSH(POP + 123) 356 | 2.) DUP 123 + => PUSH(TOS + 123) 357 | ]]-- 358 | function BinaryInlineP2:optimize(ast, i, result) 359 | self:log("inlining binary operator's 2nd param") 360 | local p1, p2, op = ast[i], ast[i + 1], ast[i + 2] 361 | if op.exp.p1.name == "stack_consume" then 362 | if op.exp.p1.op == "pop2nd" then 363 | op.exp.p1.op = "pop" 364 | end 365 | if is_dup(p1) then 366 | op.exp.p1.op = "tos" -- inline if dup 367 | op.exp.p1.name = "stack_peek" 368 | end 369 | end 370 | op.exp.p2 = p2.exp -- inline const param 371 | if not is_dup(p1) then -- dup was inlined skip it 372 | table.insert(result, p1) 373 | end 374 | table.insert(result, op) 375 | end 376 | 377 | function BinaryConstBinaryInline:optimize(ast, i, result) 378 | self:log("inlining binary to binary operator") 379 | local bin, op = ast[i], ast[i + 1] 380 | local target = op.exp 381 | if target.p1.op == "pop" then 382 | target.p1 = bin.exp 383 | if target.p2.op == "tos" then 384 | target.p2 = bin.exp 385 | elseif target.p2.op == "pop2nd" then 386 | target.p2.op = "pop" 387 | end 388 | elseif target.p2.op == "pop" then 389 | target.p2 = bin.exp 390 | if target.p1.op == "tos" then 391 | target.p1 = bin.exp 392 | elseif target.p1.op == "pop2nd" then 393 | target.p1.op = "pop" 394 | end 395 | else -- shouldn't happen 396 | error("one of binary operator's param was expected to be stack_consume") 397 | end 398 | table.insert(result, op) 399 | end 400 | 401 | function TosBinaryInline:optimize(ast, i, result) 402 | self:log("inlining binary tos operand") 403 | local cons, op = ast[i], ast[i + 1] 404 | if op.exp.p1.op == "tos" then 405 | op.exp.p1 = cons.exp 406 | end 407 | if op.exp.p2.op == "tos" then 408 | op.exp.p2 = cons.exp 409 | end 410 | table.insert(result, cons) 411 | table.insert(result, op) 412 | end 413 | 414 | function IncInline:optimize(ast, i, result) 415 | self:log("inlining inc") 416 | table.insert(result, asts.stack_op("_inc")) 417 | end 418 | 419 | function NegInline:optimize(ast, i, result) 420 | self:log("inlining neg") 421 | table.insert(result, asts.stack_op("_neg")) 422 | end 423 | 424 | function NegBinInline:optimize(ast, i, result) 425 | self:log("inlining neg binary") 426 | local op, neg = ast[i], ast[i + 1] 427 | table.insert(result, asts.push(asts.unary_op("not", op.exp))) 428 | end 429 | 430 | return { 431 | 432 | PutParamsInline:new( 433 | "inline put params ", 434 | {is_push_const, is_push_const, is_push_const, is_tbl_put}), 435 | 436 | AtParamsInline:new( 437 | "inline at params", 438 | {is_push_const, is_push_const, is_tbl_at}), 439 | 440 | AtParamsInlineP2:new( 441 | "inline at p2", 442 | {NOT(is_push_const), is_push_const, is_tbl_at}), 443 | 444 | BinaryInline:new( 445 | "binary inline", 446 | {is_push_const, is_push_const, is_exp_binop_pop}), 447 | 448 | BinaryInlineP2:new( 449 | "binary p2 inline", 450 | {NOT(is_push_const), is_push_const, is_exp_binop_pop}), 451 | 452 | BinaryConstBinaryInline:new( 453 | "binary const binary inline", 454 | {OR(inlined_push_binop, 455 | inlined_push_unop), 456 | is_wrapped_binop_free_operand}), 457 | 458 | StackOpBinaryInline:new( 459 | "stackop binary inline", 460 | {any, OR(is_dup, 461 | is_2dup, 462 | is_over), is_push_binop_pop}), 463 | 464 | TosBinaryInline:new( 465 | "tos binary inline", 466 | {is_push_const, is_wrapped_binop_tos}), 467 | 468 | InlineGeneralUnary:new( 469 | "inline general unary", 470 | {OR(is_dup, 471 | is_over, 472 | is_push_const, 473 | is_push_unop, 474 | is_push_binop), 475 | OR(is_init_local, -- init-local only optimizes one parameter 476 | is_assignment, 477 | is_if, 478 | is_push_unop_pop)}), 479 | 480 | IncInline:new("inline inc", {is_inc}), 481 | NegInline:new("inline neg", {is_neg}), 482 | 483 | NegBinInline:new( 484 | "inline negate binary", 485 | {is_push_binop, AND(is_stack_op, has_op("_neg"))}), 486 | } 487 | -------------------------------------------------------------------------------- /src/ast_optimizer.lua: -------------------------------------------------------------------------------- 1 | local Optimizer = {} 2 | local matchers = require("ast_matchers") 3 | 4 | function Optimizer:new() 5 | local obj = {logging = false, enabled = true} 6 | setmetatable(obj, {__index = self}) 7 | return obj 8 | end 9 | 10 | function Optimizer:enable_logging(bool) 11 | self.logging = bool 12 | end 13 | 14 | function Optimizer:enable(bool) 15 | self.enabled = bool 16 | end 17 | 18 | function Optimizer:log_ast(node) 19 | --require("tests/json") 20 | --self:log("AST: " .. to_json_str(node)) 21 | end 22 | 23 | function Optimizer:log(txt) 24 | if self.logging then 25 | print("[OPTI] " .. txt) 26 | end 27 | end 28 | 29 | function Optimizer:optimize_iteratively(ast) 30 | if not self.enabled then return ast end 31 | local num_of_optimizations, iterations = 0, 0 32 | repeat 33 | ast, num_of_optimizations = self:optimize(ast) 34 | iterations = iterations + 1 35 | self:log(string.format( 36 | "Iteration: %d finished. Number of optimizations: %d", 37 | iterations, num_of_optimizations)) 38 | if iterations > 100 then 39 | print("Aborting optimizer. This is likely a bug.") 40 | break 41 | end 42 | until num_of_optimizations == 0 43 | return ast 44 | end 45 | 46 | function Optimizer:optimize(ast) 47 | local result, i, num_matches = {}, 1, 0 48 | while i <= #ast do 49 | local node = ast[i] 50 | self:log_ast(node) 51 | local found = false 52 | for _, matcher in ipairs(matchers) do 53 | if matcher:matches(ast, i) then 54 | matcher.logging = self.logging 55 | matcher:optimize(ast, i, result) 56 | i = i + matcher:size() 57 | num_matches = num_matches + 1 58 | found = true 59 | end 60 | end 61 | if not found then 62 | table.insert(result, node) 63 | i = i + 1 64 | end 65 | end 66 | return result, num_matches 67 | end 68 | 69 | return Optimizer 70 | -------------------------------------------------------------------------------- /src/codegen.lua: -------------------------------------------------------------------------------- 1 | local CodeGen = {} 2 | 3 | function CodeGen:new() 4 | local obj = {} 5 | setmetatable(obj, {__index = self}) 6 | return obj 7 | end 8 | 9 | local function lit_bin_op(ast) 10 | return ast.name == "bin_op" 11 | and ast.p1.name == "literal" 12 | and ast.p2.name == "literal" 13 | end 14 | 15 | local function lit_unary_op(ast) 16 | return ast.name == "unary_op" and ast.exp.name == "literal" 17 | end 18 | 19 | local function inline_push(exp) 20 | return "stack[#stack +1] = " .. exp 21 | end 22 | 23 | function CodeGen:gen(ast) 24 | if "stack_op" == ast.name 25 | or "stack_consume" == ast.name 26 | or "stack_peek" == ast.name 27 | then 28 | return ast.op .. "()" 29 | end 30 | if "aux_op" == ast.name then 31 | return "a" .. ast.op .. "()" 32 | end 33 | if "push" == ast.name then 34 | if ast.exp.name == "literal" or 35 | lit_bin_op(ast.exp) or 36 | lit_unary_op(ast.exp) 37 | then 38 | return inline_push(self:gen(ast.exp)) -- bypass NIL check 39 | else 40 | return string.format("push(%s)", self:gen(ast.exp)) 41 | end 42 | end 43 | if "push_many" == ast.name then 44 | return string.format("push_many(%s)", self:gen(ast.func_call)) 45 | end 46 | if "push_aux" == ast.name then 47 | return string.format("apush(%s)", self:gen(ast.exp)) 48 | end 49 | if "unary_op" == ast.name then 50 | return string.format("%s %s", ast.op, self:gen(ast.exp)) 51 | end 52 | if "bin_op" == ast.name then 53 | return string.format( 54 | "(%s %s %s)", self:gen(ast.p1), ast.op, self:gen(ast.p2)) 55 | end 56 | if "local" == ast.name then 57 | return "local " .. ast.var 58 | end 59 | if "global" == ast.name then 60 | return ast.var .. "=nil" 61 | end 62 | if "init_local" == ast.name then 63 | return "local " .. ast.var .. "=" .. self:gen(ast.exp) 64 | end 65 | if "init_global" == ast.name then 66 | return ast.var .. "=" .. self:gen(ast.exp) 67 | end 68 | if "assignment" == ast.name then 69 | return ast.var .. " = " .. self:gen(ast.exp) 70 | end 71 | if "literal" == ast.name and "boolean" == ast.kind then 72 | return ast.value 73 | end 74 | if "literal" == ast.name and "string" == ast.kind then 75 | return '"' .. ast.value .. '"' 76 | end 77 | if "literal" == ast.name and "number" == ast.kind then 78 | return ast.value 79 | end 80 | if "while" == ast.name then 81 | return string.format("while (%s) do", self:gen(ast.cond)) 82 | end 83 | if "until" == ast.name then 84 | return string.format("until %s", self:gen(ast.cond)) 85 | end 86 | if "for" == ast.name and not ast.step then 87 | return string.format( 88 | "for %s=%s,%s do", 89 | ast.loop_var, self:gen(ast.start), self:gen(ast.stop)) 90 | end 91 | if "for" == ast.name and ast.step then 92 | return string.format( 93 | "for %s=%s,%s,%s do", 94 | ast.loop_var, 95 | self:gen(ast.start), 96 | self:gen(ast.stop), 97 | self:gen(ast.step)) 98 | end 99 | if "for_each" == ast.name then 100 | if ast.loop_var1 and ast.loop_var2 then 101 | return string.format( 102 | "for %s,%s in %s do", 103 | ast.loop_var1, ast.loop_var2, self:gen(ast.iterable)) 104 | else 105 | return string.format( 106 | "for %s in %s do", 107 | ast.loop_var1, self:gen(ast.iterable)) 108 | end 109 | end 110 | if "pairs" == ast.name then 111 | return string.format("pairs(%s)", self:gen(ast.iterable)) 112 | end 113 | if "ipairs" == ast.name then 114 | return string.format("ipairs(%s)", self:gen(ast.iterable)) 115 | end 116 | if "if" == ast.name then 117 | if ast.body then 118 | return "if " .. self:gen(ast.exp) .. " then " .. self:gen(ast.body) .. " end" 119 | else 120 | return "if " .. self:gen(ast.exp) .. " then" 121 | end 122 | end 123 | if "return" == ast.name then 124 | if ast.arg then 125 | return "return " .. self:gen(ast.arg) 126 | else 127 | return "do return end" 128 | end 129 | end 130 | if "keyword" == ast.name then return ast.keyword end 131 | if "identifier" == ast.name then return ast.id end 132 | if "table_new" == ast.name then return inline_push("{}") end 133 | if "table_at" == ast.name then 134 | return string.format("%s[%s]", 135 | self:gen(ast.tbl), self:gen(ast.key)) 136 | end 137 | if "table_put" == ast.name then 138 | return string.format( 139 | "%s[%s]=%s", 140 | self:gen(ast.tbl), self:gen(ast.key), self:gen(ast.value)) 141 | end 142 | if "func_call" == ast.name then 143 | local params = "" 144 | for i, p in ipairs(ast.args) do 145 | params = params .. self:gen(p) 146 | if i < #ast.args then 147 | params = params .. "," 148 | end 149 | end 150 | return string.format("%s(%s)", ast.func_name, params) 151 | end 152 | if "func_header" == ast.name then 153 | local prefix = "" 154 | if not ast.global then prefix = "local " end 155 | local result = string.format("%sfunction %s(", prefix, ast.func_name) 156 | for i, p in ipairs(ast.params) do 157 | result = result .. p 158 | if i < #ast.params then result = result .. "," end 159 | end 160 | return result .. ")" 161 | end 162 | if "end_func" == ast.name then 163 | return "end" 164 | end 165 | error("Unknown AST: " .. tostring(ast) .. 166 | " with name: " .. tostring(ast.name)) 167 | end 168 | 169 | return CodeGen 170 | -------------------------------------------------------------------------------- /src/compiler.lua: -------------------------------------------------------------------------------- 1 | local macros = require("macros") 2 | local Dict = require("dict") 3 | local Parser = require("parser") 4 | local LineMapping = require("line_mapping") 5 | local Output = require("output") 6 | local Source = require("source") 7 | local Env = require("env") 8 | local interop = require("interop") 9 | local ast = require("ast") 10 | local utils = require("utils") 11 | 12 | local Compiler = {} 13 | local marker = "<>") 47 | self.output:append( 48 | "local stack, aux = require(\"stack\"), require(\"stack_aux\")") 49 | self.output:new_line() 50 | self.ast = {} 51 | self.code_start = self.output:size() 52 | if self.source.type == "chunk" then 53 | self.chunks[self.source.name] = self.source 54 | end 55 | end 56 | 57 | function Compiler:new_env(name) 58 | self.env = Env:new(self.env, name) 59 | end 60 | 61 | function Compiler:remove_env(name, item) 62 | if name and self.env.name ~= name then 63 | self:err("Incorrect nesting: " .. name, item) 64 | end 65 | if self.env.parent then 66 | self.env = self.env.parent 67 | else 68 | error("cannot drop root environment") 69 | end 70 | end 71 | 72 | function Compiler:def_var(name) 73 | return self.env:def_var(name) 74 | end 75 | 76 | function Compiler:def_global(name) 77 | return self.root_env:def_var(name) 78 | end 79 | 80 | function Compiler:has_var(name) 81 | return self.env:has_var(name) 82 | end 83 | 84 | function Compiler:find_var(name) 85 | return self.env:find_var(name) 86 | end 87 | 88 | function Compiler:var_names() 89 | return self.env:var_names() 90 | end 91 | 92 | function Compiler:word() 93 | local item = self:next_item() 94 | if item then 95 | return item.token 96 | else 97 | return nil 98 | end 99 | end 100 | 101 | function Compiler:next_item() 102 | return self.parser:next_item() 103 | end 104 | 105 | function Compiler:find(forth_name) 106 | return self.dict:find(forth_name) 107 | end 108 | 109 | function Compiler:reveal(lua_name) 110 | self.dict:reveal(lua_name) 111 | end 112 | 113 | function Compiler:next_chr() 114 | return self.parser:next_chr() 115 | end 116 | 117 | function Compiler:peek_chr() 118 | return self.parser:peek_chr() 119 | end 120 | 121 | function Compiler:word_list() 122 | return self.dict:word_list() 123 | end 124 | 125 | function Compiler:alias(lua_name, forth_alias) 126 | return self.dict:def_lua_alias(lua_name, forth_alias) 127 | end 128 | 129 | function Compiler:def_word(alias, name, immediate, hidden) 130 | self.dict:def_word(alias, name, immediate, hidden) 131 | end 132 | 133 | function Compiler:err(message, item) 134 | self.source:show_lines(item.line_number, 1) 135 | self:reset_state() 136 | error(message .. " at line: " .. item.line_number) 137 | end 138 | 139 | function Compiler:warn(message, item) 140 | print("[WARN] " .. message .. " at line: " .. item.line_number) 141 | end 142 | 143 | function Compiler:exec_macro(item) 144 | local mod, fun = self.dict:find(item.token).lua_name:match("^(.-)%.(.+)$") 145 | if mod == "macros" and type(macros[fun]) == "function" then 146 | return macros[fun](self, item) 147 | else 148 | self:err("Unknown macro: " .. item.token, item) 149 | end 150 | end 151 | 152 | function Compiler:add_ast_nodes(nodes, item) 153 | if #nodes > 0 then 154 | for i, each in ipairs(nodes) do 155 | self:add_ast_nodes(each, item) 156 | end 157 | else 158 | nodes.forth_line_number = item.line_number 159 | table.insert(self.ast, nodes) -- single node 160 | end 161 | end 162 | 163 | function Compiler:valid_ref(name) 164 | return self.env:has_var(name) or interop.resolve_lua_obj(name) 165 | end 166 | 167 | function Compiler:compile_token(item) 168 | if item.kind == "symbol" then 169 | return ast.push(ast.literal("string", item.token:sub(2))) 170 | end 171 | if item.kind == "number" then 172 | return ast.push(ast.literal(item.kind, tonumber(item.token))) 173 | end 174 | if item.kind == "string" then 175 | return ast.push(ast.literal(item.kind, item.token:sub(2, -2))) 176 | end 177 | if item.kind == "word" then 178 | local word = self.dict:find(item.token) 179 | if word and word.immediate then 180 | return self:exec_macro(item) 181 | end 182 | if word and word.is_lua_alias then 183 | -- Prevent optimizer to overwrite original definition 184 | return utils.deepcopy(word.lua_name) 185 | end 186 | if self.env:has_var(item.token) then -- Forth variable 187 | return ast.push( 188 | ast.identifier(self.env:find_var(item.token).lua_name)) 189 | end 190 | if word then -- Regular Forth word 191 | return ast.func_call(word.lua_name) 192 | end 193 | if interop.dot_or_colon_notation(item.token) then 194 | -- Table lookup: math.pi or tbl.key or method call a:b a:b.c 195 | local parts = interop.explode(item.token) 196 | local name = parts[1] 197 | if self:valid_ref(name) then 198 | if self.env:has_var(name) then 199 | parts[1] = self.env:find_var(name).lua_name 200 | end 201 | -- This can result multiple values, like img:getDimensions, 202 | -- a single value like tbl.key or str:upper, or void like img:draw 203 | if interop.is_lua_prop_lookup(item.token) then 204 | return ast.push(ast.identifier(interop.join(parts))) 205 | else 206 | return ast.push_many(ast.identifier(interop.join(parts))) 207 | end 208 | else 209 | self:err("Unkown variable: " .. name .. " in expression: " .. item.token, item) 210 | end 211 | end 212 | if interop.resolve_lua_obj(item.token) then 213 | -- Lua globals from _G, such as math, table, io 214 | return ast.push(ast.identifier(item.token)) 215 | end 216 | end 217 | self:err("Unknown token: " .. item.token .. " kind: " .. item.kind, item) 218 | end 219 | 220 | function Compiler:compile(source) 221 | self:init(source) 222 | local item = self.parser:next_item() 223 | while item do 224 | local node = self:compile_token(item) 225 | if node then self:add_ast_nodes(node, item) end 226 | item = self.parser:next_item() 227 | end 228 | self.ast = self.optimizer:optimize_iteratively(self.ast) 229 | return self:generate_code() 230 | end 231 | 232 | function Compiler:generate_code() 233 | for i, ast in ipairs(self.ast) do 234 | local code = self.codegen:gen(ast) 235 | if ast.forth_line_number then 236 | self.line_mapping:map_target_to_source( 237 | self.source.name, 238 | ast.forth_line_number, 239 | self.output.line_number) 240 | end 241 | if ast.name == "func_header" then 242 | local word = self.dict:find_by_lua_name(ast.func_name) 243 | -- methods are not stored in dict 244 | if word then word.line_number = self.output.line_number end 245 | end 246 | self.output:append(code) 247 | self.output:new_line() 248 | if ast.name == "end_func" then 249 | local word = self.dict:find_by_lua_name(ast.func_name) 250 | if word then -- methods are not stored in dict 251 | word.code = string.gsub( 252 | self.output:text(word.line_number), "[\n\r]+$", "") 253 | end 254 | end 255 | end 256 | return self.output 257 | end 258 | 259 | function Compiler:traceback(err) 260 | local info 261 | local file = "N/A" 262 | for level = 1, math.huge do 263 | info = debug.getinfo(level, "Sl") 264 | if not info then 265 | break 266 | end 267 | if info.source and 268 | info.currentline > 0 and 269 | info.source:sub(1, #marker) == marker 270 | then 271 | file = info.source:match(marker .. "(.-)>>") 272 | local src_line_num = 273 | self.line_mapping:resolve_target(file, info.currentline) 274 | if src_line_num then 275 | io.stderr:write(string.format( 276 | " File \"%s\", line %d (%d)\n", file, src_line_num, info.currentline)) 277 | if utils.exists(file) then 278 | Source:from_file(file):show_lines(src_line_num, 1) 279 | elseif self.chunks[file] then 280 | self.chunks[file]:show_lines(src_line_num, 1) 281 | end 282 | end 283 | end 284 | end 285 | return err 286 | end 287 | 288 | function Compiler:eval_file(path, log_result) 289 | return self:_eval(Source:from_file(path), log_result) 290 | end 291 | 292 | function Compiler:eval_text(text, log_result) 293 | return self:_eval(Source:from_text(text), log_result) 294 | end 295 | 296 | function Compiler:compile_and_load(source, log_result) -- used by REPL for multiline 297 | local out = self:compile(source) 298 | if log_result then 299 | io.write(self.output:text(self.code_start)) 300 | end 301 | return out:load() 302 | end 303 | 304 | function Compiler:_eval(source, log_result) 305 | local code, err = self:compile_and_load(source, log_result, path) 306 | if err then 307 | self:traceback(err) -- error during load 308 | error(err) 309 | end 310 | local success, result = xpcall(code, function(e) return self:traceback(e) end) 311 | if success then 312 | return result 313 | else 314 | error(result) -- error during execute 315 | end 316 | end 317 | 318 | return Compiler 319 | -------------------------------------------------------------------------------- /src/console.lua: -------------------------------------------------------------------------------- 1 | local console = {} 2 | 3 | local is_windows = (os.getenv("OS") and string.find(os.getenv("OS"), "Windows")) 4 | or package.config:sub(1,1) == '\\' 5 | 6 | console.RED = "\27[91m" 7 | console.GREEN = "\27[92m" 8 | console.CYAN = "\27[1;96m" 9 | console.PURPLE = "\27[1;95m" 10 | console.RESET = "\27[0m" 11 | 12 | function console.message(text, color, no_cr) 13 | if no_cr then 14 | new_line = "" 15 | else 16 | new_line = "\n" 17 | end 18 | if is_windows then 19 | color = "" 20 | reset = "" 21 | else 22 | reset = console.RESET 23 | end 24 | io.write(string.format("%s%s%s%s", color, text, reset, new_line)) 25 | end 26 | 27 | function console.colorize(text, color) 28 | if is_windows then 29 | return text 30 | else 31 | return string.format("%s%s%s", color, text, console.RESET) 32 | end 33 | end 34 | 35 | return console 36 | -------------------------------------------------------------------------------- /src/dict.lua: -------------------------------------------------------------------------------- 1 | local interop = require("interop") 2 | 3 | local Dict = {} 4 | 5 | function Dict:new() 6 | local obj = {words = {}} 7 | setmetatable(obj, {__index = self}) 8 | obj:init() 9 | return obj 10 | end 11 | 12 | local function entry(forth_name, lua_name, immediate, is_lua_alias, hidden) 13 | return { 14 | forth_name = forth_name, 15 | lua_name = lua_name, 16 | immediate = immediate, 17 | is_lua_alias = is_lua_alias, 18 | hidden = hidden, 19 | line_number = nil 20 | } 21 | end 22 | 23 | function Dict:def_word(forth_name, lua_name, immediate, hidden) 24 | table.insert(self.words, 25 | entry(forth_name, lua_name, immediate, false, hidden)) 26 | end 27 | 28 | function Dict:def_macro(forth_name, lua_name) 29 | self:def_word(forth_name, lua_name, true, false) 30 | end 31 | 32 | function Dict:def_lua_alias(lua_name, forth_name) 33 | table.insert(self.words, 34 | entry(forth_name, lua_name, immediate, true, false)) 35 | end 36 | 37 | function Dict:find(name) 38 | return self:find_by( 39 | function (item) 40 | return item.forth_name == name 41 | end) 42 | end 43 | 44 | function Dict:find_by_lua_name(name) 45 | return self:find_by( 46 | function (item) 47 | return item.lua_name == name 48 | end) 49 | end 50 | 51 | function Dict:find_by(pred) 52 | for i = #self.words, 1, -1 do 53 | local each = self.words[i] 54 | if not each.hidden and pred(each) then 55 | return each 56 | end 57 | end 58 | return nil 59 | end 60 | 61 | function Dict:reveal(name) 62 | for i = #self.words, 1, -1 do 63 | local each = self.words[i] 64 | if each.lua_name == name then 65 | each.hidden = false 66 | return 67 | end 68 | end 69 | end 70 | 71 | function Dict:word_list() 72 | local result, seen = {}, {} 73 | for i, each in ipairs(self.words) do 74 | if not seen[each.forth_name] and 75 | not each.hidden 76 | then 77 | if each.is_lua_alias or 78 | each.immediate or 79 | interop.resolve_lua_func(each.lua_name) 80 | then 81 | table.insert(result, each.forth_name) 82 | end 83 | seen[each.forth_name] = true 84 | end 85 | end 86 | return result 87 | end 88 | 89 | function Dict:init() 90 | self:def_macro("+", "macros.add") 91 | self:def_macro("-", "macros.sub") 92 | self:def_macro("*", "macros.mul") 93 | self:def_macro("/", "macros.div") 94 | self:def_macro("%", "macros.mod") 95 | self:def_macro(".", "macros.dot") 96 | self:def_macro("cr", "macros.cr") 97 | self:def_macro("=", "macros.eq") 98 | self:def_macro("!=", "macros.neq") 99 | self:def_macro(">", "macros.lt") 100 | self:def_macro(">=", "macros.lte") 101 | self:def_macro("<", "macros.gt") 102 | self:def_macro("<=", "macros.gte") 103 | self:def_macro("swap", "macros.swap") 104 | self:def_macro("over", "macros.over") 105 | self:def_macro("rot", "macros.rot") 106 | self:def_macro("-rot", "macros.mrot") 107 | self:def_macro("nip", "macros.nip") 108 | self:def_macro("drop", "macros.drop") 109 | self:def_macro("dup", "macros.dup") 110 | self:def_macro("2dup", "macros.dup2") 111 | self:def_macro("tuck", "macros.tuck") 112 | self:def_macro("depth", "macros.depth") 113 | self:def_macro("pick", "macros.pick") 114 | self:def_macro("roll", "macros.roll") 115 | self:def_macro("adepth", "macros.adepth") 116 | self:def_macro("not", "macros._not") 117 | self:def_macro("and", "macros._and") 118 | self:def_macro("or", "macros._or") 119 | self:def_macro("..", "macros.concat") 120 | self:def_macro(">a", "macros.to_aux") 121 | self:def_macro("a>", "macros.from_aux") 122 | self:def_macro("{}", "macros.new_table") 123 | self:def_macro("[]", "macros.new_table") 124 | self:def_macro("size", "macros.table_size") 125 | self:def_macro("@", "macros.table_at") 126 | self:def_macro("!", "macros.table_put") 127 | self:def_macro("words", "macros.words") 128 | self:def_macro("exit", "macros._exit") 129 | self:def_macro("return", "macros.ret") 130 | self:def_macro("if", "macros._if") 131 | self:def_macro("then", "macros._then") 132 | self:def_macro("else", "macros._else") 133 | self:def_macro("begin", "macros._begin") 134 | self:def_macro("until", "macros._until") 135 | self:def_macro("while", "macros._while") 136 | self:def_macro("repeat", "macros._repeat") 137 | self:def_macro("again", "macros._again") 138 | self:def_macro("case", "macros._case") 139 | self:def_macro("of", "macros._of") 140 | self:def_macro("endof", "macros._endof") 141 | self:def_macro("endcase", "macros._endcase") 142 | self:def_macro("do", "macros._do") 143 | self:def_macro("loop", "macros._loop") 144 | self:def_macro("ipairs:", "macros.for_ipairs") 145 | self:def_macro("pairs:", "macros.for_pairs") 146 | self:def_macro("iter:", "macros.for_each") 147 | self:def_macro("to:", "macros._to") 148 | self:def_macro("step:", "macros._step") 149 | self:def_macro("#(", "macros.arity_call_lua") 150 | self:def_macro("->", "macros.assignment") 151 | self:def_macro("var", "macros.var") 152 | self:def_macro("global", "macros.var_global") 153 | self:def_macro("(", "macros.comment") 154 | self:def_macro("\\", "macros.single_line_comment") 155 | self:def_macro("alias:", "macros.def_alias") 156 | self:def_macro(":", "macros.colon") 157 | self:def_macro("::", "macros.local_colon") 158 | self:def_macro(";", "macros.end_word") 159 | self:def_macro("recursive", "macros.reveal") 160 | self:def_macro("exec", "macros.exec") 161 | self:def_macro("'", "macros.tick") 162 | self:def_macro("$", "macros.keyval") 163 | self:def_macro("(:", "macros.formal_params") 164 | self:def_macro("block", "macros.block") 165 | self:def_macro("end", "macros._end") 166 | self:def_macro("see", "macros.see") 167 | end 168 | 169 | return Dict 170 | -------------------------------------------------------------------------------- /src/env.lua: -------------------------------------------------------------------------------- 1 | local interop = require("interop") 2 | 3 | local Env = {} 4 | 5 | function Env:new(parent, name) 6 | local obj = {parent = parent, 7 | name = name, 8 | vars = {}} 9 | setmetatable(obj, {__index = self}) 10 | return obj 11 | end 12 | 13 | function Env:def_var_unsafe(forth_name, lua_name) 14 | table.insert(self.vars, {forth_name = forth_name, 15 | lua_name = lua_name}) 16 | end 17 | 18 | function Env:def_var(name) 19 | local lua_name = interop.sanitize(name) 20 | self:def_var_unsafe(name, lua_name) 21 | return lua_name 22 | end 23 | 24 | function Env:has_var(forth_name) 25 | return self:find_var(forth_name) ~= nil 26 | end 27 | 28 | function Env:find_var(forth_name) 29 | for i, each in ipairs(self.vars) do 30 | if each.forth_name == forth_name then 31 | return each 32 | end 33 | end 34 | return self.parent and self.parent:find_var(forth_name) 35 | end 36 | 37 | function Env:var_names() 38 | local result 39 | if not self.parent then 40 | result = {} 41 | else 42 | result = self.parent:var_names() 43 | end 44 | for _, each in ipairs(self.vars) do 45 | table.insert(result, each.forth_name) 46 | end 47 | return result 48 | end 49 | 50 | return Env 51 | -------------------------------------------------------------------------------- /src/equinox.lua: -------------------------------------------------------------------------------- 1 | __VERSION__=nil 2 | 3 | local Compiler = require("compiler") 4 | local Optimizer = require("ast_optimizer") 5 | local CodeGen = require("codegen") 6 | local Repl = require("repl") 7 | 8 | local equinox = {} 9 | local optimizer = Optimizer:new() 10 | local compiler = Compiler:new(optimizer, CodeGen:new()) 11 | local repl = Repl:new(compiler, optimizer) 12 | 13 | local lua_require = require 14 | 15 | function require(module_name) 16 | if module_name:lower():match("%.eqx$") then 17 | return equinox.eval_file(module_name, false) 18 | else 19 | return lua_require(module_name) 20 | end 21 | end 22 | 23 | local lib = [[ 24 | alias: append #( table.insert 2 0 ) 25 | alias: insert #( table.insert 3 0 ) 26 | alias: remove #( table.remove 2 0 ) 27 | alias: >str #( tostring 1 1 ) 28 | alias: >num #( tonumber 1 1 ) 29 | alias: need #( require 1 1 ) 30 | alias: type #( type 1 1 ) 31 | alias: max #( math.max 2 1 ) 32 | alias: min #( math.min 2 1 ) 33 | alias: pow #( math.pow 2 1 ) 34 | alias: # size 35 | alias: emit #( string.char 1 1 ) #( io.write 1 0 ) 36 | 37 | : assert-true #( assert 1 0 ) ; 38 | : assert-false not assert-true ; 39 | : =assert = assert-true ; 40 | 41 | : [ depth >a ; 42 | : ] 43 | [] 44 | depth a> - 1 - 0 45 | do 46 | dup >a 47 | 1 rot insert ( tbl idx value ) 48 | a> 49 | loop ; 50 | 51 | : { depth >a ; 52 | : } 53 | {} 54 | depth a> - 1 - 55 | dup 2 % 0 != if 56 | "Table needs even number of items" #( error 1 ) 57 | then 58 | 2 / 0 do 59 | dup >a -rot ! a> 60 | loop ; 61 | ]] 62 | 63 | local function version() 64 | if __VERSION__ then 65 | return __VERSION__ 66 | else 67 | version = require("version/version") 68 | version.load() 69 | return version.current 70 | end 71 | end 72 | 73 | local function start_repl() 74 | repl:welcome(version()) 75 | repl:start() 76 | end 77 | 78 | function equinox.eval_files(files, log_result) 79 | local result = nil 80 | for i, filename in ipairs(files) do 81 | if log_result then 82 | print("Loading " .. filename) 83 | end 84 | result = equinox.eval_file(filename, log_result) 85 | end 86 | return result 87 | end 88 | 89 | function equinox.init() 90 | compiler:eval_text(lib) 91 | end 92 | 93 | function equinox.main() 94 | if #arg < 1 then 95 | equinox.init() 96 | start_repl() 97 | else 98 | local log_result, repl = false, false 99 | local files = {} 100 | for i, param in ipairs(arg) do 101 | if param == "-d" then 102 | log_result = true 103 | elseif param == "-o0" then 104 | optimizer:enable(false) 105 | elseif param == "-o1" then 106 | optimizer:enable(true) 107 | elseif param == "-od" then 108 | optimizer:enable_logging(true) 109 | elseif param == "-repl" then 110 | repl = true 111 | else 112 | table.insert(files, param) 113 | end 114 | end 115 | equinox.init() 116 | equinox.eval_files(files, log_result) 117 | if repl then start_repl() end 118 | end 119 | end 120 | 121 | function equinox.eval_text(str, log_result) 122 | return compiler:eval_text(str, log_result) 123 | end 124 | 125 | function equinox.eval_file(str, log_result) 126 | return compiler:eval_file(str, log_result) 127 | end 128 | 129 | equinox.traceback = function(err) 130 | return compiler:traceback(err) 131 | end 132 | 133 | if arg and arg[0] and 134 | (arg[0]:match("equinox.lua$") or 135 | arg[0]:match("equinox_bundle.lua$")) then 136 | equinox.main(arg) 137 | end 138 | 139 | return equinox 140 | -------------------------------------------------------------------------------- /src/interop.lua: -------------------------------------------------------------------------------- 1 | local interop = {} 2 | 3 | function interop.resolve_lua_obj(name) 4 | local obj = _G 5 | for part in name:gmatch("[^%.]+") do 6 | obj = obj[part] 7 | if obj == nil then return nil end 8 | end 9 | return obj 10 | end 11 | 12 | function interop.resolve_lua_func(name) 13 | local obj = interop.resolve_lua_obj(name) 14 | if obj and type(obj) == "function" then 15 | return obj 16 | else 17 | return nil 18 | end 19 | end 20 | 21 | function interop.table_name(token) 22 | return string.match(token, "^[^.]+") 23 | end 24 | 25 | function interop.explode(token) 26 | local result = {} 27 | for part, sep in token:gmatch("([^:%.]+)([:%.]?)") do 28 | table.insert(result, part) 29 | if sep ~= "" then 30 | table.insert(result, sep) 31 | end 32 | end 33 | return result 34 | end 35 | 36 | function interop.join(parts) 37 | local exp = "" 38 | for i, each in ipairs(parts) do 39 | exp = exp .. each 40 | if each ~= ":" and 41 | each ~= "." and 42 | parts[i-1] == ":" 43 | then 44 | exp = exp .. "()" 45 | end 46 | end 47 | return exp 48 | end 49 | 50 | function interop.is_lua_prop_lookup(token) 51 | return token:sub(2, #token -1):find("[.]") 52 | end 53 | 54 | function interop.dot_or_colon_notation(token) 55 | return token:sub(2, #token -1):find("[.:]") 56 | end 57 | 58 | function interop.is_valid_lua_identifier(name) 59 | local keywords = { 60 | ["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true, ["elseif"] = true, 61 | ["end"] = true, ["false"] = true, ["for"] = true, ["function"] = true, ["goto"] = true, 62 | ["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true, ["not"] = true, 63 | ["or"] = true, ["repeat"] = true, ["return"] = true, ["then"] = true, ["true"] = true, 64 | ["until"] = true, ["while"] = true } 65 | if keywords[name] then 66 | return false 67 | end 68 | return name:match("^[a-zA-Z_][a-zA-Z0-9_]*$") ~= nil 69 | end 70 | 71 | function interop.sanitize(str) 72 | str = str:gsub("-", "_mi_") 73 | :gsub("%+", "_pu_") 74 | :gsub("%%", "_pe_") 75 | :gsub("/", "_fs_") 76 | :gsub("\\", "_bs_") 77 | :gsub("~", "_ti_") 78 | :gsub("#", "_hs_") 79 | :gsub("%*", "_sr_") 80 | :gsub(";", "_sc_") 81 | :gsub("&", "_an_") 82 | :gsub("|", "_or_") 83 | :gsub("@", "_at_") 84 | :gsub("`", "_bt_") 85 | :gsub("=", "_eq_") 86 | :gsub("'", "_sq_") 87 | :gsub('"', "_dq_") 88 | :gsub("?", "_qe_") 89 | :gsub("!", "_ex_") 90 | :gsub(",", "_ca_") 91 | :gsub("%>", "_gt_") 92 | :gsub("%<", "_lt_") 93 | :gsub("%{", "_c1_") 94 | :gsub("%}", "_c2_") 95 | :gsub("%[", "_b1_") 96 | :gsub("%]", "_b2_") 97 | :gsub("%(", "_p1_") 98 | :gsub("%(", "_p2_") 99 | if str:match("^%d+") then 100 | str = "_" .. str 101 | end 102 | -- . and : are only allowed at the beginning or end 103 | if str:match("^%.") then str = "dot_" .. str:sub(2) end 104 | if str:match("^%:") then str = "col_" .. str:sub(2) end 105 | if str:match("%.$") then str = str:sub(1, #str -1) .. "_dot" end 106 | if str:match("%:$") then str = str:sub(1, #str -1) .. "_col" end 107 | return str 108 | end 109 | 110 | return interop 111 | -------------------------------------------------------------------------------- /src/line_mapping.lua: -------------------------------------------------------------------------------- 1 | local LineMapping = {} 2 | 3 | function LineMapping:new() 4 | local obj = { 5 | mapping = {}, 6 | } 7 | setmetatable(obj, {__index = self}) 8 | return obj 9 | end 10 | 11 | function LineMapping:map_target_to_source(tag, source_line_num, target_line_num) 12 | if not self.mapping[tag] then 13 | self.mapping[tag] = {} 14 | end 15 | self.mapping[tag][target_line_num] = source_line_num 16 | end 17 | 18 | function LineMapping:resolve_target(tag, target_line_num) 19 | local mapping = self.mapping[tag] 20 | if mapping then 21 | return mapping[target_line_num] 22 | end 23 | return nil 24 | end 25 | 26 | return LineMapping 27 | -------------------------------------------------------------------------------- /src/ln_repl_backend.lua: -------------------------------------------------------------------------------- 1 | local console = require("console") 2 | local utils = require("utils") 3 | local ln = require("linenoise") 4 | 5 | ln.enableutf8() 6 | 7 | local Backend = {} 8 | 9 | function Backend:new(compiler, history_file, commands) 10 | local obj = {compiler = compiler, 11 | input = "", 12 | commands = commands, 13 | history_file = history_file} 14 | setmetatable(obj, {__index = self}) 15 | if history_file then 16 | ln.historyload(history_file) 17 | end 18 | obj:setup() 19 | return obj 20 | end 21 | 22 | function Backend:setup() 23 | ln.setcompletion(function(completion, str) 24 | for _, match in ipairs(self:completer(str)) do 25 | completion:add(match) 26 | end 27 | end) 28 | end 29 | 30 | local function add_completions(input, words, result) 31 | for _, word in ipairs(words) do 32 | local before, after = input:match("^(.*)%s(.*)$") 33 | if not after then 34 | after = input 35 | before = "" 36 | else 37 | before = before .. " " 38 | end 39 | if utils.startswith(word, after) then 40 | table.insert(result, before .. word) 41 | end 42 | end 43 | end 44 | 45 | local function resolve(input) 46 | local obj = _G 47 | for part in input:gmatch("[^%.]+") do 48 | if obj[part] then 49 | obj = obj[part] 50 | else 51 | return obj 52 | end 53 | end 54 | return obj 55 | end 56 | 57 | local function add_props(input, result) 58 | local obj = resolve(input) 59 | if type(obj) ~= "table" or obj == _G then 60 | return 61 | end 62 | local prefix = input:match("(.+%.)") 63 | if not prefix then prefix = "" end 64 | local last = input:match("[^%.]+$") 65 | for key, val in pairs(obj) do 66 | if not last or utils.startswith(key, last) then 67 | table.insert(result, prefix .. key) 68 | end 69 | end 70 | end 71 | 72 | local function add_commands(input, result, commands) 73 | for _, cmd in ipairs(commands) do 74 | if utils.startswith(cmd, input) then 75 | table.insert(result, cmd) 76 | end 77 | end 78 | end 79 | 80 | local function modules() 81 | local result = {} 82 | for key, val in pairs(_G) do 83 | if type(val) == "table" then 84 | table.insert(result, key) 85 | end 86 | end 87 | return result 88 | end 89 | 90 | function Backend:completer(input) 91 | local matches = {} 92 | add_completions(input, self.compiler:word_list(), matches) 93 | add_completions(input, self.compiler:var_names(), matches) 94 | add_commands(input, matches, self.commands) 95 | if input:find("%.") then 96 | add_props(input, matches) 97 | else 98 | add_completions(input, modules(), matches) 99 | end 100 | return utils.unique(matches) 101 | end 102 | 103 | function Backend:prompt() 104 | if self.multi_line then 105 | return "..." 106 | else 107 | return "#" 108 | end 109 | end 110 | 111 | function Backend:save_history(input) 112 | if self.history_file then 113 | ln.historyadd(input) 114 | ln.historysave(self.history_file) 115 | end 116 | end 117 | 118 | function Backend:read_line(prompt) 119 | return utils.trim(ln.linenoise(prompt .. " ")) 120 | end 121 | 122 | function Backend:read() 123 | local prompt = console.colorize(self:prompt(), console.PURPLE) 124 | if self.multi_line then 125 | self.input = self.input .. "\n" .. self:read_line(prompt) 126 | else 127 | self.input = self:read_line(prompt) 128 | end 129 | return self.input 130 | end 131 | 132 | function Backend:set_multiline(bool) 133 | self.multi_line = bool 134 | ln.setmultiline(bool) 135 | end 136 | 137 | return Backend 138 | -------------------------------------------------------------------------------- /src/macros.lua: -------------------------------------------------------------------------------- 1 | local aux = require("stack_aux") 2 | local interop = require("interop") 3 | local ast = require("ast") 4 | local unpack = table.unpack or unpack 5 | 6 | local macros = {} 7 | 8 | function macros.add() 9 | return ast.push(ast.bin_op("+", ast.pop(), ast.pop())) 10 | end 11 | 12 | function macros.mul() 13 | return ast.push(ast.bin_op("*", ast.pop(), ast.pop())) 14 | end 15 | 16 | function macros.sub() 17 | return ast.push(ast.bin_op("-", ast.pop2nd(), ast.pop())) 18 | end 19 | 20 | function macros.div() 21 | return ast.push(ast.bin_op("/", ast.pop2nd(), ast.pop())) 22 | end 23 | 24 | function macros.mod() 25 | return ast.push(ast.bin_op("%", ast.pop2nd(), ast.pop())) 26 | end 27 | 28 | function macros.eq() 29 | return ast.push(ast.bin_op("==", ast.pop(), ast.pop())) 30 | end 31 | 32 | function macros.neq() 33 | return ast.push(ast.bin_op("~=", ast.pop(), ast.pop())) 34 | end 35 | 36 | function macros.lt() 37 | return ast.push(ast.bin_op(">", ast.pop2nd(), ast.pop())) 38 | end 39 | 40 | function macros.lte() 41 | return ast.push(ast.bin_op(">=", ast.pop2nd(), ast.pop())) 42 | end 43 | 44 | function macros.gt() 45 | return ast.push(ast.bin_op("<", ast.pop2nd(), ast.pop())) 46 | end 47 | 48 | function macros.gte() 49 | return ast.push(ast.bin_op("<=", ast.pop2nd(), ast.pop())) 50 | end 51 | 52 | function macros._not() 53 | return ast.push(ast.unary_op("not", ast.pop())) 54 | end 55 | 56 | function macros._and() 57 | return ast.stack_op("_and") 58 | end 59 | 60 | function macros._or() 61 | return ast.stack_op("_or") 62 | end 63 | 64 | function macros.concat() 65 | return ast.push(ast.bin_op("..", ast.pop2nd(), ast.pop())) 66 | end 67 | 68 | function macros.new_table() 69 | return ast.new_table() 70 | end 71 | 72 | function macros.table_size() 73 | return ast.push(ast.unary_op("#", ast.pop())) 74 | end 75 | 76 | function macros.table_at() 77 | return ast.push(ast.table_at(ast.pop2nd(), ast.pop())) 78 | end 79 | 80 | function macros.table_put() 81 | return ast.table_put(ast.pop3rd(), ast.pop2nd(), ast.pop()) 82 | end 83 | 84 | function macros.depth() 85 | return ast.push(ast.stack_op("depth")) 86 | end 87 | 88 | function macros.adepth() 89 | return ast.push(ast.aux_op("depth")) 90 | end 91 | 92 | function macros.dup() 93 | return ast.stack_op("dup") 94 | end 95 | 96 | function macros.drop() 97 | return ast.pop() 98 | end 99 | 100 | function macros.over() 101 | return ast.stack_op("over") 102 | end 103 | 104 | function macros.nip() 105 | return ast.stack_op("nip") 106 | end 107 | 108 | function macros.dup2() 109 | return ast.stack_op("dup2") 110 | end 111 | 112 | function macros.mrot() 113 | return ast.stack_op("mrot") 114 | end 115 | 116 | function macros.tuck() 117 | return ast.stack_op("tuck") 118 | end 119 | 120 | function macros.rot() 121 | return ast.stack_op("rot") 122 | end 123 | 124 | function macros.swap() 125 | return ast.stack_op("swap") 126 | end 127 | 128 | function macros.to_aux() 129 | return ast.aux_push(ast.pop()) 130 | end 131 | 132 | function macros.from_aux() 133 | return ast.push(ast.aux_op("pop")) 134 | end 135 | 136 | function macros.pick() 137 | return ast.push(ast.func_call("pick", ast.pop())) 138 | end 139 | 140 | function macros.roll() 141 | return ast.func_call("roll", ast.pop()) 142 | end 143 | 144 | function macros.dot() 145 | return { 146 | ast.func_call("io.write", ast.func_call("tostring", ast.pop())), 147 | ast.func_call("io.write", ast.str(" ")) 148 | } 149 | end 150 | 151 | function macros.cr() 152 | return ast.func_call("print") 153 | end 154 | 155 | function macros.def_alias(compiler, item) 156 | local forth_name = compiler:word() 157 | local alias = {} 158 | if not forth_name then 159 | compiler:err("Missing alias name", item) 160 | end 161 | 162 | repeat 163 | local exp = compiler:next_item() 164 | if exp then 165 | table.insert(alias, compiler:compile_token(exp)) 166 | end 167 | until not exp 168 | or compiler:peek_chr() == "\n" 169 | or compiler:peek_chr() == "\r" 170 | 171 | if #alias == 0 then 172 | compiler:err("Missing alias body", item) 173 | end 174 | compiler:alias(alias, forth_name) 175 | end 176 | 177 | local function def_word(compiler, is_global, item) 178 | local forth_name = compiler:word() 179 | if not forth_name then 180 | compiler:err("Missing name for colon definition.", item) 181 | end 182 | local lua_name = interop.sanitize(forth_name) 183 | if select(2, forth_name:gsub("%:", "")) > 1 or 184 | select(2, forth_name:gsub("%.", "")) > 1 185 | then 186 | compiler:err("Name '" .. forth_name .. "' " .. 187 | "can't contain multiple . or : characters.", item) 188 | end 189 | if interop.dot_or_colon_notation(forth_name) then -- method syntax 190 | local parts = interop.explode(forth_name) 191 | local obj = parts[1] 192 | local method = parts[3] -- parts[2] is expected to be . or : 193 | if not interop.is_valid_lua_identifier(method) then 194 | compiler:err("Name '" .. method .. "' " .. 195 | "is not a valid name for dot or colon notation.", item) 196 | end 197 | if not compiler:has_var(obj) then 198 | compiler:warn("Unknown object: '" .. tostring(obj) .. "'" .. 199 | "in method definition: " .. forth_name, item) 200 | end 201 | if forth_name:find(":") then 202 | compiler:def_var("self") 203 | end 204 | elseif compiler:find(forth_name) then -- Regular Forth word 205 | -- emulate hyper static glob env for funcs but not for methods 206 | lua_name = lua_name .. "__s" .. compiler.state.sequence 207 | compiler.state.sequence = compiler.state.sequence + 1 208 | end 209 | if not interop.dot_or_colon_notation(forth_name) then 210 | compiler:def_word(forth_name, lua_name, false, true) 211 | end 212 | compiler:new_env("colon_" .. lua_name) 213 | local header = ast.func_header(lua_name, is_global) 214 | if compiler.state.last_word then 215 | compiler:err("Word definitions cannot be nested.", item) 216 | else 217 | compiler.state.last_word = header 218 | end 219 | return header 220 | end 221 | 222 | function macros.colon(compiler, item) 223 | return def_word(compiler, true, item) 224 | end 225 | 226 | function macros.local_colon(compiler, item) 227 | return def_word(compiler, false, item) 228 | end 229 | 230 | function macros.tick(compiler, item) 231 | local name = compiler:word() 232 | if not name then 233 | compiler:err("A word is required for '", item) 234 | end 235 | local word = compiler:find(name) 236 | if not word then 237 | compiler:err(name .. " is not found in dictionary", item) 238 | elseif word.immediate then 239 | compiler:err("' cannot be used on a macro: " .. name, item) 240 | elseif word.is_lua_alias then 241 | compiler:err("' cannot be used on an alias: " .. name, item) 242 | end 243 | return ast.push(ast.identifier(word.lua_name)) 244 | end 245 | 246 | function macros.exec(compiler) 247 | return ast.func_call("pop()") 248 | end 249 | 250 | function macros.ret(compiler) 251 | return ast._return(ast.pop()) 252 | end 253 | 254 | function macros.comment(compiler) 255 | repeat 256 | local ch = compiler:next_chr() 257 | until ")" == ch or "" == ch 258 | end 259 | 260 | function macros.single_line_comment(compiler) 261 | repeat 262 | local ch = compiler:next_chr() 263 | until "\n" == ch or "\r" == ch or "" == ch 264 | if ch == "\r" and compiler:peek_chr() == "\n" then 265 | compiler:next_chr() 266 | end 267 | end 268 | 269 | local function is_valid_exp(exp, compiler) 270 | local name = exp 271 | if interop.dot_or_colon_notation(exp) then 272 | name = interop.explode(exp)[1] 273 | end 274 | return compiler:valid_ref(name) or compiler:find(name) 275 | end 276 | 277 | function macros.arity_call_lua(compiler, item) 278 | local func = compiler:word() 279 | if not is_valid_exp(func, compiler) then 280 | compiler:err("Unkown function or word: " .. func, item) 281 | end 282 | local numret = -1 283 | local arity = 0 284 | local token = compiler:word() 285 | if token ~= ")" then 286 | arity = tonumber(token) 287 | if not arity or arity < 0 then 288 | compiler:err("expected arity number, got '" 289 | .. tostring(token) .. "'", item) 290 | end 291 | token = compiler:word() 292 | if token ~= ")" then 293 | numret = tonumber(token) 294 | if not numret or numret < -1 or numret > 1 then 295 | compiler:err("expected number of return values (0/1/-1), got '" 296 | .. tostring(token) .. "'", item) 297 | end 298 | token = compiler:word() 299 | if token ~= ")" then 300 | compiler:err("expected closing ), got '" 301 | .. tostring(token) .. "'", item) 302 | end 303 | end 304 | end 305 | local params = {} 306 | local stmts = {} 307 | if arity > 0 then 308 | for i = 1, arity do 309 | table.insert(params, 310 | ast.identifier(ast.gen_id("__p"))) 311 | end 312 | for i = arity, 1, -1 do -- reverse parameter order 313 | table.insert(stmts, 314 | ast.init_local(params[i].id, ast.pop())) 315 | end 316 | end 317 | if numret == 0 then 318 | table.insert(stmts, ast.func_call(func, unpack(params))) 319 | elseif numret == 1 then 320 | table.insert(stmts, ast.push( 321 | ast.func_call(func, unpack(params)))) 322 | elseif numret == -1 then 323 | table.insert(stmts, ast.push_many( 324 | ast.func_call(func, unpack(params)))) 325 | else 326 | compiler:err("Invalid numret:" .. tostring(numret), item) 327 | end 328 | return stmts 329 | end 330 | 331 | function macros.var(compiler, item) 332 | local name = compiler:word() 333 | if name then 334 | return ast.def_local(compiler:def_var(name)) 335 | else 336 | compiler:err("Missing variable name.", item) 337 | end 338 | end 339 | 340 | function macros.var_global(compiler, item) 341 | local name = compiler:word() 342 | if name then 343 | return ast.def_global(compiler:def_global(name)) 344 | else 345 | compiler:err("Missing variable name.", item) 346 | end 347 | end 348 | 349 | local function valid_tbl_assignment(compiler, name) 350 | if interop.is_lua_prop_lookup(name) then 351 | local tbl = interop.table_name(name) 352 | return compiler:has_var(tbl) 353 | or interop.resolve_lua_obj(name) 354 | end 355 | return false 356 | end 357 | 358 | function macros.assignment(compiler, item) 359 | local name = compiler:word() 360 | if not name then 361 | compiler:err("Missing variable name.", item) 362 | end 363 | if name == "var" then 364 | -- declare and assign of a new var 365 | name = compiler:word() 366 | if not name then 367 | compiler:err("Missing variable name.", item) 368 | end 369 | return ast.init_local(compiler:def_var(name), ast.pop()) 370 | elseif name == "global" then 371 | -- declare and assign of a new global 372 | name = compiler:word() 373 | if not name then 374 | compiler:err("Missing variable name.", item) 375 | end 376 | return ast.init_global(compiler:def_global(name), ast.pop()) 377 | else 378 | -- assignment of existing var 379 | if compiler:has_var(name) then 380 | return ast.assignment( 381 | compiler:find_var(name).lua_name, ast.pop()) 382 | elseif valid_tbl_assignment(compiler, name) then -- 123 -> tbl.x 383 | local parts = interop.explode(name) 384 | if compiler:has_var(parts[1]) then 385 | parts[1] = compiler:find_var(parts[1]).lua_name 386 | return ast.assignment(interop.join(parts), ast.pop()) 387 | else 388 | return ast.assignment(name, ast.pop()) 389 | end 390 | else 391 | compiler:err("Undeclared variable: " .. name, item) 392 | end 393 | end 394 | end 395 | 396 | function macros._if(compiler) 397 | compiler:new_env('IF') 398 | return ast._if(ast.pop()) 399 | end 400 | 401 | function macros._else() 402 | return ast.keyword("else") 403 | end 404 | 405 | function macros._then(compiler, item) 406 | compiler:remove_env('IF', item) 407 | return ast.keyword("end") 408 | end 409 | 410 | function macros._begin(compiler) 411 | -- begin..until / begin..again / begin..while..repeat 412 | compiler:new_env('BEGIN_LOOP') 413 | return ast._while(ast.literal("boolean", "true")) 414 | end 415 | 416 | function macros._again(compiler, item) 417 | compiler:remove_env('BEGIN_LOOP', item) 418 | return ast.keyword("end") 419 | end 420 | 421 | function macros._repeat(compiler, item) 422 | compiler:remove_env('BEGIN_LOOP', item) 423 | return ast.keyword("end") 424 | end 425 | 426 | function macros._until(compiler, item) 427 | compiler:remove_env('BEGIN_LOOP', item) 428 | return { 429 | ast._if(ast.pop(), ast.keyword("break")), 430 | ast.keyword("end") 431 | } 432 | end 433 | 434 | function macros.block(compiler) 435 | compiler:new_env('BLOCK') 436 | return ast.keyword("do") 437 | end 438 | 439 | function macros._while() 440 | return ast._if(ast.unary_op("not", ast.pop()), ast.keyword("break")) 441 | end 442 | 443 | function macros._case(compiler) -- simulate goto with break, in pre lua5.2 since GOTO was not yet supported 444 | compiler:new_env('CASE') 445 | return ast.keyword("repeat") 446 | end 447 | 448 | function macros._of(compiler) 449 | compiler:new_env('OF') 450 | return { 451 | ast.stack_op("over"), 452 | ast._if(ast.bin_op("==", ast.pop(), ast.pop())), 453 | ast.pop() -- drop selector 454 | } 455 | end 456 | 457 | function macros._endof(compiler, item) -- GOTO endcase 458 | compiler:remove_env('OF', item) 459 | return { ast.keyword("break"), ast.keyword("end") } 460 | end 461 | 462 | function macros._endcase(compiler, item) 463 | compiler:remove_env('CASE', item) 464 | return ast._until(ast.literal("boolean", "true")) 465 | end 466 | 467 | function macros._exit() 468 | return ast._return(nil) -- exit from Forth word 469 | end 470 | 471 | function macros._do(compiler) 472 | local do_loop_vars = {"i", "j", "k"} 473 | local state = compiler.state 474 | local loop_var = 475 | do_loop_vars[((state.do_loop_nesting -1) % #do_loop_vars) +1] 476 | state.do_loop_nesting = state.do_loop_nesting + 1 477 | compiler:new_env('DO_LOOP') 478 | compiler:def_var(loop_var) 479 | return ast._for( 480 | loop_var, 481 | ast.pop(), 482 | ast.bin_op("-", ast.pop(), ast.literal("number", 1)), 483 | nil) 484 | end 485 | 486 | function macros._loop(compiler, item) 487 | compiler:remove_env('DO_LOOP', item) 488 | compiler.state.do_loop_nesting = 489 | compiler.state.do_loop_nesting - 1 490 | return ast.keyword("end") 491 | end 492 | 493 | function macros.for_ipairs(compiler, item) 494 | local var_name1 = compiler:word() 495 | local var_name2 = compiler:word() 496 | if not var_name1 or not var_name2 then 497 | compiler:err("ipairs needs two loop variables", item) 498 | end 499 | compiler:new_env('IPAIRS_LOOP') 500 | compiler:def_var(var_name1) 501 | compiler:def_var(var_name2) 502 | return ast._foreach(var_name1, var_name2, ast._ipairs(ast.pop())) 503 | end 504 | 505 | function macros.for_pairs(compiler, item) 506 | local var_name1 = compiler:word() 507 | local var_name2 = compiler:word() 508 | if not var_name1 or not var_name2 then 509 | compiler:err("pairs needs two loop variables", item) 510 | end 511 | compiler:new_env('PAIRS_LOOP') 512 | compiler:def_var(var_name1) 513 | compiler:def_var(var_name2) 514 | return ast._foreach(var_name1, var_name2, ast._pairs(ast.pop())) 515 | end 516 | 517 | function macros.for_each(compiler, item) 518 | local var_name = compiler:word() 519 | if not var_name then 520 | compiler:err("iter needs one loop variable", item) 521 | end 522 | compiler:new_env('ITER_LOOP') 523 | compiler:def_var(var_name) 524 | return ast._foreach(var_name, nil, ast.pop()) 525 | end 526 | 527 | function macros._to(compiler, item) 528 | local loop_var = compiler:word() 529 | if not loop_var then 530 | compiler:err("to loop needs a loop variable.", item) 531 | end 532 | compiler:new_env('TO_LOOP') 533 | compiler:def_var(loop_var) 534 | return ast._for(loop_var, ast.pop2nd(), ast.pop(), nil) 535 | end 536 | 537 | function macros._step(compiler, item) 538 | local loop_var = compiler:word() 539 | if not loop_var then 540 | compiler:err("step loop needs a loop variable.", item) 541 | end 542 | compiler:new_env('STEP_LOOP') 543 | compiler:def_var(loop_var) 544 | return ast._for(loop_var, ast.pop3rd(), ast.pop2nd(), ast.pop(), nil) 545 | end 546 | 547 | function macros._end(compiler) 548 | compiler:remove_env() -- can belong to multiple 549 | return ast.keyword("end") 550 | end 551 | 552 | function macros.end_word(compiler, item) 553 | if not compiler.state.last_word then 554 | compiler:err("Unexpected semicolon", item) 555 | end 556 | local name = compiler.state.last_word.func_name 557 | macros.reveal(compiler, item) 558 | compiler.state.last_word = nil 559 | compiler:remove_env() 560 | return ast.end_func(name) 561 | end 562 | 563 | function macros.see(compiler, item) 564 | local name = compiler:word() 565 | if not name then 566 | compiler:err("See needs a word name", item) 567 | end 568 | local word = compiler:find(name) 569 | if not word then 570 | compiler:err(name .. " is not found in dictionary", item) 571 | elseif word.immediate then 572 | print("N/A. Macro (immediate word)") 573 | elseif word.is_lua_alias then 574 | print("N/A. Alias") 575 | else 576 | print(word.code) 577 | end 578 | end 579 | 580 | function macros.keyval(compiler) 581 | local name = compiler:word() 582 | return { 583 | ast.push(ast.str(name)), 584 | ast.push(ast.identifier(name)) 585 | } 586 | end 587 | 588 | function macros.formal_params(compiler, item) 589 | if not compiler.state.last_word then 590 | compiler:err("Unexpected (:", item) 591 | end 592 | local func_header = compiler.state.last_word 593 | local param_name = compiler:word() 594 | while param_name ~= ":)" do 595 | compiler:def_var(param_name) 596 | table.insert(func_header.params, param_name) 597 | param_name = compiler:word() 598 | end 599 | return result 600 | end 601 | 602 | function macros.reveal(compiler, item) 603 | if not compiler.state.last_word then 604 | compiler:err("Reveal must be used within a word definition.", item) 605 | end 606 | compiler:reveal(compiler.state.last_word.func_name) 607 | end 608 | 609 | function macros.words(compiler) 610 | for i, each in ipairs(compiler:word_list()) do 611 | io.write(each .. " ") 612 | end 613 | print() 614 | end 615 | 616 | return macros 617 | -------------------------------------------------------------------------------- /src/output.lua: -------------------------------------------------------------------------------- 1 | local Output = {} 2 | 3 | function Output:new(name) 4 | local obj = {lines = {""}, line_number = 1, name = name} 5 | setmetatable(obj, {__index = self}) 6 | return obj 7 | end 8 | 9 | function Output:append(str) 10 | self.lines[self:size()] = self.lines[self:size()] .. str 11 | end 12 | 13 | function Output:new_line() 14 | self.line_number = self.line_number +1 15 | table.insert(self.lines, "") 16 | end 17 | 18 | function Output:size() 19 | return #self.lines 20 | end 21 | 22 | function Output:text(from) 23 | return table.concat(self.lines, "\n", from) 24 | end 25 | 26 | function Output:load() 27 | local text = self:text() 28 | if loadstring then 29 | return loadstring(text, self.name) 30 | else -- Since Lua 5.2, loadstring has been replaced by load. 31 | return load(text, self.name) 32 | end 33 | end 34 | 35 | return Output 36 | -------------------------------------------------------------------------------- /src/parser.lua: -------------------------------------------------------------------------------- 1 | local interop = require("interop") 2 | 3 | local Parser = {} 4 | 5 | function Parser:new(source) 6 | local obj = {index = 1, 7 | line_number = 1, 8 | source = source} 9 | setmetatable(obj, {__index = self}) 10 | return obj 11 | end 12 | 13 | function Parser:parse_all() 14 | local result = {} 15 | local item = self:next_item() 16 | while item do 17 | table.insert(result, item) 18 | item = self:next_item() 19 | end 20 | return result 21 | end 22 | 23 | local function is_quote(chr) 24 | return chr:match('"') 25 | end 26 | 27 | local function is_escape(chr) 28 | return chr:match("\\") 29 | end 30 | 31 | local function is_whitespace(chr) 32 | return chr:match("%s") 33 | end 34 | 35 | function Parser:next_item() 36 | local token = "" 37 | local begin_str = false 38 | local stop = false 39 | local kind = "word" 40 | while not self:ended() and not stop do 41 | local chr = self:read_chr() 42 | if is_quote(chr) then 43 | if begin_str then 44 | stop = true 45 | else 46 | kind = "string" 47 | begin_str = true 48 | end 49 | token = token .. chr 50 | elseif begin_str 51 | and is_escape(chr) 52 | and (is_escape(self:peek_chr()) or is_quote(self:peek_chr())) 53 | then 54 | token = token .. chr .. self:read_chr() -- consume \" or \\ 55 | elseif begin_str and ("\r" == chr or "\n" == chr) then 56 | error(string.format( 57 | "Unterminated string: %s at line: %d", token, self.line_number)) 58 | elseif is_whitespace(chr) and not begin_str then 59 | if #token > 0 then 60 | self.index = self.index -1 -- don't consume next WS as it breaks single line comment 61 | stop = true 62 | else 63 | self:update_line_number(chr) 64 | end 65 | else 66 | token = token .. chr 67 | end 68 | end 69 | if token == "" then 70 | return nil -- EOF 71 | end 72 | if token:match("^$.+") then kind = "symbol" end 73 | if tonumber(token) then kind = "number" end 74 | return {token=token, kind=kind, line_number=self.line_number} 75 | end 76 | 77 | function Parser:update_line_number(chr) 78 | if chr == '\r' then 79 | if self:peek_chr() == '\n' then self:read_chr() end 80 | self.line_number = self.line_number +1 81 | elseif chr == '\n' then 82 | self.line_number = self.line_number +1 83 | end 84 | end 85 | 86 | function Parser:next_chr() 87 | local chr = self:read_chr() 88 | self:update_line_number(chr) 89 | return chr 90 | end 91 | 92 | function Parser:read_chr() 93 | local chr = self:peek_chr() 94 | self.index = self.index + 1 95 | return chr 96 | end 97 | 98 | function Parser:peek_chr() 99 | return self.source:sub(self.index, self.index) 100 | end 101 | 102 | function Parser:ended() 103 | return self.index > #self.source 104 | end 105 | 106 | return Parser 107 | -------------------------------------------------------------------------------- /src/repl.lua: -------------------------------------------------------------------------------- 1 | local stack = require("stack") 2 | local utils = require("utils") 3 | local console = require("console") 4 | local Source = require("source") 5 | 6 | local function load_backend(preferred, fallback) 7 | local success, mod = pcall(require, preferred) 8 | if success then 9 | return mod 10 | else 11 | return require(fallback) 12 | end 13 | end 14 | 15 | local Repl = {} 16 | 17 | local repl_ext = "repl_ext.eqx" 18 | 19 | local commands = { 20 | bye = "bye", 21 | help = "help", 22 | log_on = "log-on", 23 | log_off = "log-off", 24 | stack_on = "stack-on", 25 | stack_off = "stack-off", 26 | opt_on = "opt-on", 27 | opt_off = "opt-off", 28 | load_file = "load-file" 29 | } 30 | 31 | function Repl:new(compiler, optimizer) 32 | local ReplBackend = load_backend("ln_repl_backend", "simple_repl_backend") 33 | local obj = {backend = ReplBackend:new( 34 | compiler, 35 | utils.in_home(".equinox_repl_history"), 36 | utils.values(commands)), 37 | compiler = compiler, 38 | optimizer = optimizer, 39 | ext_dir = os.getenv("EQUINOX_EXT_DIR") or "./ext", 40 | always_show_stack = false, 41 | repl_ext_loaded = false, 42 | log_result = false } 43 | setmetatable(obj, {__index = self}) 44 | return obj 45 | end 46 | 47 | local messages = { 48 | "The Prime Directive: Preserve Stack Integrity at All Costs.", 49 | "Engage warp speed and may your stack never overflow.", 50 | "Welcome Commander. The stack is ready for your orders.", 51 | "Our mission is to explore new words and seek out new stack operations.", 52 | "Welcome, Officer. May your debugging skills be as sharp as a phaser.", 53 | "Your mission is to PUSH the boundaries of programming.", 54 | "In the Delta Quadrant every stack operation is a new discovery.", 55 | "One wrong stack move and your program could warp into an infinite loop.", 56 | "Take responsibility for your code as errors will affect the entire fleet.", 57 | "Picard's programming tip: Complexity can be a form of the enemy.", 58 | "Spock's programming tip: Logic is the foundation of all good code.", 59 | "Spock's programming tip: Do not let emotion cloud your debugging.", 60 | "Worf's programming tip: A true programmer fights for correctness.", 61 | "Worf's programming tip: When facing a bug, fire your phasers at full power.", 62 | "One misplaced DROP can send your code into warp core breach.", 63 | "To reach warp speed, the code must be optimized for maximum efficiency.", 64 | "Working in Forth sometimes feels like working in a Jeffries tube.", 65 | "A balanced stack is a stable warp core. Keep it well protected.", 66 | "All systems are stable, commander. Stack integrity is 100%.", 67 | "Captain, the stack is clear and ready for warp.", 68 | "Deflector shields holding steady captain, the stack is well protected." 69 | } 70 | 71 | math.randomseed(os.time()) 72 | 73 | function Repl:welcome(version) 74 | print("Equinox Forth Console (" .. _VERSION .. ") @ Delta Quadrant.") 75 | print(messages[math.random(1, #messages)]) 76 | console.message(string.format([[ 77 | __________________ _-_ 78 | \__(=========/_=_/ ____.---'---`---.___ 79 | \_ \ \----._________.---/ 80 | \ \ / / `-_-' 81 | ___,--`.`-'..'-_ 82 | /____ (| 83 | `--.____,-' v%s 84 | ]], version), console.CYAN) 85 | print("Type 'words' for wordlist, 'bye' to exit or 'help'.") 86 | print("First time Forth user? Type: load-file tutorial") 87 | end 88 | 89 | local function show_help() 90 | print([[ 91 | - log-on "turn on logging" 92 | - log-off "turn off logging" 93 | - opt-on "turn on optimization" 94 | - opt-off "turn off optimization" 95 | - load-file "load an eqx file" 96 | - stack-on "always show stack after each input" 97 | - stack-off "don't show stack after each input" 98 | - bye "exit repl" 99 | - help "show this help"]]) 100 | end 101 | 102 | function Repl:read() 103 | return self.backend:read() 104 | end 105 | 106 | function Repl:process_commands(input) 107 | local command = utils.trim(input) 108 | if command == commands.bye then 109 | os.exit(0) 110 | end 111 | if command == commands.help then 112 | show_help() 113 | return true 114 | end 115 | if command == commands.log_on then 116 | self.log_result = true 117 | print("Log turned on") 118 | return true 119 | end 120 | if command == commands.log_off then 121 | self.log_result = false 122 | print("Log turned off") 123 | return true 124 | end 125 | if command == commands.stack_on then 126 | if self.repl_ext_loaded then 127 | self.always_show_stack = true 128 | print("Show stack after input is on") 129 | else 130 | print("Requires " .. repl_ext) 131 | end 132 | return true 133 | end 134 | if command == commands.stack_off then 135 | self.always_show_stack = off 136 | print("Show stack after input is off") 137 | return true 138 | end 139 | if command == commands.opt_on then 140 | self.optimizer:enable(true) 141 | print("Optimization turned on") 142 | return true 143 | end 144 | if command == commands.opt_off then 145 | self.optimizer:enable(false) 146 | print("Optimization turned off") 147 | return true 148 | end 149 | if command:sub(1, #commands.load_file) == commands.load_file 150 | then 151 | local path = utils.trim(command:sub(#commands.load_file + 1)) 152 | if path and path ~= "" then 153 | if not utils.exists(path) and not utils.extension(path) then 154 | path = path .. ".eqx" 155 | end 156 | if not utils.exists(path) and 157 | not (string.find(path, "/") or 158 | string.find(path, "\\")) 159 | then 160 | path = utils.join(self.ext_dir, path) 161 | end 162 | if utils.exists(path) then 163 | self:safe_call(function() self.compiler:eval_file(path) end) 164 | else 165 | print("File does not exist: " .. path) 166 | end 167 | else 168 | print("Missing file path.") 169 | end 170 | return true 171 | end 172 | return false 173 | end 174 | 175 | function Repl:print_err(result) 176 | console.message("Red Alert: ", console.RED, true) 177 | print(tostring(result)) 178 | end 179 | 180 | function Repl:print_ok() 181 | if depth() > 0 then 182 | console.message("OK(".. depth() .. ")", console.GREEN) 183 | if self.always_show_stack and self.repl_ext_loaded then 184 | self.compiler:eval_text(".s") 185 | end 186 | else 187 | console.message("OK", console.GREEN) 188 | end 189 | end 190 | 191 | function Repl:safe_call(func) 192 | local success, result = pcall(func) 193 | if success then 194 | self:print_ok() 195 | else 196 | self:print_err(result) 197 | end 198 | end 199 | 200 | function Repl:start() 201 | local ext = utils.file_exists_in_any_of(repl_ext, {self.ext_dir}) 202 | if ext then 203 | self.compiler:eval_file(ext) 204 | self.repl_ext_loaded = true 205 | end 206 | while true do 207 | local input = self:read() 208 | if self:process_commands(input) then 209 | self.backend:save_history(input) 210 | else 211 | local success, result = pcall(function () 212 | return self.compiler:compile_and_load( 213 | Source:from_text(input), self.log_result) 214 | end) 215 | if not success then 216 | self:print_err(result) 217 | elseif not result then 218 | self.backend:set_multiline(true) 219 | self.compiler:reset_state() 220 | else 221 | self.backend:set_multiline(false) 222 | self:safe_call(function() result() end) 223 | self.backend:save_history(input:gsub("[\r\n]", " ")) 224 | end 225 | end 226 | end 227 | end 228 | 229 | return Repl 230 | -------------------------------------------------------------------------------- /src/simple_repl_backend.lua: -------------------------------------------------------------------------------- 1 | local console = require("console") 2 | 3 | local Backend = {} 4 | 5 | function Backend:new() 6 | local obj = {input = ""} 7 | setmetatable(obj, {__index = self}) 8 | return obj 9 | end 10 | 11 | function Backend:prompt() 12 | if self.multi_line then 13 | return "..." 14 | else 15 | return "#" 16 | end 17 | end 18 | 19 | function Backend:save_history(input) 20 | -- unsupported 21 | end 22 | 23 | function Backend:read() 24 | console.message(self:prompt() .. " ", console.PURPLE, true) 25 | if self.multi_line then 26 | self.input = self.input .. "\n" .. io.read() 27 | else 28 | self.input = io.read() 29 | end 30 | return self.input 31 | end 32 | 33 | function Backend:set_multiline(bool) 34 | self.multi_line = bool 35 | end 36 | 37 | return Backend 38 | -------------------------------------------------------------------------------- /src/source.lua: -------------------------------------------------------------------------------- 1 | local Source = {} 2 | 3 | local seq = 1 4 | 5 | local function lines_of(input) 6 | local lines = {} 7 | for line in input:gmatch("([^\r\n]*)\r?\n?") do 8 | table.insert(lines, line) 9 | end 10 | return lines 11 | end 12 | 13 | function Source:new(text, path) 14 | local obj = {text = text, 15 | path = path, 16 | name = nil, 17 | type = nil, 18 | lines = lines_of(text)} 19 | setmetatable(obj, {__index = self}) 20 | if path then 21 | obj.name = path 22 | obj.type = "file" 23 | else 24 | obj.name = "chunk" .. seq 25 | seq = seq + 1 26 | obj.type = "chunk" 27 | end 28 | return obj 29 | end 30 | 31 | function Source:from_text(text) 32 | return self:new(text, nil) 33 | end 34 | 35 | function Source:empty() 36 | return self:from_text("") 37 | end 38 | 39 | function Source:from_file(path) 40 | local file = io.open(path, "r") 41 | if not file then 42 | error("Could not open file: " .. path) 43 | end 44 | local src = self:new(file:read("*a"), path) 45 | file:close() 46 | return src 47 | end 48 | 49 | function Source:show_lines(src_line_num, n) 50 | for i = src_line_num - n, src_line_num + n do 51 | local line = self.lines[i] 52 | if line then 53 | local mark = " " 54 | if i == src_line_num then mark = "=>" end 55 | io.stderr:write(string.format(" %s%03d. %s\n", mark, i , line)) 56 | end 57 | end 58 | end 59 | 60 | return Source 61 | -------------------------------------------------------------------------------- /src/stack.lua: -------------------------------------------------------------------------------- 1 | local stack 2 | local NIL = {} -- nil cannot be stored in table, use this placeholder 3 | local name = "data-stack" 4 | 5 | if table.create then 6 | stack = table.create(32) 7 | else 8 | stack = {nil, nil, nil, nil, nil, nil, nil, nil} 9 | end 10 | 11 | function push(e) 12 | if e ~= nil then 13 | stack[#stack + 1] = e 14 | else 15 | stack[#stack + 1] = NIL 16 | end 17 | end 18 | 19 | function push_many(...) 20 | local args = {...} 21 | local n = #stack 22 | for i = 1, #args do 23 | if args[i] ~= nil then 24 | stack[n + i] = args[i] 25 | else 26 | stack[n + i] = NIL 27 | end 28 | end 29 | end 30 | 31 | function pop() 32 | local size = #stack 33 | if size == 0 then error("Stack underflow: " .. name) end 34 | local item = stack[size] 35 | stack[size] = nil 36 | if item ~= NIL then return item else return nil end 37 | end 38 | 39 | function pop2nd() 40 | local n = #stack 41 | if n < 2 then error("Stack underflow: " .. name) end 42 | local item = stack[n - 1] 43 | stack[n -1] = stack[n] 44 | stack[n] = nil 45 | if item ~= NIL then return item else return nil end 46 | end 47 | 48 | function pop3rd() 49 | local n = #stack 50 | if n < 3 then error("Stack underflow: " .. name) end 51 | local item = table.remove(stack, n - 2) 52 | if item ~= NIL then return item else return nil end 53 | end 54 | 55 | function swap() 56 | local n = #stack 57 | if n < 2 then error("Stack underflow: " .. name) end 58 | stack[n], stack[n - 1] = stack[n - 1], stack[n] 59 | end 60 | 61 | function rot() 62 | local n = #stack 63 | if n < 3 then error("Stack underflow: " .. name) end 64 | local new_top = stack[n -2] 65 | table.remove(stack, n - 2) 66 | stack[n] = new_top 67 | end 68 | 69 | function mrot() 70 | local n = #stack 71 | if n < 3 then error("Stack underflow: " .. name) end 72 | local temp = stack[n] 73 | stack[n] = nil 74 | table.insert(stack, n - 2, temp) 75 | end 76 | 77 | function over() 78 | local n = #stack 79 | if n < 2 then error("Stack underflow: " .. name) end 80 | stack[n + 1] = stack[n - 1] 81 | end 82 | 83 | function tuck() 84 | local n = #stack 85 | if n < 2 then error("Stack underflow: " .. name) end 86 | table.insert(stack, n - 1, stack[n]) 87 | end 88 | 89 | function nip() 90 | local n = #stack 91 | if n < 2 then error("Stack underflow: " .. name) end 92 | stack[n - 1] = stack[n] 93 | stack[n] = nil 94 | end 95 | 96 | function dup() 97 | local n = #stack 98 | if n < 1 then error("Stack underflow: " .. name) end 99 | stack[n + 1] = stack[n] 100 | end 101 | 102 | function dup2() 103 | local n = #stack 104 | if n < 2 then error("Stack underflow: " .. name) end 105 | local tos1 = stack[n] 106 | local tos2 = stack[n - 1] 107 | stack[n + 1] = tos2 108 | stack[n + 2] = tos1 109 | end 110 | 111 | function tos() 112 | local item = stack[#stack] 113 | if item == nil then error("Stack underflow: " .. name) end 114 | if item ~= NIL then return item else return nil end 115 | end 116 | 117 | function tos2() 118 | local item = stack[#stack - 1] 119 | if item == nil then error("Stack underflow: " .. name) end 120 | if item ~= NIL then return item else return nil end 121 | end 122 | 123 | function _and() 124 | local a, b = pop(), pop() 125 | push(a and b) 126 | end 127 | 128 | function _or() 129 | local a, b = pop(), pop() 130 | push(a or b) 131 | end 132 | 133 | function _inc() 134 | local n = #stack 135 | if n < 1 then error("Stack underflow: " .. name) end 136 | stack[n] = stack[n] + 1 137 | end 138 | 139 | function _neg() 140 | local n = #stack 141 | if n < 1 then error("Stack underflow: " .. name) end 142 | stack[n] = not stack[n] 143 | end 144 | 145 | function depth() 146 | return #stack 147 | end 148 | 149 | function pick(index) 150 | local item = stack[#stack - index] 151 | if item == nil then error("Stack underflow: " .. name) end 152 | if item ~= NIL then return item else return nil end 153 | end 154 | 155 | function roll(index) 156 | if index == 0 then return end 157 | local n = #stack 158 | if n <= index then error("Stack underflow: " .. name) end 159 | local new_top = stack[n -index] 160 | table.remove(stack, n - index) 161 | stack[n] = new_top 162 | end 163 | 164 | return stack 165 | -------------------------------------------------------------------------------- /src/stack_aux.lua: -------------------------------------------------------------------------------- 1 | local stack = {} 2 | local NIL = {} -- nil cannot be stored in table, use this placeholder 3 | local name = "aux-stack" 4 | 5 | function apush(e) 6 | if e ~= nil then 7 | stack[#stack + 1] = e 8 | else 9 | stack[#stack + 1] = NIL 10 | end 11 | end 12 | 13 | function apop() 14 | local size = #stack 15 | if size == 0 then 16 | error("Stack underflow: " .. name) 17 | end 18 | local item = stack[size] 19 | stack[size] = nil 20 | if item ~= NIL then return item else return nil end 21 | end 22 | 23 | function adepth() 24 | return #stack 25 | end 26 | 27 | return stack 28 | -------------------------------------------------------------------------------- /src/utils.lua: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | function utils.trim(str) 4 | return str:match("^%s*(.-)%s*$") 5 | end 6 | 7 | function utils.deepcopy(orig) 8 | local orig_type = type(orig) 9 | local copy 10 | if orig_type == 'table' then 11 | copy = {} 12 | for orig_key, orig_value in next, orig, nil do 13 | copy[utils.deepcopy(orig_key)] = utils.deepcopy(orig_value) 14 | end 15 | setmetatable(copy, utils.deepcopy(getmetatable(orig))) 16 | else -- number, string, boolean, etc 17 | copy = orig 18 | end 19 | return copy 20 | end 21 | 22 | function utils.home() 23 | return os.getenv("USERPROFILE") or os.getenv("HOME") 24 | end 25 | 26 | function utils.in_home(file) 27 | return utils.join(utils.home(), file) 28 | end 29 | 30 | function utils.values(tbl) 31 | local vals = {} 32 | for k, v in pairs(tbl) do 33 | table.insert(vals, v) 34 | end 35 | return vals 36 | end 37 | 38 | function utils.extension(filename) 39 | return filename:match("^.+(%.[^%.]+)$") 40 | end 41 | 42 | function utils.join(dir, child) 43 | if not dir or "" == dir then return child end 44 | local sep = "" 45 | if dir:sub(-1) ~= "/" and dir:sub(-1) ~= "\\" then 46 | sep = package.config:sub(1, 1) 47 | end 48 | return dir .. sep .. child 49 | end 50 | 51 | 52 | function utils.exists(filename) 53 | local file = io.open(filename, "r") 54 | if file then 55 | file:close() 56 | return true 57 | else 58 | return false 59 | end 60 | end 61 | 62 | function utils.file_exists_in_any_of(filename, dirs) 63 | for i, dir in ipairs(dirs) do 64 | local path = utils.join(dir, filename) 65 | if utils.exists(path) then 66 | return path 67 | end 68 | end 69 | return nil 70 | end 71 | 72 | function utils.unique(tbl) 73 | local seen = {} 74 | local result = {} 75 | for _, v in ipairs(tbl) do 76 | if not seen[v] then 77 | seen[v] = true 78 | table.insert(result, v) 79 | end 80 | end 81 | return result 82 | end 83 | 84 | function utils.keys(tbl) 85 | local result = {} 86 | for key, _ in pairs(tbl) do 87 | table.insert(result, key) 88 | end 89 | return result 90 | end 91 | 92 | function utils.startswith(str, prefix) 93 | return string.sub(str, 1, #prefix) == prefix 94 | end 95 | 96 | function utils.module_available(name) 97 | local ok, _ = pcall(require, name) 98 | return ok 99 | end 100 | 101 | return utils 102 | -------------------------------------------------------------------------------- /src/version/version.lua: -------------------------------------------------------------------------------- 1 | local version = {} 2 | 3 | local filename = "src/version/version.txt" 4 | 5 | function version.increment() 6 | local major, minor, patch = version.current:match("^(%d+)%.(%d+)%-(%d+)$") 7 | if not major or not minor or not patch then 8 | error("Invalid version format. Expected format: major.minor.patch") 9 | end 10 | major = tonumber(major) 11 | minor = tonumber(minor) 12 | patch = tonumber(patch) + 1 13 | version.current = string.format("%d.%d-%d", major, minor, patch) 14 | end 15 | 16 | function version.load() 17 | local file = io.open(filename, "r") 18 | if not file then 19 | error("Could not open file: " .. filename) 20 | end 21 | version.current = file:read("*line") 22 | file:close() 23 | return version 24 | end 25 | 26 | function version.save() 27 | local file = io.open(filename, "w") 28 | if not file then 29 | error("Could not open file for writing: " .. filename) 30 | end 31 | file:write(version.current .. "\n") 32 | file:close() 33 | end 34 | 35 | if arg and arg[0]:match("version.lua") then 36 | version.load() 37 | print("Increasign current version " .. version.current) 38 | version.increment() 39 | print("New version " .. version.current) 40 | version.save() 41 | end 42 | 43 | return version 44 | -------------------------------------------------------------------------------- /src/version/version.txt: -------------------------------------------------------------------------------- 1 | 0.1-413 2 | -------------------------------------------------------------------------------- /tests/json.lua: -------------------------------------------------------------------------------- 1 | -- from: 2 | -- https://github.com/ReallySnazzy/single-file-lua-json-parser/blob/main/json.lua 3 | 4 | --json lib 5 | 6 | do 7 | -- Converts non-string-friendly characters into a friendly string 8 | local function escape(ch) 9 | if ch == '\a' then return '\\a' end 10 | if ch == '\b' then return '\\b' end 11 | if ch == '\f' then return '\\f' end 12 | if ch == '\n' then return '\\n' end 13 | if ch == '\r' then return '\\r' end 14 | if ch == '\t' then return '\\t' end 15 | if ch == '\v' then return '\\v' end 16 | if ch == '\\' then return '\\\\' end 17 | if ch == '\"' then return '\\\"' end 18 | if ch == '\'' then return '\\\'' end 19 | return ch 20 | end 21 | 22 | -- Converts a friendly string into its original 23 | local function unescape_str(str) 24 | local escapes = { 25 | ['a'] = '\a', 26 | ['b'] = '\b', 27 | ['f'] = '\f', 28 | ['n'] = '\n', 29 | ['r'] = '\r', 30 | ['t'] = '\t', 31 | ['v'] = '\v', 32 | ['\\'] = '\\', 33 | ['"'] = '\"', 34 | ['\''] = '\'', 35 | ['['] = '[', 36 | [']'] = ']', 37 | } 38 | local escaped = false 39 | local res = {} 40 | for i = 1, str:len() do 41 | local ch = str:sub(i, i) 42 | if not escaped then 43 | if ch == '\\' then 44 | escaped = true 45 | else 46 | table.insert(res, ch) 47 | end 48 | else 49 | local match = escapes[ch] 50 | assert(match ~= nil, "Unknown escape sequence") 51 | table.insert(res, match) 52 | end 53 | end 54 | return table.concat(res, '') 55 | end 56 | 57 | -- Converts a string into its JSON representation 58 | local function str_to_str(s) 59 | local res = {} 60 | for i = 1, s:len() do 61 | table.insert(res, escape(s:sub(i, i))) 62 | end 63 | return "\"" .. table.concat(res, '') .. "\"" 64 | end 65 | 66 | -- Converts a table into its JSON representation 67 | local function tab_to_str(t) 68 | if #t == 0 then 69 | -- Sorting keys to give a deterministic output 70 | local keys = {} 71 | for k, _ in pairs(t) do 72 | table.insert(keys, k) 73 | end 74 | table.sort(keys) 75 | -- Creating list of "k": to_str(v) 76 | local items = {} 77 | for i = 1, #keys do 78 | local k = keys[i] 79 | local v = t[k] 80 | table.insert(items, "\"" .. k .. "\"" .. ":" .. to_json_str(v)) 81 | end 82 | -- Concatenate list together 83 | return '{' .. table.concat(items, ',') .. '}' 84 | else 85 | local items = {} 86 | for i, v in ipairs(t) do 87 | table.insert(items, to_json_str(v)) 88 | end 89 | return '[' .. table.concat(items, ',') .. ']' 90 | end 91 | end 92 | 93 | local TOKEN_TYPE_OP = 'op' 94 | local TOKEN_TYPE_STR = 'str' 95 | local TOKEN_TYPE_NUM = 'num' 96 | local TOKEN_TYPE_SPACE = 'space' 97 | local TOKEN_TYPE_ID = 'id' -- mostly for true/false/null 98 | 99 | local function Token(typ, val) 100 | assert(typ == TOKEN_TYPE_OP or typ == TOKEN_TYPE_STR or typ == TOKEN_TYPE_ID or typ == TOKEN_TYPE_SPACE or typ == TOKEN_TYPE_NUM, "Invalid token type") 101 | return { 102 | typ = typ, 103 | val = val 104 | } 105 | end 106 | 107 | local function ParserState(str) 108 | return { 109 | source = str, 110 | indx = 1, 111 | tokens = {} 112 | } 113 | end 114 | 115 | -- Returns true only when the source string has finished being consumed 116 | local function parser_has_more(parser_state) 117 | return parser_state.indx <= parser_state.source:len() 118 | end 119 | 120 | -- A few tests. TODO: More rigorous tests 121 | function test_json_lib() 122 | local result = "{\"escapes\":\"\\r\\n\\tf\",\"nums\":[1,2,4,5,9,12],\"other\":[true,false],\"people\":[\"john\",\"becky\"]}" 123 | local t = { 124 | ["nums"] = {1, 2, 4, 5, 9, 12}, 125 | ["people"] = {"john", "becky"}, 126 | ["escapes"] = "\r\n\tf", 127 | ["other"] = {true, false, nil} 128 | } 129 | assert(to_json_str(t) == result, "to_str test 1 failed") 130 | print("test_json_lib() tests passed") 131 | end 132 | 133 | -- Consumes a string from the ParserState and saves it in its tokens 134 | local function parse_next_str(parser_state) 135 | local indx = parser_state.indx 136 | local done = false 137 | indx = indx + 1 138 | local data = {} 139 | while indx < parser_state.source:len() and (parser_state.source:sub(indx, indx) ~= '\"' or parser_state.source:sub(indx - 1, indx - 1) == '\\') do 140 | table.insert(data, parser_state.source:sub(indx, indx)) 141 | indx = indx + 1 142 | end 143 | assert(parser_state.source:sub(indx, indx) == '\"', "Unclosed string") 144 | local raw_str = table.concat(data, '') 145 | parser_state.indx = indx + 1 146 | table.insert(parser_state.tokens, Token(TOKEN_TYPE_STR, unescape_str(raw_str))) 147 | end 148 | 149 | -- Consumes a number from the ParserState and saves it in its tokens 150 | local function parse_next_num(parser_state) 151 | local indx = parser_state.indx 152 | local data = {} 153 | while true do 154 | local ch = parser_state.source:sub(indx, indx) 155 | if ch == '.' or ch == '-' or ch:match('%d') then 156 | table.insert(data, ch) 157 | indx = indx + 1 158 | else 159 | break 160 | end 161 | end 162 | local num_str = table.concat(data, '') 163 | local num = tonumber(num_str) 164 | if num == nil then 165 | error("Invalid number" .. num_str) 166 | end 167 | parser_state.indx = indx 168 | table.insert(parser_state.tokens, Token(TOKEN_TYPE_NUM, num)) 169 | end 170 | 171 | -- Parses the next operator in the ParserState 172 | local function parse_next_op(parser_state) 173 | -- Already checked op is valid in parse_next() 174 | local ch = parser_state.source:sub(parser_state.indx, parser_state.indx) 175 | table.insert(parser_state.tokens, Token(TOKEN_TYPE_OP, ch)) 176 | parser_state.indx = parser_state.indx + 1 177 | end 178 | 179 | -- Parses the next identifier (true/false/null) in the ParserState 180 | local function parse_next_identifier(parser_state) 181 | local indx = parser_state.indx 182 | local data = {} 183 | while true do 184 | local ch = parser_state.source:sub(indx, indx) 185 | if string.match(ch, '%a') then 186 | table.insert(data, ch) 187 | indx = indx + 1 188 | else 189 | break 190 | end 191 | end 192 | parser_state.indx = indx 193 | local str = table.concat(data, '') 194 | if str ~= "true" and str ~= "false" and str ~= "null" then 195 | error("Unknown identifier") 196 | end 197 | local val = nil 198 | if str == 'true' then val = true end 199 | if str == 'false' then val = false end 200 | table.insert(parser_state.tokens, Token(TOKEN_TYPE_ID, val)) 201 | end 202 | 203 | -- Strips the whitespace from the ParserState 204 | local function parse_next_space(parser_state) 205 | while parser_state.source:sub(parser_state.indx, parser_state.indx):match('%s') do 206 | parser_state.indx = parser_state.indx + 1 207 | end 208 | end 209 | 210 | -- Gets the next token from the ParserState 211 | local function parse_next(parser_state) 212 | local ch = parser_state.source:sub(parser_state.indx, parser_state.indx) 213 | if ch == '\"' then 214 | parse_next_str(parser_state) 215 | elseif ch == '-' or ch == '.' or tonumber(ch) ~= nil then 216 | parse_next_num(parser_state) 217 | elseif ch == ':' or ch == ',' or ch == '{' or ch == '}' or ch == '[' or ch == ']' then 218 | parse_next_op(parser_state) 219 | elseif string.match(ch, '%a') then 220 | parse_next_identifier(parser_state) 221 | elseif string.match(ch, '%s') then 222 | parse_next_space(parser_state) 223 | else 224 | error("Invalid token") 225 | end 226 | end 227 | 228 | -- An object for keeping track of the document building 229 | local function DocumentTreeBuildState(tokens) 230 | return { 231 | tokens = tokens, 232 | indx = 1 233 | } 234 | end 235 | 236 | -- Checks to see what token is next 237 | local function tree_peek_tk(tree_state) 238 | return tree_state.tokens[tree_state.indx] 239 | end 240 | 241 | -- Consumes a token from the DocumentTreeBuildState 242 | local function tree_consume_tk(tree_state) 243 | local result = tree_peek_tk(tree_state) 244 | tree_state.indx = tree_state.indx + 1 245 | return result 246 | end 247 | 248 | -- Checks if any tokens remain in the DocumentTreeBuildState 249 | local function tree_has_more(tree_state) 250 | return tree_peek_tk(tree_state) ~= nil 251 | end 252 | 253 | -- Predefining construct_tree 254 | local construct_tree 255 | 256 | -- Consumes the map parts of a JSON document 257 | local function construct_tree_map(tree_state) 258 | local result = {} 259 | local done = false 260 | while not done do 261 | if tree_peek_tk(tree_state).typ == TOKEN_TYPE_STR then 262 | local key = tree_consume_tk(tree_state).val 263 | assert(tree_consume_tk(tree_state).val == ':', "Expected :") 264 | local val = construct_tree(tree_state) 265 | result[key] = val 266 | else 267 | assert("String key expected") 268 | end 269 | assert(tree_has_more(tree_state), "Expected , or } to complete {") 270 | if tree_peek_tk(tree_state).val == ',' then 271 | tree_consume_tk(tree_state) 272 | else 273 | done = true 274 | end 275 | end 276 | return result 277 | end 278 | 279 | -- Consumes the array parts of a JSON document 280 | local function construct_tree_array(tree_state) 281 | local result = {} 282 | local done = false 283 | while not done do 284 | local peek = tree_peek_tk(tree_state) 285 | if peek.typ == TOKEN_TYPE_OP then 286 | if peek.val == ']' then 287 | done = true 288 | elseif peek.val == ',' then 289 | tree_consume_tk(tree_state) 290 | elseif peek.val == '{' or peek.val == '[' then 291 | table.insert(result, construct_tree(tree_state)) 292 | else 293 | error("Expected ] or , got " .. peek.val) 294 | end 295 | else 296 | table.insert(result, tree_consume_tk(tree_state).val) 297 | end 298 | end 299 | return result 300 | end 301 | 302 | -- Consumes all other parts of a JSON document 303 | construct_tree = function(tree_state) 304 | if tree_peek_tk(tree_state).typ == TOKEN_TYPE_OP then 305 | local op = tree_consume_tk(tree_state).val 306 | if op == '{' then 307 | local result = construct_tree_map(tree_state) 308 | assert(tree_peek_tk(tree_state).val == '}', "Expected } to close {, got " .. tree_peek_tk(tree_state).typ) 309 | tree_consume_tk(tree_state) 310 | return result 311 | elseif op == '[' then 312 | local result = construct_tree_array(tree_state) 313 | assert(tree_consume_tk(tree_state).val == ']', "Expected ] to close [") 314 | return result 315 | else 316 | error('Unexpected op ' .. op) 317 | end 318 | else 319 | return tree_consume_tk(tree_state).val 320 | end 321 | end 322 | 323 | -- Takes in a string and gives back a Lua table 324 | function parse_json(str) 325 | -- Get tokens 326 | local state = ParserState(str) 327 | while parser_has_more(state) do 328 | parse_next(state) 329 | end 330 | -- Construct tree 331 | local tree_state = DocumentTreeBuildState(state.tokens) 332 | local result = construct_tree(tree_state) 333 | assert(tree_has_more(tree_state) == false, "Unexpected tokens following end of document") 334 | return result 335 | end 336 | 337 | -- Converts a Lua table into a JSON string 338 | function to_json_str(t) 339 | if t == nil then 340 | return 'null' 341 | elseif type(t) == 'table' then 342 | return tab_to_str(t) 343 | elseif type(t) == 'number' then 344 | return tostring(t) 345 | elseif type(t) == 'boolean' then 346 | return tostring(t) 347 | elseif type(t) == 'string' then 348 | return str_to_str(t) 349 | end 350 | end 351 | end 352 | -------------------------------------------------------------------------------- /tests/mymod.eqx: -------------------------------------------------------------------------------- 1 | {} -> var mod 2 | 3 | : mod.w1 1 ; 4 | : mod.w2 2 ; 5 | : mod.w3 nil ; 6 | 7 | 8 | mod return 9 | -------------------------------------------------------------------------------- /tests/mymodule.eqx: -------------------------------------------------------------------------------- 1 | { $const1 1337 2 | $const2 1991 } -> var my 3 | 4 | : my.word1 42 ; 5 | : my.word2 (: self :) self.const1 ; 6 | 7 | : my:method self.const2 ; 8 | 9 | my return 10 | -------------------------------------------------------------------------------- /tests/opt.expected: -------------------------------------------------------------------------------- 1 | [OPTI] inline neg: inlining neg 2 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 3 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 4 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 5 | [OPTI] inline general unary: if 6 | [OPTI] inline general unary: init_local 7 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 8 | [OPTI] Iteration: 1 finished. Number of optimizations: 7 9 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 10 | [OPTI] Iteration: 2 finished. Number of optimizations: 1 11 | [OPTI] binary const binary inline: inlining binary to binary operator 12 | [OPTI] Iteration: 3 finished. Number of optimizations: 1 13 | [OPTI] Iteration: 4 finished. Number of optimizations: 0 14 | [OPTI] inline general unary: assignment 15 | [OPTI] inline general unary: assignment 16 | [OPTI] inline general unary: assignment 17 | [OPTI] binary inline: inlining binary operator params 18 | [OPTI] binary inline: inlining binary operator params 19 | [OPTI] binary inline: inlining binary operator params 20 | [OPTI] binary inline: inlining binary operator params 21 | [OPTI] binary inline: inlining binary operator params 22 | [OPTI] binary inline: inlining binary operator params 23 | [OPTI] binary inline: inlining binary operator params 24 | [OPTI] binary inline: inlining binary operator params 25 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 26 | [OPTI] stackop binary inline: inlining stack op in binary 27 | [OPTI] stackop binary inline: single dup 28 | [OPTI] stackop binary inline: inlining stack op in binary 29 | [OPTI] stackop binary inline: dup dup 30 | [OPTI] stackop binary inline: inlining stack op in binary 31 | [OPTI] stackop binary inline: 2dup 32 | [OPTI] inline general unary: push 33 | [OPTI] inline general unary: push 34 | [OPTI] inline general unary: if 35 | [OPTI] inline general unary: if 36 | [OPTI] binary inline: inlining binary operator params 37 | [OPTI] binary inline: inlining binary operator params 38 | [OPTI] inline general unary: if (dup) 39 | [OPTI] inline at params: inlining tbl at params 40 | [OPTI] inline general unary: push 41 | [OPTI] inline at params: inlining tbl at params 42 | [OPTI] inline put params : inlining tbl put params 43 | [OPTI] inline at params: inlining tbl at params 44 | [OPTI] inline at p2: inlining tbl at 2nd param 45 | [OPTI] inline general unary: push (dup) 46 | [OPTI] inline general unary: if (dup) 47 | [OPTI] inline general unary: if (over) 48 | [OPTI] stackop binary inline: inlining stack op in binary 49 | [OPTI] stackop binary inline: over 50 | [OPTI] stackop binary inline: inlining stack op in binary 51 | [OPTI] stackop binary inline: over 52 | [OPTI] inline general unary: push (over) 53 | [OPTI] binary inline: inlining binary operator params 54 | [OPTI] binary inline: inlining binary operator params 55 | [OPTI] stackop binary inline: inlining stack op in binary 56 | [OPTI] stackop binary inline: dup dup 57 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 58 | [OPTI] binary inline: inlining binary operator params 59 | [OPTI] stackop binary inline: inlining stack op in binary 60 | [OPTI] stackop binary inline: dup dup 61 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 62 | [OPTI] inline neg: inlining neg 63 | [OPTI] binary inline: inlining binary operator params 64 | [OPTI] inline neg: inlining neg 65 | [OPTI] binary inline: inlining binary operator params 66 | [OPTI] inline neg: inlining neg 67 | [OPTI] binary inline: inlining binary operator params 68 | [OPTI] inline neg: inlining neg 69 | [OPTI] binary inline: inlining binary operator params 70 | [OPTI] inline neg: inlining neg 71 | [OPTI] Iteration: 1 finished. Number of optimizations: 50 72 | [OPTI] tos binary inline: inlining binary tos operand 73 | [OPTI] tos binary inline: inlining binary tos operand 74 | [OPTI] tos binary inline: inlining binary tos operand 75 | [OPTI] tos binary inline: inlining binary tos operand 76 | [OPTI] inline general unary: if 77 | [OPTI] inline general unary: if 78 | [OPTI] inline general unary: if 79 | [OPTI] binary const binary inline: inlining binary to binary operator 80 | [OPTI] tos binary inline: inlining binary tos operand 81 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 82 | [OPTI] tos binary inline: inlining binary tos operand 83 | [OPTI] inline inc: inlining inc 84 | [OPTI] inline negate binary: inlining neg binary 85 | [OPTI] inline negate binary: inlining neg binary 86 | [OPTI] inline negate binary: inlining neg binary 87 | [OPTI] inline negate binary: inlining neg binary 88 | [OPTI] Iteration: 2 finished. Number of optimizations: 16 89 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 90 | [OPTI] binary const binary inline: inlining binary to binary operator 91 | [OPTI] Iteration: 3 finished. Number of optimizations: 2 92 | [OPTI] binary const binary inline: inlining binary to binary operator 93 | [OPTI] Iteration: 4 finished. Number of optimizations: 1 94 | [OPTI] binary const binary inline: inlining binary to binary operator 95 | [OPTI] Iteration: 5 finished. Number of optimizations: 1 96 | [OPTI] Iteration: 6 finished. Number of optimizations: 0 97 | -------------------------------------------------------------------------------- /tests/opt.out: -------------------------------------------------------------------------------- 1 | [OPTI] inline neg: inlining neg 2 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 3 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 4 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 5 | [OPTI] inline general unary: if 6 | [OPTI] inline general unary: init_local 7 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 8 | [OPTI] Iteration: 1 finished. Number of optimizations: 7 9 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 10 | [OPTI] Iteration: 2 finished. Number of optimizations: 1 11 | [OPTI] binary const binary inline: inlining binary to binary operator 12 | [OPTI] Iteration: 3 finished. Number of optimizations: 1 13 | [OPTI] Iteration: 4 finished. Number of optimizations: 0 14 | [OPTI] inline general unary: assignment 15 | [OPTI] inline general unary: assignment 16 | [OPTI] inline general unary: assignment 17 | [OPTI] binary inline: inlining binary operator params 18 | [OPTI] binary inline: inlining binary operator params 19 | [OPTI] binary inline: inlining binary operator params 20 | [OPTI] binary inline: inlining binary operator params 21 | [OPTI] binary inline: inlining binary operator params 22 | [OPTI] binary inline: inlining binary operator params 23 | [OPTI] binary inline: inlining binary operator params 24 | [OPTI] binary inline: inlining binary operator params 25 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 26 | [OPTI] stackop binary inline: inlining stack op in binary 27 | [OPTI] stackop binary inline: single dup 28 | [OPTI] stackop binary inline: inlining stack op in binary 29 | [OPTI] stackop binary inline: dup dup 30 | [OPTI] stackop binary inline: inlining stack op in binary 31 | [OPTI] stackop binary inline: 2dup 32 | [OPTI] inline general unary: push 33 | [OPTI] inline general unary: push 34 | [OPTI] inline general unary: if 35 | [OPTI] inline general unary: if 36 | [OPTI] binary inline: inlining binary operator params 37 | [OPTI] binary inline: inlining binary operator params 38 | [OPTI] inline general unary: if (dup) 39 | [OPTI] inline at params: inlining tbl at params 40 | [OPTI] inline general unary: push 41 | [OPTI] inline at params: inlining tbl at params 42 | [OPTI] inline put params : inlining tbl put params 43 | [OPTI] inline at params: inlining tbl at params 44 | [OPTI] inline at p2: inlining tbl at 2nd param 45 | [OPTI] inline general unary: push (dup) 46 | [OPTI] inline general unary: if (dup) 47 | [OPTI] inline general unary: if (over) 48 | [OPTI] stackop binary inline: inlining stack op in binary 49 | [OPTI] stackop binary inline: over 50 | [OPTI] stackop binary inline: inlining stack op in binary 51 | [OPTI] stackop binary inline: over 52 | [OPTI] inline general unary: push (over) 53 | [OPTI] binary inline: inlining binary operator params 54 | [OPTI] binary inline: inlining binary operator params 55 | [OPTI] stackop binary inline: inlining stack op in binary 56 | [OPTI] stackop binary inline: dup dup 57 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 58 | [OPTI] binary inline: inlining binary operator params 59 | [OPTI] stackop binary inline: inlining stack op in binary 60 | [OPTI] stackop binary inline: dup dup 61 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 62 | [OPTI] inline neg: inlining neg 63 | [OPTI] binary inline: inlining binary operator params 64 | [OPTI] inline neg: inlining neg 65 | [OPTI] binary inline: inlining binary operator params 66 | [OPTI] inline neg: inlining neg 67 | [OPTI] binary inline: inlining binary operator params 68 | [OPTI] inline neg: inlining neg 69 | [OPTI] binary inline: inlining binary operator params 70 | [OPTI] inline neg: inlining neg 71 | [OPTI] Iteration: 1 finished. Number of optimizations: 50 72 | [OPTI] tos binary inline: inlining binary tos operand 73 | [OPTI] tos binary inline: inlining binary tos operand 74 | [OPTI] tos binary inline: inlining binary tos operand 75 | [OPTI] tos binary inline: inlining binary tos operand 76 | [OPTI] inline general unary: if 77 | [OPTI] inline general unary: if 78 | [OPTI] inline general unary: if 79 | [OPTI] binary const binary inline: inlining binary to binary operator 80 | [OPTI] tos binary inline: inlining binary tos operand 81 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 82 | [OPTI] tos binary inline: inlining binary tos operand 83 | [OPTI] inline inc: inlining inc 84 | [OPTI] inline negate binary: inlining neg binary 85 | [OPTI] inline negate binary: inlining neg binary 86 | [OPTI] inline negate binary: inlining neg binary 87 | [OPTI] inline negate binary: inlining neg binary 88 | [OPTI] Iteration: 2 finished. Number of optimizations: 16 89 | [OPTI] binary p2 inline: inlining binary operator's 2nd param 90 | [OPTI] binary const binary inline: inlining binary to binary operator 91 | [OPTI] Iteration: 3 finished. Number of optimizations: 2 92 | [OPTI] binary const binary inline: inlining binary to binary operator 93 | [OPTI] Iteration: 4 finished. Number of optimizations: 1 94 | [OPTI] binary const binary inline: inlining binary to binary operator 95 | [OPTI] Iteration: 5 finished. Number of optimizations: 1 96 | [OPTI] Iteration: 6 finished. Number of optimizations: 0 97 | -------------------------------------------------------------------------------- /tests/sout/linenum1.eqx: -------------------------------------------------------------------------------- 1 | \ throw an error 2 | : nested3 3 | ( errror comes here) 4 | nil "invalid concat" .. ; 5 | 6 | : nested2 nested3 ; 7 | : nested1 nested2 ; 8 | 9 | : babbage 10 | 1 11 | begin 12 | 1 + 13 | \ "xx" error/1 14 | dup dup * 15 | nested1 16 | 1000000 % 17 | \ call the faulty word 18 | nested3 19 | 269696 = 20 | until ; 21 | 22 | babbage 23 | -------------------------------------------------------------------------------- /tests/sout/linenum1.eqx.expected: -------------------------------------------------------------------------------- 1 | File "tests/sout/linenum1.eqx", line 4 2 | 003. ( errror comes here) 3 | =>004. nil "invalid concat" .. ; 4 | 005. 5 | File "tests/sout/linenum1.eqx", line 6 6 | 005. 7 | -------------------------------------------------------------------------------- /tests/sout/linenum1.eqx.out: -------------------------------------------------------------------------------- 1 | File "tests/sout/linenum1.eqx", line 4 2 | 003. ( errror comes here) 3 | =>004. nil "invalid concat" .. ; 4 | 005. 5 | File "tests/sout/linenum1.eqx", line 6 6 | 005. 7 | -------------------------------------------------------------------------------- /tests/sout/linenum2.eqx: -------------------------------------------------------------------------------- 1 | : my-abort ( -- ) 2 | "test" #( error 1 0 ) ; 3 | 4 | 2 3 + dup * 5 | 6 | my-abort 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/sout/linenum2.eqx.expected: -------------------------------------------------------------------------------- 1 | File "tests/sout/linenum2.eqx", line 2 2 | 001. : my-abort ( -- ) 3 | =>002. "test" #( error 1 0 ) ; 4 | 003. 5 | File "tests/sout/linenum2.eqx", line 6 6 | 005. 7 | -------------------------------------------------------------------------------- /tests/sout/linenum2.eqx.out: -------------------------------------------------------------------------------- 1 | File "tests/sout/linenum2.eqx", line 2 2 | 001. : my-abort ( -- ) 3 | =>002. "test" #( error 1 0 ) ; 4 | 003. 5 | File "tests/sout/linenum2.eqx", line 6 6 | 005. 7 | -------------------------------------------------------------------------------- /tests/sout/linenum3.eqx: -------------------------------------------------------------------------------- 1 | "tests/mymod.eqx" need -> var mymod 2 | 3 | mymod:w1 mymod:w2 + 3 =assert 4 | 5 | : myword 6 | mymod:w1 mymod:w3 * ; 7 | 8 | myword 9 | -------------------------------------------------------------------------------- /tests/sout/linenum3.eqx.expected: -------------------------------------------------------------------------------- 1 | File "tests/sout/linenum3.eqx", line 6 2 | 005. : myword 3 | =>006. mymod:w1 mymod:w3 * ; 4 | 007. 5 | File "tests/sout/linenum3.eqx", line 8 6 | 007. 7 | -------------------------------------------------------------------------------- /tests/sout/linenum3.eqx.out: -------------------------------------------------------------------------------- 1 | File "tests/sout/linenum3.eqx", line 6 2 | 005. : myword 3 | =>006. mymod:w1 mymod:w3 * ; 4 | 007. 5 | File "tests/sout/linenum3.eqx", line 8 6 | 007. 7 | -------------------------------------------------------------------------------- /tests/sout/mymod.eqx.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroflag/equinox/66b9111597d7172a752198110d9d1b21ece1782f/tests/sout/mymod.eqx.out -------------------------------------------------------------------------------- /tests/sout/prn.eqx: -------------------------------------------------------------------------------- 1 | "hello world" . cr 2 | 42 . 3 | -------------------------------------------------------------------------------- /tests/sout/prn.eqx.expected: -------------------------------------------------------------------------------- 1 | hello world 2 | 42 -------------------------------------------------------------------------------- /tests/sout/prn.eqx.out: -------------------------------------------------------------------------------- 1 | hello world 2 | 42 -------------------------------------------------------------------------------- /tests/sout/see.eqx: -------------------------------------------------------------------------------- 1 | see { 2 | see max 3 | see + 4 | -------------------------------------------------------------------------------- /tests/sout/see.eqx.expected: -------------------------------------------------------------------------------- 1 | function _c1_() 2 | push(depth()) 3 | apush(pop()) 4 | end 5 | N/A. Alias 6 | N/A. Macro (immediate word) 7 | -------------------------------------------------------------------------------- /tests/sout/see.eqx.out: -------------------------------------------------------------------------------- 1 | function _c1_() 2 | push(depth()) 3 | apush(pop()) 4 | end 5 | N/A. Alias 6 | N/A. Macro (immediate word) 7 | -------------------------------------------------------------------------------- /tests/sout/words.eqx: -------------------------------------------------------------------------------- 1 | words 2 | -------------------------------------------------------------------------------- /tests/sout/words.eqx.expected: -------------------------------------------------------------------------------- 1 | + - * / % . cr = != > >= < <= swap over rot -rot nip drop dup 2dup tuck depth pick roll adepth not and or .. >a a> {} [] size @ ! words exit return if then else begin until while repeat again case of endof endcase do loop ipairs: pairs: iter: to: step: #( -> var global ( \ alias: : :: ; recursive exec ' $ (: block end see append insert remove >str >num need type max min pow # emit assert-true assert-false =assert [ ] { } 2 | -------------------------------------------------------------------------------- /tests/sout/words.eqx.out: -------------------------------------------------------------------------------- 1 | + - * / % . cr = != > >= < <= swap over rot -rot nip drop dup 2dup tuck depth pick roll adepth not and or .. >a a> {} [] size @ ! words exit return if then else begin until while repeat again case of endof endcase do loop ipairs: pairs: iter: to: step: #( -> var global ( \ alias: : :: ; recursive exec ' $ (: block end see append insert remove >str >num need type max min pow # emit assert-true assert-false =assert [ ] { } 2 | -------------------------------------------------------------------------------- /tests/test_alias.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | alias: this dup 5 | alias: that over 6 | 7 | 3 this * 9 =assert 8 | 9 | 4 5 that 4 =assert 10 | + 9 =assert 11 | 12 | alias: ADD + 13 | alias: MUL * 14 | 15 | 2 3 ADD 4 MUL 20 =assert 16 | 17 | alias: type #( type 1 ) 18 | 19 | "str" type "string" =assert 20 | 21 | alias: sub1 #( string.sub 2 ) 22 | alias: sub2 #( string.sub 3 ) 23 | 24 | "Hello, Lua" -> var s 25 | 26 | s 1 5 sub2 "Hello" =assert 27 | s 8 sub1 "Lua" =assert 28 | 29 | alias: ADD2 ADD 30 | 31 | 4 5 ADD2 9 =assert 32 | 33 | -1 -9 max -1 =assert 34 | -1 -9 min -9 =assert 35 | 36 | alias: cons 12 37 | cons 12 =assert 38 | 39 | alias: xx 10 20 40 | xx 41 | 20 =assert 42 | 10 =assert 43 | 44 | depth 0 =assert 45 | adepth 0 =assert 46 | -------------------------------------------------------------------------------- /tests/test_begin_again.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : begin-again-test 5 | 0 6 | begin 7 | dup 5 < if 1 + else dup * exit then 8 | again ; 9 | 10 | begin-again-test 25 =assert 11 | 12 | : begin-again-test-2 13 | 0 begin dup 6 < if 1 + else dup * exit then again ; 14 | 15 | begin-again-test-2 16 | 36 =assert 17 | 18 | depth 0 =assert 19 | adepth 0 =assert 20 | -------------------------------------------------------------------------------- /tests/test_begin_until.eqx: -------------------------------------------------------------------------------- 1 | adepth 0 =assert 2 | depth 0 =assert 3 | 4 | 2 10 begin 1 - swap 2 * swap dup 0 = until drop 2048 =assert 5 | 6 | : tst 7 | 2 10 8 | begin 9 | 1 - swap 2 * swap dup 10 | 0 = until drop ; 11 | 12 | tst 2048 =assert 13 | 14 | adepth 0 =assert 15 | depth 0 =assert 16 | -------------------------------------------------------------------------------- /tests/test_begin_while_repeat.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : factorial ( n -- n! ) 5 | 1 2 rot 6 | begin 7 | 2dup <= 8 | while 9 | -rot tuck 10 | * swap 11 | 1 + rot 12 | repeat 13 | drop drop ; 14 | 15 | 8 factorial 40320 =assert 16 | 17 | 0 begin dup 5 < while 1 + repeat 5 =assert 18 | 19 | 20 | depth 0 =assert 21 | adepth 0 =assert 22 | -------------------------------------------------------------------------------- /tests/test_block.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | 10 -> var x 5 | 20 -> var y 6 | 7 | block 8 | 30 -> var x 9 | x 30 =assert 10 | y 20 =assert 11 | end 12 | 13 | x 10 =assert 14 | y 20 =assert 15 | 16 | depth 0 =assert 17 | adepth 0 =assert 18 | -------------------------------------------------------------------------------- /tests/test_case.eqx: -------------------------------------------------------------------------------- 1 | adepth 0 =assert 2 | depth 0 =assert 3 | 4 | : test-case ( n -- strr ) 5 | case 6 | 1 of "monday" endof 7 | 2 of "tuesday" endof 8 | 3 of 3 5 + endof 9 | 4 of "thursday" endof 10 | 5 of 0 11 1 do i + loop endof 11 | 6 of "saturday" endof 12 | 7 of "sun" "day" .. endof 13 | "unknown day " swap .. 14 | endcase ; 15 | 16 | 0 test-case "unknown day 0" =assert 17 | 1 test-case "monday" =assert 18 | 2 test-case "tuesday" =assert 19 | 3 test-case 8 =assert 20 | 4 test-case "thursday" =assert 21 | 5 test-case 55 =assert 22 | 6 test-case "saturday" =assert 23 | 7 test-case "sunday" =assert 24 | 8 test-case "unknown day 8" =assert 25 | 26 | 2 4 + 27 | case 28 | 0 of "no" endof 29 | 3 dup + of "ok" endof 30 | endcase 31 | 32 | "ok" =assert 33 | 34 | adepth 0 =assert 35 | depth 0 =assert 36 | -------------------------------------------------------------------------------- /tests/test_comment.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | \ 1 2 + 5 | \ 1 1 * 6 | \ 3 4 + 7 | 3 2 + 8 | \ this is a comment 9 | 5 =assert 10 | depth 0 =assert 11 | 12 | ( 1 2 + 13 | 1 1 * 14 | 3 4 + ) 15 | 3 2 + 16 | ( this is a comment) 17 | 5 =assert 18 | 19 | \ 20 | 1 21 | 1 =assert 22 | 23 | depth 0 =assert 24 | adepth 0 =assert 25 | -------------------------------------------------------------------------------- /tests/test_compiler.lua: -------------------------------------------------------------------------------- 1 | local Compiler = require("compiler") 2 | local Optimizer = require("ast_optimizer") 3 | local CodeGen = require("codegen") 4 | local stack = require("stack") 5 | 6 | local compiler = Compiler:new(Optimizer:new(), CodeGen:new()) 7 | 8 | function assert_tos(result, code) 9 | compiler:eval_text(code) 10 | assert(depth() == 1, 11 | "'" .. code .. "' depth: " .. depth()) 12 | assert(tos() == result, 13 | "'" .. code .. "' " .. tostring(tos()) 14 | .. " <> " .. tostring(result)) 15 | pop() 16 | end 17 | 18 | assert_tos(2, "1 2 #( math.max 2 )") 19 | -------------------------------------------------------------------------------- /tests/test_core.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | 0 1 - -1 =assert 5 | -10 1 + -9 =assert 6 | -10 4 < assert-true 7 | -10 -4 < assert-true 8 | 324 12 > assert-true 9 | -24 -212 > assert-true 10 | 24 -2 > assert-true 11 | 1 1 - 0 =assert 12 | -1 1 + 0 =assert 13 | 14 | 1 2 + 3 =assert 15 | 0 33 + 33 =assert 16 | -2 -4 + -6 =assert 17 | -21 4 + -17 =assert 18 | 145 -5 + 140 =assert 19 | 3.3 2.5 + 5.8 =assert 20 | 21 | 2 1 - 1 =assert 22 | 0 33 - -33 =assert 23 | -2 -4 - 2 =assert 24 | -21 4 - -25 =assert 25 | 145 -5 - 150 =assert 26 | 11.5 1.5 - 10 =assert 27 | 28 | 2 3 * 6 =assert 29 | 0 33 * 0 =assert 30 | -2 -4 * 8 =assert 31 | 100 -3 * -300 =assert 32 | 2.5 4 * 10 =assert 33 | 2.5 2.5 * 6.25 =assert 34 | 35 | 10 2 / 5 =assert 36 | 2 10 / 0.2 =assert 37 | -4 -2 / 2 =assert 38 | 6 -3 / -2 =assert 39 | 10 4 / 2.5 =assert 40 | 2.5 2.5 / 1 =assert 41 | 42 | 10 3 % 1 =assert 43 | 10 4 % 2 =assert 44 | 10 13 % 10 =assert 45 | 10 5 % 0 =assert 46 | 47 | 5 5 =assert 48 | 6 5 = assert-false 49 | -6 -6 =assert 50 | 6 -6 = assert-false 51 | nil nil = assert-true 52 | nil "nil" = assert-false 53 | 54 | 5 5 != assert-false 55 | 6 5 != assert-true 56 | -6 -6 != assert-false 57 | 6 -6 != assert-true 58 | nil 6 != assert-true 59 | nil nil != assert-false 60 | 61 | 5 5 < assert-false 62 | 6 5 < assert-false 63 | -6 -6 < assert-false 64 | 6 -6 < assert-false 65 | 3 5 < assert-true 66 | -1 5 < assert-true 67 | -6 -2 < assert-true 68 | 69 | 5 5 > assert-false 70 | 6 5 > assert-true 71 | -6 -6 > assert-false 72 | 6 -6 > assert-true 73 | 3 5 > assert-false 74 | -1 5 > assert-false 75 | -6 -2 > assert-false 76 | 77 | 5 5 <= assert-true 78 | 6 5 <= assert-false 79 | -6 -6 <= assert-true 80 | 6 -6 <= assert-false 81 | 3 5 <= assert-true 82 | -1 5 <= assert-true 83 | -6 -2 <= assert-true 84 | 85 | 5 5 >= assert-true 86 | 6 5 >= assert-true 87 | -6 -6 >= assert-true 88 | 6 -6 >= assert-true 89 | 3 5 >= assert-false 90 | -1 5 >= assert-false 91 | -6 -2 >= assert-false 92 | 93 | false assert-false 94 | true assert-true 95 | true not not assert-true 96 | false not not not assert-true 97 | 98 | true true and assert-true 99 | true false and assert-false 100 | false true and assert-false 101 | false false and assert-false 102 | 103 | true true or assert-true 104 | true false or assert-true 105 | false true or assert-true 106 | false false or assert-false 107 | 108 | 5 dup + 10 =assert 109 | 7 10 swap - 3 =assert 110 | 111 | 1 2 over - + 2 =assert 112 | 3 4 over 113 | 3 =assert 114 | 4 =assert 115 | 3 =assert 116 | 117 | 1 2 nip 118 | 2 =assert 119 | depth 0 =assert 120 | 121 | 42 122 | 1 2 3 rot ( 2 3 1 ) 123 | 1 =assert 124 | 3 =assert 125 | 2 =assert 126 | 42 =assert 127 | 128 | 42 129 | 1 2 3 -rot ( 3 1 2 ) 130 | 2 =assert 131 | 1 =assert 132 | 3 =assert 133 | 42 =assert 134 | 135 | 1 2 tuck ( 2 1 2 ) 136 | 2 =assert 137 | 1 =assert 138 | 2 =assert 139 | 140 | 1 2 2dup ( 1 2 1 2 ) 141 | 2 =assert 142 | 1 =assert 143 | 2 =assert 144 | 1 =assert 145 | 146 | : tst 42 ; tst 42 =assert 147 | : dbl dup + ; 3 dbl 6 =assert 148 | 149 | : *2 2 * ; 10 *2 20 =assert 150 | : /2 2 / ; 10 /2 5 =assert 151 | 152 | 1 2 >a 3 a> - - 0 =assert 153 | 154 | nil >a a> nil =assert 155 | 156 | 2 8 pow 256 =assert 157 | 158 | 10 20 159 | 30 40 + 160 | 70 =assert 161 | 20 =assert 162 | 10 =assert 163 | 164 | 50 60 165 | true if 65 then 166 | 65 =assert 167 | 60 =assert 168 | 50 =assert 169 | 170 | true 171 | true not 172 | assert-false 173 | assert-true 174 | 175 | 1 2 3 176 | depth 3 =assert 177 | 178 | 0 pick 179 | 180 | depth 4 =assert 181 | 3 =assert 182 | 183 | 1 pick 184 | depth 4 =assert 185 | 2 =assert 186 | 187 | 2 pick 188 | depth 4 =assert 189 | 1 =assert 190 | 191 | 3 =assert 192 | 2 =assert 193 | 1 =assert 194 | 195 | 1 2 3 4 196 | 3 roll 197 | 1 =assert 198 | 4 =assert 199 | 3 =assert 200 | 2 =assert 201 | 202 | 1 2 203 | 1 roll 204 | 1 =assert 205 | 2 =assert 206 | 207 | 42 208 | 1 2 3 209 | 2 roll 210 | 1 =assert 211 | 3 =assert 212 | 2 =assert 213 | 42 =assert 214 | 215 | 0 roll 216 | 217 | 1 2 3 4 rot >a swap over >a rot swap a> swap >a swap a> rot a> swap 218 | 4 =assert 219 | 2 =assert 220 | 3 =assert 221 | 1 =assert 222 | 4 =assert 223 | 224 | depth 0 =assert 225 | adepth 0 =assert 226 | -------------------------------------------------------------------------------- /tests/test_do_loop.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | 0 5 1 do i + loop 10 =assert 5 | 0 11 1 do i + loop 55 =assert 6 | 7 | 0 8 | 8 2 do 9 | 9 3 do 10 | i j + + 11 | loop 12 | loop 13 | 360 =assert 14 | 15 | 42424242 10005 10000 do i loop 16 | 10004 =assert 10003 =assert 10002 =assert 10001 =assert 10000 =assert 17 | 42424242 =assert 18 | 19 | var sum 20 | 0 -> sum 21 | 5 0 do 22 | 5 0 do 23 | 5 0 do 24 | i j * k * sum + -> sum 25 | loop 26 | loop 27 | loop 28 | 29 | 1000 sum =assert 30 | 31 | depth 0 =assert 32 | adepth 0 =assert 33 | -------------------------------------------------------------------------------- /tests/test_env.lua: -------------------------------------------------------------------------------- 1 | local Env = require("env") 2 | 3 | local root = Env:new(nil, "root") 4 | 5 | local child1 = Env:new(root, "child1") 6 | local child2 = Env:new(root, "child2") 7 | 8 | local child2_child = Env:new(child2, "child2_child") 9 | 10 | --[[ 11 | root 12 | / \ 13 | c1 c2 14 | \ 15 | c2c 16 | ]]-- 17 | 18 | root:def_var("rv1", "rv1") 19 | child1:def_var("c1v", "c1v") 20 | child2:def_var("c2v", "c2v") 21 | child2_child:def_var("c2c", "c2c") 22 | 23 | assert(root:has_var("rv1")) 24 | assert(child1:has_var("rv1")) 25 | assert(child2:has_var("rv1")) 26 | assert(child2_child:has_var("rv1")) 27 | 28 | assert(not root:has_var("c1v")) 29 | assert(child1:has_var("c1v")) 30 | assert(not child2:has_var("c1v")) 31 | assert(not child2_child:has_var("c1v")) 32 | 33 | assert(not root:has_var("c2v")) 34 | assert(not child1:has_var("c2v")) 35 | assert(child2:has_var("c2v")) 36 | assert(child2_child:has_var("c2v")) 37 | 38 | assert(not root:has_var("c2c")) 39 | assert(not child1:has_var("c2c")) 40 | assert(not child2:has_var("c2c")) 41 | assert(child2_child:has_var("c2c")) 42 | 43 | assert(#root:var_names() == 1) 44 | assert(#child1:var_names() == 2) 45 | assert(#child2:var_names() == 2) 46 | assert(#child2_child:var_names() == 3) 47 | -------------------------------------------------------------------------------- /tests/test_for.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | var sum 5 | 0 -> sum 6 | 7 | 1 10 to: k 8 | sum k + -> sum 9 | end 10 | 11 | 55 sum =assert 12 | 13 | 42 14 | 10 1 to: k 15 | 9999 16 | end 17 | 42 =assert 18 | 19 | 100 100 to: k k end 20 | 100 =assert 21 | 22 | ( 1 3 5 7 9 ) 23 | 0 -> sum 24 | 1 10 2 step: k k sum + -> sum end 25 | sum 25 =assert 26 | 27 | 0 28 | 10 1 -1 step: i 29 | i + 30 | end 31 | 55 =assert 32 | 33 | 42 34 | 10 20 -1 step: j 35 | 9999 36 | end 37 | 42 =assert 38 | 39 | 10 10 -1 step: j j end 40 | 10 =assert 41 | 42 | depth 0 =assert 43 | adepth 0 =assert 44 | -------------------------------------------------------------------------------- /tests/test_for_each.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | 0 -> var sum1 5 | 0 -> var sum2 6 | 7 | [ 4 2 3 ] ipairs: index item 8 | sum1 item + -> sum1 9 | sum2 index + -> sum2 10 | end 11 | 12 | sum1 9 =assert 13 | sum2 6 =assert 14 | 15 | depth 0 =assert 16 | adepth 0 =assert 17 | 18 | 0 -> sum1 19 | 0 -> sum2 20 | 21 | { 2 -2 3 -3 5 -5 } pairs: key val 22 | sum1 key + -> sum1 23 | sum2 val + -> sum2 24 | end 25 | 26 | sum1 10 =assert 27 | sum2 -10 =assert 28 | 29 | depth 0 =assert 30 | adepth 0 =assert 31 | -------------------------------------------------------------------------------- /tests/test_for_iter.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | alias: gmatch #( string.gmatch 2 ) 5 | 6 | 0 7 | "1,2,3,4,5" "[^,]+" gmatch iter: each 8 | each >num + 9 | end 10 | 11 | 15 =assert 12 | 13 | depth 0 =assert 14 | adepth 0 =assert 15 | -------------------------------------------------------------------------------- /tests/test_hyperstat.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : to-be-redefined "original" ; 5 | 6 | : tst to-be-redefined ; 7 | 8 | tst "original" =assert 9 | 10 | : to-be-redefined "redefined" ; 11 | 12 | : tst2 to-be-redefined ; 13 | 14 | : to-be-redefined "redefined again" ; 15 | 16 | tst "original" =assert 17 | tst2 "redefined" =assert 18 | 19 | depth 0 =assert 20 | adepth 0 =assert 21 | -------------------------------------------------------------------------------- /tests/test_if.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | 1 2 < if 8 then 8 =assert 5 | 1 2 > if 8 else 4 then 4 =assert 6 | 7 | : abso dup 0 < if -1 * then ; 8 | 9 | 0 abso 0 =assert 10 | -1 abso 1 =assert 11 | -42.24 abso 42.24 =assert 12 | 1 abso 1 =assert 13 | 123.64 abso 123.64 =assert 14 | 15 | : tst 3 4 < if 21 dup + -1 * abso else 40 -1 * then ; 16 | tst 42 =assert 17 | 18 | depth 0 =assert 19 | adepth 0 =assert 20 | -------------------------------------------------------------------------------- /tests/test_interop.lua: -------------------------------------------------------------------------------- 1 | local interop = require("interop") 2 | 3 | assert(interop.resolve_lua_func("math.min")) 4 | assert(interop.resolve_lua_func("math.max")) 5 | assert(interop.resolve_lua_func("string.len")) 6 | 7 | assert(not interop.resolve_lua_func("non_existent")) 8 | -------------------------------------------------------------------------------- /tests/test_luacall.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | 2 8 #( math.pow 2 ) 256 =assert 5 | 2 10 #( math.pow 2 ) 1024 =assert 6 | 7 | 502 1002 #( math.min 2 ) 502 =assert 8 | 502 1002 #( math.max 2 ) 1002 =assert 9 | 502 1002 #( math.min 2 ) 502 =assert 10 | 11 | math.pi #( math.cos 1 ) -1 =assert 12 | math $pi @ #( math.cos 1 ) -1 =assert 13 | 14 | "55" #( tonumber 1 ) 55 =assert 15 | 16 #( math.sqrt 1 ) 4 =assert 16 | 17 | "abc" -> var s 18 | s:upper "ABC" =assert 19 | 20 | #( os.time ) type "number" =assert 21 | 22 | { $K { $K2 "test string" } } -> var t 23 | 24 | t.K.K2:upper "TEST STRING" =assert 25 | t.K.K2:upper:lower "test string" =assert 26 | 27 | 1 3 #( t.K.K2:sub 2 ) "tes" =assert 28 | 29 | \ XXX only works with additional () 30 | 1 3 #( t.K.K2:upper():sub 2 ) "TES" =assert 31 | 32 | depth 0 =assert 33 | adepth 0 =assert 34 | -------------------------------------------------------------------------------- /tests/test_luacallback.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : callback0 (: :) dup * ; 5 | : callback1 (: myparam :) myparam -1 * ; 6 | : callback2 (: x y :) x y - ; 7 | : callback3 (: a b c :) a b + c * ; 8 | 9 | 3 callback0 9 =assert 10 | 42 #( callback1 1 ) -42 =assert 11 | 20 13 #( callback2 2 ) 7 =assert 12 | 2 3 4 #( callback3 3 ) 20 =assert 13 | 14 | depth 0 =assert 15 | adepth 0 =assert 16 | -------------------------------------------------------------------------------- /tests/test_module.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | "tests/mymodule.eqx" need -> var mymodule 5 | 6 | #( mymodule.word1 0 ) 42 =assert 7 | mymodule:word2 1337 =assert 8 | 9 | mymodule.const1 1337 =assert 10 | mymodule.const2 1991 =assert 11 | 12 | mymodule:method 1991 =assert 13 | 14 | depth 0 =assert 15 | adepth 0 =assert 16 | -------------------------------------------------------------------------------- /tests/test_nil.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | nil dup = assert-true 5 | 6 | depth 0 =assert 7 | adepth 0 =assert 8 | -------------------------------------------------------------------------------- /tests/test_optimizer.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 9999 4 | 5 | \ assignment param inline 6 | var c 5 -> c 7 | var d 2 -> d 8 | var verus true -> verus 9 | var tbl [ "x" 1 ] -> tbl 10 | 11 | \ bin op inline [const-const] 12 | 50 30 + 80 =assert 13 | 20 15 - 5 =assert 14 | 15 | \ bin op inline [const var] 16 | 30 c + 35 =assert 17 | 20 c - 15 =assert 18 | 19 | \ bin op inline [var const] 20 | c 30 + 35 =assert 21 | c 2 - 3 =assert 22 | 23 | \ bin op inline [var var] 24 | c d + 7 =assert 25 | c d - 3 =assert 26 | 27 | \ bin op inline 2nd param 28 | 10 dup 7 - 3 =assert 29 | 10 =assert 30 | 31 | \ dup binop 32 | 4 dup * 16 =assert 33 | 34 | \ dup dup binop 35 | 3 dup dup * 9 =assert 36 | 3 =assert 37 | 38 | 10 7 2dup - 3 =assert 39 | 7 =assert 40 | 10 =assert 41 | 42 | \ unary op inline 43 | true 44 | false not assert-true 45 | verus not assert-false 46 | assert-true 47 | 48 | \ if condition inline 49 | false 50 | true if "yes" then "yes" =assert 51 | false if "no" else "yes" then "yes" =assert 52 | assert-false 53 | 54 | 1 2 < if "yes" then "yes" =assert 55 | 1 2 < dup if "yes" then "yes" =assert assert-true 56 | tbl 1 @ if "yes" then "yes" =assert 57 | 58 | verus not if "ok then" then 59 | 60 | \ inline tbl @ param 61 | tbl 1 @ "x" =assert 62 | 63 | \ inline tbl put params 64 | tbl 1 "y" ! 65 | tbl 1 @ "y" =assert 66 | 67 | \ inline table @ param2 68 | tbl 69 | dup 1 @ "y" =assert 70 | tbl =assert 71 | 72 | \ inline dup before unary 73 | "abcd" 74 | dup size 4 =assert 75 | "abcd" =assert 76 | 77 | \ inline dup before ip 78 | true 79 | dup if "yes" then 80 | "yes" =assert 81 | assert-true 82 | 83 | true 3 over if then 84 | 3 =assert 85 | assert-true 86 | 87 | 3 7 over - ( 3 7 3 -) 88 | 4 =assert 89 | 3 =assert 90 | 91 | 3 7 over + ( 3 7 3 + ) 92 | 10 =assert 93 | 3 =assert 94 | 95 | true false over not ( true false true not ) 96 | assert-false 97 | assert-false 98 | assert-true 99 | 100 | 1 2 < 3 4 1 - > 101 | assert-false 102 | assert-true 103 | 104 | 25264 105 | dup dup * 106 | 1000000 % 107 | 269696 = 108 | assert-true drop 109 | 110 | 10 5 % 0 = 111 | assert-true 112 | 113 | 3 dup dup * 9 =assert 114 | 3 =assert 115 | 116 | : ++ 1 + ; 117 | 118 | 3 ++ 4 =assert 119 | 120 | : tst not ; 121 | false tst assert-true 122 | 123 | 2 3 = not assert-true 124 | 3 3 = not assert-false 125 | 4 5 < not assert-false 126 | 4 5 > not assert-true 127 | 128 | 9999 =assert 129 | depth 0 =assert 130 | adepth 0 =assert 131 | -------------------------------------------------------------------------------- /tests/test_override.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : myword 100 ; 5 | : myword 50 myword + ; 6 | 150 myword =assert 7 | 8 | depth 0 =assert 9 | adepth 0 =assert 10 | -------------------------------------------------------------------------------- /tests/test_parser.lua: -------------------------------------------------------------------------------- 1 | local json = require("tests/json") 2 | local Parser = require("parser") 3 | 4 | function parse(text) 5 | local parser = Parser:new(text) 6 | return parser:parse_all() 7 | end 8 | 9 | function assert_table(t1, t2) 10 | local j1 = to_json_str(t1) 11 | local j2 = to_json_str(t2) 12 | assert(j1 == j2, "\nT1=" .. j1 .. "\nT2=" .. j2) 13 | end 14 | 15 | assert_table({} , parse("")) 16 | 17 | assert_table({{token = "1", kind = "number", line_number=1}}, parse("1")) 18 | assert_table({{token = "dup", kind = "word", line_number=1}}, parse("dup")) 19 | 20 | assert_table( 21 | {{token = '"test string"', kind = "string", line_number=1}}, 22 | parse('"test string"')) 23 | 24 | assert_table( 25 | {{token = "$sym", kind = "symbol", line_number=1}}, 26 | parse('$sym')) 27 | 28 | assert_table( 29 | {{token = "math.min/2", kind = "word", line_number=1}}, 30 | parse("math.min/2")) 31 | 32 | assert_table( 33 | {{token = "math.min!2", kind = "word", line_number=1}}, 34 | parse("math.min!2")) 35 | 36 | assert_table( 37 | {{token = "math.min", kind = "word", line_number=1}}, 38 | parse("math.min")) 39 | 40 | assert_table( 41 | {{token = "obj:method/3", kind = "word", line_number=1}}, 42 | parse("obj:method/3")) 43 | 44 | assert_table( 45 | {{token = "obj:method!3", kind = "word", line_number=1}}, 46 | parse("obj:method!3")) 47 | 48 | assert_table( 49 | {{token = "obj:method", kind = "word", line_number=1}}, 50 | parse("obj:method")) 51 | 52 | assert_table( 53 | {{token = "tbl1.key1@", kind = "word", line_number=1}}, 54 | parse("tbl1.key1@")) 55 | 56 | assert_table( 57 | {{token = "tbl1.key1@", kind = "word", line_number=1}}, 58 | parse("tbl1.key1@")) 59 | 60 | assert_table( 61 | {{token = "math.pi@", kind = "word", line_number=1}}, 62 | parse("math.pi@")) 63 | 64 | assert_table( 65 | {{token = "myword", kind = "word", line_number=1}}, 66 | parse("myword")) 67 | 68 | assert_table({ 69 | { token = "1", kind = "number", line_number=1}, 70 | { token = "2", kind = "number", line_number=1}, 71 | { token = "+", kind = "word", line_number=1 } 72 | }, parse("1 2 +")) 73 | 74 | assert_table({ 75 | { token = ":", kind = "word", line_number=1 }, 76 | { token = "double", kind = "word", line_number=1 }, 77 | { token = "dup", kind = "word", line_number=1 }, 78 | { token = "+", kind = "word", line_number=1 }, 79 | { token = ";", kind = "word", line_number=1 } 80 | }, parse(": double dup + ;")) 81 | 82 | -- line number tests 83 | assert_table({ 84 | { token = "1", kind = "number", line_number=1}, 85 | { token = "2", kind = "number", line_number=2}, 86 | { token = "+", kind = "word", line_number=3 } 87 | }, parse("1\n2\n+")) 88 | 89 | assert_table({ 90 | { token = "1", kind = "number", line_number=1}, 91 | { token = "2", kind = "number", line_number=2}, 92 | { token = "+", kind = "word", line_number=3 } 93 | }, parse("1\n2\n+\n")) 94 | 95 | assert_table({ 96 | { token = "123", kind = "number", line_number=1}, 97 | { token = "456", kind = "number", line_number=1}, 98 | { token = "678", kind = "number", line_number=2}, 99 | }, parse("123 456\n678\n")) 100 | 101 | assert_table({ 102 | { token = "123", kind = "number", line_number=3}, 103 | { token = "456", kind = "number", line_number=3}, 104 | { token = "678", kind = "number", line_number=6}, 105 | }, parse("\n\n123 456\n\n\n678")) 106 | 107 | assert_table({ 108 | { token = '"a\\nb c"', kind = "string", line_number=3}, 109 | { token = '"d e f"', kind = "string", line_number=3}, 110 | { token = "678", kind = "number", line_number=6}, 111 | }, parse("\n\n \"a\\nb c\" \"d e f\" \n\n\n678")) 112 | 113 | assert_table( 114 | { {token = '"\\\\"', kind = "string", line_number=1} }, 115 | parse('"\\\\"')) 116 | 117 | assert_table({ 118 | { token = '"\\\\"', kind = "string", line_number=1 }, 119 | { token = '4', kind = "number", line_number=1 } 120 | }, parse('"\\\\" 4')) 121 | -------------------------------------------------------------------------------- /tests/test_recurse.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : factorial ( n -- n! ) recursive 5 | dup 2 > if dup 1 - factorial * then ; 6 | 7 | 5 factorial 120 =assert 8 | 6 factorial 720 =assert 9 | 10 factorial 3628800 =assert 10 | 15 factorial 1307674368000 =assert 11 | 12 | depth 0 =assert 13 | adepth 0 =assert 14 | 15 | 15 factorial 1307674368000 =assert 16 | 17 | depth 0 =assert 18 | adepth 0 =assert 19 | -------------------------------------------------------------------------------- /tests/test_scope1.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | 10 -> var glob 5 | 6 | block 7 | 20 -> var blk1 8 | 50 -> var shadow 9 | 10 glob =assert 10 | block 11 | 20 blk1 =assert 12 | 30 -> var blk2 13 | 60 -> var shadow 14 | 11 -> glob 15 | 2 2 to: i 16 | 3 3 -1 step: j 17 | 30 blk2 =assert 18 | i j * -> blk1 19 | end 20 | ( nil j =assert < g 7 | 10 g =assert 8 | block 9 | 11 -> g 10 | 41 -> global g2 11 | end 12 | 11 g =assert 13 | 50 -> var g2 \ shadow g2 14 | 50 g2 =assert 15 | end 16 | 17 | 11 g =assert 18 | 41 g2 =assert 19 | 20 | depth 0 =assert 21 | adepth 0 =assert 22 | -------------------------------------------------------------------------------- /tests/test_smoke.lua: -------------------------------------------------------------------------------- 1 | local utils = require("utils") 2 | 3 | local coverage = "false" 4 | if os.getenv("ENABLE_COV") and 5 | utils.module_available("luacov") 6 | then 7 | coverage = "true" 8 | end 9 | 10 | local command = string.format( 11 | 'echo ": tst\n7 3 *\ndup +\n; tst .\nbye" | make repl coverage="%s" | grep "OK"', 12 | coverage 13 | ) 14 | 15 | local handle = io.popen(command) 16 | local result = handle:read("*a") 17 | handle:close() 18 | assert(result:match("%D*42%D*")) 19 | -------------------------------------------------------------------------------- /tests/test_stack.lua: -------------------------------------------------------------------------------- 1 | local stack = require("stack") 2 | 3 | assert(depth() == 0) 4 | 5 | push("a") 6 | assert(depth() == 1) 7 | push("b") 8 | assert(depth() == 2) 9 | 10 | assert(tos() == "b") 11 | assert(tos2() == "a") 12 | 13 | assert(pop() == "b") 14 | assert(depth() == 1) 15 | assert(tos() == "a") 16 | 17 | assert(pop() == "a") 18 | assert(depth() == 0) 19 | 20 | push("xx") 21 | push("p1") 22 | push("p2") 23 | push("p3") 24 | 25 | assert(depth() == 4) 26 | 27 | local p3, p2, p1 = pop(), pop(), pop() 28 | 29 | assert(p1 == "p1") 30 | assert(p2 == "p2") 31 | assert(p3 == "p3") 32 | 33 | assert(pop() == "xx") 34 | assert(depth() == 0) 35 | 36 | push(nil) 37 | assert(depth() == 1) 38 | assert(nil == pop()) 39 | assert(depth() == 0) 40 | 41 | push(1) 42 | push(2) 43 | push(3) 44 | assert(depth() == 3) 45 | assert(pop2nd() == 2) 46 | assert(depth() == 2) 47 | assert(pop() == 3) 48 | assert(pop() == 1) 49 | 50 | assert(depth() == 0) 51 | 52 | push(1) 53 | push(2) 54 | push(3) 55 | push(4) 56 | assert(depth() == 4) 57 | assert(pop3rd() == 2) 58 | assert(depth() == 3) 59 | assert(pop() == 4) 60 | assert(pop() == 3) 61 | assert(pop() == 1) 62 | 63 | assert(depth() == 0) 64 | 65 | push_many(1, 2, 3) 66 | assert(depth() == 3) 67 | assert(pop() == 3) 68 | assert(pop() == 2) 69 | assert(pop() == 1) 70 | 71 | push_many(nil, 2) 72 | assert(depth() == 2) 73 | assert(pop() == 2) 74 | assert(pop() == nil) 75 | 76 | function multi_return() 77 | return 4, 5 78 | end 79 | 80 | push_many(multi_return()) 81 | assert(depth() == 2) 82 | assert(pop() == 5) 83 | assert(pop() == 4) 84 | 85 | push(1) 86 | push(2) 87 | push(3) 88 | 89 | assert(3 == pick(0)) 90 | assert(2 == pick(1)) 91 | assert(1 == pick(2)) 92 | 93 | assert(pop() == 3) 94 | assert(pop() == 2) 95 | assert(pop() == 1) 96 | 97 | assert(depth() == 0) 98 | -------------------------------------------------------------------------------- /tests/test_string.eqx: -------------------------------------------------------------------------------- 1 | adepth 0 =assert 2 | depth 0 =assert 3 | 4 | alias: slen #( string.len 1 ) 5 | 6 | $xyz slen 3 =assert 7 | 8 | "asdf" slen 4 =assert 9 | "asdf jkle" slen 9 =assert 10 | "asdf jkle " slen 10 =assert 11 | " asdf jkle" slen 10 =assert 12 | " asdf jkle " slen 11 =assert 13 | "" slen 0 =assert 14 | " " slen 1 =assert 15 | " " slen 2 =assert 16 | " asdf jkle " slen 14 =assert 17 | 18 | " abc " " xyz " .. " abc xyz " =assert 19 | 20 | var str 21 | "abc" -> str 22 | str:upper "ABC" =assert 23 | str:len 3 =assert 24 | str:reverse "cba" =assert 25 | 26 | "2@4" -> str 27 | "(%d)@%d" #( str:match 1 ) 28 | "2" =assert 29 | 30 | "2@4@8" -> str 31 | "(%d)@(%d)@(%d)" #( str:match 1 ) 32 | 33 | "8" =assert 34 | "4" =assert 35 | "2" =assert 36 | 37 | str:len 5 =assert 38 | 39 | "string\twith \n escaped characters" slen 32 =assert 40 | "str with \"quotes\"" slen 17 =assert 41 | "\"" slen 1 =assert 42 | "\"\"" slen 2 =assert 43 | 44 | adepth 0 =assert 45 | depth 0 =assert 46 | -------------------------------------------------------------------------------- /tests/test_table.eqx: -------------------------------------------------------------------------------- 1 | ( sequential tables ) 2 | 3 | [ ] size 0 =assert 4 | [ 1 ] size 1 =assert 5 | 6 | [ 3 5 7 "apple" [ 4 5 ] "banana" ] size 6 =assert 7 | 8 | : make-table [ 99 7 [ 9 6 ] "orange" $grapes ] ; 9 | 10 | make-table size 5 =assert 11 | make-table 1 @ 99 =assert 12 | make-table 2 @ 7 =assert 13 | make-table 3 @ 1 @ 9 =assert 14 | make-table 3 @ 2 @ 6 =assert 15 | make-table 4 @ "orange" =assert 16 | make-table 5 @ $grapes =assert 17 | 18 | var tbl 19 | [ 1 2 3 4 5 ] -> tbl 20 | tbl size 5 =assert 21 | tbl size 1 + 1 do tbl i @ i =assert loop 22 | 23 | tbl 1 0 insert 24 | 25 | tbl 6 append 26 | tbl 7 @ 6 =assert 27 | tbl 1 @ 0 =assert 28 | 29 | [ "a" ] -> tbl 30 | tbl 1 "b" insert \ [ "b" "a" ] 31 | tbl 1 @ "b" =assert 32 | tbl 2 @ "a" =assert 33 | 34 | tbl 2 "c" insert \ [ "b" "c" "a" ] 35 | tbl 1 @ "b" =assert 36 | tbl 2 @ "c" =assert 37 | tbl 3 @ "a" =assert 38 | 39 | tbl 2 #( table.remove 2 ) "c" =assert ( returns the removed item ) 40 | 41 | tbl 1 @ "b" =assert 42 | tbl 2 @ "a" =assert 43 | tbl size 2 =assert 44 | 45 | \ overwrite @ index 46 | 47 | [ "a" "b" ] -> tbl 48 | tbl 1 @ "a" =assert 49 | tbl 2 @ "b" =assert 50 | 51 | tbl 1 "x" ! 52 | tbl 2 "y" ! 53 | 54 | tbl 1 @ "x" =assert 55 | tbl 2 @ "y" =assert 56 | tbl size 2 =assert 57 | 58 | depth 0 =assert 59 | adepth 0 =assert 60 | 61 | ( key value tables ) 62 | 63 | { } size 0 =assert 64 | 65 | { "k1" "val1" 66 | "k2" "val2" } -> tbl 67 | 68 | tbl "k1" @ "val1" =assert 69 | tbl "k2" @ "val2" =assert 70 | 71 | tbl "k3" "val3" ! 72 | tbl "k3" @ "val3" =assert 73 | 74 | tbl "notfound" @ nil =assert 75 | 76 | tbl "k2" nil ! \ remove k2 77 | tbl "k2" @ nil =assert \ k2 is removed 78 | tbl "k3" @ "val3" =assert \ k3 is still there 79 | 80 | tbl size 0 =assert \ size doesn't work with non-seq tables 81 | 82 | depth 0 =assert 83 | adepth 0 =assert 84 | 85 | \ test aliases 86 | [ 1 2 ] -> tbl 87 | tbl "a" append 88 | tbl 3 @ "a" =assert 89 | tbl 1 @ 1 =assert 90 | tbl 2 @ 2 =assert 91 | tbl size 3 =assert 92 | 93 | [ 1 2 ] -> tbl 94 | tbl 2 "a" insert 95 | tbl 2 @ "a" =assert 96 | tbl 1 @ 1 =assert 97 | tbl 3 @ 2 =assert 98 | tbl size 3 =assert 99 | 100 | [ "x" "y" ] -> tbl 101 | tbl 1 remove 102 | tbl 1 @ "y" =assert 103 | tbl size 1 =assert 104 | 105 | \ key lookup with dot syntax 106 | { $k1 42 $k2 "apple" } -> tbl 107 | tbl.k1 42 =assert 108 | tbl.k2 "apple" =assert 109 | 110 | \ assigment 111 | 15 -> tbl.k1 112 | tbl.k1 15 =assert 113 | 114 | "orange" -> tbl.k2 115 | tbl.k2 "orange" =assert 116 | 117 | { $k1 { $k2 { $k3 { $k4 42 } } } } -> tbl 118 | tbl.k1.k2.k3.k4 42 =assert 119 | 120 | [ "x" "y" ] -> tbl 121 | 10 20 tbl 2 @ "y" =assert 122 | 20 =assert 123 | 10 =assert 124 | 125 | 30 40 126 | tbl 2 "z" ! 127 | tbl 2 @ "z" =assert 128 | 40 =assert 129 | 30 =assert 130 | 131 | "apple" -> var abc 132 | "orange" -> var def 133 | 134 | { $ abc $ def } -> tbl 135 | 136 | tbl $abc @ "apple" =assert 137 | tbl.def "orange" =assert 138 | 139 | 140 | depth 0 =assert 141 | adepth 0 =assert 142 | -------------------------------------------------------------------------------- /tests/test_tick.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : even 2 % 0 = ; 5 | : odd even not ; 6 | 7 | 2 even assert-true 8 | 2 odd assert-false 9 | 5 even assert-false 10 | 5 odd assert-true 11 | 12 | : filter ( pred array -- c ) 13 | var result 14 | [ ] -> result 15 | ipairs: i elem 16 | elem over exec if result elem append then 17 | end 18 | drop 19 | result ; 20 | 21 | var a 22 | 23 | ' odd [ 1 2 3 4 ] filter -> a 24 | a size 2 =assert 25 | a 1 @ 1 =assert 26 | a 2 @ 3 =assert 27 | 28 | ' even [ 1 2 3 4 ] filter -> a 29 | a size 2 =assert 30 | a 1 @ 2 =assert 31 | a 2 @ 4 =assert 32 | 33 | depth 0 =assert 34 | adepth 0 =assert 35 | -------------------------------------------------------------------------------- /tests/test_unloop_exit.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : exit-test 5 exit 6 ; 5 | exit-test 5 =assert 6 | 7 | : tst 10 0 do i 5 = if i exit then loop ; 8 | 9 | tst 5 =assert 10 | adepth 0 =assert 11 | 12 | : tst2 13 | 10 0 do 14 | 10 0 do 15 | i 5 = j 5 = and if 16 | i j * 17 | then 18 | loop 19 | loop ; 20 | 21 | tst2 25 =assert 22 | 23 | depth 0 =assert 24 | adepth 0 =assert 25 | -------------------------------------------------------------------------------- /tests/test_utils.lua: -------------------------------------------------------------------------------- 1 | local utils = require("utils") 2 | 3 | assert(utils.trim(" a bc ") == "a bc") 4 | 5 | local u = utils.unique({"a", "b", "a", 1, 1, 1}) 6 | 7 | assert(#u == 3) 8 | assert(u[1] == "a") 9 | assert(u[2] == "b") 10 | assert(u[3] == 1) 11 | 12 | local u = utils.keys({k1 = 1}) 13 | assert(#u == 1) 14 | assert(u[1] == "k1") 15 | 16 | 17 | assert(utils.startswith("abc", "a")) 18 | assert(utils.startswith("abc", "ab")) 19 | assert(utils.startswith("abc", "abc")) 20 | assert(not utils.startswith("abc", "abcd")) 21 | 22 | assert(utils.extension("a f.eqx") == ".eqx") 23 | 24 | assert(utils.file_exists_in_any_of("repl.lua", {"src", "tests"})) 25 | assert(not utils.file_exists_in_any_of("non_existent", {"src", "tests"})) 26 | -------------------------------------------------------------------------------- /tests/test_var.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | var v1 5 | var v2 6 | 10 -> v1 7 | 12 -> v2 8 | v1 v2 + 9 | 22 =assert 10 | 11 | var v1 12 | var v2 13 | 10 -> v1 v1 -> v2 14 | 3 v2 + -> v2 ( 13 = v2 ) 15 | v1 v2 - ( 10 13 - ) 16 | -3 =assert 17 | 18 | var x 19 | 10 20 30 -> x 20 | 30 x =assert 21 | 20 =assert 22 | 10 =assert 23 | 24 | \ create and init in one step 25 | 26 | 44 -> var n 27 | n 44 =assert 28 | 29 | 12 -> var var-with-hyphen 30 | var-with-hyphen 12 =assert 31 | 32 | var 1spec-var+_*%#@! 33 | 3 -> 1spec-var+_*%#@! 34 | 1spec-var+_*%#@! 3 =assert 35 | 36 | var 5x! 37 | { $x 555 } -> 5x! 38 | 39 | 5x! $x @ 555 =assert 40 | 5x!.x 555 =assert 41 | 42 | 678 -> 5x!.y 43 | 5x! $y @ 678 =assert 44 | 5x!.y 678 =assert 45 | 46 | var t-1 47 | { $a { $b 3 } } -> t-1 48 | t-1.a.b 3 =assert 49 | 50 | 4 -> t-1.a.b 51 | t-1.a.b 4 =assert 52 | 53 | depth 0 =assert 54 | adepth 0 =assert 55 | -------------------------------------------------------------------------------- /tests/test_words.eqx: -------------------------------------------------------------------------------- 1 | depth 0 =assert 2 | adepth 0 =assert 3 | 4 | : >! 3 / ; 5 | 6 | 9 >! 3 =assert 7 | 8 | :: loc-word 4 + ; 9 | 10 | 6 loc-word 10 =assert 11 | 12 | depth 0 =assert 13 | adepth 0 =assert 14 | --------------------------------------------------------------------------------