├── .gitattributes ├── .github └── workflows │ └── ink.yml ├── .gitignore ├── .ink-version ├── LICENSE ├── Makefile ├── README.md ├── examples.ink ├── examples ├── control-flow.ink ├── execing-processes.ink ├── files.ink ├── functions.ink ├── hello-world.ink ├── http.ink ├── io.ink ├── lists.ink ├── loops.ink ├── maps.ink ├── random.ink ├── sorting.ink └── values.ink ├── netlify.toml ├── src ├── build.ink ├── highlight.ink ├── ink-darwin-1.9 ├── ink-linux-1.9 └── test.ink ├── static ├── 404.html ├── favicon.ico ├── preview.png └── site.css ├── templates ├── example.html └── index.html └── vendor ├── iota.ink ├── quicksort.ink ├── std.ink ├── str.ink └── tokenize.ink /.gitattributes: -------------------------------------------------------------------------------- 1 | public/* linguist-generated=true 2 | vendor/* linguist-vendored=true 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/ink.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | # Runs a single command using the runners shell 29 | - name: Run examples 30 | run: make test-linux 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | .vscode 4 | tmp/ 5 | public/ -------------------------------------------------------------------------------- /.ink-version: -------------------------------------------------------------------------------- 1 | v0.1.9 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew Healey 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 | build-linux: 2 | cd ./src && chmod +x ./ink-linux-1.9 && ./ink-linux-1.9 ./build.ink 3 | 4 | build-mac: 5 | cd ./src && chmod +x ./ink-darwin-1.9 && ./ink-darwin-1.9 ./build.ink 6 | 7 | test-linux: 8 | cd ./src && chmod +x ./ink-linux-1.9 && ./ink-linux-1.9 ./build.ink && ./ink-linux-1.9 test.ink 9 | 10 | test-mac: 11 | cd ./src && chmod +x ./ink-darwin-1.9 && ./ink-darwin-1.9 ./build.ink && ./ink-darwin-1.9 test.ink 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/healeycodes/inkbyexample/actions/workflows/ink.yml/badge.svg)](https://github.com/healeycodes/inkbyexample/actions/workflows/ink.yml) 2 | 3 | # Ink by Example 4 | 5 | Content and build toolchain for [Ink by Example](https://inkbyexample.com), a site that teaches [Ink](https://dotink.co/) via annotated example programs. 6 | 7 | All tooling is written in Ink and the programs are evaluated as part of the build process. 8 | 9 | Syntax highlighting powered by [September](https://github.com/thesephist/september). 10 | 11 |
12 | 13 | [![Image of https://inkbyexample.com](https://github.com/healeycodes/inkbyexample/blob/main/static/preview.png)](https://inkbyexample.com/) 14 | 15 |
16 | 17 | ## Add an Annotated Program 18 | 19 | - Add the title to `examples.ink` 20 | - Create a matching Ink program in `/examples` that exports `intro`, `rows`, and `end`. 21 | - A title of `Foo Bar` needs a program named `foo-bar.ink` 22 | 23 | ``` 24 | intro := 'The introduction precedes the code sections. It should not be left empty.' 25 | 26 | rows := [ 27 | { 28 | docs: 'A that function adds two numbers together. This text will appear on the left of the code.' 29 | code: 'add := (x, y) => x + y' 30 | }, 31 | { 32 | docs: 'A function multiplies two numbers together.' 33 | code: 'multiply := (x, y) => x * y' 34 | } 35 | ] 36 | 37 | end := 'The end section is left of the terminal output. It can be left empty.' 38 | ``` 39 | 40 |
41 | 42 | ## Contributing 43 | 44 | Raise an issue before starting a PR. **I'm happy to help out!** You can also DM me on Twitter [@healeycodes](https://twitter.com/healeycodes) 45 | 46 |
47 | 48 | ## Build 49 | 50 | `make build-linux` or `make build-mac` 51 | 52 | The website is built to `/public`. 53 | 54 |
55 | 56 | ## Test 57 | 58 | `make test-linux` or `make test-mac` 59 | 60 | Check stdout for runtime errors. 61 | 62 |
63 | 64 | ## Deploy 65 | 66 | This repository uses Netlify for CI/CD and there's a `netlify.toml` with the configuration required. 67 | 68 | Otherwise, serve the `/public` directory. 69 | 70 |
71 | 72 | ## Thanks to 73 | 74 | [@thesephist](https://github.com/thesephist) for creating the Ink programming language, writing the `SPEC.md` and amazing documentation at https://dotink.co/ which served as a jumping off point. 75 | 76 | Mark McGranaghan for creating the original [Go by Example](https://github.com/mmcgrana/gobyexample) which provided styling and structure for this project. 77 | 78 |
79 | 80 | ## License 81 | 82 | Go by Example is copyright Mark McGranaghan and licensed under a 83 | [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/). 84 | 85 | Everything else is licensed under MIT. 86 | -------------------------------------------------------------------------------- /examples.ink: -------------------------------------------------------------------------------- 1 | all := [ 2 | 'Hello World', 3 | 'Values', 4 | 'IO', 5 | 'Loops', 6 | 'Control Flow', 7 | 'Lists', 8 | 'Maps', 9 | 'Functions', 10 | 'Files', 11 | 'HTTP', 12 | 'Random', 13 | 'Sorting', 14 | 'Execing Processes' 15 | ] 16 | -------------------------------------------------------------------------------- /examples/control-flow.ink: -------------------------------------------------------------------------------- 1 | intro := 'Instead of if/else branching, match expressions are used for control flow.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | filter := std.filter 8 | log := std.log 9 | 10 | 11 | ' 12 | }, 13 | { 14 | docs: 'Like the switch expression from other programming languages, the case clauses are checked from top to bottom. The checking stops when there\'s a match.' 15 | code: 'error := true 16 | error :: { 17 | false -> log(\'No problem!\') 18 | true -> log(\'There is a problem..\') 19 | } 20 | 21 | ' 22 | }, 23 | { 24 | docs: 'The default, or else, branch is signified with the underscore character. This will match anything.' 25 | code: 'device := \'windows\' 26 | device :: { 27 | \'linux\' -> log(\'Linux!\') 28 | \'macOS\' -> log(\'Mac!\') 29 | _ -> log(\'Neither Linux or Mac.\') 30 | } 31 | 32 | ' 33 | }, 34 | { 35 | docs: 'A match target can be an expression.' 36 | code: 'greeting := \'Hello!\' 37 | greeting :: { 38 | \'Hello\' -> log(\'They greeted us.\') 39 | \'Hello\' + \'!\' -> log(\'They greeted us loudly.\') 40 | } 41 | 42 | ' 43 | }, 44 | { 45 | docs: 'Composite values are deep compared so the underscore can be used to catch complex objects that, while different, have a similar characteristic.' 46 | code: 'first := { 47 | code: 2, 48 | user: \'alice@google\' 49 | } 50 | second := { 51 | code: 2, 52 | user: \'claire@amazon\' 53 | } 54 | 55 | checkEvent := event => event :: { 56 | {code: 2, user: _} -> log(\'All good!\') 57 | _ -> log(\'Bad event..\') 58 | } 59 | checkEvent(first) 60 | checkEvent(second) 61 | 62 | ' 63 | }, 64 | { 65 | docs: 'std.filter can be passed a match expression. Here we use it to filter out odd numbers.' 66 | code: 'numbers := [1, 2, 3, 4, 5, 6] 67 | onlyEven := num => num % 2 :: { 68 | 0 -> true 69 | _ -> false 70 | } 71 | even := filter(numbers, onlyEven) 72 | log(even) 73 | 74 | ' 75 | } 76 | ] 77 | 78 | end := '' 79 | -------------------------------------------------------------------------------- /examples/execing-processes.ink: -------------------------------------------------------------------------------- 1 | intro := 'Ink can spawn child processes. The stdout data is passed to a callback when the process ends.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | each := std.each 8 | log := std.log 9 | 10 | 11 | ' 12 | }, 13 | { 14 | docs: 'Let\'s take a look at the files in the directory relative to the executing file (in this instance, this website\'s build script).' 15 | code: 'path := \'ls\' `` path to executable 16 | arguments := [] 17 | stdin := \'\' 18 | stdoutFn := out => log(out) 19 | exec(path, arguments, stdin, stdoutFn) 20 | 21 | ' 22 | }, 23 | { 24 | docs: 'args gives us the command-line arguments for the current context. This can be useful if you\'re chaining Ink programs.' 25 | code: ' 26 | log(\'Arguments:\') 27 | each(args(), arg => log(\' \' + arg)) 28 | ' 29 | } 30 | ] 31 | 32 | end := '' 33 | -------------------------------------------------------------------------------- /examples/files.ink: -------------------------------------------------------------------------------- 1 | intro := 'You will usually interact with files through two standard library APIs — std.readFile and std.writeFile. The runtimes and builtins documentation lists the operating system interfaces.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | writeFile := std.writeFile 8 | readFile := std.readFile 9 | slice := std.slice 10 | log := std.log 11 | 12 | 13 | ' 14 | }, 15 | { 16 | docs: 'Let\'s read in the README for this project.' 17 | code: ' 18 | readFile(\'../README.md\', data => ( 19 | log(\'Read:\') 20 | log( 21 | ` data is the entire file content ` 22 | slice(data, 0, 16)) 23 | )) 24 | 25 | ' 26 | }, 27 | { 28 | docs: 'If the read fails, null is passed to the callback. The read can fail if you try to access a file that doesn\'t exist. So we should handle that.' 29 | code: ' 30 | readFile(\'../doesnt-exist.md\', data => data :: { 31 | () -> log(\'The read failed.\') 32 | _ -> log(\'The read succeeded.\') 33 | }) 34 | 35 | ' 36 | }, 37 | { 38 | docs: 'Here we write content to a file. If the file exists, it\'s truncated — otherwise the file is created. The path is relative to the executing file.' 39 | code: ' 40 | path := \'../tmp/_writeExample.txt\' 41 | data := \'Some data. \' 42 | writeFile(path, data, cb => cb :: { 43 | ` either true or null ` 44 | true -> log(\'The write succeeded.\') 45 | _ -> log(\'The write failed.\') 46 | }) 47 | 48 | ' 49 | }, 50 | { 51 | docs: 'make can be used to recursively create directories.' 52 | code: ' 53 | make(\'../tmp/_makeExample/\', cb => cb.type :: { 54 | \'error\' -> ( 55 | log(\'Make:\') 56 | log(\' \' + string(cb.message)) 57 | ) 58 | _ -> log(\'Make succeeded.\') 59 | }) 60 | 61 | ' 62 | }, 63 | { 64 | docs: 'We can get directory information with dir.' 65 | code: ' 66 | dir(\'../src/\', cb => cb.type :: { 67 | \'error\' -> log(\'Dir failed.\') 68 | ` cb.data is a list of files and directories` 69 | _ -> ( 70 | log(\'Dir:\') 71 | log(\' \' + string(cb.data.0)) 72 | ) 73 | }) 74 | 75 | ' 76 | }, 77 | { 78 | docs: 'Similarly, stat returns metadata about an object in the file tree.' 79 | code: ' 80 | 81 | stat(\'../README.md\', cb => cb.type :: { 82 | \'error\' -> log(\'Stat failed.\') 83 | _ -> ( 84 | log(\'Stat:\') 85 | log(\' \' + string(cb)) 86 | ) 87 | }) 88 | 89 | ' 90 | } 91 | ] 92 | 93 | end := 'We\'re not using ordered callbacks so the order of the logs will be dependant on the system calls when this website was last built and the code was evaluated. This is an example of Ink\'s event loop!' 94 | -------------------------------------------------------------------------------- /examples/functions.ink: -------------------------------------------------------------------------------- 1 | intro := 'Functions are the building blocks of Ink and enable us to write terse functional code.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | log := std.log 8 | 9 | 10 | ' 11 | }, 12 | { 13 | docs: 'Functions are created with the arrow => symbol.' 14 | code: ' 15 | shout := (word) => word + \'!\' 16 | log(shout(\'Hello\')) 17 | 18 | multiply := (x, y) => x * y 19 | log(multiply(2, 8)) 20 | 21 | ' 22 | }, 23 | { 24 | docs: 'When a group of expressions follow the arrow symbol, the result of the final expression is the return value.' 25 | code: '` single argument parentheses are optional` 26 | sayAndMultiply := num => ( 27 | log(\'About to multiply \' + string(num) + \':\') 28 | 29 | ` the return value ` 30 | num * num 31 | ) 32 | log(sayAndMultiply(8)) 33 | 34 | ' 35 | }, 36 | { 37 | docs: 'Function invocation () takes precedence over property-access . — instead of obj.func(argument), we use (obj.func)(argument).' 38 | code: 'math := { 39 | swapSign: n => 0 - n 40 | } 41 | weight := (math.swapSign)(100) 42 | log(weight) 43 | 44 | ' 45 | }, 46 | { 47 | docs: 'Here\'s an example of tail recursion.' 48 | code: 'factorial := n => n :: { 49 | 0 -> 1 50 | _ -> n * factorial(n-1) 51 | } 52 | log(factorial(5)) 53 | 54 | ' 55 | }, 56 | { 57 | docs: 'It\'s natural to use closures — functions that have access to the parent scope after the parent function has closed.' 58 | code: 'makeMultiplier := x => ( 59 | y := y => x * y 60 | ) 61 | multiplySeven := makeMultiplier(7) 62 | multiplyTwenty := makeMultiplier(20) 63 | log(multiplySeven(2)) 64 | log(multiplyTwenty(4)) 65 | 66 | ' 67 | }, 68 | { 69 | docs: 'Functions that immediately evalute and return another function is called currying in functional programming land.' 70 | code: 'greeting := a => b => a + \' \' + b 71 | message := greeting(\'Hello there,\')(\'General Kenobi\') 72 | log(message) 73 | ' 74 | } 75 | ] 76 | 77 | end := '' 78 | -------------------------------------------------------------------------------- /examples/hello-world.ink: -------------------------------------------------------------------------------- 1 | intro := 'Our first program will print the classic "hello world" message. Here\'s the full source code.' 2 | 3 | rows := [ 4 | { 5 | docs: 'Ink programs are imported by calling load and passing the relative path without .ink. Top level values are brought in as a map.' 6 | code: 'std := load(\'../vendor/std\') 7 | log := std.log 8 | 9 | 10 | log(\'hello world\') 11 | ' 12 | }, 13 | { 14 | docs: 'We can add comments using the grave accent (U+0060).' 15 | code: '` a single line comment ` 16 | 17 | ` 18 | a multi-line comment 19 | ` 20 | 21 | ' 22 | }, 23 | { 24 | docs: 'A double accent comments the rest of the line.' 25 | code: '`` log(\'This should not run!\') 26 | ' 27 | } 28 | ] 29 | 30 | end := 'To run the program, put the code in hello-world.ink and run it with the interpreter.' 31 | -------------------------------------------------------------------------------- /examples/http.ink: -------------------------------------------------------------------------------- 1 | intro := 'Ink can both send and receive HTTP requests.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | log := std.log 8 | 9 | 10 | ' 11 | }, 12 | { 13 | docs: 'We can initiate an HTTP request with req. It returns a function that aborts the request. Let\'s also time this request.' 14 | code: ' 15 | t := time() 16 | data := { 17 | method: \'GET\' 18 | url: \'https://example.org\' 19 | headers: {} 20 | body: \'\' 21 | } 22 | 23 | close := req(data, evt => evt.type :: { 24 | ` this matches during a request fail 25 | or if the request is aborted ` 26 | \'error\' -> log(\'Error: \' + evt.message) 27 | _ -> ( 28 | log(\'Req: \') 29 | elasped := string(time() - t) 30 | log(\' elapsed time: \' + elasped) 31 | log(\' \' + string(keys(evt.data))) 32 | ) 33 | }) 34 | log(\'This code runs while we request.\') 35 | `` close() would abort the request 36 | 37 | 38 | ' 39 | }, 40 | { 41 | docs: 'A HTTP web server can be started with listen. Similar to req, it returns a function that stops the server.' 42 | code: ' 43 | resp := { 44 | status: 200 45 | headers: {\'Content-Type\': \'text/plain\'} 46 | body: \'Hello from Ink land!\' 47 | } 48 | close := listen(\'0.0.0.0:80\', evt => evt.type :: { 49 | \'error\' -> log(\'Error: \' + evt.message) 50 | \'req\' -> (evt.end)(resp) 51 | }) 52 | log(\'This code runs while we listen.\') 53 | 54 | ` stop the server ` 55 | close() 56 | ' 57 | } 58 | ] 59 | 60 | end := 'listen errors in this project\'s deploy pipeline due to permissions but will work locally.' 61 | -------------------------------------------------------------------------------- /examples/io.ink: -------------------------------------------------------------------------------- 1 | intro := 'Ink has access to standard input (stdin) and standard output (stdout). You can pipe input to an Ink program in the form echo "Hello, World!" | ink program.ink.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | scan := std.scan 8 | log := std.log 9 | f := std.format 10 | 11 | 12 | ' 13 | }, 14 | { 15 | docs: 'To send data to standard output we can use the builtin out, which accepts a string, or the helper std.log which accepts strings and non-strings and adds a linebreak.' 16 | code: 'out(\'Hello, with a manual linebreak. 17 | \') 18 | log(7) `` print a number 19 | log([1, 2]) `` a composite 20 | log(() => ()) `` a function 21 | log(()) `` or null 22 | ' 23 | }, 24 | { 25 | docs: 'For reading in data from standard input, there is the builtin in as well as the helper std.scan which reads in a line.' 26 | code: 'prompt := (msg, cb) => ( 27 | log(msg) 28 | scan(cb) 29 | ) 30 | ` usage: 31 | prompt(\'Are you sure?\', msg => ( 32 | log(msg) 33 | ))` 34 | 35 | ' 36 | }, 37 | { 38 | docs: 'std.format allows us to build template strings and pass in a map of values.' 39 | code: 'log(f(\'A message without any passed in values.\', {})) 40 | log(f(\'The time is: {{ time }}.\', {time: time()})) 41 | ' 42 | } 43 | ] 44 | 45 | end := '' 46 | -------------------------------------------------------------------------------- /examples/lists.ink: -------------------------------------------------------------------------------- 1 | intro := 'Lists and maps share the same data structure — the composite value. At runtime, lists are hashmaps with string keys representing indices.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | log := std.log 8 | reverse := std.reverse 9 | slice := std.slice 10 | range := std.range 11 | each := std.each 12 | join := std.join 13 | 14 | 15 | ' 16 | }, 17 | { 18 | docs: 'Lists can be declared with array syntax.' 19 | code: 'names := [\'Alice\', \'Andrew\'] 20 | log(names) 21 | 22 | ' 23 | }, 24 | { 25 | docs: 'To access or mutate an element we use the dot syntax.' 26 | code: 'log(names.0) `` first element 27 | log(names.(len(names) - 1)) `` last element 28 | 29 | names.0 := \'Madeline\' 30 | log(names) 31 | 32 | ' 33 | }, 34 | { 35 | docs: 'Like strings, we can append to the end of a list by mutating the next index value.' 36 | code: 'names.len(names) := \'Francisca\' 37 | log(names) 38 | 39 | ' 40 | }, 41 | { 42 | docs: '' 43 | code: ' 44 | indexer := (name, index) => ( 45 | log(index) 46 | log(name) 47 | ) 48 | each(names, indexer) 49 | 50 | ' 51 | }, 52 | { 53 | docs: 'std.range works similar to Python\'s range. It takes start, end, and step values and returns a list.' 54 | code: ' 55 | numbers := range(0, 6, 1) 56 | log(numbers) 57 | 58 | ' 59 | }, 60 | { 61 | docs: 'We can get a sublist with std.slice, and join two lists to create a third with std.join.' 62 | code: ' 63 | half := slice(numbers, 0, 3) 64 | log(half) 65 | 66 | first := [98, 99] 67 | second := [100, 101] 68 | third := join(first, second) 69 | log(third) 70 | 71 | ' 72 | }, 73 | { 74 | docs: 'To reverse, use std.reverse. For other list utilities, refer to the standard library.' 75 | code: ' 76 | log(reverse([\'one\', \'two\', \'three\', ])) 77 | ' 78 | } 79 | ] 80 | 81 | end := '' 82 | -------------------------------------------------------------------------------- /examples/loops.ink: -------------------------------------------------------------------------------- 1 | intro := 'Instead of a for loop construct, we use tail recursion. 2 | The standard library contains utility functions — like std.each, std.map, and std.reduce — that can be used in place of a for loop.' 3 | 4 | rows := [ 5 | { 6 | docs: '' 7 | code: 'std := load(\'../vendor/std\') 8 | log := std.log 9 | reduce := std.reduce 10 | each := std.each 11 | map := std.map 12 | 13 | 14 | ' 15 | }, 16 | { 17 | docs: '' 18 | code: 'numbers := [0, 1, 2, 3, 4, 5, 6] 19 | 20 | ' 21 | }, 22 | { 23 | docs: 'We can loop over a list with std.each, which accepts a list and a function as arguments.' 24 | code: 'logger := num => log(num) 25 | each(numbers, logger) 26 | 27 | ' 28 | }, 29 | { 30 | docs: 'Let\'s create a new list with the squares of these numbers with std.map.' 31 | code: 'squares := map(numbers, num => num * num) 32 | log(squares) 33 | 34 | ' 35 | }, 36 | { 37 | docs: 'We can get the sum of these squares by using std.reduce.' 38 | code: 'sum := reduce(squares, (acc, num) => acc + num, 0) 39 | log(sum)' 40 | } 41 | ] 42 | 43 | end := '' 44 | -------------------------------------------------------------------------------- /examples/maps.ink: -------------------------------------------------------------------------------- 1 | intro := 'Like lists, maps use the composite value — Ink\'s single built-in data structure. At runtime, maps are hashmaps with string keys.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | clone := std.clone 8 | each := std.each 9 | log := std.log 10 | 11 | 12 | ' 13 | }, 14 | { 15 | docs: '' 16 | code: 'observation := { 17 | weather: \'Sunny\' 18 | \'observedAt\': { 19 | time: time() 20 | } 21 | } 22 | log(observation) 23 | 24 | ' 25 | }, 26 | { 27 | docs: 'Like lists, we use the dot syntax to access and mutate.' 28 | code: 'observation.(\'weather\') := \'Raining\' 29 | 30 | ' 31 | }, 32 | { 33 | docs: 'If we know the key before runtime, we can use a shortcut and just write it out.' 34 | code: 'observation.weather := \'Raining\' 35 | log(observation) 36 | 37 | ' 38 | }, 39 | { 40 | docs: 'std.clone can be used to create a copy of a map.' 41 | code: 'cloned := clone(observation) 42 | 43 | ' 44 | }, 45 | { 46 | docs: 'We can iterate through the keys by calling keys.' 47 | code: 'each(keys(observation), (key) => ( 48 | log(observation.(key)) 49 | )) 50 | ' 51 | } 52 | ] 53 | 54 | end := '' 55 | -------------------------------------------------------------------------------- /examples/random.ink: -------------------------------------------------------------------------------- 1 | intro := 'We can access randomness through two builtins — rand and urand. On Linux they typically map to /dev/random and /dev/urandom.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | range := std.range 8 | each := std.each 9 | log := std.log 10 | 11 | 12 | ' 13 | }, 14 | { 15 | docs: 'For a random number in the range [0, 1) — including zero but excluding one — we use rand.' 16 | code: 'choices := [rand(), rand(), rand()] 17 | log(choices) 18 | 19 | ' 20 | }, 21 | { 22 | docs: 'We can use this to shuffle a list using the Fisher-Yates algorithm.' 23 | code: ' 24 | ` an ordered list ` 25 | numbers := range(0, 10, 1) 26 | 27 | shuffle := list => ( 28 | each(list, (val, i) => ( 29 | pick := floor(rand() * (len(list) - i - 1)) 30 | current := len(list) - 1 - i 31 | tmp := list.(current) 32 | list.(current) := list.(pick) 33 | list.(pick) := tmp 34 | )) 35 | list 36 | ) 37 | log(shuffle(numbers)) 38 | 39 | ' 40 | }, 41 | { 42 | docs: 'For cryptographically secure randomness, use urand. This uses the most secure random number generation interface your system provides to generate a string of random data.' 43 | code: ' 44 | length := 10 45 | log(urand(10)) 46 | 47 | ' 48 | } 49 | ] 50 | 51 | end := '' 52 | -------------------------------------------------------------------------------- /examples/sorting.ink: -------------------------------------------------------------------------------- 1 | intro := 'The quicksort library provides a minimal Quicksort implementation that uses Hoare\'s partitioning.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | log := std.log 8 | 9 | qs := load(\'../vendor/quicksort\') 10 | sortBy := qs.sortBy 11 | sort! := qs.sort! 12 | sort := qs.sort 13 | 14 | 15 | ' 16 | }, 17 | { 18 | docs: 'We can sort a list in ascending order with quicksort.sort.' 19 | code: 'numbers := [921, 14, 0, 0.002] 20 | log(sort(numbers)) 21 | 22 | ' 23 | }, 24 | { 25 | docs: 'Function definitions that end in ! mutate their arguments — like quicksort.sort!.' 26 | code: 'temperatures := [33, 1, 5, 22] 27 | sort!(temperatures) 28 | 29 | ` the existing list has changed ` 30 | log(temperatures) 31 | 32 | ' 33 | }, 34 | { 35 | docs: 'quicksort.sortBy allows us to pass a predicate — a function that each item should be measured with.' 36 | code: 'menu := { 37 | apples: 1.50 38 | oranges: 2.00 39 | coconut: 3.50 40 | grapes: 1.75 41 | } 42 | items := [\'apples\', \'oranges\', \'coconut\', \'grapes\'] 43 | 44 | ` sort items by their menu price ` 45 | log(sortBy(items, item => menu.(item))) 46 | 47 | ' 48 | } 49 | ] 50 | 51 | end := '' 52 | -------------------------------------------------------------------------------- /examples/values.ink: -------------------------------------------------------------------------------- 1 | intro := 'Ink has number, string, boolean, null, and composite values. Here are a few basic examples.' 2 | 3 | rows := [ 4 | { 5 | docs: '' 6 | code: 'std := load(\'../vendor/std\') 7 | log := std.log 8 | 9 | 10 | ' 11 | }, 12 | { 13 | docs: 'Numbers, which support arithmetic operations, and more advanced functions using built-in native functions.' 14 | code: 'length := (0.5 + 2 * 3 / 4 - 5) + ~6 % 7 15 | log(length) 16 | 17 | ` a negative literal ` 18 | temperature := ~5 19 | log(temperature) 20 | 21 | year := pow(10, 3) * 2 + 20 22 | log(year) 23 | 24 | ' 25 | }, 26 | { 27 | docs: 'Strings, which can be added together with +, and appended to by mutating the next index value.' 28 | code: 'name := \'ada\' + \' \' + \'lovelace\' 29 | log(name) 30 | name.0 := \'A\' 31 | name.len(name) := \'!\' 32 | log(name) 33 | 34 | ' 35 | }, 36 | { 37 | docs: 'Boolean.' 38 | code: 'authed := true 39 | log(authed) 40 | log(~authed) 41 | log(true | false) 42 | log(true & false) 43 | 44 | ' 45 | }, 46 | { 47 | docs: 'Null.' 48 | code: 'exists := () 49 | log(exists) 50 | 51 | ' 52 | }, 53 | { 54 | docs: 'Composite values, which are used for lists and maps.' 55 | code: 'primes := [2, 3, 5, 7] 56 | log(primes) 57 | 58 | menu := { 59 | apples: 1.00 60 | oranges: 1.50 61 | } 62 | log(menu) 63 | 64 | ' 65 | }, 66 | { 67 | docs: 'We can get the value of a type with type.' 68 | code: 'log(type(0)) 69 | 70 | ' 71 | } 72 | ] 73 | 74 | end := '' -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public/" 3 | command = "make build-linux" 4 | -------------------------------------------------------------------------------- /src/build.ink: -------------------------------------------------------------------------------- 1 | std := load('../vendor/std') 2 | writeFile := std.writeFile 3 | readFile := std.readFile 4 | append := std.append 5 | reduce := std.reduce 6 | each := std.each 7 | f := std.format 8 | map := std.map 9 | cat := std.cat 10 | log := std.log 11 | 12 | str := load('../vendor/str') 13 | replace := str.replace 14 | lower := str.lower 15 | split := str.split 16 | 17 | highlight := load('highlight').main 18 | 19 | ` in order to execute the code examples, they need to be written to disk ` 20 | evaluate := (fileName, source, cb) => ( 21 | writeFile('../tmp/' + fileName + '.ink', source, result => result :: { 22 | () -> log('error creating source code file for: ' + example) 23 | _ -> ( 24 | exec(args().0, ['../tmp/' + fileName + '.ink'], '', output => output :: { 25 | () -> log('error executing program for: ' + example) 26 | _ -> cb(output) 27 | }) 28 | ) 29 | }) 30 | ) 31 | 32 | renderIndexPage := (template, examples, inkVersion) => ( 33 | ` render the index page with the list of examples and links ` 34 | exampleList := reduce(examples, (acc, item) => ( 35 | acc := acc + f('
  • 36 | {{ name }} 37 |
  • ', { 38 | name: item 39 | path: replace(lower(item), ' ', '-') + '.html' 40 | }) 41 | ), '') 42 | 43 | page := f(template, { 44 | firstExample: replace(lower(examples.0), ' ', '-') + '.html' 45 | exampleList: exampleList 46 | inkVersion: inkVersion 47 | }) 48 | writeFile('../public/index.html', page, result => result :: { 49 | () -> log('error writing index.html') 50 | }) 51 | ) 52 | 53 | renderExamplePages := (template, examples) => ( 54 | each(examples, (example, i) => ( 55 | fileName := replace(lower(example), ' ', '-') 56 | data := load('../examples/' + fileName) 57 | source := reduce(data.rows, (acc, item) => acc := acc + ' 58 | ' + item.code, '') 59 | 60 | evaluate(fileName, source, result => ( 61 | ` pages link to the next example if one exists ` 62 | len(examples) :: { 63 | (i + 1) -> next := '' 64 | _ -> next := f('

    65 | Next example: {{ example }}. 66 |

    ', 67 | { 68 | path: replace(lower(examples.(i + 1)), ' ', '-') + '.html' 69 | example: examples.(i + 1) 70 | }) 71 | } 72 | 73 | page := f(template, { 74 | name: example 75 | rows: reduce(data.rows, (acc, item) => ( 76 | class := (len(item.code) :: { 77 | 0 -> 'empty' 78 | _ -> '' 79 | }) 80 | acc := acc + f(' 81 |

    {{ docs }}

    82 | 83 |
    {{ code }}
    84 | 85 | ', 86 | { 87 | docs: item.docs 88 | code: highlight(item.code) 89 | class: class 90 | }) 91 | ), f(' 92 |

    {{ intro }}

    93 | 94 | ', 95 | { 96 | intro: data.intro 97 | }) 98 | ) 99 | program: f(' 100 | 101 | 102 | 106 | 107 |

    {{ end }}

    103 |
    $ ink {{ fileName }}
    104 | {{ result }}
    105 |
    ', 108 | { 109 | fileName: fileName + '.ink' 110 | result: result.data 111 | end: data.end 112 | }) 113 | next: next 114 | }) 115 | 116 | writeFile('../public/' + fileName + '.html', page, result => result :: { 117 | () -> log('error creating example: ' + example) 118 | }) 119 | )) 120 | )) 121 | ) 122 | 123 | ` move over static content ` 124 | moveStatic := () => dir('../static', item => ( 125 | each(item.data, file => ( 126 | readFile('../static/' + file.name, data => ( 127 | writeFile('../public/' + file.name, data, result => result :: { 128 | () -> log('error creating static file: ' + file.name) 129 | }) 130 | )) 131 | )) 132 | )) 133 | 134 | ` setup folders ` 135 | createPublic := cb => make('../public', result => result :: { 136 | () -> log('error creating public folder') 137 | _ -> ( 138 | moveStatic() 139 | cb() 140 | ) 141 | }) 142 | createTmp := () => make('../tmp', result => result :: { 143 | () -> log('error creating tmp folder') 144 | }) 145 | 146 | examples := load('../examples').all 147 | createPublic(() => ( 148 | 149 | ` grab the ink version ` 150 | readFile('../.ink-version', inkVersion => inkVersion :: { 151 | () -> log('error reading .ink-version') 152 | _ -> ( 153 | 154 | ` render index page ` 155 | readFile('../templates/index.html', template => template :: { 156 | () -> log('error reading index template') 157 | _ -> renderIndexPage(template, examples, inkVersion) 158 | }) 159 | ) 160 | }) 161 | 162 | ` render example pages ` 163 | readFile('../templates/example.html', template => template :: { 164 | () -> log('error reading example template') 165 | _ -> renderExamplePages(template, examples) 166 | }) 167 | )) 168 | createTmp() 169 | -------------------------------------------------------------------------------- /src/highlight.ink: -------------------------------------------------------------------------------- 1 | ` adapted from https://github.com/thesephist/september to enabled HTML syntax highlighting ` 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 | Tokenize := load('../vendor/tokenize') 12 | Tok := Tokenize.Tok 13 | tokenize := Tokenize.tokenizeWithComments 14 | 15 | ` associating token types with their highlight colors ` 16 | colorFn := tok => tok.type :: { 17 | (Tok.Separator) -> s => s 18 | 19 | (Tok.Comment) -> s => '' + s + '' 20 | 21 | (Tok.Ident) -> s => s 22 | (Tok.EmptyIdent) -> s => s 23 | 24 | (Tok.NumberLiteral) -> s => '' + s + '' 25 | (Tok.StringLiteral) -> s => '' + s + '' 26 | (Tok.TrueLiteral) -> s => '' + s + '' 27 | (Tok.FalseLiteral) -> s => '' + s + '' 28 | 29 | (Tok.AccessorOp) -> s => '' + s + '' 30 | (Tok.EqOp) -> s => '' + s + '' 31 | 32 | (Tok.FunctionArrow) -> s => '' + s + '' 33 | 34 | ` operators are all Name.Function color ` 35 | (Tok.KeyValueSeparator) -> s => '' + s + '' 36 | (Tok.DefineOp) -> s => '' + s + '' 37 | (Tok.MatchColon) -> s => '' + s + '' 38 | (Tok.CaseArrow) -> s => '' + s + '' 39 | (Tok.SubOp) -> s => '' + s + '' 40 | (Tok.NegOp) -> s => '' + s + '' 41 | (Tok.AddOp) -> s => '' + s + '' 42 | (Tok.MulOp) -> s => '' + s + '' 43 | (Tok.DivOp) -> s => '' + s + '' 44 | (Tok.ModOp) -> s => '' + s + '' 45 | (Tok.GtOp) -> s => '' + s + '' 46 | (Tok.LtOp) -> s => '' + s + '' 47 | (Tok.AndOp) -> s => '' + s + '' 48 | (Tok.OrOp) -> s => '' + s + '' 49 | (Tok.XorOp) -> s => '' + s + '' 50 | 51 | (Tok.LParen) -> s => '' + s + '' 52 | (Tok.RParen) -> s => '' + s + '' 53 | (Tok.LBracket) -> s => '' + s + '' 54 | (Tok.RBracket) -> s => '' + s + '' 55 | (Tok.LBrace) -> s => '' + s + '' 56 | (Tok.RBrace) -> s => '' + s + '' 57 | 58 | _ -> () `` should error, unreachable 59 | } 60 | 61 | main := prog => ( 62 | tokens := tokenize(prog) 63 | spans := map(tokens, (tok, i) => { 64 | colorFn: [tok.type, tokens.(i + 1)] :: { 65 | ` direct function calls are marked Name.Function color 66 | on a best-effort basis ` 67 | [ 68 | Tok.Ident 69 | {type: Tok.LParen, val: _, line: _, col: _, i: _} 70 | ] -> s => '' + s + '' 71 | _ -> colorFn(tok) 72 | } 73 | start: tok.i 74 | end: tokens.(i + 1) :: { 75 | () -> len(prog) 76 | _ -> tokens.(i + 1).i 77 | } 78 | }) 79 | pcs := map( 80 | spans 81 | span => (span.colorFn)(slice(prog, span.start, span.end)) 82 | ) 83 | cat(pcs, '') 84 | ) 85 | -------------------------------------------------------------------------------- /src/ink-darwin-1.9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/healeycodes/inkbyexample/661fa148161b488b8dba2b63ea8b669a6c2580f2/src/ink-darwin-1.9 -------------------------------------------------------------------------------- /src/ink-linux-1.9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/healeycodes/inkbyexample/661fa148161b488b8dba2b63ea8b669a6c2580f2/src/ink-linux-1.9 -------------------------------------------------------------------------------- /src/test.ink: -------------------------------------------------------------------------------- 1 | std := load('../vendor/std') 2 | each := std.each 3 | log := std.log 4 | 5 | str := load('../vendor/str') 6 | split := str.split 7 | 8 | 9 | dir('../examples/', files => each(files.data, file => file.dir :: { 10 | false -> ( 11 | example := split(file.name, '.ink').0 12 | load('../tmp/' + example) 13 | ) 14 | _ -> 'skip directory' 15 | })) 16 | -------------------------------------------------------------------------------- /static/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ink by Example: Not Found 6 | 7 | 8 | 9 |
    10 |

    11 | Ink by Example 12 |

    13 |

    Sorry, we couldn't find that! Check out the home page?

    14 |

    Ping @healeycodes and I'll fix this ASAP!

    15 | 18 |
    19 | 20 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/healeycodes/inkbyexample/661fa148161b488b8dba2b63ea8b669a6c2580f2/static/favicon.ico -------------------------------------------------------------------------------- /static/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/healeycodes/inkbyexample/661fa148161b488b8dba2b63ea8b669a6c2580f2/static/preview.png -------------------------------------------------------------------------------- /static/site.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap'); 3 | 4 | /* CSS reset: http://meyerweb.com/eric/tools/css/reset/ */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, hgroup, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; 24 | } 25 | article, aside, details, figcaption, figure, 26 | footer, header, hgroup, menu, nav, section { 27 | display: block; 28 | } 29 | body { 30 | line-height: 1; 31 | } 32 | ol, ul { 33 | list-style: none; 34 | } 35 | blockquote, q { 36 | quotes: none; 37 | } 38 | blockquote:before, blockquote:after, 39 | q:before, q:after { 40 | content: ''; 41 | content: none; 42 | } 43 | table { 44 | border-collapse: collapse; 45 | border-spacing: 0; 46 | } 47 | 48 | /* Layout and typography */ 49 | body { 50 | font-family: 'Inter', sans-serif; 51 | font-size: 16px; 52 | line-height: 20px; 53 | color: #252519; 54 | } 55 | em { 56 | font-style: italic; 57 | } 58 | a, a:visited { 59 | color: #261a3b; 60 | } 61 | h2 { 62 | font-size: 32px; 63 | line-height: 40px; 64 | margin-top: 40px; 65 | } 66 | h2 a { 67 | text-decoration: none; 68 | } 69 | div.example { 70 | width: 900px; 71 | min-width: 900px; 72 | max-width: 900px; 73 | margin-left: auto; 74 | margin-right: auto; 75 | margin-bottom: 120px; 76 | } 77 | div.example table { 78 | margin-top: 15px; 79 | margin-bottom: 20px; 80 | } 81 | p.next { 82 | margin-bottom: 20px; 83 | } 84 | p.footer { 85 | color: grey; 86 | } 87 | p.footer a, p.footer a:visited { 88 | color: grey; 89 | } 90 | div#intro { 91 | max-width: 420px; 92 | margin-left: auto; 93 | margin-right: auto; 94 | margin-bottom: 120px; 95 | padding: 40px; 96 | } 97 | div#intro p { 98 | padding-top: 20px; 99 | } 100 | div#intro ul { 101 | padding-top: 20px; 102 | } 103 | table td { 104 | border: 0; 105 | outline: 0; 106 | } 107 | td.docs { 108 | width: 420px; 109 | max-width: 420px; 110 | min-width: 420px; 111 | min-height: 5px; 112 | vertical-align: top; 113 | text-align: left; 114 | } 115 | td.docs p { 116 | padding-right: 5px; 117 | padding-top: 5px; 118 | padding-bottom: 15px; 119 | } 120 | td.code { 121 | width: 480px; 122 | max-width: 480px; 123 | min-width: 480px; 124 | padding-top: 5px; 125 | padding-right: 5px; 126 | padding-left: 5px; 127 | padding-bottom: 5px; 128 | vertical-align: top; 129 | background: #f0f0f0; 130 | } 131 | td.code.leading { 132 | padding-bottom: 11px; 133 | } 134 | td.code.empty { 135 | background: #ffffff; 136 | } 137 | pre { 138 | white-space: pre-wrap; 139 | } 140 | pre, code { 141 | font-size: 15px; 142 | line-height: 18px; 143 | font-family: 'Roboto Mono', 'Menlo', 'Monaco', 'Consolas', 'Lucida Console', monospace; 144 | } 145 | code { 146 | background: #f0f0f0; 147 | } 148 | img.copy, img.run { 149 | height: 16px; 150 | width: 16px; 151 | float: right 152 | } 153 | img.copy, img.run { 154 | cursor: pointer; 155 | } 156 | img.copy { 157 | margin-right: 4px; 158 | } 159 | 160 | /* Syntax highlighting */ 161 | body .hll { background-color: #ffffcc } 162 | body .err { border: 1px solid #FF0000 } /* Error */ 163 | body .c { color: #408080; font-style: italic } /* Comment */ 164 | body .k { color: #954121 } /* Keyword */ 165 | body .o { color: #666666 } /* Operator */ 166 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 167 | body .cp { color: #BC7A00 } /* Comment.Preproc */ 168 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */ 169 | body .cs { color: #408080; font-style: italic } /* Comment.Special */ 170 | body .gd { color: #A00000 } /* Generic.Deleted */ 171 | body .ge { font-style: italic } /* Generic.Emph */ 172 | body .gr { color: #FF0000 } /* Generic.Error */ 173 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 174 | body .gi { color: #00A000 } /* Generic.Inserted */ 175 | body .go { color: #808080 } /* Generic.Output */ 176 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 177 | body .gs { font-weight: bold } /* Generic.Strong */ 178 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 179 | body .gt { color: #0040D0 } /* Generic.Traceback */ 180 | body .kc { color: #954121 } /* Keyword.Constant */ 181 | body .kd { color: #954121 } /* Keyword.Declaration */ 182 | body .kn { color: #954121 } /* Keyword.Namespace */ 183 | body .kp { color: #954121 } /* Keyword.Pseudo */ 184 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ 185 | body .kt { color: #B00040 } /* Keyword.Type */ 186 | body .m { color: #666666 } /* Literal.Number */ 187 | body .s { color: #219161 } /* Literal.String */ 188 | body .na { color: #7D9029 } /* Name.Attribute */ 189 | body .nb { color: #954121 } /* Name.Builtin */ 190 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 191 | body .no { color: #880000 } /* Name.Constant */ 192 | body .nd { color: #AA22FF } /* Name.Decorator */ 193 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */ 194 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 195 | body .nf { color: #0000FF } /* Name.Function */ 196 | body .nl { color: #A0A000 } /* Name.Label */ 197 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 198 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */ 199 | body .nv { color: #19469D } /* Name.Variable */ 200 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 201 | body .w { color: #bbbbbb } /* Text.Whitespace */ 202 | body .mf { color: #666666 } /* Literal.Number.Float */ 203 | body .mh { color: #666666 } /* Literal.Number.Hex */ 204 | body .mi { color: #666666 } /* Literal.Number.Integer */ 205 | body .mo { color: #666666 } /* Literal.Number.Oct */ 206 | body .sb { color: #219161 } /* Literal.String.Backtick */ 207 | body .sc { color: #219161 } /* Literal.String.Char */ 208 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ 209 | body .s2 { color: #219161 } /* Literal.String.Double */ 210 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 211 | body .sh { color: #219161 } /* Literal.String.Heredoc */ 212 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 213 | body .sx { color: #954121 } /* Literal.String.Other */ 214 | body .sr { color: #BB6688 } /* Literal.String.Regex */ 215 | body .s1 { color: #219161 } /* Literal.String.Single */ 216 | body .ss { color: #19469D } /* Literal.String.Symbol */ 217 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */ 218 | body .vc { color: #19469D } /* Name.Variable.Class */ 219 | body .vg { color: #19469D } /* Name.Variable.Global */ 220 | body .vi { color: #19469D } /* Name.Variable.Instance */ 221 | body .il { color: #666666 } /* Literal.Number.Integer.Long */ 222 | -------------------------------------------------------------------------------- /templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 10 | 11 | 12 | Ink by Example — {{ name }} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
    31 |

    32 | Ink by Example: {{ name }}

    33 | 34 | 35 | {{ rows }} 36 | {{ program }} 37 |
    38 | 39 | {{ next }} 40 | 41 | 44 |
    45 | 46 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 10 | 11 | 12 | 13 | Ink by Example 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
    32 |

    33 | Ink by Example 34 |

    35 |

    36 | Ink is a minimal programming language inspired by modern JavaScript and Go, with a functional style. 37 |

    38 | 39 |

    40 | Ink by Example is a hands-on introduction 41 | to Ink using annotated example programs. Check out 42 | the first example or 43 | browse the full list below. 44 |

    45 | 46 | 49 | 50 |

    51 | This website's toolchain is powered by Ink {{ inkVersion }} 52 |

    53 | 56 |
    57 | 58 | -------------------------------------------------------------------------------- /vendor/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 | -------------------------------------------------------------------------------- /vendor/quicksort.ink: -------------------------------------------------------------------------------- 1 | ` minimal quicksort implementation 2 | using hoare partition 3 | https://github.com/thesephist/ink/blob/master/samples/quicksort.ink ` 4 | 5 | std := load('../vendor/std') 6 | 7 | map := std.map 8 | clone := std.clone 9 | 10 | sortBy := (v, pred) => ( 11 | vPred := map(v, pred) 12 | partition := (v, lo, hi) => ( 13 | pivot := vPred.(lo) 14 | lsub := i => vPred.(i) < pivot :: { 15 | true -> lsub(i + 1) 16 | false -> i 17 | } 18 | rsub := j => vPred.(j) > pivot :: { 19 | true -> rsub(j - 1) 20 | false -> j 21 | } 22 | (sub := (i, j) => ( 23 | i := lsub(i) 24 | j := rsub(j) 25 | i < j :: { 26 | false -> j 27 | true -> ( 28 | ` inlined swap! ` 29 | tmp := v.(i) 30 | tmpPred := vPred.(i) 31 | v.(i) := v.(j) 32 | v.(j) := tmp 33 | vPred.(i) := vPred.(j) 34 | vPred.(j) := tmpPred 35 | 36 | sub(i + 1, j - 1) 37 | ) 38 | } 39 | ))(lo, hi) 40 | ) 41 | (quicksort := (v, lo, hi) => len(v) :: { 42 | 0 -> v 43 | _ -> lo < hi :: { 44 | false -> v 45 | true -> ( 46 | p := partition(v, lo, hi) 47 | quicksort(v, lo, p) 48 | quicksort(v, p + 1, hi) 49 | ) 50 | } 51 | })(v, 0, len(v) - 1) 52 | ) 53 | 54 | sort! := v => sortBy(v, x => x) 55 | 56 | sort := v => sort!(clone(v)) 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vendor/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('../vendor/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 | --------------------------------------------------------------------------------