├── LICENSE ├── README.md ├── samples ├── factorial.xi ├── fib.xi └── fizzbuzz.xi ├── test └── unittests.xi └── xi.oak /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Linus Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xi 🗼 2 | 3 | **Xi** (pronounced _Zai_) is a dynamic, stack-based concatenative language, written in [Oak](https://oaklang.org/) and using Oak types and semantics. I wrote Xi over the 2021 Labor Day weekend as a learning exercise to understand how stack languages work and why they're interesting. Xi is modeled mainly after [Factor](https://factorcode.org/), but this implementation is neither complete nor robust -- there's basically no error handling, for example, and Xi is not meant to be a faithful _re-implementation_ of Factor. It should run correct programs correctly, but will often fail on bad input. 4 | 5 | ```js 6 | // Factorials to 10! 7 | factorial : nat prod 8 | 10 ( ++ factorial print ) each-integer 9 | 10 | // Fibonacci sequence to fib(25) 11 | (fib) : dup 2 < ( drop swap drop ) ( ( swap over + ) dip -- (fib) ) if 12 | fib : 1 1 rot (fib) 13 | 25 ( fib print ) each-integer 14 | ``` 15 | 16 | I've written more in-depth about my experiments with concatenative languages on [the Oak blog](https://oaklang.org/posts/xi/). 17 | 18 | ## Overview 19 | 20 | Xi is dynamically typed, and operates on the same basic values as Oak except that all numbers are `float`s. Like other concatenative stack programming languages, each statement (line) in a Xi program is a sequence of _words_, where each word manipulates a single global _data stack_ in some way, usually by moving and changing a few values at the top of the stack. Literal values like numbers and strings simply move those values onto the stack. 21 | 22 | For example, the word `+` pops the top two values off the stack, adds them together, and pushes the sum back on the stack. 23 | 24 | ```js 25 | 1 2 10 26 | // stack: < 1 2 10 > 27 | + 28 | // stack: < 1 12 > 29 | + 30 | // stack: < 13 > 31 | ``` 32 | 33 | We can also write these words all next to each other, and have the same program. 34 | 35 | ```js 36 | 1 2 10 + + 37 | // stack: < 13 > 38 | ``` 39 | 40 | This is where the name _concatenative_ language comes from -- putting words next to each other composes those functions together in a predictable way. 41 | 42 | Sometimes, we need to shuffle some items in the stack around to work on the right values without doing any other computation. These are called _stack shuffling_ words. Xi provides 4 basic ones, from which more complex words can be defined: 43 | 44 | ```js 45 | 2 dup 46 | // stack: < 2 2 > — duplicates the top value 47 | 48 | 1 2 3 ( + ) dip 49 | // stack: < 3 3 > — runs a quotation (words inside `( ... )`) underneath the 50 | // topmost value on the stack 51 | 52 | 1 2 drop 53 | // stack: < 1 > — simply drops the topmost value on the stack 54 | 55 | 10 20 swap 56 | // stack: < 20 10 > — swaps the top 2 values' places on the stack 57 | ``` 58 | 59 | As an example of basic composition, we can define `rot`, which rotates the top 3 items' places in the stack, like this. 60 | 61 | ```js 62 | // define the word "rot" 63 | rot : ( swap ) dip swap 64 | 65 | 1 2 3 rot 66 | // stack: < 2 3 1 > 67 | ``` 68 | 69 | Xi's syntax is, like most concatenative languages, minimal. There are three kinds of primitive values: single-quoted strings, number (floating point) literals, and booleans `true` and `false`. Xi has lists and objects, delimited using `[ ... ]` and `{ ... }` respectively, though there isn't much of a vocabulary to work with objects. Finally, Xi has _quotations_, which are sequences of words that can be evaluated later, analogous to closures in other high-level languages. These types of values also represent the entirely of Xi's type system. There is no more sophisticated class system as in Factor. 70 | 71 | Xi is a learning project, and thus not a great introduction to concatenative programming if you're new to it yourself. If you want to learn more about concatenative programming, you might want to check out these resources I found helpful as I learned about this space myself. 72 | 73 | - [A panoramic tour of Factor](https://andreaferretti.github.io/factor-tutorial/), which is the most beginner-friendly treatment of Factor and concatenative programming I could find 74 | - [A survey of stack shufflers](http://useless-factor.blogspot.com/2007/09/survey-of-stack-shufflers.html), which helped me get a better sense of how to use stack shuffling words, and how to "think in Factor", i.e. think about programming by composing words together 75 | - [Google TechTalk on Factor by its creator Slava Pestov](https://www.youtube.com/watch?v=f_0QlhYlS8g), which gives a great high-level overview of what makes concatenative programming and Factor attractive 76 | - [Bare metal x86 Forth](https://ph1lter.bitbucket.io/blog/2021-01-15-baremetal-x86-forth.html), an advanced and insightful deep dive into bootstrapping a concatenative programming language from assembly 77 | 78 | ### Xi repl 79 | 80 | Xi has a basic repl, which I used to test and debug words before adding them to my programs. Simply running `./xi.oak` without any arguments will start the repl. Through the repl, you can run any Xi code. However, there are a few specific "debugging words" that are useful for inspecting program state when in the repl. 81 | 82 | - `.` will pop the top value off of the stack and print it out. 83 | - `.s` ("s" for "stack") will print out a representation of the entire data stack at that point in the program 84 | - `.e` ("e" for "environment") will print out a dictionary of every word currently defined in scope and their definitions 85 | 86 | ## Examples 87 | 88 | Though Xi is a pedagogical toy language and not exactly practical due to its brittleness and minimalism, there are a few sample programs I wrote to demonstrate how Xi programs work, and test my implementation. Besides these two, there is a small unit testing helper and test suite in `./test/unittests.xi`. 89 | 90 | ### Fizzbuzz 91 | 92 | Here is some sample Xi code, the [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) program. Though each statement must be in a single line in Xi, I've broken them up here into multiple lines for readability. 93 | 94 | ```js 95 | // FizzBuzz in Xi 96 | 97 | // n -> _ 98 | fizzbuzz : dup 15 divisible? ( 99 | 'FizzBuzz' print drop 100 | ) ( 101 | dup 3 divisible? 102 | ( 103 | 'Fizz' print drop 104 | ) ( 105 | dup 5 divisible? 106 | ( 'Buzz' print drop ) ( print ) if 107 | ) 108 | if 109 | ) if 110 | 111 | // main 112 | 100 ( ++ fizzbuzz ) each-integer 113 | ``` 114 | 115 | Here, the word `fizzbuzz` consumes a number at the top of the data stack and prints either 'Fizz', 'Buzz', 'FizzBuzz', or the number to output. The main program `100 ( ++ fizzbuzz ) each-integer` performs the quotation (`++ fizzbuzz`) for each integer counting up from 0 to 100, exclusive. Running this program with 116 | 117 | ```sh 118 | ./xi.oak ./samples/fizzbuzz.xi 119 | ``` 120 | 121 | should produce the correct output. 122 | 123 | ### Factorial 124 | 125 | The sample [`./samples/factorial.xi`](samples/factorial.xi) computes factorials of every number from 1 to 10, inclusive, and prints it. This program is a great demonstration of how elegant and concise well-designed concatenative programs can be, if the right primitives are composed well. This program is just two short lines: 126 | 127 | ```js 128 | factorial : nat prod 129 | 10 ( ++ factorial print ) each-integer 130 | ``` 131 | 132 | First, we define the word `factorial` that takes a number, generates a list of numbers counting up from 1 to that number (`nat`), and takes their total product (`prod`). Then we loop through every number from 1 to 10, and compute the factorial and print it. This generates the correct output 133 | 134 | ``` 135 | 1 136 | 2 137 | 6 138 | 24 139 | 120 140 | 720 141 | 5040 142 | 40320 143 | 362880 144 | 3.6288e+06 145 | ``` 146 | 147 | ## Development 148 | 149 | Xi is a project written in the [Oak programming language](https://oaklang.org/). All of the core language and "kernel" is defined in a single Oak program, `./xi.oak`. 150 | 151 | A small unit test suite is defined in `./test/unittests.xi`. To run it, simply run 152 | 153 | ```sh 154 | ./xi.oak ./test/unittests.xi 155 | ``` 156 | -------------------------------------------------------------------------------- /samples/factorial.xi: -------------------------------------------------------------------------------- 1 | // Factorial 2 | 3 | // n -> factorial(n) 4 | factorial : nat prod 5 | 6 | // main 7 | 10 ( ++ factorial print ) each-integer 8 | 9 | -------------------------------------------------------------------------------- /samples/fib.xi: -------------------------------------------------------------------------------- 1 | // Fibonacci sequence 2 | 3 | // a b n -> fib(n): if n < 2, return b; else, recurse on b, a+b 4 | (fib) : dup 2 < ( drop swap drop ) ( ( swap over + ) dip -- (fib) ) if 5 | // n -> fib(n) 6 | fib : 1 1 rot (fib) 7 | 8 | // main 9 | 25 ( fib print ) each-integer 10 | 11 | -------------------------------------------------------------------------------- /samples/fizzbuzz.xi: -------------------------------------------------------------------------------- 1 | // FizzBuzz 2 | 3 | // n -> _ 4 | fizzbuzz : dup 15 divisible? ( 'FizzBuzz' print drop ) ( dup 3 divisible? ( 'Fizz' print drop ) ( dup 5 divisible? ( 'Buzz' print drop ) ( print ) if ) if ) if 5 | 6 | // main 7 | 100 ( ++ fizzbuzz ) each-integer 8 | 9 | -------------------------------------------------------------------------------- /test/unittests.xi: -------------------------------------------------------------------------------- 1 | // Basic unit test library and tests for Xi 2 | 3 | // asserts that the top two things on the stack are equal 4 | // a b -> _ 5 | assert! : rot pick pick = ( ' ' swap + ': ok' + print ) ( '! ' swap + ': failed, results below:' + print .s ) if drop2 6 | 7 | // asserts whether results of top two quotations on stack are the same to N 8 | // depths in stack 9 | eq! : ( call ) dip call assert! 10 | eq2! : ( call list2 ) dip call list2 assert! 11 | eq3! : ( call list3 ) dip call list3 assert! 12 | eq4! : ( call list4 ) dip call list4 assert! 13 | eq5! : ( call list5 ) dip call list5 assert! 14 | 15 | 'Xi unit tests:' print 16 | 17 | // primitive ops 18 | 'Simple binary operator' ( 1 2 + ) ( 3 ) eq! 19 | 'Compound binary expression' ( 10 20 30 * + ) ( 610 ) eq! 20 | 21 | // stack shuffling 22 | 'dup' ( 2 dup ) ( 2 2 ) eq2! 23 | 'dip' ( 2 3 4 10 ( + * ) dip ) ( 14 10 ) eq2! 24 | 'drop' ( 1 100 1000 drop ) ( 1 100 ) eq2! 25 | 'swap' ( 20 50 swap ) ( 50 20 ) eq2! 26 | // some library words 27 | 'nip' ( 5 7 9 nip ) ( 5 9 ) eq2! 28 | 'dip2' ( 1 2 3 4 5 ( + ) dip2 ) ( 1 5 4 5 ) eq4! 29 | 'drop3' ( 1 2 3 4 drop3 ) ( 1 ) eq! 30 | 'swapd' ( 5 6 7 swapd ) ( 6 5 7 ) eq3! 31 | '-rotd' ( 1 2 3 4 rotd ) ( 2 3 1 4 ) eq4! 32 | 'reach' ( 1 2 3 4 reach ) ( 1 2 3 4 1 ) eq5! 33 | 'keep' ( 20 30 ( * ) keep ) ( 600 30 ) eq2! 34 | 35 | // control flow 36 | '? for ternary choice' ( true 10 100 ? false 20 200 ? ) ( 10 200 ) eq2! 37 | 'if conditional' ( 10 true ( 20 + ) ( 50 + ) if ) ( 30 ) eq! 38 | 'when conditional' ( 10 true ( 20 + ) when false ( 100 * ) when ) ( 30 ) eq! 39 | 'unless conditional' ( 10 true ( 20 + ) unless false ( 100 * ) unless ) ( 1000 ) eq! 40 | 'call' ( 10 ( 100 * ) call ) ( 1000 ) eq! 41 | // combinators 42 | square : dup * 43 | 'twice' ( 3 ( square ) twice ) ( 81 ) eq! 44 | 'thrice' ( 2 ( square ) thrice ) ( 256 ) eq! 45 | 'bi' ( 5 ( 2 + ) ( square ) bi ) ( 7 25 ) eq2! 46 | 'tri' ( 5 ( 2 + ) ( square ) ( dup + ) tri ) ( 7 25 10 ) eq3! 47 | 'quad' ( 5 ( 2 + ) ( square ) ( dup + ) ( 2 - ) quad ) ( 7 25 10 3 ) eq4! 48 | 49 | // list operations 50 | 'nth access' ( [ 1 2 3 4 5 ] 2 nth ) ( 3 ) eq! 51 | 'nth mutation' ( [ 1 2 3 4 5 ] 2 100 nth! ) ( [ 1 2 100 4 5 ] ) eq! 52 | 'length' ( [ 1 2 3 4 5 ] len ) ( 5 ) eq! 53 | 'empty?' ( [ ] empty? [ 1 2 ] empty? ) ( true false ) eq2! 54 | // iterators 55 | 'each-integer' ( 5 ( dup + ) each-integer ) ( 0 2 4 6 8 ) eq5! 56 | 'reduce' ( [ 2 3 4 5 ] ( square + ) 1000 reduce ) ( 1054 ) eq! 57 | 'map' ( [ 2 3 4 5 ] ( dup + ) map ) ( [ 4 6 8 10 ] ) eq! 58 | 'filter' ( [ 1 10 2 4 3 6 ] ( even? ) filter ) ( [ 10 2 4 6 ] ) eq! 59 | 'slice' ( [ 1 2 3 4 5 6 7 8 9 10 ] 3 7 slice ) ( [ 4 5 6 7 ] ) eq! 60 | 'sum' ( [ 10 20 30 1 ] sum ) ( 61 ) eq! 61 | 'prod' ( [ 2 3 4 5 ] prod ) ( 120 ) eq! 62 | 63 | // library functions 64 | 'list2' ( 10 20 list2 ) ( [ 10 20 ] ) eq! 65 | 'list5' ( 5 4 3 2 100 list5 ) ( [ 5 4 3 2 100 ] ) eq! 66 | // sequencers 67 | 'generic a..b:c' ( 2 20 3 a..b:c ) ( [ 2 5 8 11 14 17 ] ) eq! 68 | 'seq, nat' ( 5 ( seq ) ( nat ) bi ) ( [ 0 1 2 3 4 ] [ 1 2 3 4 5 ] ) eq2! 69 | 70 | 'Final stack (should be empty):' print .s 71 | 72 | -------------------------------------------------------------------------------- /xi.oak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oak 2 | 3 | // Xi is a minimal concatenative stack programming language that uses Oak types 4 | // and semantics. 5 | 6 | { 7 | println: println 8 | slice: slice 9 | map: map 10 | each: each 11 | filter: filter 12 | reduce: reduce 13 | flatten: flatten 14 | every: every 15 | partition: partition 16 | entries: entries 17 | loop: loop 18 | } := import('std') 19 | { 20 | digit?: digit? 21 | join: join 22 | split: split 23 | trim: trim 24 | } := import('str') 25 | { 26 | sort!: sort! 27 | } := import('sort') 28 | { 29 | printf: printf 30 | } := import('fmt') 31 | fs := import('fs') 32 | cli := import('cli') 33 | 34 | // Stack implements a simple LIFO stack to be used as the data stack for Xi 35 | // programs. It also implements a peek() operation to look at the topmost item 36 | // on the stack. 37 | fn Stack { 38 | // buffer backing the stack 39 | mem := [] 40 | // index points to the next insertion slot 41 | index := 0 42 | 43 | fn pop if index { 44 | 0 -> ? 45 | _ -> { 46 | index <- index - 1 47 | mem.(index) 48 | } 49 | } 50 | fn push(it) { 51 | mem.(index) := it 52 | index <- index + 1 53 | } 54 | fn peek if index { 55 | 0 -> ? 56 | _ -> mem.(index - 1) 57 | } 58 | 59 | { 60 | pop: pop 61 | push: push 62 | peek: peek 63 | clear: fn() index <- 0 64 | items: fn() mem |> slice(0, index) 65 | } 66 | } 67 | 68 | // tokenize splits a line of Xi program into valid tokens, which are 69 | // punctuations separated by space or string literals or numbers/booleans. 70 | // Returns a list of strings. 71 | fn tokenize(program) program |> split('\'') |> map(fn(part, i) if i % 2 { 72 | // odd items are string content 73 | 1 -> ['\'' << part << '\''] 74 | // even items are normal tokens 75 | _ -> part |> split(' ') 76 | }) |> flatten() |> with filter() fn(s) s |> trim() != '' 77 | 78 | // parse translates a line of Xi program into "AST nodes" which are really just 79 | // nested lists of strings. It accounts for nesting in quotations ( ), lists, [ 80 | // ], and objects { }. 81 | // For example, it converts 'xyz ( 1 2 ) abc' -> ['xyz', ['1', '2'], 'abc'] 82 | // 83 | // parse produces special composite literal nodes to represent lists and 84 | // objects, that have the form: 85 | // { 86 | // type: :list (or :object) 87 | // val: 88 | // } 89 | fn parse(program) { 90 | index := 0 91 | tokens := program |> tokenize() 92 | 93 | fn parseTokens(stmts) if index >= len(tokens) { 94 | true -> stmts 95 | _ -> if token := tokens.(index) { 96 | '(' -> { 97 | index <- index + 1 98 | parseTokens(stmts << parseTokens([])) 99 | } 100 | '[' -> { 101 | index <- index + 1 102 | parseTokens(stmts << { 103 | type: :list 104 | val: parseTokens([]) 105 | }) 106 | } 107 | '{' -> { 108 | index <- index + 1 109 | parseTokens(stmts << { 110 | type: :object 111 | val: { 112 | obj := {} 113 | parseTokens([]) |> partition(2) |> with each() fn(pair) { 114 | [key, val] := pair 115 | obj.(key) := val 116 | } 117 | obj 118 | } 119 | }) 120 | } 121 | ']', '}', ')' -> { 122 | index <- index + 1 123 | stmts 124 | } 125 | _ -> { 126 | index <- index + 1 127 | parseTokens(stmts << token) 128 | } 129 | } 130 | } 131 | parseTokens([]) 132 | } 133 | 134 | // toString converts Xi values to string representations suitable for printing 135 | // in a REPL environment. 136 | fn toString(word) if type(word) { 137 | :list -> '( ' + word |> map(toString) |> join(' ') + ' )' 138 | :object -> if word { 139 | { type: :list, val: _ } -> '[ ' + word.val |> map(toString) |> join(' ') + ' ]' 140 | { type: :object, val: _ } -> '{ ' + word.val |> 141 | entries() |> 142 | map(fn(entry) toString(entry.key) << ' ' << toString(entry.val)) |> 143 | join(' ') + ' }' 144 | } 145 | :int, :float -> string(word) 146 | :string -> '\'' + word + '\'' 147 | _ -> string(word) 148 | } 149 | 150 | // number? reports whether a string is a valid Oak (and Xi) number 151 | fn number?(s) float(s) != ? 152 | // str? reports whether a string is a valid Xi string literal. Note that Xi 153 | // string literals are pretty dumb -- you can't escape quotes, for example. 154 | fn str?(s) s.0 = '\'' & s.(len(s) - 1) = '\'' 155 | 156 | // literal? reports whether a parsed word represents a Xi value literal 157 | fn literal?(word) if { 158 | word = ? -> false 159 | 160 | type(word) = :list 161 | word = { type: _, val: _ } 162 | number?(word) 163 | str?(word) 164 | word = 'true' 165 | word = 'false' -> true 166 | 167 | _ -> false 168 | } 169 | 170 | // literal translates a Xi literal token into a Xi value 171 | fn literal(word) if { 172 | type(word) = :list -> word 173 | word = { type: :list, val: _ } -> { 174 | type: :list 175 | val: word.val |> map(literal) 176 | } 177 | word = { type: :object, val: _ } -> { 178 | type: :object 179 | val: word.val |> entries() |> with reduce({}) fn(o, entry) { 180 | [key, val] := entry 181 | o.(literal(key)) := literal(val) 182 | } 183 | } 184 | number?(word) -> float(word) 185 | str?(word) -> word |> slice(1, len(word) - 1) 186 | word = 'true' -> true 187 | word = 'false' -> false 188 | } 189 | 190 | // eval evaluates a list of words (a line of Xi program) against a stack and a 191 | // scope. The stack is used as the data stack, and the scope is used to store 192 | // user-defined words. eval does not return anything meaningful, and instead 193 | // mutates the stack. 194 | fn eval(words, stack, scope) if { 195 | // comment 196 | words.0 = '//' -> ? 197 | // define a new word 198 | words.1 = ':' -> scope.(words.0) := words |> slice(2) 199 | // evaluate a statement 200 | _ -> words |> with each() fn(word) if word { 201 | // introspection 202 | '.' -> println(stack.pop() |> toString()) 203 | // .s for stack, following the Factor convention 204 | '.s' -> println('< ' + stack.items() |> map(toString) |> join(' ') + ' >') 205 | // .e for environment, for the local lexical environment 206 | '.e' -> println('{\n' + scope |> entries() |> sort!(fn(pair) pair.0) |> map(fn(pair) { 207 | [key, val] := pair 208 | ' ' << key << ' : ' << toString(val) 209 | }) |> join('\n') + '\n}') 210 | 211 | // stack manipulation 212 | 'dup' -> stack.push(stack.peek()) 213 | 'dip' -> { 214 | defn := stack.pop() 215 | dipped := stack.pop() 216 | defn |> eval(stack, scope) 217 | stack.push(dipped) 218 | } 219 | 'drop' -> stack.pop() 220 | 'swap' -> { 221 | a := stack.pop() 222 | b := stack.pop() 223 | stack.push(a) 224 | stack.push(b) 225 | } 226 | 'clear' -> stack.clear() 227 | 228 | // operators 229 | '+' -> stack.push({ 230 | tmp := stack.pop() 231 | stack.pop() + tmp 232 | }) 233 | '-' -> stack.push({ 234 | tmp := stack.pop() 235 | stack.pop() - tmp 236 | }) 237 | '*' -> stack.push({ 238 | tmp := stack.pop() 239 | stack.pop() * tmp 240 | }) 241 | '/' -> stack.push({ 242 | tmp := stack.pop() 243 | stack.pop() / tmp 244 | }) 245 | '%' -> stack.push({ 246 | tmp := stack.pop() 247 | stack.pop() % tmp 248 | }) 249 | '&' -> stack.push(stack.pop() & stack.pop()) 250 | '|' -> stack.push(stack.pop() | stack.pop()) 251 | '^' -> stack.push(stack.pop() ^ stack.pop()) 252 | '=' -> stack.push(stack.pop() = stack.pop()) 253 | '<' -> stack.push(stack.pop() > stack.pop()) 254 | '>' -> stack.push(stack.pop() < stack.pop()) 255 | '<=' -> stack.push(stack.pop() >= stack.pop()) 256 | '>=' -> stack.push(stack.pop() <= stack.pop()) 257 | '!' -> stack.push(!stack.pop()) 258 | 259 | // primitives 260 | '?' -> { 261 | ifFalse := stack.pop() 262 | ifTrue := stack.pop() 263 | stack.push(if cond := stack.pop() { 264 | true -> ifTrue 265 | _ -> ifFalse 266 | }) 267 | } 268 | 'call' -> stack.pop() |> eval(stack, scope) 269 | // list and object functions 270 | 'nth' -> { 271 | key := stack.pop() 272 | if target := stack.pop() { 273 | { type: :list, val: _ } -> stack.push(target.val.(int(key))) 274 | { type: :object, val: _ } -> stack.push(target.val.(key)) 275 | _ -> { 276 | printf('Error: cannot access key {{0}} of {{1}}', key, target) 277 | } 278 | } 279 | } 280 | 'nth!' -> { 281 | val := stack.pop() 282 | key := stack.pop() 283 | if target := stack.peek() { 284 | { type: :list, val: _ } -> target.val.(int(key)) := val 285 | { type: :object, val: _ } -> target.val.(key) := val 286 | _ -> { 287 | printf('Error: cannot access key {{0}} of {{1}}', key, target) 288 | } 289 | } 290 | } 291 | 'len' -> if target := stack.pop() { 292 | { type: _, val: _ } -> stack.push(float(len(target.val))) 293 | _ -> { 294 | printf('Error: cannot get len of {{0}}', target) 295 | stack.push(0) 296 | } 297 | } 298 | 'print' -> if type(word := stack.pop()) { 299 | :string -> println(word) 300 | _ -> println(word |> toString()) 301 | } 302 | 303 | // literals and definitions 304 | _ -> if literal?(word) { 305 | true -> literal(word) |> stack.push() 306 | _ -> if defn := scope.(word) { 307 | ? -> printf('Error: unknown word "{{0}}"', word) 308 | _ -> defn |> eval(stack, scope) 309 | } 310 | } 311 | } 312 | } 313 | 314 | // main 315 | 316 | scope := {} 317 | stack := Stack() 318 | 319 | // language prelude 320 | // 321 | // The prelude defines the "kernel" of Xi, the equivalent of Factor's 322 | // kernel.factor. The stack effect of each definition is commented above each 323 | // definition line. 324 | [ 325 | // arithmetic 326 | '++ : 1 +' 327 | '-- : 1 -' 328 | 'neg : 0 swap -' 329 | 'abs : dup 0 >= ( ) ( neg ) if' 330 | 'zero? : 0 =' 331 | 'even? : 2 % 0 =' 332 | 'odd? : 2 % 1 =' 333 | 'pos? : 0 >' 334 | 'neg? : 0 <' 335 | 'max : dup2 > ( drop ) ( nip ) if' 336 | 'min : dup2 < ( drop ) ( nip ) if' 337 | // n factor -> bool 338 | 'divisible? : % zero?' 339 | 340 | // stack shuffling 341 | 'nip : swap drop' 342 | 'nipd : ( nip ) dip' 343 | 'dip2 : swap ( dip ) dip' 344 | 'dip3 : swap ( dip2 ) dip' 345 | 'dip4 : swap ( dip3 ) dip' 346 | 'drop2 : drop drop' 347 | 'drop3 : drop2 drop' 348 | // x y -> x x y 349 | 'dupd : ( dup ) dip' 350 | // x y -> x y x y 351 | 'dup2 : over over' 352 | // x y z -> x y z x y z 353 | 'dup3 : pick pick pick' 354 | // x y z -> y x z 355 | 'swapd : ( swap ) dip' 356 | // x y -> x y x 357 | 'over : dupd swap' 358 | // x y z -> y z x 359 | 'rot : swapd swap' 360 | // x y z -> z x y 361 | '-rot : swap swapd' 362 | // w x y z -> x y w z 363 | 'rotd : ( rot ) dip' 364 | // w x y z -> y w x z 365 | '-rotd : ( -rot ) dip' 366 | // x y -> y x y 367 | 'tuck : dup -rot' 368 | // x y z -> z y x 369 | 'spin : -rot swap' 370 | // x y z -> x y z x 371 | 'pick : rot dup ( -rot ) dip' 372 | // w x y z -> w x y w z 373 | 'pickd : ( pick ) dip' 374 | // w x y z -> x y z w 375 | 'rot4 : ( swap ) dip2 rot' 376 | // w x y z -> z w x y 377 | '-rot4 : -rot ( swap ) dip2' 378 | // w x y z -> w x y z w 379 | 'reach : pickd swap' 380 | // x y quot -> quot(x,y) y 381 | 'keep : over ( call ) dip' 382 | 383 | // primitive ops 384 | '<< : ( dup len ) dip nth!' 385 | // put top N items in stack into a new list 386 | 'list : [ ] swap <<' 387 | 'list2 : ( list ) dip <<' 388 | 'list3 : ( list2 ) dip <<' 389 | 'list4 : ( list3 ) dip <<' 390 | 'list5 : ( list4 ) dip <<' 391 | 392 | // combinators 393 | 'if : ? call' 394 | 'when : ( ) if' 395 | 'unless : ( ) swap if' 396 | 'twice : dup ( call ) dip call' 397 | 'thrice : dup dup ( call ) dip2 ( call ) dip call' 398 | 'bi : ( keep ) dip call' 399 | 'tri : ( ( keep ) dip keep ) dip call' 400 | 'quad : ( ( ( keep ) dip keep ) dip keep ) dip call' 401 | 402 | // sequencers 403 | // [x..] -> bool 404 | 'empty? : len zero?' 405 | // n quot: ( i -> _ ) -> _ 406 | '(each-integer) : pick pick > ( rot ( dup2 ( call ) dip2 ) dip -rot ( ++ ) dip (each-integer) ) ( drop3 ) if' 407 | // Takes a number N and a quotation and calls the quotation N times, each 408 | // time with the iteration count counting up from 0 409 | 'each-integer : 0 swap (each-integer)' 410 | // [x..] quot: ( x -> _ ) -> _ 411 | 'each : ( dup len ) dip swap ( swap ( dupd nth ) dip tuck call ) each-integer drop2' 412 | // [x..] quot: ( acc x -> acc ) acc -> acc 413 | 'reduce : -rot ( dup len ) dip swap ( swap ( dupd nth ) dip ( swap ) dip2 dup ( call ) dip swapd ) each-integer drop2' 414 | // [x..] quot: ( x -> y ) -> [y..] 415 | 'map : [ ] -rot ( dup len ) dip swap ( swap ( dupd nth ) dip ( swap ) dip2 dup ( call << swap ) dip ) each-integer drop2' 416 | // [x..] quot: ( x -> bool ) -> [x..] 417 | 'filter : [ ] -rot ( dup len ) dip swap ( swap ( dupd nth ) dip ( swap ) dip2 dup ( dupd call ( << ) ( drop ) if swap ) dip ) each-integer drop2' 418 | // [x..] a b [y..] -> [y.. x..] 419 | '(slice) : pick pick < ( reach reach nth << ( ++ ) dip2 (slice) ) ( -rot4 drop3 ) if' 420 | // [x..] a b -> [x..] 421 | 'slice : [ ] (slice)' 422 | // [x..] -> sum 423 | 'sum : ( + ) 0 reduce' 424 | // [x..] -> prod 425 | 'prod : ( * ) 1 reduce' 426 | 427 | // stdlib 428 | // c [x..] a b -> c [x.. a] a+=c b 429 | '(push-into-list-and-incr) : dupd ( << ) dip2 ( pick + ) dip' 430 | // c [x..] a b -> [x..b:c] 431 | '(iterate-if-less) : dup2 < ( (push-into-list-and-incr) (iterate-if-less) ) ( drop2 nip ) if' 432 | // a b c -> [a..b:c] 433 | 'a..b:c : -rot [ ] -rot (iterate-if-less)' 434 | // a b -> [a..b:1] 435 | 'a..b : 1 a..b:c' 436 | '0..b : 0 swap a..b' 437 | '1..b : 1 swap a..b' 438 | // n -> [1,n) 439 | 'seq : 0..b' 440 | // n -> [1,n] 441 | 'nat : ++ 1..b' 442 | ] |> with each() fn(stmt) stmt |> parse() |> eval(stack, scope) 443 | 444 | Help := 'Xi is a minimal stack-based concatenative stack language. 445 | 446 | Usage 447 | ./xi.oak --help OR -h 448 | Show this help message 449 | ./xi.oak 450 | Start an interactive REPL 451 | ./xi.oak 452 | Run a Xi program from a source file 453 | 454 | See also: github.com/thesephist/xi 455 | ' 456 | 457 | Cli := cli.parse() 458 | if [Cli.opts.help, Cli.opts.h] { 459 | [true, _], [_, true] -> println(Help) 460 | _ -> if filePath := Cli.verb { 461 | // run as REPL 462 | ? -> with loop() fn(_, break) { 463 | print('xi) ') 464 | evt := input() 465 | if evt.type { 466 | :error -> break() 467 | _ -> if program := evt.data |> trim() { 468 | 'exit' -> break() 469 | _ -> program |> parse() |> eval(stack, scope) 470 | } 471 | } 472 | } 473 | // run from file 474 | _ -> with fs.readFile(filePath) fn(file) if file { 475 | ? -> printf('Could not read file "{{0}}"\n', filePath) 476 | _ -> file |> split('\n') |> with each() fn(line) { 477 | line |> parse() |> eval(stack, scope) 478 | } 479 | } 480 | } 481 | } 482 | 483 | --------------------------------------------------------------------------------