├── LICENSE ├── Makefile ├── README.md ├── bin └── main.ink ├── runtime ├── ink.js ├── quicksort.js ├── std.js └── str.js ├── september.jpg ├── src ├── analyze.ink ├── gen.ink ├── highlight.ink ├── iota.ink ├── parse.ink ├── tokenize.ink └── translate.ink ├── test ├── cases │ ├── 000.ink │ ├── 001.ink │ ├── 002.ink │ ├── 003.ink │ ├── 004.ink │ ├── 005.ink │ ├── 006.ink │ ├── 007.ink │ ├── 008.ink │ ├── 009.ink │ └── 010.ink ├── parse │ └── 000.ink └── test.ink └── vendor ├── ansi.ink ├── cli.ink ├── quicksort.ink ├── std.ink └── str.ink /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: run 2 | 3 | # run main binary 4 | run: 5 | ./bin/main.ink translate-full test/cases/*.ink 6 | 7 | # run all tests under test/ 8 | check: 9 | ./bin/main.ink run test/cases/*.ink 10 | t: check 11 | 12 | fmt: 13 | inkfmt fix bin/*.ink src/*.ink test/*.ink test/cases/*.ink 14 | f: fmt 15 | 16 | fmt-check: 17 | inkfmt bin/*.ink src/*.ink test/*.ink test/cases/*.ink 18 | fk: fmt-check 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # September 🐞 2 | 3 | **September** is an [Ink](https://github.com/thesephist/ink) to JavaScript compiler and toolchain for cross-compiling Ink applications to Node.js and the Web. September aims to be self-hosting on Node.js and pass all tests against Ink's core standard library test suite. I've also written about building September on the [Ink language blog](https://dotink.co/posts/september/). 4 | 5 | ![September banner image](september.jpg) 6 | 7 | ## Contents 8 | 9 | 1. [Usage](#usage) 10 | 2. [How it works](#how-it-works) 11 | 3. [Performance](#performance) 12 | 4. [Future work](#future-work) 13 | 5. [Prior art](#prior-art) 14 | 15 | --- 16 | 17 | ## Usage 18 | 19 | We can use September to translate Ink programs to JavaScript programs. If you have Node.js installed, September can also compile and run Ink code on Node.js. 20 | 21 | To compile Ink program files (say, `a.ink`), run `september translate` to compile and print the result. 22 | 23 | ```sh 24 | $ cat a.ink # example program 25 | std := load('./runtime/std') 26 | 27 | log := std.log 28 | 29 | a := 2 30 | b := 3 31 | 32 | log(a + b) 33 | log('Hello, World!') 34 | 35 | $ september translate a.ink # compile to JS 36 | std = load(__Ink_String(`./runtime/std`)); 37 | log = (() => {let __ink_acc_trgt = __as_ink_string(std); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[log] || null : (__ink_acc_trgt.log !== undefined ? __ink_acc_trgt.log : null)})(); 38 | a = 2; 39 | b = 3; 40 | log(__as_ink_string(a + b)); 41 | log(__Ink_String(`Hello, World!`)) 42 | ``` 43 | 44 | When we run Ink programs compiled to JavaScript, Ink requires a small [runtime](https://en.wikipedia.org/wiki/Runtime_library) (available in [runtime/ink.js](runtime/ink.js)). `september translate-full` prints the compiled JavaScript program alongside the necessary Ink runtime. 45 | 46 | `september run` will compile the Ink program and pass it to Node.js, if installed, to execute it immediately. 47 | 48 | ```sh 49 | $ september run a.ink # run Ink program in Node.js 50 | 5 51 | Hello, World! 52 | ``` 53 | 54 | `september run` currently swallows any errors emitted by Node.js. During debugging it's helpful to run Node separately and pass it the program through stdin, so Node.js can emit any errors to stderr. 55 | 56 | ```sh 57 | $ september translate-full a.ink | node -- # run Ink program through Node, emit stderr 58 | 5 59 | Hello, World! 60 | ``` 61 | 62 | Because September contains a fully self-hosted tokenizer and parser, September can also simply syntax highlight Ink programs. To print an Ink program with syntax highlighting, use `september print`. 63 | 64 | ```sh 65 | $ september print a.ink 66 | std := load('./runtime/std') 67 | 68 | log := std.log 69 | 70 | a := 2 71 | b := 3 72 | 73 | log(a + b) 74 | log('Hello, World!') 75 | ``` 76 | 77 | All September commands accept multiple files as input. If we want to run `a.ink` and `b.ink`, just run `september run a.ink b.ink`. 78 | 79 | ## How it works 80 | 81 | September, like any compiler, works in stages: 82 | 83 | 1. **Construct an Ink parse tree** (`tokenize.ink`, `parse.ink`). This produces a data structure called the abstract syntax tree that represents well-formed Ink programs in a way the rest of September can understand. The syntax tree contains constructs like "binary addition expression" and "variable reference" and "function call". 84 | 2. **Semantic analysis** and annotation over the syntax tree (`analyze.ink`). During semantic analysis, September tries to infer some information that isn't available in the syntax tree itself, and makes it available to the code generator so it can generate better, correct code. Semantic analysis also transforms some syntax tree nodes to simpler or more correct ones, like eliminating unused expressions, moving some variable declarations to the top of a scope, or transforming tail calls to be eliminated into loops. 85 | 3. **Code generation** (`gen.ink`). The code generator takes the annotated syntax tree and outputs a JavaScript program that implements it. 86 | 87 | Because Ink and JavaScript have many high-level similarities, September doesn't need an intermediate representation -- code generation is done straight from an annotated syntax tree. 88 | 89 | For operations where Ink and JavaScript behave differently, September has two solutions with different tradeoffs. 90 | 91 | 1. **Inline code**: During code generation, September can insert a layer of indirection inline to mimic Ink's behavior within JavaScript. For example, Ink doesn't have explicit variable declarations (like Python, first use is declaration) but JavaScript uses `let` for local variables. September therefore computes where first use of variables are in the Ink syntax tree, and annotates them so the code generator can insert `let` statements where appropriate. 92 | 2. **Runtime library**: Some differences, like different behaviors of arithmetic operators, are better emulated by calling into a pre-defined JavaScript function from the generated code. For example, the negation operator `~` in Ink maps to two different JavaScript operators, depending on the type of the operand. Rather than pack this code inline, the generated code calls out to `__ink_negate()`, a runtime library function written in JavaScript that implements `~` in JavaScript. 93 | 94 | Most of the time, the correct translation is a combination of some inline transformation and runtime facility. For example, in Ink, the assignment operator `:=` can mutate strings. JavaScript strings are not mutable. So Ink transforms string mutations in Ink to a JavaScript expression that checks the type of an object with inline code, and conditionally calls a runtime function to mutate the mutable string object, which is implemented in the runtime. 95 | 96 | **September makes the following code transformations.** 97 | 98 | ### Operators 99 | 100 | - `~`: runtime function `__ink_negate` which performs a `-` if argument is number, `!` otherwise. 101 | - Basic arithmetic (`+ - * / %`) are translated verbatim, because they are valid JavaScript. 102 | - The equality comparison `=` does deep comparisons in Ink, which does not have a native JavaScript equivalent. Instead, we call out to a performance-optimized runtime function `__ink_eq` which has fast paths for simple types, and does deep comparisons for composite values. 103 | - `>` and `<` are translated literally, as the semantics in Ink and JavaScript match. 104 | - `.`: the property access operator works the same way in JavaScript (with some caveats outlined in the "composite values" section below), but the operator precedence is different. Specifically, function calls on the right side of `.` need to be parenthesized in JavaScript because it has higher precedence in Ink. 105 | - Binary combinators `& | ^` have different behavior for numbers and booleans in Ink, like the fact that they do not short circuit in Ink, so they call out to their respective runtime functions, `__ink_{and, or, xor}`. 106 | 107 | ### Values and types 108 | 109 | - The **null** value `()` is translated to `null` in JavaScript. 110 | - Ink **numbers** are 64-bit floating point numbers, and are translated directly to JavaScript `number` values. 111 | - Ink **booleans** are `true` and `false` symbols and are translated literally to JavaScript boolean values. 112 | - **Strings** in Ink are mutable, which means we cannot simply substitute them for JavaScript strings. However, JavaScript strings are heavily optimized, and we want to take advantage of those optimizations. So to represent Ink strings, we wrap JavaScript strings in an `__Ink_String` class, which exposes methods for mutation and degrades gracefully to native JavaScript strings when interfacing with native JavaScript functions that take strings. 113 | - Translating **composite** values in Ink is more involved. While the value itself behaves like JavaScript objects or arrays, property access and assignment semantics differ, and Ink uses the single composite type for both list and map style data structures. Specifically, we make the following translations: 114 | - Composites initialized with `[]` are translated to JavaScript arrays. 115 | - Composites initialized with `{}` are translated to JavaScript object literals. 116 | - Assignment to a composite value is translated directly. i.e. The Ink program `c.(k) := v` is translated to `c[k] = v`. Notably, `c.k := v` is also translated to `c[k] = v` because `k` can be a numeric identifier in Ink, as in `c.2 := 3`. 117 | - Assignment `c.k := v` evaluates to `c` in Ink, but `v` in JavaScript. The compiler wraps assignments to composite properties so this behavior is preserved. 118 | - Property access like `c[k]` is wrapped with a nullability check. This is because accessing properties in Ink returns `()` but `undefined` in JavaScript, so we need to check if the returned value is `undefined` and return `null` instead if so. 119 | 120 | Ink has a special value `_` (the empty identifier), which is mapped to a `Symbol` in JavaScript. The empty identifier has special behavior in equality checks, defined in `__ink_eq`. 121 | 122 | ### Variable binding and scope 123 | 124 | Ink variables follow strict lexical binding and closely mirrors JavaScript's lexical binding rules. Because Ink variable bindings are always mutable, September defaults to translate all variable declarations (first variable reference in a scope) to a `let` declaration in JavaScript except top-level variables, which become globals in JavaScript. During semantic analysis, the compiler recognizes first assignments to names in a given scope and annotates each so it can be compiled to a `let` binding. 125 | 126 | In Ink, a variable declaration is an expression; in JavaScript it is a statement. This means variable bindings may need to be pulled out of an expression in Ink into its own statement in JavaScript. 127 | 128 | Further optimizations may be added in the future. In particular, normalizing expressions to [SSA](https://en.wikipedia.org/wiki/Static_single_assignment_form) might be interesting, though I suspect V8 already optimizes JS this way under the hood, so performance advantages might be underwhelming. 129 | 130 | ### Functions 131 | 132 | The behavior of Ink functions is a strict subset of that of JavaScript functions, so in translating Ink to JavaScript, we can map function definitions and invocations directly to JavaScript equivalents. 133 | 134 | One caveat is that, although modern JavaScript functions are tail-call-optimized by specification, only JavaScriptCore (Safari) implements it in practice. This means functions with recursive tail calls need to be transformed during compilation into a non-recursive form. 135 | 136 | When calling a function that invokes recursive tail calls (calls to itself by its original bound name within its body), September detects it and automatically unrolls it into a [trampoline](https://en.wikipedia.org/wiki/Trampoline_(computing)) construct with a JavaScript `while` loop. This means September eliminates self-tail calls. However, JS limitations mean September cannot tail-call-eliminate mutual recursion. 137 | 138 | ### Match expressions 139 | 140 | In this first version of September, match expressions call out to the Ink runtime using the `__ink_match()` function, which takes match targets and clauses as closures. The compiler transforms a match expression into a single call to `__ink_match` with the correct closures. 141 | 142 | In the future, I hope to optimize the function away and compile matches straight down to `if...else` or `switch` blocks. 143 | 144 | ### Module system and imports 145 | 146 | The `load()` Ink builtin, normally used for module imports, is mapped transparently to Node.js's `require()` in the runtime. This means Ink code compiled with September can reference other JavaScript programs with the built-in module system. 147 | 148 | Importing Ink modules with `load()` is a work in progress. 149 | 150 | ## Performance 151 | 152 | Modern JavaScript runtimes are fast (though perhaps at the expense of memory footprint). Even in the current prototype that performs little to no optimizing transformations, Ink code compiled with September runs significantly faster on the V8 engine than when the Ink code is run natively with the [Go-based interpreter](https://github.com/thesephist/ink), for certain workloads like numeric compute. 153 | 154 | ### Optimizations 155 | 156 | September is designed to be an optimizing compiler. It aims to make the following optimizations. 157 | 158 | - [ ] Dead branch elimination 159 | - [ ] Common subexpression elimination 160 | - [ ] Static branch elimination by type propagation 161 | - [ ] Inlined match expressions (rather than relying on a runtime function) 162 | 163 | ## Future work 164 | 165 | This is an informal list of things I'd like to implement, but haven't had time to yet. 166 | 167 | - Performance optimizations 168 | - Tests should run through all valid test cases in `test/cases/` and compare that the output for programs running on Node.js matches the output when run on Ink's Go interpreter. 169 | - Interoperability with JavaScript classes. Ink programs can currently construct objects with the runtime function `obj := jsnew(constructor, args)`, but cannot extend JavaScript classes properly. 170 | 171 | ## Prior art 172 | 173 | Language-to-language compilers is a rich and deep field with lots of priors. A few that were especially helpful are: 174 | 175 | - [Fengari](https://fengari.io), a Lua runtime on top of JavaScript. Especially helpful in thinking about how to translate mutable strings and Ink-JavaScript interoperability. 176 | - [Babel](https://babeljs.io), the industry standard JavaScript compiler. 177 | - [Ivy](https://github.com/robpike/ivy), an interpreter written in Go for an APL-like language, with an elegant parser design. 178 | - [The 2ality blog](https://2ality.com/2011/12/fake-operator-overloading.html) for ideas about how to implement mutable strings that fallback gracefully to native JS strings, and further minutiae of JavaScript's lesser known rules. 179 | 180 | and of course, 181 | 182 | - [The canonical implementation of Ink](https://github.com/thesephist). The parser and tokenizer in September are very much inspired by their Go equivalents. 183 | -------------------------------------------------------------------------------- /bin/main.ink: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ink 2 | 3 | std := load('../vendor/std') 4 | 5 | log := std.log 6 | f := std.format 7 | cat := std.cat 8 | 9 | each := std.each 10 | readFile := std.readFile 11 | 12 | cli := load('../vendor/cli') 13 | 14 | ` september subcommands ` 15 | highlight := load('../src/highlight').main 16 | translate := load('../src/translate').main 17 | 18 | Newline := char(10) 19 | Tab := char(9) 20 | PreamblePath := './runtime/ink.js' 21 | 22 | given := (cli.parsed)() 23 | given.verb :: { 24 | ` syntax-highlight input Ink programs from the token stream 25 | and print them to stdout ` 26 | 'print' -> ( 27 | files := given.args 28 | each(files, path => ( 29 | readFile(path, data => out(highlight(data))) 30 | )) 31 | ) 32 | ` translate translates input Ink programs to JavaScript and 33 | print them to stdout ` 34 | 'translate' -> ( 35 | js := [] 36 | files := given.args 37 | each(files, (path, i) => readFile(path, data => ( 38 | js.(i) := translate(data) 39 | len(files) :: { 40 | len(js) -> log(cat(js, Newline)) 41 | } 42 | ))) 43 | ) 44 | 'translate-full' -> readFile(PreamblePath, preamble => ( 45 | js := [preamble] 46 | files := given.args 47 | each(files, (path, i) => readFile(path, data => ( 48 | js.(i + 1) := translate(data) 49 | len(files) + 1 :: { 50 | len(js) -> log(cat(js, Newline)) 51 | } 52 | ))) 53 | )) 54 | 'run' -> readFile(PreamblePath, preamble => ( 55 | js := [preamble] 56 | files := given.args 57 | each(files, (path, i) => readFile(path, data => ( 58 | js.(i + 1) := translate(data) 59 | len(files) + 1 :: { 60 | len(js) -> exec( 61 | 'node' 62 | ['--'] 63 | cat(js, ';' + Newline) 64 | evt => out(evt.data) 65 | ) 66 | } 67 | ))) 68 | )) 69 | ` start an interactive REPL backed by Node.js, if installed. 70 | might end up being the default behavior ` 71 | 'repl' -> log('command "repl" not implemented!') 72 | _ -> ( 73 | commands := [ 74 | 'print' 75 | 'translate' 76 | 'translate-full' 77 | 'run' 78 | ] 79 | log(f('command "{{ verb }}" not recognized', given)) 80 | log('September supports: ' + Newline + Tab + 81 | cat(commands, Newline + Tab)) 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /runtime/ink.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink/JavaScript runtime/interop layer 3 | * implements Ink system interfaces for web and Node JS runtimes 4 | */ 5 | 6 | const __NODE = typeof process === 'object'; 7 | 8 | /* Ink builtins */ 9 | 10 | function args() { 11 | return process.argv; 12 | } 13 | 14 | function __ink_ident_in(cb) { 15 | // TODO 16 | } 17 | 18 | function out(s) { 19 | s = __as_ink_string(s); 20 | if (__NODE) { 21 | process.stdout.write(string(s).valueOf()); 22 | } else { 23 | console.log(string(s).valueOf()); 24 | } 25 | return null; 26 | } 27 | 28 | function dir(path, cb) { 29 | // TODO 30 | } 31 | 32 | function make(path, cb) { 33 | // TODO 34 | } 35 | 36 | function stat(path, cb) { 37 | // TODO 38 | } 39 | 40 | function read(path, offset, length, cb) { 41 | // TODO 42 | } 43 | 44 | function write(path, offset, data, cb) { 45 | // TODO 46 | } 47 | 48 | function __ink_ident_delete(path, cb) { 49 | // TODO 50 | } 51 | 52 | function listen(host, handler) { 53 | // TODO 54 | } 55 | 56 | function req(data, callback) { 57 | // TODO 58 | } 59 | 60 | function rand() { 61 | return Math.random(); 62 | } 63 | 64 | function urand(length) { 65 | // TODO 66 | } 67 | 68 | function time() { 69 | return Date.now() / 1000; 70 | } 71 | 72 | function wait(duration, cb) { 73 | setTimeout(cb, duration * 1000); 74 | return null; 75 | } 76 | 77 | function exec(path, args, stdin, stdoutFn) { 78 | // TODO 79 | } 80 | 81 | function env() { 82 | if (__NODE) { 83 | return process.env; 84 | } 85 | return {}; 86 | } 87 | 88 | function exit(code) { 89 | if (__NODE) { 90 | process.exit(code); 91 | } 92 | return null; 93 | } 94 | 95 | function sin(n) { 96 | return Math.sin(n); 97 | } 98 | 99 | function cos(n) { 100 | return Math.cos(n); 101 | } 102 | 103 | function asin(n) { 104 | return Math.asin(n); 105 | } 106 | 107 | function acos(n) { 108 | return Math.acos(n); 109 | } 110 | 111 | function pow(b, n) { 112 | return Math.pow(b, n); 113 | } 114 | 115 | function ln(n) { 116 | return Math.log(n); 117 | } 118 | 119 | function floor(n) { 120 | return Math.floor(n); 121 | } 122 | 123 | function load(path) { 124 | if (__NODE) { 125 | return require(string(path).valueOf()); 126 | } else { 127 | throw new Error('load() not implemented!'); 128 | } 129 | } 130 | 131 | function __is_ink_string(x) { 132 | if (x == null) { 133 | return false; 134 | } 135 | return x.__mark_ink_string; 136 | } 137 | 138 | // both JS native strings and __Ink_Strings are valid in the runtime 139 | // semantics but we want to coerce values to __Ink_Strings 140 | // within runtime builtins; this utility fn is useful for this. 141 | function __as_ink_string(x) { 142 | if (typeof x === 'string') { 143 | return __Ink_String(x); 144 | } 145 | return x; 146 | } 147 | 148 | function string(x) { 149 | x = __as_ink_string(x); 150 | if (x === null) { 151 | return '()'; 152 | } else if (typeof x === 'number') { 153 | return x.toString(); 154 | } else if (__is_ink_string(x)) { 155 | return x; 156 | } else if (typeof x === 'boolean') { 157 | return x.toString(); 158 | } else if (typeof x === 'function') { 159 | return x.toString(); // implementation-dependent, not specified 160 | } else if (Array.isArray(x) || typeof x === 'object') { 161 | const entries = []; 162 | for (const key of keys(x)) { 163 | entries.push(`${key}: ${__is_ink_string(x[key]) ? `'${x[key].valueOf().replace('\\', '\\\\').replace('\'', '\\\'')}'` : string(x[key])}`); 164 | } 165 | return '{' + entries.join(', ') + '}'; 166 | } else if (x === undefined) { 167 | return 'undefined'; // undefined behavior 168 | } 169 | throw new Error('string() called on unknown type ' + x); 170 | } 171 | 172 | function number(x) { 173 | x = __as_ink_string(x); 174 | if (x === null) { 175 | return 0; 176 | } else if (typeof x === 'number') { 177 | return x; 178 | } else if (__is_ink_string(x)) { 179 | const n = parseFloat(x); 180 | return isNaN(n) ? null : n; 181 | } else if (typeof x === 'boolean') { 182 | return x ? 1 : 0; 183 | } 184 | return 0; 185 | } 186 | 187 | function point(c) { 188 | c = __as_ink_string(c); 189 | return c.valueOf().charCodeAt(0); 190 | } 191 | 192 | function char(n) { 193 | return String.fromCharCode(n); 194 | } 195 | 196 | function type(x) { 197 | x = __as_ink_string(x); 198 | if (x === null) { 199 | return '()'; 200 | } else if (typeof x === 'number') { 201 | return 'number'; 202 | } else if (__is_ink_string(x)) { 203 | return 'string'; 204 | } else if (typeof x === 'boolean') { 205 | return 'boolean' 206 | } else if (typeof x === 'function') { 207 | return 'function'; 208 | } else if (Array.isArray(x) || typeof x === 'object') { 209 | return 'composite'; 210 | } 211 | throw new Error('type() called on unknown type ' + x); 212 | } 213 | 214 | function len(x) { 215 | x = __as_ink_string(x); 216 | switch (type(x)) { 217 | case 'string': 218 | return x.valueOf().length; 219 | case 'composite': 220 | if (Array.isArray(x)) { 221 | // -1 for .length 222 | return Object.getOwnPropertyNames(x).length - 1; 223 | } else { 224 | return Object.getOwnPropertyNames(x).length; 225 | } 226 | default: 227 | throw new Error('len() takes a string or composite value, but got ' + string(x)); 228 | } 229 | } 230 | 231 | function keys(x) { 232 | if (type(x).valueOf() === 'composite') { 233 | if (Array.isArray(x)) { 234 | return Object.getOwnPropertyNames(x).filter(name => name !== 'length'); 235 | } else { 236 | return Object.getOwnPropertyNames(x); 237 | } 238 | } 239 | throw new Error('keys() takes a composite value, but got ' + string(x).valueOf()); 240 | } 241 | 242 | /* Ink semantics polyfill */ 243 | 244 | function __ink_negate(x) { 245 | if (x === true) { 246 | return false; 247 | } 248 | if (x === false) { 249 | return true; 250 | } 251 | 252 | return -x; 253 | } 254 | 255 | function __ink_eq(a, b) { 256 | a = __as_ink_string(a); 257 | b = __as_ink_string(b); 258 | if (a === __Ink_Empty || b === __Ink_Empty) { 259 | return true; 260 | } 261 | 262 | if (a === null && b === null) { 263 | return true; 264 | } 265 | if (a === null || b === null) { 266 | return false; 267 | } 268 | 269 | if (typeof a !== typeof b) { 270 | return false; 271 | } 272 | if (__is_ink_string(a) && __is_ink_string(b)) { 273 | return a.valueOf() === b.valueOf(); 274 | } 275 | if (typeof a === 'number' || typeof a === 'boolean' || typeof a === 'function') { 276 | return a === b; 277 | } 278 | 279 | // deep equality check for composite types 280 | if (typeof a !== 'object') { 281 | return false; 282 | } 283 | if (len(a) !== len(b)) { 284 | return false; 285 | } 286 | for (const key of keys(a)) { 287 | if (!__ink_eq(a[key], b[key])) { 288 | return false; 289 | } 290 | } 291 | return true; 292 | } 293 | 294 | function __ink_and(a, b) { 295 | if (typeof a === 'boolean' && typeof b === 'boolean') { 296 | return a && b; 297 | } 298 | 299 | if (__is_ink_string(a) && __is_ink_string(b)) { 300 | const max = Math.max(a.length, b.length); 301 | const get = (s, i) => s.valueOf().charCodeAt(i) || 0; 302 | 303 | let res = ''; 304 | for (let i = 0; i < max; i ++) { 305 | res += String.fromCharCode(get(a, i) & get(b, i)); 306 | } 307 | return res; 308 | } 309 | 310 | return a & b; 311 | } 312 | 313 | function __ink_or(a, b) { 314 | if (typeof a === 'boolean' && typeof b === 'boolean') { 315 | return a || b; 316 | } 317 | 318 | if (__is_ink_string(a) && __is_ink_string(b)) { 319 | const max = Math.max(a.length, b.length); 320 | const get = (s, i) => s.valueOf().charCodeAt(i) || 0; 321 | 322 | let res = ''; 323 | for (let i = 0; i < max; i ++) { 324 | res += String.fromCharCode(get(a, i) | get(b, i)); 325 | } 326 | return res; 327 | } 328 | 329 | return a | b; 330 | } 331 | 332 | function __ink_xor(a, b) { 333 | if (typeof a === 'boolean' && typeof b === 'boolean') { 334 | return (a && !b) || (!a && b); 335 | } 336 | 337 | if (__is_ink_string(a) && __is_ink_string(b)) { 338 | const max = Math.max(a.length, b.length); 339 | const get = (s, i) => s.valueOf().charCodeAt(i) || 0; 340 | 341 | let res = ''; 342 | for (let i = 0; i < max; i ++) { 343 | res += String.fromCharCode(get(a, i) ^ get(b, i)); 344 | } 345 | return res; 346 | } 347 | 348 | return a ^ b; 349 | } 350 | 351 | function __ink_match(cond, clauses) { 352 | for (const [target, expr] of clauses) { 353 | if (__ink_eq(cond, target())) { 354 | return expr(); 355 | } 356 | } 357 | return null; 358 | } 359 | 360 | /* Ink types */ 361 | 362 | const __Ink_Empty = Symbol('__Ink_Empty'); 363 | 364 | const __Ink_String = s => { 365 | if (__is_ink_string(s)) return s; 366 | 367 | return { 368 | __mark_ink_string: true, 369 | assign(i, slice) { 370 | if (i === s.length) { 371 | return s += slice; 372 | } 373 | 374 | return s = s.substr(0, i) + slice + s.substr(i + slice.length); 375 | }, 376 | toString() { 377 | return s; 378 | }, 379 | valueOf() { 380 | return s; 381 | }, 382 | get length() { 383 | return s.length; 384 | }, 385 | } 386 | } 387 | 388 | /* TCE trampoline helpers */ 389 | 390 | function __ink_resolve_trampoline(fn, ...args) { 391 | let rv = fn(...args); 392 | while (rv && rv.__is_ink_trampoline) { 393 | rv = rv.fn(...rv.args); 394 | } 395 | return rv; 396 | } 397 | 398 | function __ink_trampoline(fn, ...args) { 399 | return { 400 | __is_ink_trampoline: true, 401 | fn: fn, 402 | args: args, 403 | } 404 | } 405 | 406 | /* Ink -> JavaScript interop helpers */ 407 | 408 | const bind = (target, fn) => target[fn].bind(target); 409 | 410 | function jsnew(Constructor, args) { 411 | return new Constructor(...args); 412 | } 413 | -------------------------------------------------------------------------------- /runtime/quicksort.js: -------------------------------------------------------------------------------- 1 | sortBy = (v, pred) => (() => { let partition; let vPred; vPred = map(v, pred); partition = (v, lo, hi) => (() => { let __ink_trampolined_lsub; let __ink_trampolined_rsub; let lsub; let pivot; let rsub; pivot = (() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return lo })()] || null : (__ink_acc_trgt[(() => { return lo })()] !== undefined ? __ink_acc_trgt[(() => { return lo })()] : null)})(); lsub = i => (() => { __ink_trampolined_lsub = i => __ink_match(((() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() < pivot), [[() => (true), () => (__ink_trampoline(__ink_trampolined_lsub, __as_ink_string(i + 1)))], [() => (false), () => (i)]]); return __ink_resolve_trampoline(__ink_trampolined_lsub, i) })(); rsub = j => (() => { __ink_trampolined_rsub = j => __ink_match(((() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})() > pivot), [[() => (true), () => (__ink_trampoline(__ink_trampolined_rsub, (j - 1)))], [() => (false), () => (j)]]); return __ink_resolve_trampoline(__ink_trampolined_rsub, j) })(); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, j) => (() => { __ink_trampolined_sub = (i, j) => (() => { i = lsub(i); j = rsub(j); return __ink_match((i < j), [[() => (false), () => (j)], [() => (true), () => ((() => { let tmp; let tmpPred; tmp = (() => {let __ink_acc_trgt = __as_ink_string(v); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(); tmpPred = (() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(); (() => {let __ink_assgn_trgt = __as_ink_string(v); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), (() => {let __ink_acc_trgt = __as_ink_string(v); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})()) : (__ink_assgn_trgt[(() => { return i })()]) = (() => {let __ink_acc_trgt = __as_ink_string(v); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})(); return __ink_assgn_trgt})(); (() => {let __ink_assgn_trgt = __as_ink_string(v); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return j })(), tmp) : (__ink_assgn_trgt[(() => { return j })()]) = tmp; return __ink_assgn_trgt})(); (() => {let __ink_assgn_trgt = __as_ink_string(vPred); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), (() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})()) : (__ink_assgn_trgt[(() => { return i })()]) = (() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})(); return __ink_assgn_trgt})(); (() => {let __ink_assgn_trgt = __as_ink_string(vPred); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return j })(), tmpPred) : (__ink_assgn_trgt[(() => { return j })()]) = tmpPred; return __ink_assgn_trgt})(); return __ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), (j - 1)) })())]]) })(); return __ink_resolve_trampoline(__ink_trampolined_sub, i, j) })() })()(lo, hi) })(); return (() => { let __ink_trampolined_quicksort; let quicksort; return quicksort = (v, lo, hi) => (() => { __ink_trampolined_quicksort = (v, lo, hi) => __ink_match(len(v), [[() => (0), () => (v)], [() => (__Ink_Empty), () => (__ink_match((lo < hi), [[() => (false), () => (v)], [() => (true), () => ((() => { let p; p = partition(v, lo, hi); quicksort(v, lo, p); return __ink_trampoline(__ink_trampolined_quicksort, v, __as_ink_string(p + 1), hi) })())]]))]]); return __ink_resolve_trampoline(__ink_trampolined_quicksort, v, lo, hi) })() })()(v, 0, (len(v) - 1)) })(); 2 | module.exports.sortBy = sortBy; 3 | sort__ink_em__ = v => sortBy(v, x => x); 4 | module.exports.sort__ink_em__ = sort__ink_em__; 5 | sort = v => sort__ink_em__(clone(v)); 6 | module.exports.sort = sort; 7 | 8 | -------------------------------------------------------------------------------- /runtime/std.js: -------------------------------------------------------------------------------- 1 | log = val => out(__as_ink_string(string(val) + __Ink_String('\n'))); 2 | module.exports.log = log; 3 | scan = cb => (() => { let acc; acc = [__Ink_String(``)]; return __ink_ident_in(evt => __ink_match((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[type] || null : (__ink_acc_trgt.type !== undefined ? __ink_acc_trgt.type : null)})(), [[() => (__Ink_String(`end`)), () => (cb((() => {let __ink_acc_trgt = __as_ink_string(acc); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})()))], [() => (__Ink_String(`data`)), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(0, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(acc); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})() + slice((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})(), 0, (len((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()) - 1)))) : (__ink_assgn_trgt[0]) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(acc); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})() + slice((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})(), 0, (len((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()) - 1))); return __ink_assgn_trgt})(); return false })())]])) })(); 4 | module.exports.scan = scan; 5 | hToN = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, [__Ink_String(`a`)]: 10, [__Ink_String(`b`)]: 11, [__Ink_String(`c`)]: 12, [__Ink_String(`d`)]: 13, [__Ink_String(`e`)]: 14, [__Ink_String(`f`)]: 15}; 6 | module.exports.hToN = hToN; 7 | nToH = __Ink_String(`0123456789abcdef`); 8 | module.exports.nToH = nToH; 9 | hex = n => (() => { let __ink_trampolined_sub; let sub; return sub = (p, acc) => (() => { __ink_trampolined_sub = (p, acc) => __ink_match((p < 16), [[() => (true), () => (__as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(nToH); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return p })()] || null : (__ink_acc_trgt[(() => { return p })()] !== undefined ? __ink_acc_trgt[(() => { return p })()] : null)})() + acc))], [() => (false), () => (__ink_trampoline(__ink_trampolined_sub, floor((p / 16)), __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(nToH); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (p % 16) })()] || null : (__ink_acc_trgt[(() => { return (p % 16) })()] !== undefined ? __ink_acc_trgt[(() => { return (p % 16) })()] : null)})() + acc)))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, p, acc) })() })()(floor(n), __Ink_String(``)); 10 | module.exports.hex = hex; 11 | xeh = s => (() => { let max; max = len(s); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), __as_ink_string((acc * 16) + (() => {let __ink_acc_trgt = __as_ink_string(hToN); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() })()] || null : (__ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() })()] !== undefined ? __ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() })()] : null)})())))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(0, 0) })(); 12 | module.exports.xeh = xeh; 13 | min = numbers => reduce(numbers, (acc, n) => __ink_match((n < acc), [[() => (true), () => (n)], [() => (false), () => (acc)]]), (() => {let __ink_acc_trgt = __as_ink_string(numbers); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})()); 14 | module.exports.min = min; 15 | max = numbers => reduce(numbers, (acc, n) => __ink_match((n > acc), [[() => (true), () => (n)], [() => (false), () => (acc)]]), (() => {let __ink_acc_trgt = __as_ink_string(numbers); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})()); 16 | module.exports.max = max; 17 | range = (start, end, step) => (() => { let __ink_trampolined_sub; let span; let sub; span = (end - start); sub = (i, v, acc) => (() => { __ink_trampolined_sub = (i, v, acc) => __ink_match((((() => { return (v - start) })() / span) < 1), [[() => (true), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), v) : (__ink_assgn_trgt[(() => { return i })()]) = v; return __ink_assgn_trgt})(); return __ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), __as_ink_string(v + step), acc) })())], [() => (false), () => (acc)]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, v, acc) })(); return __ink_match((((() => { return (end - start) })() / step) > 0), [[() => (true), () => (sub(0, start, []))], [() => (false), () => ([])]]) })(); 18 | module.exports.range = range; 19 | clamp = (start, end, min, max) => (() => { start = (() => { return __ink_match((start < min), [[() => (true), () => (min)], [() => (false), () => (start)]]) })(); end = (() => { return __ink_match((end < min), [[() => (true), () => (min)], [() => (false), () => (end)]]) })(); end = (() => { return __ink_match((end > max), [[() => (true), () => (max)], [() => (false), () => (end)]]) })(); start = (() => { return __ink_match((start > end), [[() => (true), () => (end)], [() => (false), () => (start)]]) })(); return {start: start, end: end} })(); 20 | module.exports.clamp = clamp; 21 | slice = (s, start, end) => (() => { let max; let x; x = clamp(start, end, 0, len(s)); start = (() => {let __ink_acc_trgt = __as_ink_string(x); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[start] || null : (__ink_acc_trgt.start !== undefined ? __ink_acc_trgt.start : null)})(); max = ((() => {let __ink_acc_trgt = __as_ink_string(x); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[end] || null : (__ink_acc_trgt.end !== undefined ? __ink_acc_trgt.end : null)})() - start); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return __as_ink_string(start + i) })()] || null : (__ink_acc_trgt[(() => { return __as_ink_string(start + i) })()] !== undefined ? __ink_acc_trgt[(() => { return __as_ink_string(start + i) })()] : null)})()) : (__ink_assgn_trgt[(() => { return i })()]) = (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return __as_ink_string(start + i) })()] || null : (__ink_acc_trgt[(() => { return __as_ink_string(start + i) })()] !== undefined ? __ink_acc_trgt[(() => { return __as_ink_string(start + i) })()] : null)})(); return __ink_assgn_trgt})()))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(0, __ink_match(type(s), [[() => (__Ink_String(`string`)), () => (__Ink_String(``))], [() => (__Ink_String(`composite`)), () => ([])]])) })(); 22 | module.exports.slice = slice; 23 | append = (base, child) => (() => { let baseLength; let childLength; baseLength = len(base); childLength = len(child); return (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match(i, [[() => (childLength), () => (base)], [() => (__Ink_Empty), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(base); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return __as_ink_string(baseLength + i) })(), (() => {let __ink_acc_trgt = __as_ink_string(child); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})()) : (__ink_assgn_trgt[(() => { return __as_ink_string(baseLength + i) })()]) = (() => {let __ink_acc_trgt = __as_ink_string(child); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(); return __ink_assgn_trgt})(); return __ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1)) })())]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0) })(); 24 | module.exports.append = append; 25 | join = (base, child) => append(clone(base), child); 26 | module.exports.join = join; 27 | clone = x => __ink_match(type(x), [[() => (__Ink_String(`string`)), () => (__as_ink_string(__Ink_String(``) + x))], [() => (__Ink_String(`composite`)), () => (reduce(keys(x), (acc, k) => (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return k })(), (() => {let __ink_acc_trgt = __as_ink_string(x); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return k })()] || null : (__ink_acc_trgt[(() => { return k })()] !== undefined ? __ink_acc_trgt[(() => { return k })()] : null)})()) : (__ink_assgn_trgt[(() => { return k })()]) = (() => {let __ink_acc_trgt = __as_ink_string(x); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return k })()] || null : (__ink_acc_trgt[(() => { return k })()] !== undefined ? __ink_acc_trgt[(() => { return k })()] : null)})(); return __ink_assgn_trgt})(), {}))], [() => (__Ink_Empty), () => (x)]]); 28 | module.exports.clone = clone; 29 | stringList = list => __as_ink_string(__as_ink_string(__Ink_String(`[`) + cat(map(list, string), __Ink_String(`, `))) + __Ink_String(`]`)); 30 | module.exports.stringList = stringList; 31 | reverse = list => (() => { let __ink_trampolined_sub; let sub; return sub = (acc, i) => (() => { __ink_trampolined_sub = (acc, i) => __ink_match((i < 0), [[() => (true), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})()) : (__ink_assgn_trgt[len(acc)]) = (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(); return __ink_assgn_trgt})(), (i - 1)))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, acc, i) })() })()([], (len(list) - 1)); 32 | module.exports.reverse = reverse; 33 | map = (list, f) => reduce(list, (l, item, i) => (() => {let __ink_assgn_trgt = __as_ink_string(l); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), f(item, i)) : (__ink_assgn_trgt[(() => { return i })()]) = f(item, i); return __ink_assgn_trgt})(), {}); 34 | module.exports.map = map; 35 | filter = (list, f) => reduce(list, (l, item, i) => __ink_match(f(item, i), [[() => (true), () => ((() => {let __ink_assgn_trgt = __as_ink_string(l); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(l), item) : (__ink_assgn_trgt[len(l)]) = item; return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => (l)]]), []); 36 | module.exports.filter = filter; 37 | reduce = (list, f, acc) => (() => { let max; max = len(list); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), f(acc, (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(), i)))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(0, acc) })(); 38 | module.exports.reduce = reduce; 39 | reduceBack = (list, f, acc) => (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (__ink_negate(1)), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, (i - 1), f(acc, (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(), i)))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()((len(list) - 1), acc); 40 | module.exports.reduceBack = reduceBack; 41 | flatten = list => reduce(list, append, []); 42 | module.exports.flatten = flatten; 43 | some = list => reduce(list, (acc, x) => __ink_or(acc, x), false); 44 | module.exports.some = some; 45 | every = list => reduce(list, (acc, x) => __ink_and(acc, x), true); 46 | module.exports.every = every; 47 | cat = (list, joiner) => (() => { let max; return __ink_match(max = len(list), [[() => (0), () => (__Ink_String(``))], [() => (__Ink_Empty), () => ((() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), __as_ink_string(joiner + (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})())) : (__ink_assgn_trgt[len(acc)]) = __as_ink_string(joiner + (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})()); return __ink_assgn_trgt})()))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(1, clone((() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})())))]]) })(); 48 | module.exports.cat = cat; 49 | each = (list, f) => (() => { let max; max = len(list); return (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match(i, [[() => (max), () => (null)], [() => (__Ink_Empty), () => ((() => { f((() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(), i); return __ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1)) })())]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0) })(); 50 | module.exports.each = each; 51 | encode = str => (() => { let max; max = len(str); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), point((() => {let __ink_acc_trgt = __as_ink_string(str); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})())) : (__ink_assgn_trgt[(() => { return i })()]) = point((() => {let __ink_acc_trgt = __as_ink_string(str); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})()); return __ink_assgn_trgt})()))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(0, []) })(); 52 | module.exports.encode = encode; 53 | decode = data => reduce(data, (acc, cp) => (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), char(cp)) : (__ink_assgn_trgt[len(acc)]) = char(cp); return __ink_assgn_trgt})(), __Ink_String(``)); 54 | module.exports.decode = decode; 55 | readFile = (path, cb) => (() => { let BufSize; BufSize = 4096; return (() => { let sub; return sub = (offset, acc) => read(path, offset, BufSize, evt => __ink_match((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[type] || null : (__ink_acc_trgt.type !== undefined ? __ink_acc_trgt.type : null)})(), [[() => (__Ink_String(`error`)), () => (cb(null))], [() => (__Ink_String(`data`)), () => ((() => { let dataLen; dataLen = len((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()); return __ink_match(__ink_eq(dataLen, BufSize), [[() => (true), () => (sub(__as_ink_string(offset + dataLen), (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), (() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()) : (__ink_assgn_trgt[len(acc)]) = (() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})(); return __ink_assgn_trgt})()))], [() => (false), () => (cb((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), (() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()) : (__ink_assgn_trgt[len(acc)]) = (() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})(); return __ink_assgn_trgt})()))]]) })())]])) })()(0, __Ink_String(``)) })(); 56 | module.exports.readFile = readFile; 57 | writeFile = (path, data, cb) => __ink_ident_delete(path, evt => __ink_match((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[type] || null : (__ink_acc_trgt.type !== undefined ? __ink_acc_trgt.type : null)})(), [[() => (__Ink_String(`end`)), () => (write(path, 0, data, evt => __ink_match((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[type] || null : (__ink_acc_trgt.type !== undefined ? __ink_acc_trgt.type : null)})(), [[() => (__Ink_String(`error`)), () => (cb(null))], [() => (__Ink_String(`end`)), () => (cb(true))]])))], [() => (__Ink_Empty), () => (cb(null))]])); 58 | module.exports.writeFile = writeFile; 59 | format = (raw, values) => (() => { let append; let max; let readNext; let state; state = {idx: 0, which: 0, key: __Ink_String(``), buf: __Ink_String(``)}; append = c => (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(buf, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})() + c)) : (__ink_assgn_trgt.buf) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})() + c); return __ink_assgn_trgt})(); readNext = () => (() => { let c; c = (() => {let __ink_acc_trgt = __as_ink_string(raw); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() })()] || null : (__ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() })()] !== undefined ? __ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() })()] : null)})(); __ink_match((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[which] || null : (__ink_acc_trgt.which !== undefined ? __ink_acc_trgt.which : null)})(), [[() => (0), () => (__ink_match(c, [[() => (__Ink_String(`{`)), () => ((() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 1) : (__ink_assgn_trgt.which) = 1; return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => (append(c))]]))], [() => (1), () => (__ink_match(c, [[() => (__Ink_String(`{`)), () => ((() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 2) : (__ink_assgn_trgt.which) = 2; return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => ((() => { append(__as_ink_string(__Ink_String(`{`) + c)); return (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 0) : (__ink_assgn_trgt.which) = 0; return __ink_assgn_trgt})() })())]]))], [() => (2), () => (__ink_match(c, [[() => (__Ink_String(`}`)), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(buf, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})() + string((() => {let __ink_acc_trgt = __as_ink_string(values); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] || null : (__ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] !== undefined ? __ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] : null)})()))) : (__ink_assgn_trgt.buf) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})() + string((() => {let __ink_acc_trgt = __as_ink_string(values); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] || null : (__ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] !== undefined ? __ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] : null)})())); return __ink_assgn_trgt})(); (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(key, __Ink_String(``)) : (__ink_assgn_trgt.key) = __Ink_String(``); return __ink_assgn_trgt})(); return (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 3) : (__ink_assgn_trgt.which) = 3; return __ink_assgn_trgt})() })())], [() => (__Ink_String(` `)), () => (null)], [() => (__Ink_Empty), () => ((() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(key, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() + c)) : (__ink_assgn_trgt.key) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() + c); return __ink_assgn_trgt})())]]))], [() => (3), () => (__ink_match(c, [[() => (__Ink_String(`}`)), () => ((() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 0) : (__ink_assgn_trgt.which) = 0; return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => (null)]]))]]); return (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(idx, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() + 1)) : (__ink_assgn_trgt.idx) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() + 1); return __ink_assgn_trgt})() })(); max = len(raw); return (() => { let __ink_trampolined_sub; let sub; return sub = () => (() => { __ink_trampolined_sub = () => __ink_match(((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() < max), [[() => (true), () => ((() => { readNext(); return __ink_trampoline(__ink_trampolined_sub) })())], [() => (false), () => ((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})())]]); return __ink_resolve_trampoline(__ink_trampolined_sub) })() })()() })() 60 | module.exports.format = format; 61 | 62 | -------------------------------------------------------------------------------- /runtime/str.js: -------------------------------------------------------------------------------- 1 | checkRange = (lo, hi) => c => (() => { let p; p = point(c); return __ink_and((lo < p), (p < hi)) })(); 2 | module.exports.checkRange = checkRange; 3 | upper__ink_qm__ = checkRange((point(__Ink_String(`A`)) - 1), __as_ink_string(point(__Ink_String(`Z`)) + 1)); 4 | module.exports.upper__ink_qm__ = upper__ink_qm__; 5 | lower__ink_qm__ = checkRange((point(__Ink_String(`a`)) - 1), __as_ink_string(point(__Ink_String(`z`)) + 1)); 6 | module.exports.lower__ink_qm__ = lower__ink_qm__; 7 | digit__ink_qm__ = checkRange((point(__Ink_String(`0`)) - 1), __as_ink_string(point(__Ink_String(`9`)) + 1)); 8 | module.exports.digit__ink_qm__ = digit__ink_qm__; 9 | letter__ink_qm__ = c => __ink_or(upper__ink_qm__(c), lower__ink_qm__(c)); 10 | module.exports.letter__ink_qm__ = letter__ink_qm__; 11 | ws__ink_qm__ = c => __ink_match(point(c), [[() => (32), () => (true)], [() => (10), () => (true)], [() => (9), () => (true)], [() => (13), () => (true)], [() => (__Ink_Empty), () => (false)]]); 12 | module.exports.ws__ink_qm__ = ws__ink_qm__; 13 | hasPrefix__ink_qm__ = (s, prefix) => reduce(prefix, (acc, c, i) => __ink_and(acc, (() => { return __ink_eq((() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(), c) })()), true); 14 | module.exports.hasPrefix__ink_qm__ = hasPrefix__ink_qm__; 15 | hasSuffix__ink_qm__ = (s, suffix) => (() => { let diff; diff = (len(s) - len(suffix)); return reduce(suffix, (acc, c, i) => __ink_and(acc, (() => { return __ink_eq((() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return __as_ink_string(i + diff) })()] || null : (__ink_acc_trgt[(() => { return __as_ink_string(i + diff) })()] !== undefined ? __ink_acc_trgt[(() => { return __as_ink_string(i + diff) })()] : null)})(), c) })()), true) })(); 16 | module.exports.hasSuffix__ink_qm__ = hasSuffix__ink_qm__; 17 | matchesAt__ink_qm__ = (s, substring, idx) => (() => { let max; max = len(substring); return (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match(i, [[() => (max), () => (true)], [() => (__Ink_Empty), () => (__ink_match((() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return __as_ink_string(idx + i) })()] || null : (__ink_acc_trgt[(() => { return __as_ink_string(idx + i) })()] !== undefined ? __ink_acc_trgt[(() => { return __as_ink_string(idx + i) })()] : null)})(), [[() => ((() => {let __ink_acc_trgt = __as_ink_string(substring); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})()), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1)))], [() => (__Ink_Empty), () => (false)]]))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0) })(); 18 | module.exports.matchesAt__ink_qm__ = matchesAt__ink_qm__; 19 | index = (s, substring) => (() => { let max; max = (len(s) - 1); return (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match(matchesAt__ink_qm__(s, substring, i), [[() => (true), () => (i)], [() => (false), () => (__ink_match((i < max), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1)))], [() => (false), () => (__ink_negate(1))]]))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0) })(); 20 | module.exports.index = index; 21 | contains__ink_qm__ = (s, substring) => (index(s, substring) > __ink_negate(1)); 22 | module.exports.contains__ink_qm__ = contains__ink_qm__; 23 | lower = s => reduce(s, (acc, c, i) => __ink_match(upper__ink_qm__(c), [[() => (true), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), char(__as_ink_string(point(c) + 32))) : (__ink_assgn_trgt[(() => { return i })()]) = char(__as_ink_string(point(c) + 32)); return __ink_assgn_trgt})())], [() => (false), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), c) : (__ink_assgn_trgt[(() => { return i })()]) = c; return __ink_assgn_trgt})())]]), __Ink_String(``)); 24 | module.exports.lower = lower; 25 | upper = s => reduce(s, (acc, c, i) => __ink_match(lower__ink_qm__(c), [[() => (true), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), char((point(c) - 32))) : (__ink_assgn_trgt[(() => { return i })()]) = char((point(c) - 32)); return __ink_assgn_trgt})())], [() => (false), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), c) : (__ink_assgn_trgt[(() => { return i })()]) = c; return __ink_assgn_trgt})())]]), __Ink_String(``)); 26 | module.exports.upper = upper; 27 | title = s => (() => { let lowered; lowered = lower(s); return (() => {let __ink_assgn_trgt = __as_ink_string(lowered); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(0, upper((() => {let __ink_acc_trgt = __as_ink_string(lowered); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})())) : (__ink_assgn_trgt[0]) = upper((() => {let __ink_acc_trgt = __as_ink_string(lowered); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})()); return __ink_assgn_trgt})() })(); 28 | module.exports.title = title; 29 | replaceNonEmpty = (s, old, __ink_ident_new) => (() => { let lnew; let lold; lold = len(old); lnew = len(__ink_ident_new); return (() => { let __ink_trampolined_sub; let sub; return sub = (acc, i) => (() => { __ink_trampolined_sub = (acc, i) => __ink_match(matchesAt__ink_qm__(acc, old, i), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(__as_ink_string(slice(acc, 0, i) + __ink_ident_new) + slice(acc, __as_ink_string(i + lold), len(acc))), __as_ink_string(i + lnew)))], [() => (false), () => (__ink_match((i < len(acc)), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, acc, __as_ink_string(i + 1)))], [() => (false), () => (acc)]]))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, acc, i) })() })()(s, 0) })(); 30 | module.exports.replaceNonEmpty = replaceNonEmpty; 31 | replace = (s, old, __ink_ident_new) => __ink_match(old, [[() => (__Ink_String(``)), () => (s)], [() => (__Ink_Empty), () => (replaceNonEmpty(s, old, __ink_ident_new))]]); 32 | module.exports.replace = replace; 33 | splitNonEmpty = (s, delim) => (() => { let coll; let ldelim; coll = []; ldelim = len(delim); return (() => { let __ink_trampolined_sub; let sub; return sub = (acc, i, last) => (() => { __ink_trampolined_sub = (acc, i, last) => __ink_match(matchesAt__ink_qm__(acc, delim, i), [[() => (true), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(coll); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(coll), slice(acc, last, i)) : (__ink_assgn_trgt[len(coll)]) = slice(acc, last, i); return __ink_assgn_trgt})(); return __ink_trampoline(__ink_trampolined_sub, acc, __as_ink_string(i + ldelim), __as_ink_string(i + ldelim)) })())], [() => (false), () => (__ink_match((i < len(acc)), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, acc, __as_ink_string(i + 1), last))], [() => (false), () => ((() => {let __ink_assgn_trgt = __as_ink_string(coll); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(coll), slice(acc, last, len(acc))) : (__ink_assgn_trgt[len(coll)]) = slice(acc, last, len(acc)); return __ink_assgn_trgt})())]]))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, acc, i, last) })() })()(s, 0, 0) })(); 34 | module.exports.splitNonEmpty = splitNonEmpty; 35 | split = (s, delim) => __ink_match(delim, [[() => (__Ink_String(``)), () => (map(s, c => c))], [() => (__Ink_Empty), () => (splitNonEmpty(s, delim))]]); 36 | module.exports.split = split; 37 | trimPrefixNonEmpty = (s, prefix) => (() => { let idx; let lpref; let max; max = len(s); lpref = len(prefix); idx = (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match((i < max), [[() => (true), () => (__ink_match(matchesAt__ink_qm__(s, prefix, i), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + lpref)))], [() => (false), () => (i)]]))], [() => (false), () => (i)]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0); return slice(s, idx, len(s)) })(); 38 | module.exports.trimPrefixNonEmpty = trimPrefixNonEmpty; 39 | trimPrefix = (s, prefix) => __ink_match(prefix, [[() => (__Ink_String(``)), () => (s)], [() => (__Ink_Empty), () => (trimPrefixNonEmpty(s, prefix))]]); 40 | module.exports.trimPrefix = trimPrefix; 41 | trimSuffixNonEmpty = (s, suffix) => (() => { let idx; let lsuf; lsuf = len(suffix); idx = (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match((i > __ink_negate(1)), [[() => (true), () => (__ink_match(matchesAt__ink_qm__(s, suffix, (i - lsuf)), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, (i - lsuf)))], [() => (false), () => (i)]]))], [() => (false), () => (i)]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(len(s)); return slice(s, 0, idx) })(); 42 | module.exports.trimSuffixNonEmpty = trimSuffixNonEmpty; 43 | trimSuffix = (s, suffix) => __ink_match(suffix, [[() => (__Ink_String(``)), () => (s)], [() => (__Ink_Empty), () => (trimSuffixNonEmpty(s, suffix))]]); 44 | module.exports.trimSuffix = trimSuffix; 45 | trim = (s, ss) => trimPrefix(trimSuffix(s, ss), ss) 46 | module.exports.trim = trim; 47 | 48 | -------------------------------------------------------------------------------- /september.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/september/e8f3a8660ca44e471f2cc01e7f59a654a8fce733/september.jpg -------------------------------------------------------------------------------- /src/analyze.ink: -------------------------------------------------------------------------------- 1 | std := load('../vendor/std') 2 | 3 | log := std.log 4 | f := std.format 5 | map := std.map 6 | each := std.each 7 | filter := std.filter 8 | clone := std.clone 9 | append := std.append 10 | 11 | Tokenize := load('tokenize') 12 | Tok := Tokenize.Tok 13 | tkString := Tokenize.tkString 14 | 15 | Parse := load('parse') 16 | Node := Parse.Node 17 | ident? := Parse.ident? 18 | ndString := Parse.ndString 19 | 20 | decl? := expr => expr.type = Node.BinaryExpr & expr.op = Tok.DefineOp & ident?(expr.left) 21 | 22 | analyzeSubexpr := (node, ctx, tail?) => node.type :: { 23 | Node.ExprList -> ( 24 | ctx := clone(ctx) 25 | 26 | ctx.decls := {} 27 | node.exprs := map( 28 | node.exprs 29 | (n, i) => analyzeSubexpr(n, ctx, i + 1 = len(node.exprs)) 30 | ) 31 | 32 | ` do not re-declare function parameters ` 33 | node.decls := filter(keys(ctx.decls), decl => ctx.args.(decl) = ()) 34 | node 35 | ) 36 | Node.FnLiteral -> ( 37 | ctx := clone(ctx) 38 | 39 | ` we ought only count as "recursion" when a function directly calls 40 | itself -- we do not count references to itself in other callbacks, 41 | which may be called asynchronously. ` 42 | ctx.enclosingFnLit :: { 43 | node -> () 44 | _ -> ctx.enclosingFn := () 45 | } 46 | 47 | ctx.decls := {} 48 | ctx.args := {} 49 | each(node.args, n => n.type :: { 50 | Node.Ident -> ctx.args.(n.val) := true 51 | }) 52 | 53 | node.body := analyzeSubexpr(node.body, ctx, true) 54 | 55 | ` do not re-declare function parameters ` 56 | node.decls := filter(keys(ctx.decls), decl => ctx.args.(decl) = ()) 57 | node 58 | ) 59 | Node.MatchExpr -> ( 60 | node.condition := analyzeSubexpr(node.condition, ctx, false) 61 | node.clauses := map(node.clauses, n => analyzeSubexpr(n, ctx, true)) 62 | node 63 | ) 64 | Node.MatchClause -> ( 65 | node.target := analyzeSubexpr(node.target, ctx, false) 66 | node.expr := analyzeSubexpr(node.expr, ctx, true) 67 | node 68 | ) 69 | Node.FnCall -> ( 70 | node.fn := analyzeSubexpr(node.fn, ctx, false) 71 | node.args := map(node.args, n => analyzeSubexpr(n, ctx, false)) 72 | 73 | simpleName? := node.fn.type = Node.Ident 74 | recursiveCall? := (ctx.enclosingFn :: { 75 | () -> false 76 | _ -> node.fn.val = ctx.enclosingFn.val 77 | }) 78 | 79 | simpleName? & recursiveCall? & tail? :: { 80 | true -> ( 81 | ctx.enclosingFn.recurred? := true 82 | 83 | { 84 | type: Node.FnCall 85 | fn: { 86 | type: Node.Ident 87 | val: '__ink_trampoline' 88 | } 89 | args: append([{ 90 | type: Node.Ident 91 | val: '__ink_trampolined_' + node.fn.val 92 | }], node.args) 93 | } 94 | ) 95 | _ -> node 96 | } 97 | ) 98 | Node.BinaryExpr -> ( 99 | defn? := node.op = Tok.DefineOp 100 | simpleName? := node.left.type = Node.Ident 101 | fnLiteral? := node.right.type = Node.FnLiteral 102 | 103 | defn? & simpleName? & fnLiteral? :: { 104 | true -> ( 105 | fnCtx := clone(ctx) 106 | fnCtx.enclosingFn := node.left 107 | fnCtx.enclosingFnLit := node.right 108 | 109 | node.left := analyzeSubexpr(node.left, ctx, false) 110 | node.right := analyzeSubexpr(node.right, fnCtx, false) 111 | 112 | fnCtx.enclosingFn.recurred? :: { 113 | true -> ( 114 | trampolinedFnName := '__ink_trampolined_' + fnCtx.enclosingFn.val 115 | 116 | ctx.decls.(trampolinedFnName) := true 117 | 118 | node.right := { 119 | type: Node.FnLiteral 120 | args: clone(node.right.args) 121 | decls: [] 122 | body: { 123 | type: Node.ExprList 124 | decls: [] 125 | exprs: [ 126 | { 127 | type: Node.BinaryExpr 128 | op: Tok.DefineOp 129 | left: { 130 | type: Node.Ident 131 | val: trampolinedFnName 132 | } 133 | right: node.right 134 | } 135 | { 136 | type: Node.FnCall 137 | fn: { 138 | type: Node.Ident 139 | val: '__ink_resolve_trampoline' 140 | } 141 | args: append([{ 142 | type: Node.Ident 143 | val: trampolinedFnName 144 | }], clone(node.right.args)) 145 | } 146 | ] 147 | } 148 | } 149 | ) 150 | } 151 | ) 152 | _ -> ( 153 | node.left := analyzeSubexpr(node.left, ctx, false) 154 | node.right := analyzeSubexpr(node.right, ctx, false) 155 | ) 156 | } 157 | 158 | decl?(node) :: { 159 | true -> ctx.decls.(node.left.val) := true 160 | } 161 | 162 | node 163 | ) 164 | Node.UnaryExpr -> node.left := analyzeSubexpr(node.left, ctx, false) 165 | Node.ObjectLiteral -> node.entries := map(node.entries, e => analyzeSubexpr(e, ctx, false)) 166 | Node.ObjectEntry -> ( 167 | node.key := analyzeSubexpr(node.key, ctx, false) 168 | node.val := analyzeSubexpr(node.val, ctx, false) 169 | node 170 | ) 171 | Node.ListLiteral -> node.exprs := map(node.exprs, e => analyzeSubexpr(e, ctx, false)) 172 | _ -> node 173 | } 174 | 175 | analyze := node => analyzeSubexpr(node, { 176 | decls: {} 177 | args: {} 178 | }, false) 179 | -------------------------------------------------------------------------------- /src/gen.ink: -------------------------------------------------------------------------------- 1 | std := load('../vendor/std') 2 | 3 | log := std.log 4 | f := std.format 5 | clone := std.clone 6 | map := std.map 7 | cat := std.cat 8 | 9 | str := load('../vendor/str') 10 | 11 | replace := str.replace 12 | 13 | quicksort := load('../vendor/quicksort') 14 | 15 | sort! := quicksort.sort! 16 | 17 | Tokenize := load('tokenize') 18 | Tok := Tokenize.Tok 19 | 20 | Parse := load('parse') 21 | Node := Parse.Node 22 | ident? := Parse.ident? 23 | ndString := Parse.ndString 24 | 25 | gen := node => node.type :: { 26 | Node.FnCall -> genFnCall(node) 27 | 28 | Node.UnaryExpr -> genUnaryExpr(node) 29 | Node.BinaryExpr -> genBinaryExpr(node) 30 | 31 | Node.NumberLiteral -> genNumberLiteral(node) 32 | Node.StringLiteral -> genStringLiteral(node) 33 | Node.BooleanLiteral -> genBooleanLiteral(node) 34 | Node.FnLiteral -> genFnLiteral(node) 35 | 36 | Node.ListLiteral -> genListLiteral(node) 37 | Node.ObjectLiteral -> genObjectLiteral(node) 38 | 39 | Node.Ident -> genIdent(node) 40 | Node.EmptyIdent -> genEmpty() 41 | 42 | Node.ExprList -> genExprList(node) 43 | Node.MatchExpr -> genMatchExpr(node) 44 | 45 | _ -> genErr('not implemented!') 46 | } 47 | 48 | genErr := msg => f('throw new Error("{{0}}")', [replace(msg, '"', '\\"')]) 49 | genEmpty := () => '__Ink_Empty' 50 | 51 | genBooleanLiteral := node => string(node.val) 52 | genNumberLiteral := node => string(node.val) 53 | genStringLiteral := node => f('__Ink_String(`{{0}}`)', [( 54 | s := node.val 55 | s := replace(s, '\\', '\\\\') 56 | s := replace(s, '`', '\\`') 57 | )]) 58 | 59 | genListLiteral := node => '[' + cat(map(node.exprs, gen), ', ') + ']' 60 | 61 | genObjectEntry := node => f('{{0}}: {{1}}', [ 62 | node.key.type :: { 63 | Node.Ident -> gen(node.key) 64 | Node.EmptyIdent -> gen(node.key) 65 | Node.NumberLiteral -> gen(node.key) 66 | _ -> '[' + gen(node.key) + ']' 67 | } 68 | gen(node.val) 69 | ]) 70 | genObjectLiteral := node => '{' + cat(map(node.entries, genObjectEntry), ', ') + '}' 71 | 72 | genFnArg := (node, i) => node.type :: { 73 | Node.Ident -> genIdent(node) 74 | _ -> '__' + string(i) `` avoid duplicate arg names 75 | } 76 | 77 | genFnLiteral := node => f('{{0}} => {{1}}', [ 78 | len(node.args) :: { 79 | 0 -> '()' 80 | 1 -> genFnArg(node.args.0, 0) 81 | _ -> '(' + cat(map(node.args, genFnArg), ', ') + ')' 82 | } 83 | node.body.type :: { 84 | Node.ObjectLiteral -> '(' + gen(node.body) + ')' 85 | _ -> len(node.decls) = 0 | node.body.type = Node.ExprList :: { 86 | true -> gen(node.body) 87 | _ -> gen({ 88 | type: Node.ExprList 89 | decls: node.decls 90 | exprs: [node.body] 91 | }) 92 | } 93 | } 94 | ]) 95 | 96 | genFnCall := node => f( 97 | '{{0}}({{1}})' 98 | [ 99 | node.fn.type :: { 100 | Node.FnLiteral -> '(' + gen(node.fn) + ')' 101 | _ -> gen(node.fn) 102 | } 103 | cat(map(node.args, gen), ', ') 104 | ] 105 | ) 106 | 107 | genUnaryExpr := node => node.op :: { 108 | Tok.NegOp -> f('__ink_negate({{0}})', [gen(node.left)]) 109 | _ -> genErr(f('UnaryExpr with unknown op: {{0}}', [node.op])) 110 | } 111 | 112 | genBinaryExpr := node => node.op :: { 113 | Tok.AddOp -> f( 114 | '__as_ink_string({{0}} + {{1}})' 115 | [gen(node.left), gen(node.right)] 116 | ) 117 | Tok.SubOp -> f('({{0}} - {{1}})', [gen(node.left), gen(node.right)]) 118 | Tok.MulOp -> f('({{0}} * {{1}})', [gen(node.left), gen(node.right)]) 119 | Tok.DivOp -> f('({{0}} / {{1}})', [gen(node.left), gen(node.right)]) 120 | Tok.ModOp -> f('({{0}} % {{1}})', [gen(node.left), gen(node.right)]) 121 | 122 | Tok.AndOp -> f('__ink_and({{0}}, {{1}})', [gen(node.left), gen(node.right)]) 123 | Tok.XorOp -> f('__ink_xor({{0}}, {{1}})', [gen(node.left), gen(node.right)]) 124 | Tok.OrOp -> f('__ink_or({{0}}, {{1}})', [gen(node.left), gen(node.right)]) 125 | 126 | Tok.EqOp -> f('__ink_eq({{0}}, {{1}})', [gen(node.left), gen(node.right)]) 127 | Tok.GtOp -> f('({{0}} > {{1}})', [gen(node.left), gen(node.right)]) 128 | Tok.LtOp -> f('({{0}} < {{1}})', [gen(node.left), gen(node.right)]) 129 | 130 | Tok.DefineOp -> [node.left.type, node.left.op] :: { 131 | ` DefineOp on a property ` 132 | [Node.BinaryExpr, Tok.AccessorOp] -> ( 133 | tmpDfn := clone(node.left) 134 | tmpDfn.left := { 135 | type: Node.Ident 136 | val: '__ink_assgn_trgt' 137 | } 138 | f( 139 | ` this production preserves two Ink semantics: 140 | - strings can be mutably assigned to. 141 | - assignment on strings and composites return the 142 | assignment target, not the assigned value, 143 | as the value of the expression. ` 144 | cat([ 145 | '(() => {let __ink_assgn_trgt = __as_ink_string({{0}})' 146 | '__is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign({{3}}, {{2}}) : {{1}} = {{2}}' 147 | 'return __ink_assgn_trgt})()' 148 | ], '; ') 149 | [ 150 | gen(node.left.left) 151 | 152 | ` composite assignment ` 153 | genDefineTarget(tmpDfn) 154 | gen(node.right) 155 | 156 | ` string assignment ` 157 | gen(node.left.right) 158 | ] 159 | ) 160 | ) 161 | _ -> f('{{0}} = {{1}}', [genDefineTarget(node.left), gen(node.right)]) 162 | } 163 | 164 | Tok.AccessorOp -> node.right.type :: { 165 | Node.Ident -> f( 166 | cat([ 167 | '(() => {let __ink_acc_trgt = __as_ink_string({{0}})' 168 | 'return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[{{1}}] || null : (__ink_acc_trgt.{{1}} !== undefined ? __ink_acc_trgt.{{1}} : null)})()' 169 | ], '; ') 170 | [gen(node.left), gen(node.right)] 171 | ) 172 | _ -> f( 173 | cat([ 174 | '(() => {let __ink_acc_trgt = __as_ink_string({{0}})' 175 | 'return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[{{1}}] || null : (__ink_acc_trgt[{{1}}] !== undefined ? __ink_acc_trgt[{{1}}] : null)})()' 176 | ], '; ') 177 | [gen(node.left), gen(node.right)] 178 | ) 179 | } 180 | 181 | _ -> genErr(f('BinaryExpr with unknown op: {{0}}', [node.op])) 182 | } 183 | 184 | genDefineTarget := node => node.type :: { 185 | Node.BinaryExpr -> node.right.type :: { 186 | Node.Ident -> f('({{0}}.{{1}})', [gen(node.left), gen(node.right)]) 187 | _ -> f('({{0}}[{{1}}])', [gen(node.left), gen(node.right)]) 188 | } 189 | _ -> gen(node) 190 | } 191 | 192 | genIdent := node => node.val :: { 193 | ` avoid JavaScript reserved words ` 194 | 'break' -> '__ink_ident_break' 195 | 'case' -> '__ink_ident_case' 196 | 'catch' -> '__ink_ident_catch' 197 | 'class' -> '__ink_ident_class' 198 | 'const' -> '__ink_ident_const' 199 | 'continue' -> '__ink_ident_continue' 200 | 'debugger' -> '__ink_ident_debugger' 201 | 'default' -> '__ink_ident_default' 202 | 'delete' -> '__ink_ident_delete' 203 | 'do' -> '__ink_ident_do' 204 | 'else' -> '__ink_ident_else' 205 | 'export' -> '__ink_ident_export' 206 | 'extends' -> '__ink_ident_extends' 207 | 'finally' -> '__ink_ident_finally' 208 | 'for' -> '__ink_ident_for' 209 | 'function' -> '__ink_ident_function' 210 | 'if' -> '__ink_ident_if' 211 | 'import' -> '__ink_ident_import' 212 | 'in' -> '__ink_ident_in' 213 | 'instanceof' -> '__ink_ident_instanceof' 214 | 'new' -> '__ink_ident_new' 215 | 'return' -> '__ink_ident_return' 216 | 'super' -> '__ink_ident_super' 217 | 'switch' -> '__ink_ident_switch' 218 | 'this' -> '__ink_ident_this' 219 | 'throw' -> '__ink_ident_throw' 220 | 'try' -> '__ink_ident_try' 221 | 'typeof' -> '__ink_ident_typeof' 222 | 'var' -> '__ink_ident_var' 223 | 'void' -> '__ink_ident_void' 224 | 'while' -> '__ink_ident_while' 225 | 'with' -> '__ink_ident_with' 226 | 'yield' -> '__ink_ident_yield' 227 | _ -> ( 228 | ident := replace(node.val, '?', '__ink_qm__') 229 | ident := replace(ident, '!', '__ink_em__') 230 | replace(ident, '@', '__ink_am__') 231 | ) 232 | } 233 | 234 | genExprListExprs := (decls, exprs) => f('(() => { {{0}}{{1}} })()', [ 235 | cat(map(sort!(decls), decl => f('let {{0}}; ', [genIdent({val: decl})])), '') 236 | cat(map(exprs, (expr, i) => i + 1 :: { 237 | len(exprs) -> 'return ' + gen(expr) 238 | _ -> gen(expr) 239 | }), '; ') 240 | ]) 241 | 242 | genExprList := node => node.exprs :: { 243 | [] -> 'null' 244 | _ -> genExprListExprs(node.decls, node.exprs) 245 | } 246 | 247 | genMatchExpr := node => f('__ink_match({{0}}, [{{1}}])', [ 248 | gen(node.condition) 249 | cat(map(node.clauses, genMatchClause), ', ') 250 | ]) 251 | 252 | genMatchClause := node => f('[() => ({{0}}), () => ({{1}})]', [ 253 | gen(node.target) 254 | gen(node.expr) 255 | ]) 256 | -------------------------------------------------------------------------------- /src/highlight.ink: -------------------------------------------------------------------------------- 1 | ` september syntax highlighter command ` 2 | 3 | std := load('../vendor/std') 4 | 5 | log := std.log 6 | map := std.map 7 | each := std.each 8 | slice := std.slice 9 | cat := std.cat 10 | 11 | ansi := load('../vendor/ansi') 12 | 13 | Norm := s => s 14 | Gray := ansi.Gray 15 | Red := ansi.Red 16 | Green := ansi.Green 17 | Yellow := ansi.Yellow 18 | Blue := ansi.Blue 19 | Magenta := ansi.Magenta 20 | Cyan := ansi.Cyan 21 | 22 | Tokenize := load('tokenize') 23 | Tok := Tokenize.Tok 24 | tokenize := Tokenize.tokenizeWithComments 25 | 26 | ` associating token types with their highlight colors ` 27 | colorFn := tok => tok.type :: { 28 | Tok.Separator -> Norm 29 | 30 | Tok.Comment -> Gray 31 | 32 | Tok.Ident -> Norm 33 | Tok.EmptyIdent -> Norm 34 | 35 | Tok.NumberLiteral -> Magenta 36 | Tok.StringLiteral -> Yellow 37 | Tok.TrueLiteral -> Magenta 38 | Tok.FalseLiteral -> Magenta 39 | 40 | Tok.AccessorOp -> Red 41 | Tok.EqOp -> Red 42 | 43 | Tok.FunctionArrow -> Green 44 | 45 | ` operators are all red ` 46 | Tok.KeyValueSeparator -> Red 47 | Tok.DefineOp -> Red 48 | Tok.MatchColon -> Red 49 | Tok.CaseArrow -> Red 50 | Tok.SubOp -> Red 51 | Tok.NegOp -> Red 52 | Tok.AddOp -> Red 53 | Tok.MulOp -> Red 54 | Tok.DivOp -> Red 55 | Tok.ModOp -> Red 56 | Tok.GtOp -> Red 57 | Tok.LtOp -> Red 58 | Tok.AndOp -> Red 59 | Tok.OrOp -> Red 60 | Tok.XorOp -> Red 61 | 62 | Tok.LParen -> Cyan 63 | Tok.RParen -> Cyan 64 | Tok.LBracket -> Cyan 65 | Tok.RBracket -> Cyan 66 | Tok.LBrace -> Cyan 67 | Tok.RBrace -> Cyan 68 | 69 | _ -> () `` should error, unreachable 70 | } 71 | 72 | main := prog => ( 73 | tokens := tokenize(prog) 74 | spans := map(tokens, (tok, i) => { 75 | colorFn: [tok.type, tokens.(i + 1)] :: { 76 | ` direct function calls are marked green 77 | on a best-effort basis ` 78 | [ 79 | Tok.Ident 80 | {type: Tok.LParen, val: _, line: _, col: _, i: _} 81 | ] -> Green 82 | _ -> colorFn(tok) 83 | } 84 | start: tok.i 85 | end: tokens.(i + 1) :: { 86 | () -> len(prog) 87 | _ -> tokens.(i + 1).i 88 | } 89 | }) 90 | pcs := map( 91 | spans 92 | span => (span.colorFn)(slice(prog, span.start, span.end)) 93 | ) 94 | cat(pcs, '') 95 | ) 96 | -------------------------------------------------------------------------------- /src/iota.ink: -------------------------------------------------------------------------------- 1 | ` generator for consecutive ints, to make clean enums ` 2 | 3 | new := () => self := { 4 | i: ~1 5 | next: () => ( 6 | self.i := self.i + 1 7 | self.i 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/parse.ink: -------------------------------------------------------------------------------- 1 | std := load('../vendor/std') 2 | 3 | log := std.log 4 | f := std.format 5 | slice := std.slice 6 | map := std.map 7 | each := std.each 8 | cat := std.cat 9 | 10 | Tokenize := load('tokenize') 11 | Tok := Tokenize.Tok 12 | typeName := Tokenize.typeName 13 | tkString := Tokenize.tkString 14 | 15 | mkiota := load('iota').new 16 | 17 | iota := mkiota().next 18 | Node := { 19 | UnaryExpr: iota() 20 | BinaryExpr: iota() 21 | 22 | FnCall: iota() 23 | 24 | MatchClause: iota() 25 | MatchExpr: iota() 26 | ExprList: iota() 27 | 28 | EmptyIdent: iota() 29 | Ident: iota() 30 | 31 | NumberLiteral: iota() 32 | StringLiteral: iota() 33 | BooleanLiteral: iota() 34 | ObjectLiteral: iota() 35 | ObjectEntry: iota() 36 | ListLiteral: iota() 37 | FnLiteral: iota() 38 | } 39 | 40 | ndString := node => node.type :: { 41 | Node.NumberLiteral -> f('Lit({{ val }})', node) 42 | Node.StringLiteral -> f('Lit({{ val }})', node) 43 | Node.BooleanLiteral -> f('Lit({{ val }})', node) 44 | 45 | Node.UnaryExpr -> f('UnrExpr({{0}} {{1}})' 46 | [typeName(node.op), ndString(node.left)]) 47 | Node.BinaryExpr -> f('BinExpr({{0}} {{1}} {{2}})' 48 | [ndString(node.left), typeName(node.op), ndString(node.right)]) 49 | 50 | Node.Ident -> f('Ident({{val}})', node) 51 | Node.EmptyIdent -> 'EmptyIdent' 52 | 53 | Node.FnCall -> f('Call({{0}} {{1}})', [ 54 | ndString(node.fn) 55 | '(' + cat(map(node.args, ndString), ' ') + ')' 56 | ]) 57 | Node.FnLiteral -> f('Fn({{0}} {{1}})', [ 58 | '(' + cat(map(node.args, ndString), ' ') + ')' 59 | ndString(node.body) 60 | ]) 61 | 62 | Node.ExprList -> '(' + cat(map(node.exprs, ndString), ' ') + ')' 63 | Node.MatchExpr -> f('Match({{0}} {{1}})', [ 64 | ndString(node.condition) 65 | '{' + cat(map(node.clauses, ndString), ' ') + '}' 66 | ]) 67 | Node.MatchClause -> f('Clause({{0}} {{1}})' 68 | [ndString(node.target), ndString(node.expr)]) 69 | 70 | Node.ListLiteral -> f('List({{0}})' 71 | [cat(map(node.exprs, ndString), ' ')]) 72 | Node.ObjectLiteral -> f('Obj({{0}})' 73 | [cat(map(node.entries, ndString), ' ')]) 74 | Node.ObjectEntry -> f('Entry({{0}} {{1}})' 75 | [ndString(node.key), ndString(node.val)]) 76 | 77 | _ -> 'Unknown(' + string(node) + ')' 78 | } 79 | 80 | opPriority := tok => tok.type :: { 81 | Tok.AccessorOp -> 100 82 | Tok.ModOp -> 80 83 | 84 | Tok.MulOp -> 50 85 | Tok.DivOp -> 50 86 | Tok.AddOp -> 40 87 | Tok.SubOp -> 40 88 | 89 | Tok.GtOp -> 30 90 | Tok.LtOp -> 30 91 | Tok.EqOp -> 30 92 | 93 | Tok.AndOp -> 20 94 | Tok.XorOp -> 15 95 | Tok.OrOp -> 10 96 | 97 | Tok.DefineOp -> 0 98 | 99 | _ -> ~1 100 | } 101 | 102 | ident? := node => node :: { 103 | () -> false 104 | _ -> node.type = Node.Ident 105 | } 106 | 107 | binaryOp? := tok => opPriority(tok) > ~1 108 | 109 | parse := tokens => ( 110 | nodes := [] 111 | 112 | tokens.0 :: { 113 | {type: Tok.Separator, val: _, line: _, col: _, i: _} -> ( 114 | tokens := slice(tokens, 1, len(tokens)) 115 | ) 116 | } 117 | 118 | (sub := idx => tokens.(idx) :: { 119 | () -> nodes 120 | _ -> ( 121 | result := parseExpr(tokens, idx) 122 | result.err :: { 123 | () -> ( 124 | nodes.len(nodes) := result.node 125 | sub(result.idx) 126 | ) 127 | _ -> f('parse err @ {{line}}:{{col}}: {{err}}', { 128 | err: result.err 129 | line: tokens.(result.idx - 1).line 130 | col: tokens.(result.idx - 1).col 131 | }) 132 | } 133 | ) 134 | })(0) 135 | ) 136 | 137 | parseBinaryExpr := (left, op, prevPriority, tokens, idx) => ( 138 | result := parseAtom(tokens, idx) 139 | right := result.node 140 | idx := result.idx 141 | 142 | S := { 143 | idx: idx 144 | } 145 | 146 | ops := [op] 147 | nodes := [left, right] 148 | result.err :: { 149 | () -> (sub := () => binaryOp?(tokens.(S.idx)) :: { 150 | true -> ( 151 | priority := opPriority(tokens.(S.idx)) 152 | choices := [ 153 | ` priority is lower than the calling function's last op 154 | so return control to the parent binary op ` 155 | ~(prevPriority < priority) 156 | ` priority is lower than the previous op but higher than 157 | the parent, so it's ok to be left-heavy in this tree` 158 | ~(opPriority(ops.(len(ops) - 1)) < priority) 159 | ] 160 | choices :: { 161 | [true, _] -> () 162 | [_, true] -> ( 163 | ops.len(ops) := tokens.(S.idx) 164 | S.idx := S.idx + 1 165 | 166 | tokens.(S.idx) :: { 167 | () -> { 168 | node: right 169 | idx: S.idx 170 | err: 'unexpected end of input, expected binary operator' 171 | } 172 | _ -> ( 173 | result := parseAtom(tokens, S.idx) 174 | result.err :: { 175 | () -> ( 176 | nodes.len(nodes) := result.node 177 | S.idx := result.idx 178 | sub() 179 | ) 180 | _ -> result 181 | } 182 | ) 183 | } 184 | ) 185 | _ -> tokens.(S.idx) :: { 186 | () -> { 187 | node: right 188 | idx: S.idx 189 | err: 'unexpected end of input, expected binary operator' 190 | } 191 | _ -> ( 192 | result := parseBinaryExpr( 193 | nodes.(len(nodes) - 1) 194 | tokens.(S.idx) 195 | opPriority(ops.(len(ops) - 1)) 196 | tokens 197 | S.idx + 1 198 | ) 199 | result.err :: { 200 | () -> ( 201 | nodes.(len(nodes) - 1) := result.node 202 | S.idx := result.idx 203 | sub() 204 | ) 205 | _ -> result 206 | } 207 | ) 208 | } 209 | } 210 | ) 211 | })() 212 | _ -> result 213 | } 214 | 215 | each(ops, (op, i) => ( 216 | node := nodes.(i + 1) 217 | nodes.0 := { 218 | type: Node.BinaryExpr 219 | op: op.type 220 | left: nodes.0 221 | right: node 222 | } 223 | )) 224 | 225 | { 226 | node: nodes.0 227 | idx: S.idx 228 | err: () 229 | } 230 | ) 231 | 232 | parseExpr := (tokens, idx) => ( 233 | S := { 234 | idx: idx 235 | } 236 | 237 | consumeDanglingSeparator := () => tokens.(S.idx) :: { 238 | {type: Tok.Separator, val: _, line: _, col: _, i: _} -> S.idx := S.idx + 1 239 | } 240 | 241 | result := parseAtom(tokens, S.idx) 242 | atom := result.node 243 | result.err :: { 244 | () -> ( 245 | S.idx := result.idx 246 | 247 | tokens.(S.idx) :: { 248 | () -> { 249 | node: () 250 | idx: S.idx + 1 251 | err: 'unexpected end of input, expected continued expression' 252 | } 253 | _ -> ( 254 | next := tokens.(S.idx) 255 | S.idx := S.idx + 1 256 | 257 | produceBinaryExpr := () => ( 258 | result := parseBinaryExpr(atom, next, ~1, tokens, S.idx) 259 | binExpr := result.node 260 | S.idx := result.idx 261 | result.err :: { 262 | () -> tokens.(S.idx) :: { 263 | {type: Tok.MatchColon, val: _, line: _, col: _, i: _} -> ( 264 | S.idx := S.idx + 1 265 | produceMatchExpr(binExpr) 266 | ) 267 | _ -> ( 268 | consumeDanglingSeparator() 269 | { 270 | node: binExpr 271 | idx: S.idx 272 | err: () 273 | } 274 | ) 275 | } 276 | _ -> result 277 | } 278 | ) 279 | produceMatchExpr := condition => ( 280 | result := parseMatchBody(tokens, S.idx) 281 | clauses := result.node 282 | S.idx := result.idx 283 | result.err :: { 284 | () -> ( 285 | consumeDanglingSeparator() 286 | { 287 | node: { 288 | type: Node.MatchExpr 289 | condition: condition 290 | clauses: clauses 291 | } 292 | idx: S.idx 293 | } 294 | ) 295 | _ -> result 296 | } 297 | ) 298 | 299 | next.type :: { 300 | Tok.Separator -> { 301 | node: atom 302 | idx: S.idx 303 | err: () 304 | } 305 | ` these belong to the parent atom that contains 306 | this expression, so return without consuming token ` 307 | Tok.RightParen -> { 308 | node: atom 309 | idx: S.idx - 1 310 | err: () 311 | } 312 | Tok.KeyValueSeparator -> { 313 | node: atom 314 | idx: S.idx - 1 315 | err: () 316 | } 317 | Tok.CaseArrow -> { 318 | node: atom 319 | idx: S.idx - 1 320 | err: () 321 | } 322 | Tok.AddOp -> produceBinaryExpr() 323 | Tok.SubOp -> produceBinaryExpr() 324 | Tok.MulOp -> produceBinaryExpr() 325 | Tok.DivOp -> produceBinaryExpr() 326 | Tok.ModOp -> produceBinaryExpr() 327 | Tok.AndOp -> produceBinaryExpr() 328 | Tok.XorOp -> produceBinaryExpr() 329 | Tok.OrOp -> produceBinaryExpr() 330 | Tok.GtOp -> produceBinaryExpr() 331 | Tok.LtOp -> produceBinaryExpr() 332 | Tok.EqOp -> produceBinaryExpr() 333 | Tok.DefineOp -> produceBinaryExpr() 334 | Tok.AccessorOp -> produceBinaryExpr() 335 | Tok.MatchColon -> produceMatchExpr(atom) 336 | _ -> { 337 | node: () 338 | idx: S.idx 339 | err: f('unexpected token {{0}} (parseExpr)', [tkString(next)]) 340 | } 341 | } 342 | ) 343 | } 344 | ) 345 | _ -> result 346 | } 347 | ) 348 | 349 | parseAtom := (tokens, idx) => tokens.(idx) :: { 350 | () -> { 351 | node: () 352 | idx: idx 353 | err: 'unexpected end of input, expected atom' 354 | } 355 | _ -> tokens.(idx).type :: { 356 | Tok.NegOp -> ( 357 | result := parseAtom(tokens, idx + 1) 358 | result.err :: { 359 | () -> { 360 | node: { 361 | type: Node.UnaryExpr 362 | op: Tok.NegOp 363 | left: result.node 364 | } 365 | idx: result.idx 366 | } 367 | _ -> result 368 | } 369 | ) 370 | _ -> tokens.(idx + 1) :: { 371 | () -> { 372 | node: () 373 | idx: idx + 1 374 | err: 'unexpected end of input, expected start of atom' 375 | } 376 | _ -> ( 377 | tok := tokens.(idx) 378 | 379 | consumePotentialFunctionCall := (fnNode, idx) => ( 380 | tokens.(idx) :: { 381 | {type: Tok.LParen, val: _, line: _, col: _, i: _} -> ( 382 | result := parseFnCall(fnNode, tokens, idx) 383 | result.err :: { 384 | () -> consumePotentialFunctionCall(result.node, result.idx) 385 | _ -> result 386 | } 387 | ) 388 | _ -> { 389 | node: fnNode 390 | idx: idx 391 | } 392 | } 393 | ) 394 | 395 | tok.type :: { 396 | Tok.NumberLiteral -> { 397 | node: { 398 | type: Node.NumberLiteral 399 | val: tok.val 400 | } 401 | idx: idx + 1 402 | } 403 | Tok.StringLiteral -> { 404 | node: { 405 | type: Node.StringLiteral 406 | val: tok.val 407 | } 408 | idx: idx + 1 409 | } 410 | Tok.TrueLiteral -> { 411 | node: { 412 | type: Node.BooleanLiteral 413 | val: true 414 | } 415 | idx: idx + 1 416 | } 417 | Tok.FalseLiteral -> { 418 | node: { 419 | type: Node.BooleanLiteral 420 | val: false 421 | } 422 | idx: idx + 1 423 | } 424 | Tok.Ident -> tokens.(idx + 1).type :: { 425 | Tok.FunctionArrow -> parseFnLiteral(tokens, idx) 426 | _ -> consumePotentialFunctionCall({ 427 | type: Node.Ident 428 | val: tok.val 429 | }, idx + 1) 430 | } 431 | Tok.EmptyIdent -> tokens.(idx + 1).type :: { 432 | Tok.FunctionArrow -> parseFnLiteral(tokens, idx) 433 | _ -> consumePotentialFunctionCall({ 434 | type: Node.EmptyIdent 435 | val: tok.val 436 | }, idx + 1) 437 | } 438 | Tok.LParen -> ( 439 | exprs := [] 440 | result := parseGroup(tokens, idx, parseExpr, exprs, Tok.RParen) 441 | 442 | result.err :: { 443 | () -> tokens.(result.idx) :: { 444 | () -> { 445 | node: () 446 | idx: result.idx 447 | err: 'unexpected end of input, expected continued expression' 448 | } 449 | _ -> tokens.(result.idx).type :: { 450 | Tok.FunctionArrow -> parseFnLiteral(tokens, idx) 451 | _ -> consumePotentialFunctionCall({ 452 | type: Node.ExprList 453 | exprs: exprs 454 | }, result.idx) 455 | } 456 | } 457 | _ -> result 458 | } 459 | ) 460 | Tok.LBrace -> ( 461 | entries := [] 462 | result := parseGroup(tokens, idx, parseObjectEntry, entries, Tok.RBrace) 463 | 464 | result.err :: { 465 | () -> { 466 | node: { 467 | type: Node.ObjectLiteral 468 | entries: entries 469 | } 470 | idx: result.idx 471 | } 472 | _ -> result 473 | } 474 | ) 475 | Tok.LBracket -> parseListLiteral(tokens, idx) 476 | _ -> { 477 | node: () 478 | idx: idx 479 | err: f('unexpected token {{0}} (parseAtom)', [tkString(tokens.(idx))]) 480 | } 481 | } 482 | ) 483 | } 484 | } 485 | } 486 | 487 | ` not an official part of the Ink grammar, but an abstraction to 488 | help parse the common production rule: list of homogeneous 489 | node types appearing in sequence zero or more times ` 490 | parseGroup := (tokens, idx, subparser, acc, guardTok) => ( 491 | (sub := (idx) => tokens.(idx) :: { 492 | {type: guardTok, val: _, line: _, col: _, i: _} -> { 493 | idx: idx + 1 `` guardTok 494 | } 495 | _ -> ( 496 | result := subparser(tokens, idx) 497 | expr := result.node 498 | result.err :: { 499 | () -> ( 500 | acc.len(acc) := expr 501 | tokens.(result.idx) :: { 502 | () -> { 503 | node: () 504 | idx: result.idx 505 | err: 'unexpected end of input, expected ' + typeName(guardTok) 506 | } 507 | _ -> tokens.(result.idx).type :: { 508 | guardTok -> { 509 | node: result.node 510 | idx: result.idx + 1 `` guardTok 511 | } 512 | _ -> sub(result.idx) 513 | } 514 | } 515 | ) 516 | _ -> result 517 | } 518 | ) 519 | })(idx + 1) `` opening token 520 | ) 521 | 522 | parseListLiteral := (tokens, idx) => ( 523 | exprs := [] 524 | result := parseGroup(tokens, idx, parseExpr, exprs, Tok.RBracket) 525 | 526 | result.err :: { 527 | () -> { 528 | node: { 529 | type: Node.ListLiteral 530 | exprs: exprs 531 | } 532 | idx: result.idx 533 | } 534 | _ -> result 535 | } 536 | ) 537 | 538 | parseFnLiteral := (tokens, idx) => ( 539 | args := [] 540 | 541 | processBody := idx => tokens.(idx) :: { 542 | {type: Tok.FunctionArrow, val: _, line: _, col: _, i: _} -> ( 543 | result := parseExpr(tokens, idx + 1) 544 | result.err :: { 545 | () -> { 546 | node: { 547 | type: Node.FnLiteral 548 | args: args 549 | body: result.node 550 | } 551 | ` literal values should not consume trailing separators, 552 | but the parseExpr() above does, so we give that up 553 | here to account for it for the parent that called 554 | into this parseFnLiteral ` 555 | idx: result.idx - 1 556 | } 557 | _ -> result 558 | } 559 | ) 560 | _ -> { 561 | node: () 562 | idx: idx 563 | error: 'unexpected end of input, expected =>' 564 | } 565 | } 566 | 567 | tok := tokens.(idx) 568 | tok :: { 569 | () -> { 570 | node: () 571 | idx: idx 572 | err: 'unexpected end of input, expected fn args list' 573 | } 574 | _ -> tok.type :: { 575 | Tok.EmptyIdent -> processBody(idx + 1) 576 | Tok.Ident -> ( 577 | args.0 := { 578 | type: Node.Ident 579 | val: tok.val 580 | } 581 | processBody(idx + 1) 582 | ) 583 | Tok.LParen -> ( 584 | result := parseGroup(tokens, idx, parseExpr, args, Tok.RParen) 585 | result.err :: { 586 | () -> processBody(result.idx) 587 | _ -> result 588 | } 589 | ) 590 | _ -> { 591 | node: () 592 | idx: idx 593 | err: 'unexpected token, expected start of fn literal' 594 | } 595 | } 596 | } 597 | ) 598 | 599 | parseFnCall := (fnNode, tokens, idx) => ( 600 | args := [] 601 | 602 | tokens.(idx + 1) :: { 603 | () -> { 604 | node: () 605 | idx: idx + 1 606 | err: 'unexpected end of input, expected fn args list' 607 | } 608 | _ -> ( 609 | result := parseGroup(tokens, idx, parseExpr, args, Tok.RParen) 610 | result.err :: { 611 | () -> { 612 | node: { 613 | type: Node.FnCall 614 | fn: fnNode 615 | args: args 616 | } 617 | idx: result.idx 618 | } 619 | _ -> result 620 | } 621 | ) 622 | } 623 | ) 624 | 625 | parseMatchBody := (tokens, idx) => tokens.(idx + 1) :: { 626 | () -> { 627 | node: () 628 | idx: idx + 1 629 | err: 'unexpected end of input, expected {' 630 | } 631 | _ -> ( 632 | clauses := [] 633 | result := parseGroup(tokens, idx, parseMatchClause, clauses, Tok.RBrace) 634 | 635 | result.err :: { 636 | () -> { 637 | node: clauses 638 | idx: result.idx 639 | } 640 | _ -> result 641 | } 642 | ) 643 | } 644 | 645 | parseMatchClause := (tokens, idx) => ( 646 | result := parseExpr(tokens, idx) 647 | atom := result.node 648 | 649 | result.err :: { 650 | () -> tokens.(result.idx) :: { 651 | () -> { 652 | node: () 653 | idx: result.idx 654 | err: 'unexpected end of input, expected ->' 655 | } 656 | _ -> tokens.(result.idx + 1) :: { 657 | () -> { 658 | node: () 659 | idx: result.idx + 1 660 | err: 'unexpected end of input, expected expression in clause following ->' 661 | } 662 | _ -> ( 663 | clauseResult := parseExpr(tokens, result.idx + 1) 664 | clauseResult.err :: { 665 | () -> { 666 | node: { 667 | type: Node.MatchClause 668 | target: atom 669 | expr: clauseResult.node 670 | } 671 | idx: clauseResult.idx 672 | } 673 | _ -> clauseResult 674 | } 675 | ) 676 | } 677 | } 678 | _ -> result 679 | } 680 | ) 681 | 682 | parseObjectEntry := (tokens, idx) => ( 683 | result := parseExpr(tokens, idx) 684 | atom := result.node 685 | 686 | result.err :: { 687 | () -> tokens.(result.idx) :: { 688 | () -> { 689 | node: () 690 | idx: result.idx 691 | err: 'unexpected end of input, expected :' 692 | } 693 | _ -> tokens.(result.idx + 1) :: { 694 | () -> { 695 | node: () 696 | idx: result.idx + 1 697 | err: 'unexpected end of input, expected expression in entry following :' 698 | } 699 | _ -> ( 700 | valResult := parseExpr(tokens, result.idx + 1) 701 | valResult.err :: { 702 | () -> { 703 | node: { 704 | type: Node.ObjectEntry 705 | key: atom 706 | val: valResult.node 707 | } 708 | idx: valResult.idx 709 | } 710 | _ -> valResult 711 | } 712 | ) 713 | } 714 | } 715 | _ -> result 716 | } 717 | ) 718 | -------------------------------------------------------------------------------- /src/tokenize.ink: -------------------------------------------------------------------------------- 1 | std := load('../vendor/std') 2 | 3 | log := std.log 4 | f := std.format 5 | slice := std.slice 6 | map := std.map 7 | reduce := std.reduce 8 | every := std.every 9 | 10 | str := load('../vendor/str') 11 | 12 | digit? := str.digit? 13 | hasPrefix? := str.hasPrefix? 14 | index := str.index 15 | 16 | mkiota := load('iota').new 17 | 18 | Newline := char(10) 19 | Tab := char(9) 20 | 21 | iota := mkiota().next 22 | Tok := { 23 | Separator: iota() 24 | 25 | Comment: iota() 26 | 27 | Ident: iota() 28 | EmptyIdent: iota() 29 | 30 | NumberLiteral: iota() 31 | StringLiteral: iota() 32 | 33 | TrueLiteral: iota() 34 | FalseLiteral: iota() 35 | 36 | AccessorOp: iota() 37 | 38 | EqOp: iota() 39 | FunctionArrow: iota() 40 | 41 | KeyValueSeparator: iota() 42 | DefineOp: iota() 43 | MatchColon: iota() 44 | 45 | CaseArrow: iota() 46 | SubOp: iota() 47 | 48 | NegOp: iota() 49 | AddOp: iota() 50 | MulOp: iota() 51 | DivOp: iota() 52 | ModOp: iota() 53 | GtOp: iota() 54 | LtOp: iota() 55 | 56 | AndOp: iota() 57 | OrOp: iota() 58 | XorOp: iota() 59 | 60 | LParen: iota() 61 | RParen: iota() 62 | LBracket: iota() 63 | RBracket: iota() 64 | LBrace: iota() 65 | RBrace: iota() 66 | } 67 | 68 | typeName := type => reduce(keys(Tok), (acc, k) => Tok.(k) :: { 69 | type -> k 70 | _ -> acc 71 | }, '(unknown token)') 72 | 73 | tkString := tok => f('{{ 0 }}({{ 1 }}) @ {{2}}:{{3}}' 74 | [typeName(tok.type), tok.val, tok.line, tok.col]) 75 | 76 | token := (type, val, line, col, i) => { 77 | type: type 78 | val: val 79 | line: line 80 | col: col 81 | i: i 82 | } 83 | 84 | tokenizeWithOpt := (s, lexComments) => ( 85 | S := { 86 | i: ~1 87 | buf: '' 88 | strbuf: '' 89 | strbufLine: 0 90 | strbufCol: 0 91 | 92 | lastType: Tok.Separator 93 | line: 1 94 | col: 0 95 | 96 | inStringLiteral: false 97 | } 98 | tokens := [] 99 | 100 | simpleCommit := tok => ( 101 | S.lastType := tok.type 102 | tokens.len(tokens) := tok 103 | ) 104 | simpleCommitChar := type => simpleCommit(token( 105 | type 106 | () 107 | S.line 108 | S.col 109 | type :: { 110 | Tok.TrueLiteral -> S.i - 4 111 | Tok.FalseLiteral -> S.i - 5 112 | _ -> S.i - 1 113 | } 114 | )) 115 | commitClear := () => S.buf :: { 116 | '' -> _ 117 | _ -> ( 118 | cbuf := S.buf 119 | S.buf := '' 120 | cbuf :: { 121 | 'true' -> simpleCommitChar(Tok.TrueLiteral) 122 | 'false' -> simpleCommitChar(Tok.FalseLiteral) 123 | _ -> digit?(cbuf) :: { 124 | true -> simpleCommit(token( 125 | Tok.NumberLiteral 126 | number(cbuf) 127 | S.line 128 | S.col - len(cbuf) 129 | S.i - len(cbuf) 130 | )) 131 | false -> simpleCommit(token( 132 | Tok.Ident 133 | cbuf 134 | S.line 135 | S.col - len(cbuf) 136 | S.i - len(cbuf) 137 | )) 138 | } 139 | } 140 | ) 141 | } 142 | commit := tok => ( 143 | commitClear() 144 | simpleCommit(tok) 145 | ) 146 | commitChar := type => commit(token(type, (), S.line, S.col, S.i)) 147 | ensureSeparator := () => ( 148 | commitClear() 149 | S.lastType :: { 150 | Tok.Separator -> () 151 | Tok.LParen -> () 152 | Tok.LBracket -> () 153 | Tok.LBrace -> () 154 | Tok.AddOp -> () 155 | Tok.SubOp -> () 156 | Tok.MulOp -> () 157 | Tok.DivOp -> () 158 | Tok.ModOp -> () 159 | Tok.NegOp -> () 160 | Tok.GtOp -> () 161 | Tok.LtOp -> () 162 | Tok.EqOp -> () 163 | Tok.DefineOp -> () 164 | Tok.AccessorOp -> () 165 | Tok.KeyValueSeparator -> () 166 | Tok.FunctionArrow -> () 167 | Tok.MatchColon -> () 168 | Tok.CaseArrow -> () 169 | _ -> commitChar(Tok.Separator) 170 | } 171 | ) 172 | finalize := () => ( 173 | ensureSeparator() 174 | tokens 175 | ) 176 | 177 | hasPrefix?(s, '#!') :: { 178 | true -> ( 179 | S.i := index(s, Newline) 180 | S.line := S.line + 1 181 | ) 182 | } 183 | 184 | (sub := () => ( 185 | S.i := S.i + 1 186 | S.col := S.col + 1 187 | c := s.(S.i) 188 | [c, S.inStringLiteral] :: { 189 | [(), _] -> finalize() 190 | ['\'', _] -> S.inStringLiteral :: { 191 | true -> ( 192 | commit(token( 193 | Tok.StringLiteral 194 | S.strbuf 195 | S.strbufLine 196 | S.strbufCol 197 | S.i - len(S.strbuf) - 1 198 | )) 199 | S.inStringLiteral := false 200 | sub() 201 | ) 202 | false -> ( 203 | S.strbuf := '' 204 | S.strbufLine := S.line 205 | S.strbufCol := S.col 206 | S.inStringLiteral := true 207 | sub() 208 | ) 209 | } 210 | [_, true] -> c :: { 211 | Newline -> ( 212 | S.line := S.line + 1 213 | S.col := 0 214 | S.strbuf := S.strbuf + c 215 | sub() 216 | ) 217 | '\\' -> ( 218 | S.i := S.i + 1 219 | S.strbuf := S.strbuf + s.(S.i) 220 | S.col := S.col + 1 221 | sub() 222 | ) 223 | _ -> ( 224 | S.strbuf := S.strbuf + c 225 | sub() 226 | ) 227 | } 228 | _ -> c :: { 229 | '`' -> s.(S.i + 1) :: { 230 | ` line comment ` 231 | '`' -> advance := index(slice(s, S.i, len(s)), Newline) :: { 232 | ~1 -> ( 233 | lexComments :: { 234 | true -> commit(token( 235 | Tok.Comment 236 | slice(s, S.i, len(s)) 237 | S.line 238 | S.col 239 | S.i 240 | )) 241 | } 242 | finalize() 243 | ) 244 | _ -> ( 245 | line := S.line 246 | col := S.col 247 | i := S.i 248 | 249 | S.i := S.i + advance 250 | lexComments :: { 251 | true -> commit(token( 252 | Tok.Comment 253 | slice(s, i, S.i) 254 | line 255 | col 256 | i 257 | )) 258 | } 259 | ensureSeparator() 260 | S.line := S.line + 1 261 | S.col := 0 262 | sub() 263 | ) 264 | } 265 | _ -> ( 266 | ` block comment, keep taking until end of block ` 267 | line := S.line 268 | col := S.col 269 | i := S.i 270 | 271 | S.i := S.i + 1 272 | (sub := () => s.(S.i) :: { 273 | '`' -> S.col := S.col + 1 274 | Newline -> ( 275 | S.i := S.i + 1 276 | S.line := S.line + 1 277 | S.col := 0 278 | sub() 279 | ) 280 | ` comments that don't end should be ignored ` 281 | () -> () 282 | _ -> ( 283 | S.i := S.i + 1 284 | S.col := S.col + 1 285 | sub() 286 | ) 287 | })() 288 | lexComments :: { 289 | true -> commit(token( 290 | Tok.Comment 291 | slice(s, i, S.i + 1) 292 | line 293 | col 294 | i 295 | )) 296 | } 297 | sub() 298 | ) 299 | } 300 | Newline -> ( 301 | ensureSeparator() 302 | S.line := S.line + 1 303 | S.col := 0 304 | sub() 305 | ) 306 | Tab -> ( 307 | commitClear() 308 | sub() 309 | ) 310 | ' ' -> ( 311 | commitClear() 312 | sub() 313 | ) 314 | '_' -> ( 315 | commitChar(Tok.EmptyIdent) 316 | sub() 317 | ) 318 | '~' -> ( 319 | commitChar(Tok.NegOp) 320 | sub() 321 | ) 322 | '+' -> ( 323 | commitChar(Tok.AddOp) 324 | sub() 325 | ) 326 | '*' -> ( 327 | commitChar(Tok.MulOp) 328 | sub() 329 | ) 330 | '/' -> ( 331 | commitChar(Tok.DivOp) 332 | sub() 333 | ) 334 | '%' -> ( 335 | commitChar(Tok.ModOp) 336 | sub() 337 | ) 338 | '&' -> ( 339 | commitChar(Tok.AndOp) 340 | sub() 341 | ) 342 | '|' -> ( 343 | commitChar(Tok.OrOp) 344 | sub() 345 | ) 346 | '^' -> ( 347 | commitChar(Tok.XorOp) 348 | sub() 349 | ) 350 | '<' -> ( 351 | commitChar(Tok.LtOp) 352 | sub() 353 | ) 354 | '>' -> ( 355 | commitChar(Tok.GtOp) 356 | sub() 357 | ) 358 | ',' -> ( 359 | ensureSeparator() 360 | sub() 361 | ) 362 | '.' -> [S.buf, every(map(S.buf, digit?))] :: { 363 | ['', _] -> ( 364 | commitChar(Tok.AccessorOp) 365 | sub() 366 | ) 367 | [_, true] -> ( 368 | S.buf := S.buf + '.' 369 | sub() 370 | ) 371 | _ -> ( 372 | commitChar(Tok.AccessorOp) 373 | sub() 374 | ) 375 | } 376 | ':' -> s.(S.i + 1) :: { 377 | '=' -> ( 378 | commitChar(Tok.DefineOp) 379 | S.i := S.i + 1 380 | sub() 381 | ) 382 | ':' -> ( 383 | commitChar(Tok.MatchColon) 384 | S.i := S.i + 1 385 | sub() 386 | ) 387 | _ -> ( 388 | ensureSeparator() 389 | commitChar(Tok.KeyValueSeparator) 390 | sub() 391 | ) 392 | } 393 | '=' -> s.(S.i + 1) :: { 394 | '>' -> ( 395 | commitChar(Tok.FunctionArrow) 396 | S.i := S.i + 1 397 | sub() 398 | ) 399 | _ -> ( 400 | commitChar(Tok.EqOp) 401 | sub() 402 | ) 403 | } 404 | '-' -> s.(S.i + 1) :: { 405 | '>' -> ( 406 | commitChar(Tok.CaseArrow) 407 | S.i := S.i + 1 408 | sub() 409 | ) 410 | _ -> ( 411 | commitChar(Tok.SubOp) 412 | sub() 413 | ) 414 | } 415 | '(' -> ( 416 | commitChar(Tok.LParen) 417 | sub() 418 | ) 419 | ')' -> ( 420 | ensureSeparator() 421 | commitChar(Tok.RParen) 422 | sub() 423 | ) 424 | '[' -> ( 425 | commitChar(Tok.LBracket) 426 | sub() 427 | ) 428 | ']' -> ( 429 | ensureSeparator() 430 | commitChar(Tok.RBracket) 431 | sub() 432 | ) 433 | '{' -> ( 434 | commitChar(Tok.LBrace) 435 | sub() 436 | ) 437 | '}' -> ( 438 | ensureSeparator() 439 | commitChar(Tok.RBrace) 440 | sub() 441 | ) 442 | _ -> ( 443 | ` strange hack required for mutating a nested string. 444 | might be an Ink interpreter bug... ` 445 | S.buf := S.buf + c 446 | sub() 447 | ) 448 | } 449 | } 450 | ))() 451 | ) 452 | 453 | tokenize := s => tokenizeWithOpt(s, false) 454 | 455 | tokenizeWithComments := s => tokenizeWithOpt(s, true) 456 | -------------------------------------------------------------------------------- /src/translate.ink: -------------------------------------------------------------------------------- 1 | ` september translate command ` 2 | 3 | std := load('../vendor/std') 4 | 5 | log := std.log 6 | map := std.map 7 | each := std.each 8 | cat := std.cat 9 | 10 | Tokenize := load('tokenize') 11 | tokenize := Tokenize.tokenize 12 | tkString := Tokenize.tkString 13 | 14 | Parse := load('parse') 15 | parse := Parse.parse 16 | ndString := Parse.ndString 17 | 18 | Analyze := load('analyze') 19 | analyze := Analyze.analyze 20 | 21 | Gen := load('gen') 22 | gen := Gen.gen 23 | 24 | Newline := char(10) 25 | 26 | main := prog => ( 27 | tokens := tokenize(prog) 28 | `` each(tokens, tok => log(tkString(tok))) 29 | 30 | nodes := parse(tokens) 31 | 32 | type(nodes) :: { 33 | ` tree of nodes ` 34 | 'composite' -> ( 35 | `` each(nodes, node => log(ndString(node))) 36 | analyzed := map(nodes, analyze) 37 | cat(map(analyzed, gen), ';' + Newline) + Newline 38 | ) 39 | ` parse err ` 40 | 'string' -> nodes 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /test/cases/000.ink: -------------------------------------------------------------------------------- 1 | ` 000 tests ` 2 | 3 | std := load('./runtime/std') 4 | `` std := load('../../vendor/std') 5 | 6 | ` scratchpad ` 7 | 8 | -------------------------------------------------------------------------------- /test/cases/001.ink: -------------------------------------------------------------------------------- 1 | ` arithmetic, variables, functions, match exprs ` 2 | 3 | a := 2 4 | b := 3 5 | 6 | double := x => 2 * x 7 | 8 | (console.log)(double(a + b)) 9 | 10 | log := x => out(string(x) + char(10)) 11 | 12 | even? := n => n % 2 :: { 13 | 0 -> true 14 | _ -> false 15 | } 16 | log(even?(2)) 17 | log(even?(3)) 18 | log(even?(4)) 19 | log(even?(5)) 20 | 21 | log('Hello, World!') 22 | 23 | fn := y => {hi: y} 24 | log(fn('hello').hi) 25 | -------------------------------------------------------------------------------- /test/cases/002.ink: -------------------------------------------------------------------------------- 1 | ` local and global variable scopes ` 2 | 3 | std := load('./runtime/std') 4 | 5 | log := std.log 6 | 7 | log('should print 2, 5, 5, 5, 20, 5') 8 | 9 | a := 2 10 | log(a) 11 | a := 5 12 | log(a) 13 | 14 | fn := x => a := x 15 | fn2 := (_, y) => (a := y) 16 | fn(10) 17 | log(a) 18 | 19 | x := 12 20 | (a := x) 21 | ( 22 | a := 12 23 | ) 24 | log(a) 25 | 26 | scoped := () => ( 27 | a := 10 28 | a := 20 29 | log(a) 30 | ) 31 | scoped() 32 | log(a) 33 | 34 | log('should print {x: yyy}, 2') 35 | 36 | a := {} 37 | log(a.x := 'yyy') 38 | log(({}.to := 2).to) 39 | 40 | ` scope inside match clause condition in fn literal ` 41 | max := 1 42 | fn := () => max := 3 :: { 43 | 3 -> 'right' 44 | _ -> 'wrong' 45 | } 46 | 47 | log('should be 1, not 3') 48 | fn() 49 | log(max) 50 | -------------------------------------------------------------------------------- /test/cases/003.ink: -------------------------------------------------------------------------------- 1 | ` objects and lists ` 2 | 3 | obj := { 4 | first: 1 5 | second: 2 6 | third: 3 7 | fourth: { 8 | fifth: ~4.5 9 | } 10 | } 11 | arr := [1, 2, 3, 4, 5] 12 | 13 | log := x => out(string(x) + char(10)) 14 | 15 | log(len(obj)) 16 | log(len(arr)) 17 | 18 | log(true) 19 | log(false) 20 | 21 | log(keys(obj)) 22 | log(keys(arr)) 23 | 24 | log(obj) 25 | log(arr) 26 | 27 | log(log) 28 | 29 | -------------------------------------------------------------------------------- /test/cases/004.ink: -------------------------------------------------------------------------------- 1 | ` fibonacci sequence generator 2 | copied from ink/samples/fib.ink ` 3 | 4 | std := load('./runtime/std') 5 | 6 | log := std.log 7 | 8 | ` naive implementation ` 9 | fib := n => n :: { 10 | 0 -> 0 11 | 1 -> 1 12 | _ -> fib(n - 1) + fib(n - 2) 13 | } 14 | 15 | ` memoized / dynamic programming implementation ` 16 | memo := [0, 1] 17 | fibMemo := n => ( 18 | memo.(n) :: { 19 | () -> memo.(n) := fibMemo(n - 1) + fibMemo(n - 2) 20 | } 21 | memo.(n) 22 | ) 23 | 24 | log('fib(20) is 6765:') 25 | out('Naive solution: '), log(fib(20)) 26 | out('Dynamic solution: '), log(fibMemo(20)) 27 | -------------------------------------------------------------------------------- /test/cases/005.ink: -------------------------------------------------------------------------------- 1 | ` minimal quicksort implementation 2 | using hoare partition ` 3 | 4 | std := load('./runtime/std') 5 | 6 | map := std.map 7 | clone := std.clone 8 | 9 | sortBy := (v, pred) => ( 10 | vPred := map(v, pred) 11 | partition := (v, lo, hi) => ( 12 | pivot := vPred.(lo) 13 | lsub := i => (vPred.(i) < pivot) :: { 14 | true -> lsub(i + 1) 15 | false -> i 16 | } 17 | rsub := j => (vPred.(j) > pivot) :: { 18 | true -> rsub(j - 1) 19 | false -> j 20 | } 21 | (sub := (i, j) => ( 22 | i := lsub(i) 23 | j := rsub(j) 24 | (i < j) :: { 25 | false -> j 26 | true -> ( 27 | ` inlined swap! ` 28 | tmp := v.(i) 29 | tmpPred := vPred.(i) 30 | v.(i) := v.(j) 31 | v.(j) := tmp 32 | vPred.(i) := vPred.(j) 33 | vPred.(j) := tmpPred 34 | 35 | sub(i + 1, j - 1) 36 | ) 37 | } 38 | ))(lo, hi) 39 | ) 40 | (quicksort := (v, lo, hi) => len(v) :: { 41 | 0 -> v 42 | _ -> (lo < hi) :: { 43 | false -> v 44 | true -> ( 45 | p := partition(v, lo, hi) 46 | quicksort(v, lo, p) 47 | quicksort(v, p + 1, hi) 48 | ) 49 | } 50 | })(v, 0, len(v) - 1) 51 | ) 52 | 53 | sort! := v => sortBy(v, x => x) 54 | 55 | sort := v => sort!(clone(v)) 56 | 57 | ` TEST ` 58 | range := std.range 59 | log := std.log 60 | list := std.stringList 61 | 62 | rint := () => floor(rand() * 500) 63 | L := map(range(0, 250, 1), rint) 64 | Before := clone(L) 65 | 66 | log('before quicksort: ' + list(L)) 67 | log('after quicksort: ' + list(sort(L))) 68 | log('before intact?: ' + (L :: {Before -> 'yes', _ -> 'no'})) 69 | sort!(L) 70 | log('after mutable sort: ' + list(L)) 71 | -------------------------------------------------------------------------------- /test/cases/006.ink: -------------------------------------------------------------------------------- 1 | ` Ink core test suite, modified from thesephist/ink 2 | to only test against std and str ` 3 | 4 | ` load std & str once for all tests ` 5 | std := load('./runtime/std') 6 | str := load('./runtime/str') 7 | 8 | ` borrow from std ` 9 | log := std.log 10 | each := std.each 11 | f := std.format 12 | 13 | ` ink language test suite, 14 | built on the suite library for testing ` 15 | 16 | ` suite constructor ` 17 | suite := label => ( 18 | ` suite data store ` 19 | s := { 20 | all: 0 21 | passed: 0 22 | msgs: [] 23 | } 24 | 25 | ` mark sections of a test suite with human labels ` 26 | mark := label => s.msgs.len(s.msgs) := '- ' + label 27 | 28 | ` signal end of test suite, print out results ` 29 | end := () => ( 30 | log(f('suite: {{ label }}', {label: label})) 31 | each(s.msgs, m => log(' ' + m)) 32 | s.passed :: { 33 | s.all -> log(f('ALL {{ passed }} / {{ all }} PASSED', s)) 34 | _ -> ( 35 | log(f('PARTIAL: {{ passed }} / {{ all }} PASSED', s)) 36 | exit(1) 37 | ) 38 | } 39 | ) 40 | 41 | ` log a passed test ` 42 | onSuccess := () => ( 43 | s.all := s.all + 1 44 | s.passed := s.passed + 1 45 | ) 46 | 47 | ` log a failed test ` 48 | onFail := msg => ( 49 | s.all := s.all + 1 50 | s.msgs.len(s.msgs) := msg 51 | ) 52 | 53 | ` perform a new test case ` 54 | indent := ' ' + ' ' + ' ' + ' ' 55 | test := (label, result, expected) => result :: { 56 | expected -> onSuccess() 57 | _ -> ( 58 | msg := f(' * {{ label }} 59 | {{ indent }}got {{ result }} 60 | {{ indent }}exp {{ expected }}', { 61 | label: label 62 | result: result 63 | expected: expected 64 | indent: indent 65 | }) 66 | onFail(msg) 67 | ) 68 | } 69 | 70 | ` expose API functions ` 71 | { 72 | mark: mark 73 | test: test 74 | end: end 75 | } 76 | ) 77 | 78 | s := suite('Ink language and standard library') 79 | 80 | ` short helper functions on the suite ` 81 | m := s.mark 82 | t := s.test 83 | 84 | m('value equality') 85 | ( 86 | ` with primitives ` 87 | t('() = ()', () = (), true) 88 | t('() = bool', () = false, false) 89 | t('number = number', 1 = 1.000, true) 90 | t('number = number', 100 = 1000, false) 91 | t('empty string = empty string', '' = '', true) 92 | t('string = string', 'val' = 'val', true) 93 | t('string = string', '' = 'empty', false) 94 | t('number = string', '23' = 23, false) 95 | t('string = number', 23 = '23', false) 96 | t('bool = bool', false = false, true) 97 | t('bool = bool', true = false, false) 98 | t('list = list', ['first', '_second'] = ['first', '_second'], true) 99 | t('list = list', ['first', '_second'] = ['first', '_second', '*third'], false) 100 | t('composite = composite', {} = {}, true) 101 | t('composite = list', {} = [], true) 102 | t('composite = ()', {} = (), false) 103 | 104 | fn := () => 1 105 | fn2 := () => 1 106 | t('function = function', fn = fn, true) 107 | t('function = function', fn = fn2, false) 108 | t('builtin fn = builtin fn', len = len, true) 109 | t('builtin fn = builtin fn', len = string, false) 110 | 111 | ` to empty identifier ` 112 | t('_ = _', _ = _, true) 113 | t('bool = _', true = _, true) 114 | t('_ = bool', _ = false, true) 115 | t('number = _', 0 = _, true) 116 | t('_ = number', _ = 3, true) 117 | t('string = _', '' = _, true) 118 | t('_ = string', _ = '', true) 119 | t('() = _', () = _, true) 120 | t('_ = ()', _ = (), true) 121 | t('composite = _', {} = _, true) 122 | t('_ = composite', _ = {}, true) 123 | t('_ = list', _ = [_], true) 124 | t('function = _', (() => ()) = _, true) 125 | t('_ = function', _ = (() => ()), true) 126 | t('builtin fn = _', len = _, true) 127 | t('_ = builtin fn', _ = len, true) 128 | ) 129 | 130 | m('composite value access') 131 | ( 132 | obj := { 133 | 39: 'clues' 134 | ('ex' + 'pr'): 'ession' 135 | } 136 | 137 | ` when calling a function that's a prop of a composite, 138 | we need to remember that AccessorOp is just a binary op 139 | and the function call precedes it in priority ` 140 | obj.fn := () => 'xyz' 141 | obj.fz := f => f() + f() 142 | 143 | t('calling composite property', (obj.fn)(), 'xyz') 144 | t('composite property by string value', (obj.('fn'))(), 'xyz') 145 | t('composite property by property access', (obj.fz)(obj.fn), 'xyzxyz') 146 | t('nonexistent composite key is ()', obj.nonexistent, ()) 147 | t('composite property by number value', obj.(~10), ()) 148 | t('composite property by number literal', obj.39, 'clues') 149 | t('composite property by identifier', obj.expr, 'ession') 150 | 151 | ` string index access ` 152 | t('string index access at 0', ('hello').0, 'h') 153 | t('string index access', ('what').3, 't') 154 | t('out of bounds string index access (negative)' 155 | ('hi').(~1), ()) 156 | t('out of bounds string index access (too large)' 157 | ('hello, world!').len('hello, world!'), ()) 158 | 159 | ` nested composites ` 160 | comp := {list: ['hi', 'hello', {what: 'thing'}]} 161 | 162 | ` can't just do comp.list.2.what because 163 | 2.what is not a valid identifier. 164 | these are some other recommended ways ` 165 | t('nested composite value access with number value' 166 | comp.list.(2).what, 'thing') 167 | t('nested composite value access with string value' 168 | comp.list.('2').what, 'thing') 169 | t('nested composite value access, parenthesized' 170 | (comp.list.2).what, 'thing') 171 | t('nested composite value access, double-parenthesized' 172 | (comp.list).(2).what, 'thing') 173 | t('string at index in computed string', comp.('li' + 'st').0, 'hi') 174 | t('nested property access returns composite', comp.list.2, {what: 'thing'}) 175 | 176 | ` modifying composite in chained accesses ` 177 | comp.list.4 := 'oom' 178 | comp.list.(2).what := 'arg' 179 | 180 | t('modifying composite at key leaves others unchanged', comp.list.4, 'oom') 181 | t('modifying composite at key', comp.list.(2).what, 'arg') 182 | ) 183 | 184 | m('function, expression, and lexical scope') 185 | ( 186 | thing := 3 187 | state := { 188 | thing: 21 189 | } 190 | fn := () => thing := 4 191 | fn2 := thing => thing := 24 192 | fn3 := () => ( 193 | state.thing := 100 194 | thing := ~3 195 | ) 196 | 197 | fn() 198 | fn2() 199 | fn3() 200 | ( 201 | thing := 200 202 | ) 203 | 204 | t('function body forms a new scope', fn(), 4) 205 | t('function body forms a new scope, assignment', fn2(), 24) 206 | t('function body with expression list forms a new scope', fn3(), ~3) 207 | t('assignment in child frames are isolated', thing, 3) 208 | t('modifying composites in scope from child frames causes mutation', state.thing, 100) 209 | 210 | x := 100 211 | y := 100 212 | z := 100 213 | w := 100 214 | x := 200 :: { 215 | y := 200 -> 1000 216 | (w := 200) -> 2000 217 | } 218 | nop := () => () 219 | nop(nop(z := 300, z := 400), z := 500, (w := 300)) 220 | 221 | t('assignment in match expression condition stays in scope', x, 200) 222 | t('assignment in match expression target stays in scope', y, 200) 223 | t('assignment in argument position stays in scope', z, 500) 224 | t('assignment in exprlist in match expression target is in inner scope', w, 100) 225 | t('assignment in exprlist in argument position is in inner scope', w, 100) 226 | ) 227 | 228 | m('tail call optimizations and thunk unwrap order') 229 | ( 230 | acc := [''] 231 | 232 | appender := prefix => str => acc.0 := acc.0 + prefix + str 233 | f1 := appender('f1_') 234 | f2 := appender('f2_') 235 | 236 | sub := () => ( 237 | f1('hi') 238 | ( 239 | f2('what') 240 | ) 241 | f3 := () => ( 242 | f2('hg') 243 | f1('bb') 244 | ) 245 | f1('sup') 246 | f2('sample') 247 | f3() 248 | f2('xyz') 249 | ) 250 | 251 | sub() 252 | 253 | t('tail optimized thunks are unwrapped in correct order' 254 | acc.0, 'f1_hif2_whatf1_supf2_samplef2_hgf1_bbf2_xyz') 255 | ) 256 | 257 | m('match expressions') 258 | ( 259 | x := ('what ' + string(1 + 2 + 3 + 4) :: { 260 | 'what 10' -> 'what 10' 261 | _ -> '??' 262 | }) 263 | t('match expression follows matched clause', x, 'what 10') 264 | 265 | x := ('what ' + string(1 + 2 + 3 + 4) :: { 266 | 'what 11' -> 'what 11' 267 | _ -> '??' 268 | }) 269 | t('match expression follows through to empty identifier', x, '??') 270 | 271 | x := ( 272 | y := {z: 9} 273 | 12 :: { 274 | y.z + 1 + 5 - 3 -> 'correct' 275 | 12 -> 'incorrect' 276 | 10 -> 'incorrect' 277 | _ -> 'wrong' 278 | } 279 | ) 280 | t('match expression target can be complex binary expressions', x, 'correct') 281 | 282 | x := ('a' :: { 283 | 1 :: { 284 | 2 -> 'b' 285 | 1 -> 'a' 286 | } -> 'c' 287 | _ -> 'd' 288 | }) 289 | t('match expression in match target position', x, 'c') 290 | 291 | N := { 292 | A: 1 293 | B: 2 294 | C: 3 295 | D: 4 296 | } 297 | x := (3 :: { 298 | N.A -> 'a' 299 | N.B -> 'b' 300 | N.C -> 'c' 301 | N.D -> 'd' 302 | }) 303 | t('match expression target can be object property', x, 'c') 304 | 305 | x := [1, 2, [3, 4, ['thing']], {a: ['b']}] 306 | t('composite deep equality after match expression' 307 | x, [1, 2, [3, 4, ['thing']], {a: ['b']}]) 308 | ) 309 | 310 | m('accessing properties strangely, accessing nonexistent properties') 311 | ( 312 | t('property access with number literal', {1: ~1}.1, ~1) 313 | t('list access with number literal', ['y', 'z'].1, ('z')) 314 | t('property access with bare string literal', {1: 4.2}.'1', 4.2) 315 | t('property access with number value', {1: 4.2}.(1), 4.2) 316 | t('property access with decimal number value', {1: 'hi'}.(1.0000), 'hi') 317 | 318 | ` also: composite parts can be empty ` 319 | t('composite parts can be empty', [_, _, 'hix'].('2'), 'hix') 320 | t('property access with computed string' 321 | string({test: 4200.00}.('te' + 'st')), '4200') 322 | t('nested property access with computed string' 323 | string({test: 4200.00}.('te' + 'st')).1, '2') 324 | t('nested property access with computed string, nonexistent key' 325 | string({test: 4200.00}.('te' + 'st')).10, ()) 326 | 327 | dashed := {'test-key': 14} 328 | t('property access with string literal that is not valid identifier' 329 | string(dashed.('test-key')), '14') 330 | ) 331 | 332 | m('calling functions with mismatched argument length') 333 | ( 334 | tooLong := (a, b, c, d, e) => a + b 335 | tooShort := (a, b) => a + b 336 | 337 | t('function call with too few arguments', tooLong(1, 2), 3) 338 | t('function call with too many arguments', tooShort(9, 8, 7, 6, 5, 4, 3), 17) 339 | ) 340 | 341 | m('argument order of evaluation') 342 | ( 343 | acc := [] 344 | fn := (x, y) => (acc.len(acc) := x, y) 345 | 346 | t('function arguments are evaluated in order, I' 347 | fn(fn(fn(fn('i', '?'), 'h'), 'g'), 'k'), 'k') 348 | t('function arguments are evaluated in order, II' 349 | acc, ['i', '?', 'h', 'g']) 350 | ) 351 | 352 | m('empty identifier "_" in arguments and functions') 353 | ( 354 | emptySingle := _ => 'snowman' 355 | emptyMultiple := (_, a, _, b) => a + b 356 | 357 | t('_ is a valid argument placeholder', emptySingle(), 'snowman') 358 | t('_ can be used multiple times in a single function as argument placeholders' 359 | emptyMultiple('bright', 'rain', 'sky', 'bow'), 'rainbow') 360 | ) 361 | 362 | m('comment syntaxes') 363 | ( 364 | `` t(wrong, wrong) 365 | ping := 'pong' 366 | ` t(wrong, more wrong) ` 367 | t('single line (line-lead) comments are recognized', ping, 'pong') 368 | t('inline comments are recognized', `hidden` '...thing', '...thing') 369 | t('inline comments terminate correctly', len('include `cmt` thing'), 19) 370 | ) 371 | 372 | m('more complex pattern matching') 373 | ( 374 | t('nested list pattern matches correctly', [_, [2, _], 6], [10, [2, 7], 6]) 375 | t('nested composite pattern matches correctly', { 376 | hi: 'hello' 377 | bye: { 378 | good: 'goodbye' 379 | } 380 | }, { 381 | hi: _ 382 | bye: { 383 | good: _ 384 | } 385 | }) 386 | t('nested list pattern matches with empty identifiers', [_, [2, _], 6, _], [10, [2, 7], 6, _]) 387 | t('composite pattern matches with empty identifiers', {6: 9, 7: _}, {6: _, 7: _}) 388 | ) 389 | 390 | m('order of operations') 391 | ( 392 | t('addition/subtraction', 1 + 2 - 3 + 5 - 3, 2) 393 | t('multiplication over addition/subtraction', 1 + 2 * 3 + 5 - 3, 9) 394 | t('multiplication/division', 10 - 2 * 16 / 4 + 3, 5) 395 | t('parentheses', 3 + (10 - 2) * 4, 35) 396 | t('parentheses and negation', 1 + 2 + (4 - 2) * 3 - (~1), 10) 397 | t('negating parenthesized expressions', 1 - ~(10 - 3 * 3), 2) 398 | t('modulus in bare expressions', 10 - 2 * 24 % 20 / 8 - 1 + 5 + 10 / 10, 14) 399 | t('logical operators', 1 & 5 | 4 ^ 1, (1 & 5) | (4 ^ 1)) 400 | t('logical operators, arithmetic, and parentheses', 1 + 1 & 5 % 3 * 10, (1 + 1) & ((5 % 3) * 10)) 401 | ) 402 | 403 | m('string lexicographical comparisons') 404 | ( 405 | t('less-than, I', 'a' < 'b', true) 406 | t('less-than, II', 'x' < 'A', false) 407 | ` shorter strings are lesser ` 408 | t('less-than, III', 'x long str' < 'A', false) 409 | 410 | t('greater-than, I', 'E' > 'A', true) 411 | t('greater-than, II', '0' > '_', false) 412 | t('greater-than, III', 'xxxxx' > 'xxx', true) 413 | 414 | t('empty strings', '' < ' ', true) 415 | t('uppercase < lowercase', 'S' < 's', true) 416 | t('non-printed byte arrays', char(253) > char(252), true) 417 | ) 418 | 419 | m('bitwise operations on byte strings') 420 | ( 421 | Z := char(0) 422 | ZZ := Z + Z 423 | ZZZ := ZZ + Z 424 | 425 | ` of the same lengths ` 426 | a := 'ABCDEFG' 427 | b := 'abcdEFg' 428 | 429 | t('bitwise & of byte strings' 430 | a & b, 'ABCDEFG') 431 | t('bitwise | of byte strings' 432 | a | b, 'abcdEFg') 433 | t('bitwise ^ of byte strings' 434 | a ^ b, ' ' + ZZ + ' ') 435 | 436 | ` of different lengths (byte strings are zero-extended at lower bytes) ` 437 | a := 'ABCD' 438 | b := 'abcdXYZ' 439 | 440 | t('bitwise & of diff length byte strings' 441 | a & b, 'ABCD' + ZZZ) 442 | t('bitwise | of diff length byte strings' 443 | a | b, 'abcdXYZ') 444 | t('bitwise ^ of diff length byte strings' 445 | a ^ b, ' XYZ') 446 | 447 | t('bitwise & of diff length byte strings, reverse order' 448 | b & a, 'ABCD' + ZZZ) 449 | t('bitwise | of diff length byte strings, reverse order' 450 | b | a, 'abcdXYZ') 451 | t('bitwise ^ of diff length byte strings, reverse order' 452 | b ^ a, ' XYZ') 453 | 454 | ` of same byte strings ` 455 | a := 'some_byte' 456 | 457 | t('bitwise & of same byte string' 458 | a & a, a) 459 | t('bitwise | of same byte string' 460 | a | a, a) 461 | t('bitwise ^ of same byte string' 462 | a ^ a, ZZZ + ZZZ + ZZZ) 463 | ) 464 | 465 | m('min/max') 466 | ( 467 | min := std.min 468 | max := std.max 469 | 470 | t('min of list of 1', min([~30]), ~30) 471 | t('minimum of list', min([39, 254, 5, ~2, 0, 3]), ~2) 472 | 473 | t('max of list of 1', max([101]), 101) 474 | t('maximum of list', max([39, 254, 5, ~2, 0, 3]), 254) 475 | 476 | t('min of array of same', min([2, 2, 2, 2, 2, 2, 2, 2]), 2) 477 | t('max of array of same', min([2, 2, 2, 2, 2, 2, 2, 2]), 2) 478 | ) 479 | 480 | m('logic composition correctness, std.some/std.every') 481 | ( 482 | ` and ` 483 | t('number & number, I', 1 & 4, 0) 484 | t('number & number, II', 2 & 3, 2) 485 | t('t & t', true & true, true) 486 | t('t & f', true & false, false) 487 | t('f & t', false & true, false) 488 | t('f & f', false & false, false) 489 | 490 | ` or ` 491 | t('number | number, I', 1 | 4, 5) 492 | t('number | number, II', 2 | 3, 3) 493 | t('t | t', true | true, true) 494 | t('t | f', true | false, true) 495 | t('f | t', false | true, true) 496 | t('f | f', false | false, false) 497 | 498 | ` xor ` 499 | t('number ^ number, I', 2 ^ 7, 5) 500 | t('number ^ number, II', 2 ^ 3, 1) 501 | t('t ^ t', true ^ true, false) 502 | t('t ^ f', true ^ false, true) 503 | t('f ^ t', false ^ true, true) 504 | t('f ^ f', false ^ false, false) 505 | 506 | ` std.some and std.every ` 507 | some := std.some 508 | every := std.every 509 | 510 | t('std.some() of empty list is false', some([]), false) 511 | t('std.every() of empty list is true', every([]), true) 512 | t('std.some() is true if at least one in list is true' 513 | some([false, false, true, false]), true) 514 | t('std.some() is false if none in list is true' 515 | some([false, false, false, false]), false) 516 | t('std.every() is true if all in list is true' 517 | every([true, true, true, true, true]), true) 518 | t('std.every() is false if at least one in list is false' 519 | every([true, true, true, false, true]), false) 520 | ) 521 | 522 | m('object keys / list, mutable strings, std.clone') 523 | ( 524 | clone := std.clone 525 | obj := { 526 | first: 1 527 | second: 2 528 | third: 3 529 | } 530 | list := ['red', 'green', 'blue'] 531 | 532 | ks := { 533 | first: false 534 | second: false 535 | third: false 536 | } 537 | ky := keys(obj) 538 | ` keys are allowed to be out of insertion order 539 | -- composites are unordered maps` 540 | ks.(ky.0) := true 541 | ks.(ky.1) := true 542 | ks.(ky.2) := true 543 | t('keys() builtin for composite returns keys' 544 | [ks.first, ks.second, ks.third], [true, true, true]) 545 | t('keys() builtin for composite returns all keys' 546 | len(keys(obj)), 3) 547 | 548 | cobj := clone(obj) 549 | obj.fourth := 4 550 | clist := clone(list) 551 | list.len(list) := 'alpha' 552 | 553 | t('std.clone does not affect original composite', len(keys(obj)), 4) 554 | t('std.clone creates a new copy of composite', len(keys(cobj)), 3) 555 | t('std.clone does not affect original list', len(list), 4) 556 | t('std.clone creates a new copy of list', len(clist), 3) 557 | 558 | ` len() should count the number of keys on a composite, 559 | not just integer indexes like ECMAScript ` 560 | t('len() builtin on manually indexed composite', len({ 561 | 0: 1 562 | 1: 'order' 563 | 2: 'natural' 564 | }), 3) 565 | t('len() builtin on non-number keyed composite', len({ 566 | hi: 'h' 567 | hello: 'he' 568 | thing: 'th' 569 | what: 'w' 570 | }), 4) 571 | t('len() builtin counts non-consecutive integer keys', len({ 572 | 0: 'hi' 573 | '1': 100 574 | 3: 'x' 575 | 5: [] 576 | 'word': 0 577 | }), 5) 578 | 579 | str := 'hello' 580 | twin := str 581 | ccpy := str + '' ` should yield a new copy ` 582 | tcpy := '' + twin 583 | copy := clone(str) 584 | str.2 := 'xx' 585 | copy.2 := 'yy' 586 | 587 | t('std.clone does not affect original string', str, 'hexxo') 588 | t('define op does not create a copy of string', twin, 'hexxo') 589 | t('concatenation via + creates a copy of string, I', ccpy, 'hello') 590 | t('concatenation via + creates a copy of string, II', tcpy, 'hello') 591 | t('std.clone creates a copy of string', copy, 'heyyo') 592 | ) 593 | 594 | m('string/composite pass by reference / mutation check') 595 | ( 596 | clone := std.clone 597 | 598 | obj := [1, 2, 3] 599 | twin := obj ` by reference ` 600 | clone := clone(obj) ` cloned (by value) ` 601 | 602 | obj.len(obj) := 4 603 | obj.len(obj) := 5 604 | obj.len(obj) := 6 605 | 606 | t('std.clone does not affect original composite', len(obj), 6) 607 | t('define op does not create a copy of composite', len(twin), 6) 608 | t('std.clone creates a copy of composite', len(clone), 3) 609 | 610 | t('assignment to composite key returns composite itself, updated', clone.hi := 'x', { 611 | 0: 1 612 | 1: 2 613 | 2: 3 614 | hi: 'x' 615 | }) 616 | 617 | str := 'hello, world' 618 | str2 := '' + str 619 | str.5 := '!' 620 | str.8 := 'lx' 621 | str.2 := '' 622 | str.3 := '' 623 | str.len(str) := 'x?' 624 | 625 | t('assigning to string indexes mutates original string', str, 'hello! wlxldx?') 626 | t('concatenating to string with + creates a copy of string', str2, 'hello, world') 627 | 628 | str := 'hi' 629 | t('assigning to string index returns the string itself, updated', str.1 := 'what', 'hwhat') 630 | t('assigning to string index modifies itself', str, 'hwhat') 631 | 632 | str := '00000000' 633 | mut := (i, s) => ( 634 | str.(i) := s 635 | ) 636 | mut(4, 'AAA') 637 | t('assigning to string index with more than one char modifies multiple indexes' 638 | str, '0000AAA0') 639 | mut(8, 'YYY') 640 | t('assigning to string index with more than one char appends as necessary' 641 | str, '0000AAA0YYY') 642 | ) 643 | 644 | m('number & composite/list -> string conversions') 645 | ( 646 | stringList := std.stringList 647 | 648 | t('string(number) uses least number of digits necessary, integer' 649 | string(42), '42') 650 | t('string(number) uses least number of digits necessary, short decimal' 651 | string(3.14), '3.14') 652 | t('string(number) uses least number of digits necessary, long decimal' 653 | string(5 / 3), '1.6666666666666667') 654 | ` speed of light in microns per second ` 655 | t('string(number) uses least number of digits necessary, large number' 656 | string(299792458000000), '299792458000000') 657 | t('string(number) uses least number of digits necessary, small exponential' 658 | string(0.0000000001), '1e-10') 659 | t('string(number) uses least number of digits necessary, big exponential' 660 | string(2997924580000000000000), '2.99792458e+21') 661 | t('string(true)', string(true), 'true') 662 | t('string(false)', string(false), 'false') 663 | t('string(string) returns itself', string('hello'), 'hello') 664 | t('string(list) returns string(composite)', string([0]), '{0: 0}') 665 | 666 | t('number(string) correctly parses decimal number', number('3.14'), 3.14) 667 | t('number(string) deals with leading and trailing zeroes', number('03.140000'), 3.14) 668 | t('number(string) deals with negative numbers', number('-42'), ~42) 669 | t('number(string) with large exponent', number('3e10'), 30000000000) 670 | t('number(string) with small exponent', number('3e-9'), 0.000000003) 671 | t('number(true) = 1', number(true), 1) 672 | t('number(false) = 0', number(false), 0) 673 | t('number(composite) = 0', number([]), 0) 674 | t('number(function) = 0', number(() => 100), 0) 675 | t('number(builtin fn) = 0', number(len), 0) 676 | 677 | t('string(composite)', string({a: 3.14}), '{a: 3.14}') 678 | 679 | result := string([3, 'two']) 680 | p1 := '{0: 3, 1: \'two\'}' 681 | p2 := '{1: \'two\', 0: 3}' 682 | t('string(composite) containing string and multiple keys', result = p1 | result = p2, true) 683 | 684 | t('stringList(list) for nested list', stringList(['fine', ['not']]), '[fine, {0: \'not\'}]') 685 | ) 686 | 687 | m('function/composite equality checks') 688 | ( 689 | ` function equality ` 690 | fn1 := () => (3 + 4, 'hello') 691 | fnc := fn1 692 | fn2 := () => (3 + 4, 'hello') 693 | 694 | t('functions are equal if they are the same function' 695 | fn1 = fnc, true) 696 | t('functions are different if they are defined separately, even if same effect' 697 | fn1 = fn2, false) 698 | 699 | ` composite equality ` 700 | comp1 := {1: 2, hi: '4'} 701 | comp2 := {1: 2, hi: '4'} 702 | list1 := [1, 2, 3, 4, 5] 703 | list2 := [1, 2, 3, 4, 5] 704 | complist := {0: 1, 1: 2, 2: 3, 3: 4, 4: 5} 705 | 706 | t('deep composite equality', comp1 = comp2, true) 707 | t('deep list equality', list1 = list2, true) 708 | t('deep composite inequality, I', comp1 = list1, false) 709 | t('deep composite inequality, II', comp1 = {1: '4', 2: 2}, false) 710 | t('composite = {}', comp1 = {}, false) 711 | t('deep list inequality, I', list1 = [1, 2, 3], false) 712 | t('deep list inequality, II', list1 = complist, true) 713 | ) 714 | 715 | m('type() builtin function') 716 | ( 717 | t('type(string)', type('hi'), 'string') 718 | t('type(number)', type(3.14), 'number') 719 | t('type(list) (composite)', type([0, 1, 2]), 'composite') 720 | t('type(composite)', type({hi: 'what'}), 'composite') 721 | t('type(function)', type(() => 'hi'), 'function') 722 | t('type(builtin fn) (function), I', type(type), 'function') 723 | t('type(builtin fn) (function), II', type(out), 'function') 724 | t('type(()) = ()', type(()), '()') 725 | ) 726 | 727 | m('std.range/slice/append/join/cat and stringList') 728 | ( 729 | stringList := std.stringList 730 | range := std.range 731 | reverse := std.reverse 732 | slice := std.slice 733 | join := std.join 734 | cat := std.cat 735 | 736 | ` slice returns copies ` 737 | ( 738 | st := '12345' 739 | li := [1, 2, 3, 4, 5] 740 | 741 | stc := slice(st, 0, len(st)) 742 | lic := slice(li, 0, len(li)) 743 | stc.2 := 'x' 744 | lic.2 := 'x' 745 | 746 | t('slice(string) should make a copy', st, '12345') 747 | t('slice(string) should return a copy', stc, '12x45') 748 | t('slice(list) should make a copy', li, [1, 2, 3, 4, 5]) 749 | t('slice(list) should return a copy', lic, [1, 2, 'x', 4, 5]) 750 | ) 751 | 752 | sl := (l, s, e) => stringList(slice(l, s, e)) 753 | list := range(10, ~1, ~1) 754 | str := 'abracadabra' 755 | 756 | t('slice(list)', sl(list, 0, 5), '[10, 9, 8, 7, 6]') 757 | t('slice with OOB lower bound', sl(list, ~5, 2), '[10, 9]') 758 | t('slice with OOB upper bound', sl(list, 7, 20), '[3, 2, 1, 0]') 759 | t('slice with OOB both bounds', sl(list, 20, 1), '[]') 760 | 761 | ` redefine list using range and reverse, to t those ` 762 | list := reverse(range(0, 11, 1)) 763 | 764 | t('join() homogeneous lists', stringList(join( 765 | slice(list, 0, 5), slice(list, 5, 200) 766 | )), '[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]') 767 | t('join() heterogeneous lists', stringList(join( 768 | [1, 2, 3] 769 | join([4, 5, 6], ['a', 'b', 'c']) 770 | )), '[1, 2, 3, 4, 5, 6, a, b, c]') 771 | 772 | t('slice() from 0', slice(str, 0, 5), 'abrac') 773 | t('slice() from nonzero', slice(str, 2, 4), 'ra') 774 | t('slice() with OOB lower bound', slice(str, ~5, 2), 'ab') 775 | t('slice() with OOB upper bound', slice(str, 7, 20), 'abra') 776 | t('slice() with OOB both bounds', slice(str, 20, 1), '') 777 | 778 | t('cat() empty list', cat([], '--'), '') 779 | t('cat() single-element list', cat(['hello'], '--'), 'hello') 780 | t('cat() double-element list', cat(['hello', 'world'], '--'), 'hello--world') 781 | t('cat() list containing delimiter', cat(['hello', 'world,hi'], ','), 'hello,world,hi') 782 | t('cat() with empty string delimiter', cat(['good', 'bye', 'friend'], ''), 'goodbyefriend') 783 | t('cat() with comma separator', cat(['good', 'bye', 'friend'], ', '), 'good, bye, friend') 784 | t('cat() with manually indexed composite', cat({ 785 | 0: 'first' 786 | 1: 'last' 787 | }, ' and '), 'first and last') 788 | ) 789 | 790 | m('hexadecimal conversions, hex & xeh') 791 | ( 792 | hex := std.hex 793 | xeh := std.xeh 794 | 795 | ` base cases ` 796 | t('hex(0)', hex(0), '0') 797 | t('hex(42)', hex(66), '42') 798 | t('hex(256)', hex(256), '100') 799 | t('hex(1998)', hex(1998), '7ce') 800 | t('hex(3141592)', hex(3141592), '2fefd8') 801 | t('xeh(fff)', xeh('fff'), 4095) 802 | t('xeh(a2)', xeh('a2'), 162) 803 | 804 | ` hex should floor non-integer inputs ` 805 | t('hex() of fractional number, I', hex(16.8), '10') 806 | t('hex() of fractional number, II', hex(1998.123), '7ce') 807 | 808 | ` recoverability ` 809 | t('xeh(hex()), I', xeh(hex(390420)), 390420) 810 | t('xeh(hex()), II', xeh(hex(9230423903)), 9230423903) 811 | t('hex(xeh()), I', hex(xeh('fffab123')), 'fffab123') 812 | t('hex(xeh()), II', hex(xeh('0000ab99ff33')), 'ab99ff33') 813 | t('hex(xeh(hex(xeh()))), I', hex(xeh(hex(xeh('aabbef')))), 'aabbef') 814 | t('hex(xeh(hex(xeh()))), II', xeh(hex(xeh(hex(201900123)))), 201900123) 815 | ) 816 | 817 | m('ascii <-> char point conversions and string encode/decode') 818 | ( 819 | encode := std.encode 820 | decode := std.decode 821 | 822 | s1 := 'this is a long piece of string 823 | with weird line 824 | breaks 825 | ' 826 | s2 := '' 827 | s3 := 'AaBbCcDdZzYyXx123456789!@#$%^&*()_+-=' 828 | 829 | ` note: at this point, we only care about ascii, not full Unicode ` 830 | t('point(a)', point('a'), 97) 831 | t('char(65)', char(65), 'A') 832 | t('encode(ab)', encode('ab'), [97, 98]) 833 | t('decode() => ACB', decode([65, 67, 66]), 'ACB') 834 | t('repeated decode/encode, I', decode(encode(decode(encode(s1)))), s1) 835 | t('repeated decode/encode, II', decode(encode(decode(encode(s2)))), s2) 836 | t('repeated decode/encode, III', decode(encode(decode(encode(s3)))), s3) 837 | ) 838 | 839 | m('std list: map/filter/reduce[Back]/each/reverse/flatten, join/append') 840 | ( 841 | map := std.map 842 | filter := std.filter 843 | reduce := std.reduce 844 | reduceBack := std.reduceBack 845 | each := std.each 846 | reverse := std.reverse 847 | flatten := std.flatten 848 | append := std.append 849 | join := std.join 850 | 851 | list := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 852 | 853 | t('std.map', map(list, n => n * n), [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]) 854 | t('std.filter', filter(list, n => n % 2 = 0), [2, 4, 6, 8, 10]) 855 | t('std.reduce', reduce(list, (acc, n) => acc + string(n), '') 856 | '12345678910') 857 | t('std.reduceBack', reduceBack(list, (acc, n) => acc + string(n), '') 858 | '10987654321') 859 | t('std.flatten', flatten([[1, 2, 3], [4], [], [[5], 6, 7, [8, 9, 10]]]) 860 | [1, 2, 3, 4, [5], 6, 7, [8, 9, 10]]) 861 | t('std.reverse', reverse(list), [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]) 862 | t('std.join', join(list, list), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 863 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 864 | 865 | ` degenerate cases on reverse ` 866 | t('std.reverse on empty', reverse([]), []) 867 | t('std.reverse on len 1', reverse(['a']), ['a']) 868 | t('std.reverse on len 2', reverse(['b', 'a']), ['a', 'b']) 869 | t('std.reverse on reversed', reverse(reverse(list)), list) 870 | 871 | ` passing index in callback ` 872 | t('std.map passes index to callback', map(list, (_, i) => i) 873 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 874 | t('std.filter passes index to callback', filter(list, (_, i) => i % 2 = 1) 875 | [2, 4, 6, 8, 10]) 876 | t('std.reduce passes index to callback' 877 | reduce(list, (acc, _, i) => acc + string(i), ''), '0123456789') 878 | t('std.reduceBack passes index to callback' 879 | reduceBack(list, (acc, _, i) => acc + string(i), ''), '9876543210') 880 | ( 881 | eachAcc := [] 882 | each(list, (_, i) => eachAcc.len(eachAcc) := i) 883 | t('std.each passes index to callback', eachAcc 884 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 885 | ) 886 | 887 | ` each doesn't return anything meaningful ` 888 | acc := { 889 | str: '' 890 | } 891 | twice := f => x => (f(x), f(x)) 892 | each(list, twice(n => acc.str := acc.str + string(n))) 893 | t('std.each', acc.str, '1122334455667788991010') 894 | 895 | ` append mutates ` 896 | append(list, list) 897 | t('std.append', list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 898 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 899 | ) 900 | 901 | m('std.format -- the standard library formatter / templater') 902 | ( 903 | f := std.format 904 | stringList := std.stringList 905 | 906 | values := { 907 | first: 'ABC' 908 | 'la' + 'st': 'XYZ' 909 | thingOne: 1 910 | thingTwo: stringList([5, 4, 3, 2, 1]) 911 | 'magic+eye': 'add_sign' 912 | } 913 | 914 | t('std.format empty string', f('', {}), '') 915 | t('std.format single value', f('one two {{ first }} four', values), 'one two ABC four') 916 | t('std.format with newlines in string', f('new 917 | {{ sup }} line', {sup: 42}), 'new 918 | 42 line') 919 | t('std.format with non-terminated slot ignores rest' 920 | f('now {{ then now', {then: 'then'}), 'now ') 921 | t('std.format with unusual (tighter) spacing', f( 922 | ' {{thingTwo}}+{{ magic+eye }} ' 923 | values 924 | ), ' [5, 4, 3, 2, 1]+add_sign ') 925 | t('std.format with unusual (tighter) spacing and more replacements', f( 926 | '{{last }} {{ first}} {{ thing One }} {{ thing Two }}' 927 | values 928 | ), 'XYZ ABC 1 [5, 4, 3, 2, 1]') 929 | t( 930 | 'std.format with non-format braces' 931 | f('{ { this is not } {{ thingOne } wut } {{ nonexistent }}', values) 932 | '{ { this is not } 1 ()' 933 | ) 934 | ) 935 | 936 | m('str.upper/lower/digit/letter/ws? -- checked char ranges') 937 | ( 938 | upper? := str.upper? 939 | lower? := str.lower? 940 | digit? := str.digit? 941 | letter? := str.letter? 942 | ws? := str.ws? 943 | 944 | every := std.every 945 | some := std.some 946 | map := std.map 947 | 948 | t('upper? verifies uppercase letters' 949 | every(map('ABCDEFGHIJKLMNOPQRSTUVWXYZ', upper?)), true) 950 | t('upper? rejects non-uppercase-letters' 951 | some(map('onawfepd913043?-~\'!/.,;()$@)%', upper?)), false) 952 | t('lower? verifies lowercase letters' 953 | every(map('abcdefghijklmnopqrstuvwxyz', lower?)), true) 954 | t('lower? rejects non-lowercase-letters' 955 | some(map('ONAWFEPD913043?-~\'!/.,;()$@)%', lower?)), false) 956 | t('digit? verifies digits' 957 | every(map('0123456789', digit?)), true) 958 | t('digit? rejects non-digits, including punctuations' 959 | some(map('~@!#@$%^()&?!.;,-', digit?)), false) 960 | t('letter? verifies all alphabet letters' 961 | every(map('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', letter?)) 962 | true) 963 | t('letter? rejects non-letters' 964 | some(map('913043?-~\'!/.,;()$@)%', upper?)), false) 965 | t('ws? verifies whitespace characters' 966 | every(map(' 967 | ', ws?)), true) 968 | t('ws? rejects all non-whitespace' 969 | some(map('jafsioSINDFOEJ#@%@()_#9u40529' + char(250), ws?)), false) 970 | 971 | hasPrefix? := str.hasPrefix? 972 | hasSuffix? := str.hasSuffix? 973 | 974 | t('hasPrefix? detects prefix' 975 | hasPrefix?('programming', 'prog'), true) 976 | t('hasPrefix? returns true for empty prefix' 977 | hasPrefix?('programming', ''), true) 978 | t('hasPrefix? returns true if s = prefix' 979 | hasPrefix?('programming', 'programming'), true) 980 | t('hasPrefix? returns false if not prefix' 981 | hasPrefix?('programming', 'progx'), false) 982 | t('hasPrefix? accumulates equality correctly, pos 2' 983 | hasPrefix?('d: test', '::'), false) 984 | t('hasPrefix? accumulates equality correctly, pos 5' 985 | hasPrefix?('e123e test', 'e321e'), false) 986 | 987 | t('hasSuffix? detects suffix' 988 | hasSuffix?('programming', 'mming'), true) 989 | t('hasSuffix? returns true for empty suffix' 990 | hasSuffix?('programming', ''), true) 991 | t('hasSuffix? returns true if s = suffix' 992 | hasSuffix?('programming', 'programming'), true) 993 | t('hasSuffix? returns false if not suffix' 994 | hasSuffix?('programming', 'science'), false) 995 | t('hasSuffix? accumulates equality correctly, pos 2' 996 | hasSuffix?('test: xa', 'xb'), false) 997 | t('hasSuffix? accumulates equality correctly, pos 5' 998 | hasSuffix?('__ e123e', 'e321e'), false) 999 | 1000 | both := '_x_init()_x_' 1001 | piece := '_x_' 1002 | t('hasPrefix? and hasSuffix? used together' 1003 | hasPrefix?(both, piece) & hasSuffix?(both, piece), true) 1004 | 1005 | matchesAt? := str.matchesAt? 1006 | 1007 | t('matchesAt? returns true for empty substring' 1008 | matchesAt?('some substring', ''), true) 1009 | t('matchesAt? returns true if string matches at idx' 1010 | matchesAt?('some substring', 'substr', 5), true) 1011 | t('matchesAt? returns false if string matches not at idx' 1012 | matchesAt?('some substring', 'substr', 2), false) 1013 | t('matchesAt? returns false if no match' 1014 | matchesAt?('some substring', 'other', 5), false) 1015 | 1016 | index := str.index 1017 | 1018 | t('index = 0 for empty string', index('quick brown fox', ''), 0) 1019 | t('index returns index of substring' 1020 | index('quick brown fox', 'ick'), 2) 1021 | t('index returns 0 if matches whole string' 1022 | index('quick brown fox', 'quick brown fox'), 0) 1023 | t('index returns ~1 if no match' 1024 | index('quick brown fox', 'lazy dog'), ~1) 1025 | t('index returned is first occurrence' 1026 | index('quick brown fox', 'o'), 8) 1027 | t('index works if substring longer than string' 1028 | index('quick brown fox', 'jumps over the lazy dog'), ~1) 1029 | 1030 | contains? := str.contains? 1031 | 1032 | t('contains? = true for empty string' 1033 | contains?('quick brown fox', ''), true) 1034 | t('contains? = true if string fits substring' 1035 | contains?('quick brown fox', 'fox'), true) 1036 | t('contains? = true if substring fits multiple times' 1037 | contains?('quick brown fox', 'o'), true) 1038 | t('contains? = false if not contained' 1039 | contains?('quick brown fox', 'lazy dog'), false) 1040 | 1041 | lower := str.lower 1042 | upper := str.upper 1043 | title := str.title 1044 | given := 'MIXED case StrinG with ?!~:punct' 1045 | 1046 | t('lower transforms string to lowercase' 1047 | lower(given), 'mixed case string with ?!~:punct') 1048 | t('upper transforms string to uppercase' 1049 | upper(given), 'MIXED CASE STRING WITH ?!~:PUNCT') 1050 | t('title returns uppercase first + lowercase rest' 1051 | title(given), 'Mixed case string with ?!~:punct') 1052 | 1053 | replace := str.replace 1054 | 1055 | t('replace is no-op if empty string' 1056 | replace('he stared in amazement', '', '__') 1057 | 'he stared in amazement') 1058 | t('replace replaces all instances of given substring' 1059 | replace('he stared in amazement', 'e', 'j') 1060 | 'hj starjd in amazjmjnt') 1061 | t('replace works for multi-character substring' 1062 | replace('he is staring in amazement', 'in', 'xx') 1063 | 'he is starxxg xx amazement') 1064 | t('replace accounts for different old/new substring lengths' 1065 | replace('he is staring in amazement', 'in', 'wonder') 1066 | 'he is starwonderg wonder amazement') 1067 | t('replace deals gracefully with overlapping matches' 1068 | replace('wow what a sight, wow', 'ow', 'wow') 1069 | 'wwow what a sight, wwow') 1070 | t('replace works if new substring is empty' 1071 | replace('wow what a sight, wow', 'wow', '') 1072 | ' what a sight, ') 1073 | t('replace works even if new str contains recursive match' 1074 | replace('a {} b {} c {}', '{}', '{}-{}') 1075 | 'a {}-{} b {}-{} c {}-{}') 1076 | 1077 | split := str.split 1078 | 1079 | t('split splits string into letters if empty' 1080 | split('alphabet', '') 1081 | ['a', 'l', 'p', 'h', 'a', 'b', 'e', 't']) 1082 | t('splits with given delimiter' 1083 | split('a,b,cde,fg', ',') 1084 | ['a', 'b', 'cde', 'fg']) 1085 | t('splits with empty strings if delimiter in start or end' 1086 | split(', original taste, ', ', ') 1087 | ['', 'original taste', '']) 1088 | t('returns one chunk if no match of delimiter found' 1089 | split('no taste whatsoever!', 'grand') 1090 | ['no taste whatsoever!']) 1091 | 1092 | trimPrefix := str.trimPrefix 1093 | trimSuffix := str.trimSuffix 1094 | trim := str.trim 1095 | 1096 | t('trimPrefix is a no-op with empty string' 1097 | trimPrefix('???????what???', ''), '???????what???') 1098 | t('trimPrefix trims given prefix until it does not prefix' 1099 | trimPrefix('???????what???', '?'), 'what???') 1100 | t('trimPrefix works with multi-char prefix' 1101 | trimPrefix('abababacdef', 'ab'), 'acdef') 1102 | t('trimPrefix only trims whole multiples of prefix' 1103 | trimPrefix('aaaaaaaadef', 'aaa'), 'aadef') 1104 | 1105 | t('trimSuffix is a no-op with empty string' 1106 | trimSuffix('???????what???', ''), '???????what???') 1107 | t('trimSuffix trims given suffix until it does not suffix' 1108 | trimSuffix('???????what???', '?'), '???????what') 1109 | t('trimSuffix works with multi-char suffix' 1110 | trimSuffix('abacdefabcabab', 'ab'), 'abacdefabc') 1111 | t('trimSuffix only trims whole multiples of suffix' 1112 | trimSuffix('xxxyyyyyyyy', 'yyy'), 'xxxyy') 1113 | 1114 | t('trim trims given string from both sides' 1115 | trim('????what?????', '?'), 'what') 1116 | t('trim is a no-op with empty string' 1117 | trim('????what?????', ''), '????what?????') 1118 | t('trim trims whole multiples of substring from both sides' 1119 | trim('????what?????', '???'), '?what??') 1120 | ) 1121 | 1122 | ` end test suite, print result ` 1123 | (s.end)() 1124 | -------------------------------------------------------------------------------- /test/cases/007.ink: -------------------------------------------------------------------------------- 1 | ` test: std.clone ` 2 | 3 | std := load('./runtime/std') 4 | 5 | log := std.log 6 | clone := std.clone 7 | 8 | x := { 9 | key: 'value' 10 | k2: 2.31 11 | ork: [1, 2, 3] 12 | } 13 | log(x) 14 | y := clone(x) 15 | log(y) 16 | 17 | x.key := 'v2' 18 | x.ork.len(x.ork) := 9 19 | log(x) 20 | log(y) 21 | 22 | -------------------------------------------------------------------------------- /test/cases/008.ink: -------------------------------------------------------------------------------- 1 | ` prime sieve 2 | taken from thesephist/ink/samples/` 3 | 4 | std := load('./runtime/std') 5 | `` std := load('../../vendor/std') 6 | 7 | log := std.log 8 | filter := std.filter 9 | stringList := std.stringList 10 | 11 | ` is a single number prime? ` 12 | isPrime := n => ( 13 | ` is n coprime with nums < p? ` 14 | max := floor(pow(n, 0.5)) + 1 15 | (ip := p => p :: { 16 | max -> true 17 | _ -> n % p :: { 18 | 0 -> false 19 | _ -> ip(p + 1) 20 | } 21 | })(2) ` start with smaller # = more efficient ` 22 | ) 23 | 24 | ` build a list of consecutive integers from 2 .. max ` 25 | buildConsecutive := max => ( 26 | peak := max + 1 27 | acc := [] 28 | (bc := i => i :: { 29 | peak -> () 30 | _ -> ( 31 | acc.(i - 2) := i 32 | bc(i + 1) 33 | ) 34 | })(2) 35 | acc 36 | ) 37 | 38 | ` primes under N are numbers 2 .. N, filtered by isPrime ` 39 | getPrimesUnder := n => filter(buildConsecutive(n), isPrime) 40 | 41 | ps := getPrimesUnder(1250) 42 | log(stringList(ps)) 43 | log('Total number of primes under 1250: ' + string(len(ps))) 44 | -------------------------------------------------------------------------------- /test/cases/009.ink: -------------------------------------------------------------------------------- 1 | ` tail call elimination tests 2 | with a large fibonacci seq ` 3 | 4 | std := load('./runtime/std') 5 | 6 | log := std.log 7 | range := std.range 8 | each := std.each 9 | 10 | `` fibonacci 11 | fib := n => (sub := (a, b, i) => i :: { 12 | 0 -> a + b 13 | 1 -> a + b 14 | _ -> ( 15 | next := i - 1 16 | sub(b, a + b, next) 17 | ) 18 | })(0, 1, n) 19 | 20 | each(range(0, 20, 1), n => log(fib(n))) 21 | 22 | `` iterated sum 23 | 24 | sub := (acc, i) => i :: { 25 | 0 -> acc 26 | _ -> sub(acc + i, i - 1) 27 | } 28 | 29 | sum := n => sub(0, n) 30 | 31 | each(map(range(1, 6, 1), exp => pow(10, exp)), n => log(sum(n))) 32 | 33 | -------------------------------------------------------------------------------- /test/cases/010.ink: -------------------------------------------------------------------------------- 1 | ` object literal {} in a value position in match expression ` 2 | 3 | r := ({a: 'b', c: 42} :: { 4 | () -> 'wrong 1' 5 | {a: _, c: 42} -> 'right' 6 | _ -> 'wrong 2' 7 | }) 8 | 9 | out('right: ') 10 | out(r + char(10)) 11 | 12 | -------------------------------------------------------------------------------- /test/parse/000.ink: -------------------------------------------------------------------------------- 1 | ` parsing tests, does not run ` 2 | 3 | 2 * 3.14 + 4, 5 - 6 / 7 4 | 'h`ello' + 'world' 5 | 6 | true & false 7 | ~~4 8 | 9 | `` empty expressions 10 | () => () 11 | () 12 | {} 13 | [] 14 | 15 | window.location.href 16 | obj.('hi', a + b) 17 | do() 18 | 19 | (console.log)('hi', 2) 20 | 21 | x => 9 + x * x 22 | (y, _) => ( 23 | log(x + y, z) 24 | y + 2 25 | ) 26 | 27 | 'x' + type('hi') :: { 28 | true -> 9 & 3 + x * x 29 | ` block comment 30 | that spans multiple lines ` 31 | Node.CaseArrow -> pow(10 + 12, 2) 32 | double(a, b, c) - 1 -> 2 + 3 = 1 + 4 33 | _ -> ( 34 | log(true) 35 | log(~false) 36 | ) 37 | } 38 | 39 | [1, 2, '3', 4 + 'a'] 40 | { 41 | key: 'value' 42 | other(this): 9.8690 43 | (2 + 3): { 44 | test: 'tes' + 'sler' 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/test.ink: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ink 2 | 3 | std := load('../vendor/std') 4 | 5 | log := std.log 6 | 7 | log('Tests not defined!') 8 | -------------------------------------------------------------------------------- /vendor/ansi.ink: -------------------------------------------------------------------------------- 1 | ` ANSI terminal escape codes ` 2 | 3 | Esc := '' 4 | 5 | Backspace := '' 6 | MoveCursor := (x, y) => Esc + '[' + string(y) + ';' + string(x) + 'f' 7 | MoveColumn := x => Esc + '[' + string(x) + 'G' 8 | 9 | Home := Esc + '[H' 10 | Reset := Esc + MoveColumn(0) 11 | 12 | Clear := Esc + '[2J' + Home 13 | ClearLine := Esc + '[2K' + Reset 14 | ClearLastLine := Esc + '[0F' + ClearLine 15 | 16 | ` color sequences ` 17 | 18 | Weight := { 19 | Regular: 0 20 | Bold: 1 21 | Dim: 2 22 | } 23 | Color := { 24 | Black: 30 25 | Red: 31 26 | Green: 32 27 | Yellow: 33 28 | Blue: 34 29 | Magenta: 35 30 | Cyan: 36 31 | White: 37 32 | Gray: 90 33 | Reset: 0 34 | } 35 | Background := { 36 | Black: 40 37 | Red: 41 38 | Green: 42 39 | Yellow: 43 40 | Blue: 44 41 | Magenta: 45 42 | Cyan: 46 43 | White: 47 44 | Gray: 100 45 | Reset: 0 46 | } 47 | 48 | style := (t, c) => s => Esc + '[' + string(t) + ';' + string(c) + 'm' + s + Esc + '[0;0m' 49 | 50 | ` shorthand functions ` 51 | 52 | Black := style(Weight.Regular, Color.Black) 53 | Red := style(Weight.Regular, Color.Red) 54 | Green := style(Weight.Regular, Color.Green) 55 | Yellow := style(Weight.Regular, Color.Yellow) 56 | Blue := style(Weight.Regular, Color.Blue) 57 | Magenta := style(Weight.Regular, Color.Magenta) 58 | Cyan := style(Weight.Regular, Color.Cyan) 59 | White := style(Weight.Regular, Color.White) 60 | Gray := style(Weight.Regular, Color.Gray) 61 | 62 | Bold := style(Weight.Bold, Color.Reset) 63 | BoldBlack := style(Weight.Bold, Color.Black) 64 | BoldRed := style(Weight.Bold, Color.Red) 65 | BoldGreen := style(Weight.Bold, Color.Green) 66 | BoldYellow := style(Weight.Bold, Color.Yellow) 67 | BoldBlue := style(Weight.Bold, Color.Blue) 68 | BoldMagenta := style(Weight.Bold, Color.Magenta) 69 | BoldCyan := style(Weight.Bold, Color.Cyan) 70 | BoldWhite := style(Weight.Bold, Color.White) 71 | BoldGray := style(Weight.Bold, Color.Gray) 72 | 73 | Dim := style(Weight.Dim, Color.Reset) 74 | DimBlack := style(Weight.Dim, Color.Black) 75 | DimRed := style(Weight.Dim, Color.Red) 76 | DimGreen := style(Weight.Dim, Color.Green) 77 | DimYellow := style(Weight.Dim, Color.Yellow) 78 | DimBlue := style(Weight.Dim, Color.Blue) 79 | DimMagenta := style(Weight.Dim, Color.Magenta) 80 | DimCyan := style(Weight.Dim, Color.Cyan) 81 | DimWhite := style(Weight.Dim, Color.White) 82 | DimGray := style(Weight.Dim, Color.Gray) 83 | 84 | -------------------------------------------------------------------------------- /vendor/cli.ink: -------------------------------------------------------------------------------- 1 | ` command-line interface abstractions 2 | for [cmd] [verb] [options] form` 3 | 4 | std := load('../vendor/std') 5 | str := load('../vendor/str') 6 | 7 | each := std.each 8 | slice := std.slice 9 | hasPrefix? := str.hasPrefix? 10 | 11 | maybeOpt := part => true :: { 12 | hasPrefix?(part, '--') -> slice(part, 2, len(part)) 13 | hasPrefix?(part, '-') -> slice(part, 1, len(part)) 14 | _ -> () 15 | } 16 | 17 | ` 18 | Supports: 19 | -opt val 20 | --opt val 21 | -opt=val 22 | --opt val 23 | all other values are considered args 24 | ` 25 | parsed := () => ( 26 | as := args() 27 | 28 | verb := as.2 29 | rest := slice(as, 3, len(as)) 30 | 31 | opts := {} 32 | args := [] 33 | 34 | s := { 35 | lastOpt: () 36 | onlyArgs: false 37 | } 38 | each(rest, part => [maybeOpt(part), s.lastOpt] :: { 39 | [(), ()] -> ( 40 | ` not opt, no prev opt ` 41 | args.len(args) := part 42 | ) 43 | [(), _] -> ( 44 | ` not opt, prev opt exists ` 45 | opts.(s.lastOpt) := part 46 | s.lastOpt := () 47 | ) 48 | [_, ()] -> ( 49 | ` is opt, no prev opt ` 50 | s.lastOpt := maybeOpt(part) 51 | ) 52 | _ -> ( 53 | ` is opt, prev opt exists ` 54 | opts.(s.lastOpt) := true 55 | s.lastOpt := maybeOpt(part) 56 | ) 57 | }) 58 | 59 | s.lastOpt :: { 60 | () -> () 61 | _ -> opts.(s.lastOpt) := true 62 | } 63 | 64 | { 65 | verb: verb 66 | opts: opts 67 | args: args 68 | } 69 | ) 70 | -------------------------------------------------------------------------------- /vendor/quicksort.ink: -------------------------------------------------------------------------------- 1 | ` minimal quicksort implementation 2 | using hoare partition ` 3 | 4 | std := load('../vendor/std') 5 | 6 | map := std.map 7 | clone := std.clone 8 | 9 | sortBy := (v, pred) => ( 10 | vPred := map(v, pred) 11 | partition := (v, lo, hi) => ( 12 | pivot := vPred.(lo) 13 | lsub := i => (vPred.(i) < pivot) :: { 14 | true -> lsub(i + 1) 15 | false -> i 16 | } 17 | rsub := j => (vPred.(j) > pivot) :: { 18 | true -> rsub(j - 1) 19 | false -> j 20 | } 21 | (sub := (i, j) => ( 22 | i := lsub(i) 23 | j := rsub(j) 24 | (i < j) :: { 25 | false -> j 26 | true -> ( 27 | ` inlined swap! ` 28 | tmp := v.(i) 29 | tmpPred := vPred.(i) 30 | v.(i) := v.(j) 31 | v.(j) := tmp 32 | vPred.(i) := vPred.(j) 33 | vPred.(j) := tmpPred 34 | 35 | sub(i + 1, j - 1) 36 | ) 37 | } 38 | ))(lo, hi) 39 | ) 40 | (quicksort := (v, lo, hi) => len(v) :: { 41 | 0 -> v 42 | _ -> (lo < hi) :: { 43 | false -> v 44 | true -> ( 45 | p := partition(v, lo, hi) 46 | quicksort(v, lo, p) 47 | quicksort(v, p + 1, hi) 48 | ) 49 | } 50 | })(v, 0, len(v) - 1) 51 | ) 52 | 53 | sort! := v => sortBy(v, x => x) 54 | 55 | sort := v => sort!(clone(v)) 56 | -------------------------------------------------------------------------------- /vendor/std.ink: -------------------------------------------------------------------------------- 1 | ` the ink standard library ` 2 | 3 | log := val => out(string(val) + ' 4 | ') 5 | 6 | scan := cb => ( 7 | acc := [''] 8 | in(evt => evt.type :: { 9 | 'end' -> cb(acc.0) 10 | 'data' -> ( 11 | acc.0 := acc.0 + slice(evt.data, 0, len(evt.data) - 1) 12 | false 13 | ) 14 | }) 15 | ) 16 | 17 | ` hexadecimal conversion utility functions ` 18 | hToN := {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15} 19 | nToH := '0123456789abcdef' 20 | 21 | ` take number, return hex string ` 22 | hex := n => (sub := (p, acc) => p < 16 :: { 23 | true -> nToH.(p) + acc 24 | false -> sub(floor(p / 16), nToH.(p % 16) + acc) 25 | })(floor(n), '') 26 | 27 | ` take hex string, return number ` 28 | xeh := s => ( 29 | ` i is the num of places from the left, 0-indexed ` 30 | max := len(s) 31 | (sub := (i, acc) => i :: { 32 | max -> acc 33 | _ -> sub(i + 1, acc * 16 + hToN.(s.(i))) 34 | })(0, 0) 35 | ) 36 | 37 | ` find minimum in list ` 38 | min := numbers => reduce(numbers, (acc, n) => n < acc :: { 39 | true -> n 40 | false -> acc 41 | }, numbers.0) 42 | 43 | ` find maximum in list ` 44 | max := numbers => reduce(numbers, (acc, n) => n > acc :: { 45 | true -> n 46 | false -> acc 47 | }, numbers.0) 48 | 49 | ` like Python's range(), but no optional arguments ` 50 | range := (start, end, step) => ( 51 | span := end - start 52 | sub := (i, v, acc) => (v - start) / span < 1 :: { 53 | true -> ( 54 | acc.(i) := v 55 | sub(i + 1, v + step, acc) 56 | ) 57 | false -> acc 58 | } 59 | 60 | ` preempt potential infinite loops ` 61 | (end - start) / step > 0 :: { 62 | true -> sub(0, start, []) 63 | false -> [] 64 | } 65 | ) 66 | 67 | ` clamp start and end numbers to ranges, such that 68 | start < end. Utility used in slice ` 69 | clamp := (start, end, min, max) => ( 70 | start := (start < min :: { 71 | true -> min 72 | false -> start 73 | }) 74 | end := (end < min :: { 75 | true -> min 76 | false -> end 77 | }) 78 | end := (end > max :: { 79 | true -> max 80 | false -> end 81 | }) 82 | start := (start > end :: { 83 | true -> end 84 | false -> start 85 | }) 86 | 87 | { 88 | start: start 89 | end: end 90 | } 91 | ) 92 | 93 | ` get a substring of a given string, or sublist of a given list ` 94 | slice := (s, start, end) => ( 95 | ` bounds checks ` 96 | x := clamp(start, end, 0, len(s)) 97 | start := x.start 98 | max := x.end - start 99 | 100 | (sub := (i, acc) => i :: { 101 | max -> acc 102 | _ -> sub(i + 1, acc.(i) := s.(start + i)) 103 | })(0, type(s) :: { 104 | 'string' -> '' 105 | 'composite' -> [] 106 | }) 107 | ) 108 | 109 | ` join one list to the end of another, return the original first list ` 110 | append := (base, child) => ( 111 | baseLength := len(base) 112 | childLength := len(child) 113 | (sub := i => i :: { 114 | childLength -> base 115 | _ -> ( 116 | base.(baseLength + i) := child.(i) 117 | sub(i + 1) 118 | ) 119 | })(0) 120 | ) 121 | 122 | ` join one list to the end of another, return the third list ` 123 | join := (base, child) => append(clone(base), child) 124 | 125 | ` clone a composite value ` 126 | clone := x => type(x) :: { 127 | 'string' -> '' + x 128 | 'composite' -> reduce(keys(x), (acc, k) => acc.(k) := x.(k), {}) 129 | _ -> x 130 | } 131 | 132 | ` tail recursive numeric list -> string converter ` 133 | stringList := list => '[' + cat(map(list, string), ', ') + ']' 134 | 135 | ` tail recursive reversing a list ` 136 | reverse := list => (sub := (acc, i) => i < 0 :: { 137 | true -> acc 138 | _ -> sub(acc.len(acc) := list.(i), i - 1) 139 | })([], len(list) - 1) 140 | 141 | ` tail recursive map ` 142 | map := (list, f) => reduce(list, (l, item, i) => l.(i) := f(item, i), {}) 143 | 144 | ` tail recursive filter ` 145 | filter := (list, f) => reduce(list, (l, item, i) => f(item, i) :: { 146 | true -> l.len(l) := item 147 | _ -> l 148 | }, []) 149 | 150 | ` tail recursive reduce ` 151 | reduce := (list, f, acc) => ( 152 | max := len(list) 153 | (sub := (i, acc) => i :: { 154 | max -> acc 155 | _ -> sub(i + 1, f(acc, list.(i), i)) 156 | })(0, acc) 157 | ) 158 | 159 | ` tail recursive reduce from list end ` 160 | reduceBack := (list, f, acc) => (sub := (i, acc) => i :: { 161 | ~1 -> acc 162 | _ -> sub(i - 1, f(acc, list.(i), i)) 163 | })(len(list) - 1, acc) 164 | 165 | ` flatten by depth 1 ` 166 | flatten := list => reduce(list, append, []) 167 | 168 | ` true iff some items in list are true ` 169 | some := list => reduce(list, (acc, x) => acc | x, false) 170 | 171 | ` true iff every item in list is true ` 172 | every := list => reduce(list, (acc, x) => acc & x, true) 173 | 174 | ` concatenate (join) a list of strings into a string ` 175 | cat := (list, joiner) => max := len(list) :: { 176 | 0 -> '' 177 | _ -> (sub := (i, acc) => i :: { 178 | max -> acc 179 | _ -> sub(i + 1, acc.len(acc) := joiner + list.(i)) 180 | })(1, clone(list.0)) 181 | } 182 | 183 | ` for-each loop over a list ` 184 | each := (list, f) => ( 185 | max := len(list) 186 | (sub := i => i :: { 187 | max -> () 188 | _ -> ( 189 | f(list.(i), i) 190 | sub(i + 1) 191 | ) 192 | })(0) 193 | ) 194 | 195 | ` encode string buffer into a number list ` 196 | encode := str => ( 197 | max := len(str) 198 | (sub := (i, acc) => i :: { 199 | max -> acc 200 | _ -> sub(i + 1, acc.(i) := point(str.(i))) 201 | })(0, []) 202 | ) 203 | 204 | ` decode number list into an ascii string ` 205 | decode := data => reduce(data, (acc, cp) => acc.len(acc) := char(cp), '') 206 | 207 | ` utility for reading an entire file ` 208 | readFile := (path, cb) => ( 209 | BufSize := 4096 ` bytes ` 210 | (sub := (offset, acc) => read(path, offset, BufSize, evt => evt.type :: { 211 | 'error' -> cb(()) 212 | 'data' -> ( 213 | dataLen := len(evt.data) 214 | dataLen = BufSize :: { 215 | true -> sub(offset + dataLen, acc.len(acc) := evt.data) 216 | false -> cb(acc.len(acc) := evt.data) 217 | } 218 | ) 219 | }))(0, '') 220 | ) 221 | 222 | ` utility for writing an entire file 223 | it's not buffered, because it's simpler, but may cause jank later 224 | we'll address that if/when it becomes a performance issue ` 225 | writeFile := (path, data, cb) => delete(path, evt => evt.type :: { 226 | ` write() by itself will not truncate files that are too long, 227 | so we delete the file and re-write. Not efficient, but writeFile 228 | is not meant for large files ` 229 | 'end' -> write(path, 0, data, evt => evt.type :: { 230 | 'error' -> cb(()) 231 | 'end' -> cb(true) 232 | }) 233 | _ -> cb(()) 234 | }) 235 | 236 | ` template formatting with {{ key }} constructs ` 237 | format := (raw, values) => ( 238 | ` parser state ` 239 | state := { 240 | ` current position in raw ` 241 | idx: 0 242 | ` parser internal state: 243 | 0 -> normal 244 | 1 -> seen one { 245 | 2 -> seen two { 246 | 3 -> seen a valid } ` 247 | which: 0 248 | ` buffer for currently reading key ` 249 | key: '' 250 | ` result build-up buffer ` 251 | buf: '' 252 | } 253 | 254 | ` helper function for appending to state.buf ` 255 | append := c => state.buf := state.buf + c 256 | 257 | ` read next token, update state ` 258 | readNext := () => ( 259 | c := raw.(state.idx) 260 | 261 | state.which :: { 262 | 0 -> c :: { 263 | '{' -> state.which := 1 264 | _ -> append(c) 265 | } 266 | 1 -> c :: { 267 | '{' -> state.which := 2 268 | ` if it turns out that earlier brace was not 269 | a part of a format expansion, just backtrack ` 270 | _ -> ( 271 | append('{' + c) 272 | state.which := 0 273 | ) 274 | } 275 | 2 -> c :: { 276 | '}' -> ( 277 | ` insert key value ` 278 | state.buf := state.buf + string(values.(state.key)) 279 | state.key := '' 280 | state.which := 3 281 | ) 282 | ` ignore spaces in keys -- not allowed ` 283 | ' ' -> () 284 | _ -> state.key := state.key + c 285 | } 286 | 3 -> c :: { 287 | '}' -> state.which := 0 288 | ` ignore invalid inputs -- treat them as nonexistent ` 289 | _ -> () 290 | } 291 | } 292 | 293 | state.idx := state.idx + 1 294 | ) 295 | 296 | ` main recursive sub-loop ` 297 | max := len(raw) 298 | (sub := () => state.idx < max :: { 299 | true -> ( 300 | readNext() 301 | sub() 302 | ) 303 | false -> state.buf 304 | })() 305 | ) 306 | -------------------------------------------------------------------------------- /vendor/str.ink: -------------------------------------------------------------------------------- 1 | ` standard string library ` 2 | 3 | std := load('std') 4 | 5 | map := std.map 6 | slice := std.slice 7 | reduce := std.reduce 8 | reduceBack := std.reduceBack 9 | 10 | ` checking if a given character is of a type ` 11 | checkRange := (lo, hi) => c => ( 12 | p := point(c) 13 | lo < p & p < hi 14 | ) 15 | upper? := checkRange(point('A') - 1, point('Z') + 1) 16 | lower? := checkRange(point('a') - 1, point('z') + 1) 17 | digit? := checkRange(point('0') - 1, point('9') + 1) 18 | letter? := c => upper?(c) | lower?(c) 19 | 20 | ` is the char a whitespace? ` 21 | ws? := c => point(c) :: { 22 | ` space ` 23 | 32 -> true 24 | ` newline ` 25 | 10 -> true 26 | ` hard tab ` 27 | 9 -> true 28 | ` carriage return ` 29 | 13 -> true 30 | _ -> false 31 | } 32 | 33 | ` hasPrefix? checks if a string begins with the given prefix substring ` 34 | hasPrefix? := (s, prefix) => reduce(prefix, (acc, c, i) => acc & (s.(i) = c), true) 35 | 36 | ` hasSuffix? checks if a string ends with the given suffix substring ` 37 | hasSuffix? := (s, suffix) => ( 38 | diff := len(s) - len(suffix) 39 | reduce(suffix, (acc, c, i) => acc & (s.(i + diff) = c), true) 40 | ) 41 | 42 | ` mostly used for internal bookkeeping, matchesAt? reports if a string contains 43 | the given substring at the given index idx. ` 44 | matchesAt? := (s, substring, idx) => ( 45 | max := len(substring) 46 | (sub := i => i :: { 47 | max -> true 48 | _ -> s.(idx + i) :: { 49 | (substring.(i)) -> sub(i + 1) 50 | _ -> false 51 | } 52 | })(0) 53 | ) 54 | 55 | ` index is indexOf() for ink strings ` 56 | index := (s, substring) => ( 57 | max := len(s) - 1 58 | (sub := i => matchesAt?(s, substring, i) :: { 59 | true -> i 60 | false -> i < max :: { 61 | true -> sub(i + 1) 62 | false -> ~1 63 | } 64 | })(0) 65 | ) 66 | 67 | ` contains? checks if a string contains the given substring ` 68 | contains? := (s, substring) => index(s, substring) > ~1 69 | 70 | ` transforms given string to lowercase ` 71 | lower := s => reduce(s, (acc, c, i) => upper?(c) :: { 72 | true -> acc.(i) := char(point(c) + 32) 73 | false -> acc.(i) := c 74 | }, '') 75 | 76 | ` transforms given string to uppercase` 77 | upper := s => reduce(s, (acc, c, i) => lower?(c) :: { 78 | true -> acc.(i) := char(point(c) - 32) 79 | false -> acc.(i) := c 80 | }, '') 81 | 82 | ` primitive "title-case" transformation, uppercases first letter 83 | and lowercases the rest. ` 84 | title := s => ( 85 | lowered := lower(s) 86 | lowered.0 := upper(lowered.0) 87 | ) 88 | 89 | replaceNonEmpty := (s, old, new) => ( 90 | lold := len(old) 91 | lnew := len(new) 92 | (sub := (acc, i) => matchesAt?(acc, old, i) :: { 93 | true -> sub( 94 | slice(acc, 0, i) + new + slice(acc, i + lold, len(acc)) 95 | i + lnew 96 | ) 97 | false -> i < len(acc) :: { 98 | true -> sub(acc, i + 1) 99 | false -> acc 100 | } 101 | })(s, 0) 102 | ) 103 | 104 | ` replace all occurrences of old substring with new substring in a string ` 105 | replace := (s, old, new) => old :: { 106 | '' -> s 107 | _ -> replaceNonEmpty(s, old, new) 108 | } 109 | 110 | splitNonEmpty := (s, delim) => ( 111 | coll := [] 112 | ldelim := len(delim) 113 | (sub := (acc, i, last) => matchesAt?(acc, delim, i) :: { 114 | true -> ( 115 | coll.len(coll) := slice(acc, last, i) 116 | sub(acc, i + ldelim, i + ldelim) 117 | ) 118 | false -> i < len(acc) :: { 119 | true -> sub(acc, i + 1, last) 120 | false -> coll.len(coll) := slice(acc, last, len(acc)) 121 | } 122 | })(s, 0, 0) 123 | ) 124 | 125 | ` split given string into a list of substrings, splitting by the delimiter ` 126 | split := (s, delim) => delim :: { 127 | '' -> map(s, c => c) 128 | _ -> splitNonEmpty(s, delim) 129 | } 130 | 131 | trimPrefixNonEmpty := (s, prefix) => ( 132 | max := len(s) 133 | lpref := len(prefix) 134 | idx := (sub := i => i < max :: { 135 | true -> matchesAt?(s, prefix, i) :: { 136 | true -> sub(i + lpref) 137 | false -> i 138 | } 139 | false -> i 140 | })(0) 141 | slice(s, idx, len(s)) 142 | ) 143 | 144 | ` trim string from start until it does not begin with prefix. 145 | trimPrefix is more efficient than repeated application of 146 | hasPrefix? because it minimizes copying. ` 147 | trimPrefix := (s, prefix) => prefix :: { 148 | '' -> s 149 | _ -> trimPrefixNonEmpty(s, prefix) 150 | } 151 | 152 | trimSuffixNonEmpty := (s, suffix) => ( 153 | lsuf := len(suffix) 154 | idx := (sub := i => i > ~1 :: { 155 | true -> matchesAt?(s, suffix, i - lsuf) :: { 156 | true -> sub(i - lsuf) 157 | false -> i 158 | } 159 | false -> i 160 | })(len(s)) 161 | slice(s, 0, idx) 162 | ) 163 | 164 | ` trim string from end until it does not end with suffix. 165 | trimSuffix is more efficient than repeated application of 166 | hasSuffix? because it minimizes copying. ` 167 | trimSuffix := (s, suffix) => suffix :: { 168 | '' -> s 169 | _ -> trimSuffixNonEmpty(s, suffix) 170 | } 171 | 172 | ` trim string from both start and end with substring ss ` 173 | trim := (s, ss) => trimPrefix(trimSuffix(s, ss), ss) 174 | --------------------------------------------------------------------------------