├── .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 |  [](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 |
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 |
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 |
--------------------------------------------------------------------------------