├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── build.oak ├── cat.oak ├── fmt.oak ├── help.oak ├── pack.oak └── version.oak ├── command.go ├── docs └── spec.md ├── env.go ├── eval.go ├── eval_test.go ├── go.mod ├── go.sum ├── lib.go ├── lib ├── cli.oak ├── crypto.oak ├── datetime.oak ├── debug.oak ├── fmt.oak ├── fs.oak ├── http.oak ├── json.oak ├── math.oak ├── md.oak ├── path.oak ├── random.oak ├── sort.oak ├── std.oak ├── str.oak ├── syntax.oak └── test.oak ├── main.go ├── parse.go ├── samples ├── fib.oak ├── fileserver.oak ├── fizzbuzz.oak ├── fs.oak ├── hello.oak ├── lists.oak ├── math.oak ├── raw-fs-async.oak ├── raw-fs-sync.oak ├── strings.oak └── tailcall.oak ├── test ├── cli.test.oak ├── crypto.test.oak ├── datetime.test.oak ├── debug.test.oak ├── fmt.test.oak ├── generative │ ├── datetime.test.oak │ ├── random.normal.test.oak │ └── randomness.test.oak ├── http.test.oak ├── json.test.oak ├── main.oak ├── math.test.oak ├── md.test.oak ├── path.test.oak ├── random.test.oak ├── runners.oak ├── sort.test.oak ├── std.test.oak ├── str.test.oak └── syntax.test.oak ├── token.go ├── tools └── oak.vim └── www ├── README.md ├── content ├── ackermann.md ├── codecols.md ├── expressive.md ├── fib-perf.md ├── http-perf.md ├── notebook.md ├── oak-perf-jan-2022.md ├── pack.md ├── times.md ├── try.md ├── why.md └── xi.md ├── oaklang.org.service ├── src ├── app.js.oak ├── gen.oak ├── highlight.js.oak └── main.oak ├── static ├── css │ ├── lib.css │ └── main.css ├── highlight.html ├── http-perf-data.txt ├── img │ ├── oak-http-latency-plot.png │ ├── oak-http-throughput-plot.png │ ├── oak-node-hyperfine-bench.jpg │ └── oak-notebook-demo.gif ├── index.html ├── js │ ├── bundle.js │ └── highlight.js ├── lib │ ├── cli.html │ ├── crypto.html │ ├── datetime.html │ ├── debug.html │ ├── fmt.html │ ├── fs.html │ ├── http.html │ ├── index.html │ ├── json.html │ ├── math.html │ ├── md.html │ ├── path.html │ ├── random.html │ ├── sort.html │ ├── std.html │ ├── str.html │ ├── syntax.html │ └── test.html ├── oak │ └── codecols.oak └── posts │ ├── ackermann.html │ ├── codecols.html │ ├── expressive.html │ ├── fib-perf.html │ ├── http-perf.html │ ├── index.html │ ├── notebook.html │ ├── oak-perf-jan-2022.html │ ├── pack.html │ ├── times.html │ ├── try.html │ ├── why.html │ └── xi.html └── tpl ├── highlight-embed.html ├── highlight.html ├── lib.html ├── list.html └── post.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Linus Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RUN = go run -race . 2 | LDFLAGS = -ldflags="-s -w" 3 | INCLUDES = std.test:test/std.test,str.test:test/str.test,math.test:test/math.test,sort.test:test/sort.test,random.test:test/random.test,fmt.test:test/fmt.test,json.test:test/json.test,datetime.test:test/datetime.test,path.test:test/path.test,http.test:test/http.test,debug.test:test/debug.test,cli.test:test/cli.test,md.test:test/md.test,crypto.test:test/crypto.test,syntax.test:test/syntax.test 4 | 5 | all: ci 6 | 7 | # run the interpreter 8 | run: 9 | ${RUN} 10 | 11 | # run the autoformatter (from system Oak) 12 | fmt: 13 | oak fmt --changes --fix 14 | f: fmt 15 | 16 | # run Go tests 17 | tests: 18 | go test -race . 19 | t: tests 20 | 21 | # run Oak tests 22 | test-oak: 23 | ${RUN} test/main.oak 24 | tk: test-oak 25 | 26 | # run oak build tests 27 | test-bundle: 28 | ${RUN} build --entry test/main.oak --output /tmp/oak-test.oak --include ${INCLUDES} 29 | ${RUN} /tmp/oak-test.oak 30 | 31 | # run oak pack tests 32 | test-pack: 33 | ${RUN} pack --entry test/main.oak --output /tmp/oak-pack --include ${INCLUDES} 34 | /tmp/oak-pack 35 | 36 | # run oak build --web tests 37 | test-js: 38 | ${RUN} build --entry test/main.oak --output /tmp/oak-test.js --web --include ${INCLUDES} 39 | node /tmp/oak-test.js 40 | 41 | # build for a specific GOOS target 42 | build-%: 43 | GOOS=$* go build ${LDFLAGS} -o oak-$* . 44 | 45 | # build for all OS targets 46 | build: build-linux build-darwin build-windows build-openbsd 47 | 48 | # build Oak sources for the website 49 | site: 50 | oak build --entry www/src/app.js.oak --output www/static/js/bundle.js --web 51 | oak build --entry www/src/highlight.js.oak --output www/static/js/highlight.js --web 52 | 53 | # build Oak source for the website on file change, using entr 54 | site-w: 55 | ls www/src/app.js.oak | entr -cr make site 56 | 57 | # generate static site pages 58 | site-gen: 59 | oak www/src/gen.oak 60 | 61 | # install as "oak" binary 62 | install: 63 | cp tools/oak.vim ~/.vim/syntax/oak.vim 64 | go build ${LDFLAGS} -o ${GOPATH}/bin/oak 65 | 66 | # ci in travis 67 | ci: tests test-oak test-bundle test-pack 68 | -------------------------------------------------------------------------------- /cmd/cat.oak: -------------------------------------------------------------------------------- 1 | // oak cat -- syntax highlighter 2 | 3 | { 4 | stdin: stdin 5 | map: map 6 | each: each 7 | first: first 8 | slice: slice 9 | append: append 10 | compact: compact 11 | } := import('std') 12 | { 13 | cut: cut 14 | join: join 15 | } := import('str') 16 | fs := import('fs') 17 | fmt := import('fmt') 18 | cli := import('cli') 19 | syntax := import('syntax') 20 | 21 | Cli := cli.parse() 22 | Html? := Cli.opts.html != ? 23 | Stdin? := Cli.opts.stdin != ? 24 | 25 | fn _ansiWrap(s, color) { 26 | colorCode := if color { 27 | :red -> 31 28 | :green -> 32 29 | :yellow -> 33 30 | :blue -> 34 31 | :magenta -> 35 32 | :cyan -> 36 33 | :gray -> 90 34 | // :error shows text against a red background 35 | :error, _ -> 41 36 | } 37 | '\x1b[0;' << string(colorCode) << 'm' << s << '\x1b[0;0m' 38 | } 39 | 40 | fn _ansiColor(s, type, prev, next) if type { 41 | :newline -> s 42 | :comment -> _ansiWrap(s, :gray) 43 | 44 | :comma -> s 45 | :dot -> s 46 | :leftParen, :rightParen 47 | :leftBracket, :rightBracket 48 | :leftBrace, :rightBrace -> s 49 | :assign, :nonlocalAssign -> _ansiWrap(s, :red) 50 | :pipeArrow -> _ansiWrap(s, :blue) // to match fnKeyword 51 | :branchArrow -> _ansiWrap(s, :red) // to match ifKeyword 52 | :pushArrow -> _ansiWrap(s, :red) 53 | :colon -> s 54 | :ellipsis -> _ansiWrap(s, :blue) 55 | :qmark -> _ansiWrap(s, :magenta) 56 | :exclam -> _ansiWrap(s, :red) 57 | 58 | :plus, :minus, :times, :divide, :modulus 59 | :xor, :and, :or 60 | :greater, :less, :eq, :geq, :leq, :neq -> _ansiWrap(s, :red) 61 | 62 | :ifKeyword -> _ansiWrap(s, :red) 63 | :fnKeyword -> _ansiWrap(s, :blue) 64 | :withKeyword -> _ansiWrap(s, :cyan) 65 | 66 | :underscore -> _ansiWrap(s, :magenta) 67 | :identifier -> if { 68 | prev = :fnKeyword 69 | next = :leftParen -> _ansiWrap(s, :green) 70 | _ -> s 71 | } 72 | :trueLiteral, :falseLiteral -> _ansiWrap(s, :magenta) 73 | :stringLiteral -> _ansiWrap(s, :yellow) 74 | :numberLiteral -> _ansiWrap(s, :cyan) 75 | 76 | _ -> _ansiWrap(s, :error) 77 | } 78 | 79 | fn _escapeHTML(s) s |> map(fn(c) if c { 80 | '&' -> '&' 81 | '<' -> '<' 82 | _ -> c 83 | }) 84 | 85 | fn _htmlColor(s, type, prev, next) if s { 86 | '', '\n' -> s 87 | _ -> { 88 | if type { 89 | :identifier -> if { 90 | prev = :fnKeyword 91 | next = :leftParen -> type <- :fnName 92 | } 93 | } 94 | '' << _escapeHTML(s) << '' 95 | } 96 | } 97 | 98 | fn _highlightAndPrintFile(file) { 99 | tokens := [] 100 | // shebang is ignored by the tokenizer, so we treat it specially 101 | if syntax.shebang?(file) -> tokens << { 102 | type: :comment 103 | pos: [0, 0, 0] 104 | val: file |> cut('\n') |> first() 105 | } 106 | tokens |> append(file |> syntax.tokenize()) 107 | spans := tokens |> with map() fn(tok, i) { 108 | type: tok.type 109 | start: tok.pos.0 110 | end: if nextTok := tokens.(i + 1) { 111 | ? -> len(file) 112 | _ -> nextTok.pos.0 113 | } 114 | } 115 | chunks := spans |> with map() fn(span, i) { 116 | { 117 | type: type 118 | start: start 119 | end: end 120 | } := span 121 | prevType := if ? != prev := spans.(i - 1) -> prev.type 122 | nextType := if ? != next := spans.(i + 1) -> next.type 123 | file |> slice(start, end) |> color(type, prevType, nextType) 124 | } 125 | chunks |> join() |> print() 126 | } 127 | 128 | color := if { 129 | Html? -> _htmlColor 130 | _ -> _ansiColor 131 | } 132 | 133 | if Stdin? -> stdin() |> _highlightAndPrintFile() 134 | 135 | Args := [Cli.verb] |> append(Cli.args) |> compact() 136 | Args |> with each() fn(path) if file := fs.readFile(path) { 137 | ? -> fmt.printf('[oak cat] Could not read file {{0}}', path) 138 | _ -> _highlightAndPrintFile(file) 139 | } 140 | 141 | -------------------------------------------------------------------------------- /cmd/fmt.oak: -------------------------------------------------------------------------------- 1 | // oak fmt -- code formatter 2 | 3 | { 4 | slice: slice 5 | each: each 6 | filter: filter 7 | } := import('std') 8 | { 9 | split: split 10 | trim: trim 11 | endsWith?: endsWith? 12 | } := import('str') 13 | { 14 | readFile: readFile 15 | writeFile: writeFile 16 | } := import('fs') 17 | { 18 | printf: printf 19 | } := import('fmt') 20 | cli := import('cli') 21 | syntax := import('syntax') 22 | 23 | Cli := cli.parse() 24 | 25 | Fix? := Cli.opts.fix != ? 26 | Diff? := Cli.opts.diff != ? 27 | 28 | // we don't need a verb, the "verb" will be a file path 29 | Args := if Cli.opts.changes != ? { 30 | // get list of files from git diff 31 | true -> { 32 | evt := exec('git', ['diff', '--name-only'], '') 33 | if evt.type { 34 | :error -> { 35 | printf('[oak fmt] Could not get git diff:\n\t{{ 0 }}', evt.error) 36 | [] 37 | } 38 | _ -> evt.stdout |> trim() |> split('\n') |> with filter() fn(path) path |> endsWith?('.oak') 39 | } 40 | } 41 | _ -> if Cli.verb { 42 | ? -> Cli.args 43 | _ -> Cli.args << Cli.verb 44 | } 45 | } 46 | 47 | Args |> with each() fn(path) with readFile(path) fn(file) if file { 48 | ? -> printf('[oak fmt] Could not read file {{ 0 }}', path) 49 | _ -> if { 50 | Fix? -> with writeFile(path, file |> syntax.print(file)) fn(res) if res { 51 | ? -> printf('[oak fmt] Could not write file {{ 0 }}', path) 52 | _ -> printf('[oak fmt] Fixed {{ 0 }}', path) 53 | } 54 | Diff? -> with exec( 55 | 'diff' 56 | [path, '-'] 57 | file |> syntax.print() 58 | ) fn(evt) if evt.type { 59 | :error -> printf('[oak fmt] Error while diffing {{ 0 }}:\n\t{{ 1 }}' 60 | path, evt.error) 61 | _ -> print(evt.stdout) 62 | } 63 | _ -> file |> syntax.print() |> print() 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /cmd/help.oak: -------------------------------------------------------------------------------- 1 | // oak help 2 | 3 | { 4 | println: println 5 | default: default 6 | } := import('std') 7 | { 8 | format: format 9 | } := import('fmt') 10 | 11 | Main := 'Oak is an expressive, dynamically typed programming language. 12 | 13 | Run an Oak program: 14 | oak [arguments] 15 | Start an Oak repl: 16 | oak 17 | 18 | General usage: 19 | oak [arguments] 20 | where commands are: 21 | version print version information 22 | help print this message 23 | repl start the Oak repl 24 | eval evaluate command line args as a program 25 | pipe use Oak in a shell pipeline 26 | cat print syntax-highlighted Oak source 27 | doc generate or view documentation 28 | fmt autoformat Oak source code 29 | test run tests in *.test.oak files 30 | pack build a static binary executable 31 | build compile to a single file, optionally to JS 32 | Run oak help for more on each command. 33 | ' 34 | 35 | Repl := 'Interactive programming environment for Oak 36 | 37 | The Oak REPL is also accessible by running `oak repl`. The REPL saves history 38 | to {{0}}/.oak_history. 39 | 40 | Special variables 41 | __ last-evaluated result 42 | ' 43 | 44 | Eval := 'Evaluate Oak programs from command line arguments 45 | 46 | Sometimes, it\'s useful to be able to run short Oak programs without opening a 47 | new file or entering an interactive prompt. It may be performing a quick 48 | numeric calculation, testing an API, or processing some one-off file. For these 49 | occasions, `oak eval` is designed to make it easy to run quick one-line 50 | scripts, including those that may operate on standard input. 51 | 52 | Usage 53 | oak eval [program] 54 | 55 | Special variables 56 | stdin string representation of the standard input piped into the Oak 57 | CLI, if it exists; otherwise, this is not defined 58 | 59 | Examples 60 | oak eval 1 + 2 + 3 61 | Perform a simple calculation 62 | oak eval "json.parse(stdin) |> debug.inspect()" < data.json 63 | Visualize JSON data 64 | oak eval "fs.listFiles(\'.\') |> std.filter(:dir) |> std.map(:name)" 65 | List only directories in the working directory 66 | ' 67 | 68 | Pipe := 'Use Oak to process data in pipelines 69 | 70 | Oak is often fit for writing string-processing scripts that may be useful 71 | within a UNIX pipeline. `oak pipe` lets a short Oak program act as a part of a 72 | UNIX pipeline, processing piped input line-by-line and emitting output in a 73 | streaming fashion. Unlike other similar tools, `oak pipe` lets programs harness 74 | the full power of Oak, including concurrent execution, I/O, and the entire Oak 75 | standard library. 76 | 77 | This command runs a short program, given as the command line arguments, for 78 | every line of the input stream. If the program returns a non-null (non-?) 79 | value, the return value of the program will be output; otherwise, the line will 80 | be filtered out. `oak pipe` is capable of processing unbuffered, continuously 81 | streaming input. 82 | 83 | Usage 84 | oak pipe [program] 85 | 86 | Special variables 87 | line string representation of the current line, except any trailing 88 | newline characters 89 | i line number, starting at 1 90 | 91 | Examples 92 | cat main.oak | oak pipe "\'{{0}}\\t{{1}}\' |> fmt.format(i, line)" 93 | Print a file with its lines numbered 94 | oak pipe "if str.startsWith?(line, \'fn \') -> string(i) + \' \' + line" < main.oak 95 | Print top-level fn declarations in a file with its lines numbered 96 | tail -f /var/log/access.log | oak pipe "if str.contains?(line, \'/about\') -> line" 97 | Continuously filter logs to print only those that access "/about" 98 | ' 99 | 100 | Cat := 'Print syntax-highlighted Oak source files 101 | 102 | Oak cat works much like the UNIX utility `cat`, except that `oak cat` 103 | syntax-highlights Oak source code in the process. This is useful when trying to 104 | read Oak programs in the terminal outside of an editor with syntax highlighting 105 | support for Oak, and also supports syntax-highlighting Oak source code to embed 106 | in HTML documents. 107 | 108 | Usage 109 | oak cat [files] [options] 110 | 111 | Options 112 | --html Format output to be an HTML string with token information 113 | embedded as CSS classes, for syntax highlighting on the web. 114 | --stdin Read text to highlight from standard input rather than from 115 | files passed to the command line arguments. 116 | ' 117 | 118 | Doc := 'Generate or read documentation for the Oak language and libraries 119 | 120 | [[ under construction ]] 121 | ' 122 | 123 | Fmt := 'Automatically format Oak source files 124 | 125 | Usage 126 | oak fmt [files] [options] 127 | 128 | Options 129 | --fix Fix any formatting errors in-place, by overwriting any source 130 | files with formatting errors on disk. Without this flag set, 131 | changed versions of the source files will simply be sent to 132 | stdout. 133 | --diff Rather than printing formatted source files in their entirely, 134 | only print a line diff between the original and formatted 135 | files, to show needed changes. 136 | Using this option requires a system `diff` to be installed. 137 | --changes Rather than formatting source files specified in the command 138 | line arguments, check only files with unstaged changes in the 139 | local git repository. 140 | Using this option requires a system `git` to be installed. 141 | ' 142 | 143 | Test := 'Run unit tests in *.test.oak files 144 | 145 | [[ under construction ]] 146 | ' 147 | 148 | Pack := 'Package Oak programs into statically distributable binaries 149 | 150 | Oak pack will compile and bundle an Oak program, then package it alongside the 151 | Oak interpreter itself so that the single executable binary that results can be 152 | distributed and run as a standalone program. 153 | 154 | Usage 155 | oak pack --entry [src] --output [dest] [options] 156 | 157 | Options 158 | --entry Entrypoint for the bundle 159 | --output Path at which to save the final binary on disk, also -o 160 | --include Comma-separated list of modules to include explicitly in the 161 | binary, even if the static analyzer cannot find static imports 162 | to it from the entrypoint. Use this option to ensure modules 163 | loaded dynamically at runtime are bundled. 164 | --interp Path to the Oak interpreter that will be packed into the binary 165 | to execute the included bundle when it is run. The currently 166 | running interpreter is used by default. An alternative --interp 167 | may be used to pack an Oak program for a different platform or 168 | operating system. 169 | ' 170 | 171 | Build := 'Compile and bundle Oak programs to Oak or JavaScript 172 | 173 | Oak build will compile and bundle an Oak program, potentially with many 174 | dependencies, into a single, self-contained source file. This is useful when 175 | deploying or distributing Oak programs. The compiler can also generate 176 | JavaScript code when using the --web option, rather than Oak code, to output a 177 | bundle that can run on the web and Node.js. 178 | 179 | Usage 180 | oak build --entry [src] --output [dest] [options] 181 | 182 | Options 183 | --entry Entrypoint for the bundle 184 | --output Path at which to save the final bundle on disk, also -o 185 | --web Compile the bundle to JavaScript, suitable for running in 186 | JavaScript runtimes like web browsers, Node.js, and Deno 187 | --include Comma-separated list of modules to include explicitly in the 188 | bundle, even if the static analyzer cannot find static imports 189 | to it from the entrypoint. Use this option to ensure modules 190 | loaded dynamically at runtime are bundled. 191 | ' 192 | 193 | // main 194 | if title := args().2 { 195 | ? -> Main 196 | 'repl' -> format(Repl, default(env().HOME, '$HOME')) 197 | 'eval' -> Eval 198 | 'pipe' -> Pipe 199 | 'cat' -> Cat 200 | 'doc' -> Doc 201 | 'fmt' -> Fmt 202 | 'test' -> Test 203 | 'pack' -> Pack 204 | 'build' -> Build 205 | _ -> format('No help message available for "{{ 0 }}"', title) 206 | } |> println() 207 | 208 | -------------------------------------------------------------------------------- /cmd/pack.oak: -------------------------------------------------------------------------------- 1 | // oak pack -- distribute Oak programs as stand-alone binaries 2 | 3 | { 4 | default: default 5 | append: append 6 | } := import('std') 7 | { 8 | padStart: padStart 9 | } := import('str') 10 | { 11 | printf: printf 12 | } := import('fmt') 13 | { 14 | readFile: readFile 15 | writeFile: writeFile 16 | statFile: statFile 17 | } := import('fs') 18 | cli := import('cli') 19 | 20 | // these 8 magic bytes are appended to the end of any Oak executable that 21 | // includes a "bundle" at the end of the file, after the executable (e.g. ELF) 22 | // itself. "oak" is obvious, and 1998-10-15 is a special day. They happen to 23 | // fit neatly into 8 bytes, which is just luck. 24 | MagicBytes := 'oak \x19\x98\x10\x15' 25 | // MaxBundleSizeLen is the number of bytes used to store the length of the Oak 26 | // bundle itself in a packed executable. UINT64_MAX is 20 decimal digits, so 24 27 | // is sufficient. This also has the aesthetically pleasing property that 24 + 8 28 | // (magic bytes) = 32 bytes. 29 | MaxBundleSizeLen := 24 30 | 31 | Cli := cli.parse() 32 | 33 | // much of these options are inherited from `oak build` 34 | Entry := Cli.opts.entry 35 | Output := Cli.opts.output |> default(Cli.opts.o) 36 | Includes := Cli.opts.include 37 | Interp := Cli.opts.interp |> 38 | // NOTE: we can't simply default to Cli.exe because we need an absolute, 39 | // fully resolved path to be able to read from this file later. 40 | default(___runtime_proc().exe) |> 41 | default(Cli.exe) 42 | 43 | if Entry { 44 | ?, '', true -> { 45 | printf('[oak pack] No --entry specified.') 46 | exit(1) 47 | } 48 | } 49 | if Output { 50 | ?, '', true -> { 51 | printf('[oak pack] No --output specified.') 52 | exit(1) 53 | } 54 | } 55 | if Interp { 56 | ?, '', true -> { 57 | printf('[oak pack] Invalid --interp specified.') 58 | exit(1) 59 | } 60 | } 61 | if statFile(Entry) = ? -> { 62 | printf('[oak pack] {{0}} does not exist.', Entry) 63 | exit(1) 64 | } 65 | 66 | with readFile(Interp) fn(packFile) if packFile { 67 | ? -> printf('[oak pack] Could not read oak executable.') 68 | _ -> with exec( 69 | Cli.exe 70 | { 71 | // We use Output as a temporary output file before writing the true 72 | // packed binary out to the same location in the filesystem. 73 | buildArgs := ['build', '--entry', Entry, '--output', Output] 74 | if Includes { 75 | ?, '', true -> buildArgs 76 | _ -> buildArgs |> append(['--include', Includes]) 77 | } 78 | } 79 | '' 80 | ) fn(evt) if { 81 | evt.type = :error -> printf('[oak pack] Could not bundle files.') 82 | evt.status != 0 -> printf('[oak pack] Bundling failed:\n' + evt.stdout) 83 | _ -> with readFile(Output) fn(oakBundleFile) if oakBundleFile { 84 | ? -> printf('[oak pack] Could not read Oak bundle') 85 | _ -> { 86 | oakBundleSizeString := string(len(oakBundleFile)) |> padStart(MaxBundleSizeLen, ' ') 87 | with writeFile( 88 | Output 89 | packFile << oakBundleFile << oakBundleSizeString << MagicBytes 90 | ) fn(res) if res { 91 | ? -> printf('[oak pack] Could not save final pack file.') 92 | _ -> with exec('chmod', ['+x', Output], '') fn(evt) if { 93 | evt.type = :error 94 | evt.status != 0 -> printf('[oak pack] Unable to mark binary as executable.') 95 | _ -> printf('[oak pack] Executable binary saved to {{0}}', Output) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /cmd/version.oak: -------------------------------------------------------------------------------- 1 | // oak version 2 | 3 | { 4 | println: println 5 | } := import('std') 6 | 7 | println('Oak v0.3') 8 | 9 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | _ "embed" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/chzyer/readline" 15 | ) 16 | 17 | const PackFileMagicBytes = "oak \x19\x98\x10\x15" 18 | 19 | //go:embed cmd/version.oak 20 | var cmdversion string 21 | 22 | //go:embed cmd/help.oak 23 | var cmdhelp string 24 | 25 | //go:embed cmd/cat.oak 26 | var cmdcat string 27 | 28 | //go:embed cmd/fmt.oak 29 | var cmdfmt string 30 | 31 | //go:embed cmd/pack.oak 32 | var cmdpack string 33 | 34 | //go:embed cmd/build.oak 35 | var cmdbuild string 36 | 37 | var cliCommands = map[string]string{ 38 | "version": cmdversion, 39 | "help": cmdhelp, 40 | "cat": cmdcat, 41 | "fmt": cmdfmt, 42 | "pack": cmdpack, 43 | "build": cmdbuild, 44 | } 45 | 46 | func isStdinReadable() bool { 47 | stdin, _ := os.Stdin.Stat() 48 | return (stdin.Mode() & os.ModeCharDevice) == 0 49 | } 50 | 51 | func performCommandIfExists(command string) bool { 52 | switch command { 53 | case "repl": 54 | runRepl() 55 | return true 56 | case "eval": 57 | runEval() 58 | return true 59 | case "pipe": 60 | runPipe() 61 | return true 62 | } 63 | 64 | commandProgram, ok := cliCommands[command] 65 | if !ok { 66 | return false 67 | } 68 | 69 | ctx := NewContextWithCwd() 70 | defer ctx.Wait() 71 | ctx.LoadBuiltins() 72 | 73 | if _, err := ctx.Eval(strings.NewReader(commandProgram)); err != nil { 74 | fmt.Println(err) 75 | os.Exit(1) 76 | } 77 | 78 | return true 79 | } 80 | 81 | func runPackFile() bool { 82 | exeFilePath, err := os.Executable() 83 | if err != nil { 84 | // NOTE: os.Executable() isn't perfect, and fails in some less than 85 | // ideal conditions (on certain operating systems, for example). So if 86 | // we can't find an executable path we just bail. 87 | return false 88 | } 89 | 90 | exeFile, err := os.Open(exeFilePath) 91 | if err != nil { 92 | return false 93 | } 94 | defer exeFile.Close() 95 | 96 | info, err := exeFile.Stat() 97 | if err != nil { 98 | return false 99 | } 100 | 101 | exeFileSize := info.Size() 102 | // 24 bundle size bytes, 8 magic bytes. See: cmd/pack.oak 103 | readFrom := exeFileSize - 24 - 8 104 | endOfFileBytes := make([]byte, 24+8, 24+8) 105 | _, err = exeFile.ReadAt(endOfFileBytes, readFrom) 106 | if err != nil { 107 | return false 108 | } 109 | if !bytes.Equal(endOfFileBytes[24:], []byte(PackFileMagicBytes)) { 110 | return false 111 | } 112 | 113 | bundleSizeString := bytes.TrimLeft(endOfFileBytes[0:24], " ") 114 | bundleSize, err := strconv.ParseInt(string(bundleSizeString), 10, 64) 115 | if err != nil { 116 | // invalid bundle size 117 | return false 118 | } 119 | if bundleSize > readFrom { 120 | // bundle size too large 121 | return false 122 | } 123 | 124 | readBundleFrom := readFrom - bundleSize 125 | bundleBytes := make([]byte, bundleSize, bundleSize) 126 | _, err = exeFile.ReadAt(bundleBytes, readBundleFrom) 127 | if err != nil { 128 | return false 129 | } 130 | 131 | ctx := NewContextWithCwd() 132 | defer ctx.Wait() 133 | ctx.LoadBuiltins() 134 | 135 | if _, err := ctx.Eval(bytes.NewReader(bundleBytes)); err != nil { 136 | fmt.Println(err) 137 | os.Exit(1) 138 | } 139 | 140 | return true 141 | } 142 | 143 | func runFile(filePath string) { 144 | file, err := os.Open(filePath) 145 | if err != nil { 146 | fmt.Printf("Could not open %s: %s\n", filePath, err) 147 | os.Exit(1) 148 | } 149 | defer file.Close() 150 | 151 | ctx := NewContext(path.Dir(filePath)) 152 | defer ctx.Wait() 153 | ctx.LoadBuiltins() 154 | 155 | if _, err = ctx.Eval(file); err != nil { 156 | fmt.Println(err) 157 | os.Exit(1) 158 | } 159 | } 160 | 161 | func runStdin() { 162 | ctx := NewContextWithCwd() 163 | defer ctx.Wait() 164 | ctx.LoadBuiltins() 165 | 166 | if _, err := ctx.Eval(os.Stdin); err != nil { 167 | fmt.Println(err) 168 | os.Exit(1) 169 | } 170 | } 171 | 172 | func runRepl() { 173 | var historyFilePath string 174 | homeDir, err := os.UserHomeDir() 175 | if err == nil { 176 | historyFilePath = path.Join(homeDir, ".oak_history") 177 | } 178 | 179 | rl, err := readline.NewEx(&readline.Config{ 180 | Prompt: "> ", 181 | HistoryFile: historyFilePath, 182 | }) 183 | if err != nil { 184 | fmt.Println("Could not open the repl") 185 | os.Exit(1) 186 | } 187 | defer rl.Close() 188 | 189 | ctx := NewContextWithCwd() 190 | ctx.LoadBuiltins() 191 | ctx.mustLoadAllLibs() 192 | 193 | for { 194 | line, err := rl.Readline() 195 | if err != nil { // io.EOF 196 | break 197 | } 198 | 199 | // if no input, don't print the null output 200 | if strings.TrimSpace(line) == "" { 201 | continue 202 | } 203 | 204 | val, err := ctx.Eval(strings.NewReader(line)) 205 | if err != nil { 206 | fmt.Println(err) 207 | continue 208 | } 209 | fmt.Println(val) 210 | 211 | // keep last evaluated result as __ in REPL 212 | ctx.scope.put("__", val) 213 | } 214 | } 215 | 216 | func runEval() { 217 | ctx := NewContextWithCwd() 218 | defer ctx.Wait() 219 | ctx.LoadBuiltins() 220 | ctx.mustLoadAllLibs() 221 | 222 | if isStdinReadable() { 223 | allInput, _ := io.ReadAll(os.Stdin) 224 | allInputValue := StringValue(allInput) 225 | ctx.scope.put("stdin", &allInputValue) 226 | } 227 | 228 | prog := strings.Join(os.Args[2:], " ") 229 | if val, err := ctx.Eval(strings.NewReader(prog)); err == nil { 230 | if stringVal, ok := val.(*StringValue); ok { 231 | fmt.Println(string(*stringVal)) 232 | } else { 233 | fmt.Println(val) 234 | } 235 | } else { 236 | fmt.Println(err) 237 | os.Exit(1) 238 | } 239 | } 240 | 241 | func runPipe() { 242 | if !isStdinReadable() { 243 | return 244 | } 245 | 246 | ctx := NewContextWithCwd() 247 | defer ctx.Wait() 248 | ctx.LoadBuiltins() 249 | ctx.mustLoadAllLibs() 250 | 251 | rootScope := ctx.scope 252 | stdin := bufio.NewReader(os.Stdin) 253 | prog := strings.Join(os.Args[2:], " ") 254 | for i := 0; ; i++ { 255 | line, err := stdin.ReadBytes('\n') 256 | if err == io.EOF { 257 | break 258 | } else if err != nil { 259 | fmt.Println("Could not read piped input") 260 | os.Exit(1) 261 | } 262 | 263 | line = bytes.TrimSuffix(line, []byte{'\n'}) 264 | lineValue := StringValue(line) 265 | // each line gets its own top-level subscope to avoid collisions 266 | ctx.subScope(&rootScope) 267 | ctx.scope.put("line", &lineValue) 268 | ctx.scope.put("i", IntValue(i)) 269 | 270 | // NOTE: currently, the same program is re-tokenized and re-parsed on 271 | // every line. This is not efficient, and can be optimized in the 272 | // future by parsing once and reusing a single AST. 273 | outValue, err := ctx.Eval(strings.NewReader(prog)) 274 | if err != nil { 275 | fmt.Println(err) 276 | os.Exit(1) 277 | } 278 | 279 | var outLine []byte 280 | switch v := outValue.(type) { 281 | case NullValue: 282 | // lines that return ? are filtered out entirely, which lets Oak's 283 | // shorthand `if pattern -> action` notation become very useful 284 | continue 285 | case *StringValue: 286 | outLine = []byte(*v) 287 | default: 288 | outLine = []byte(outValue.String()) 289 | } 290 | if _, err := os.Stdout.Write(append(outLine, '\n')); err != nil { 291 | fmt.Println("Could not write piped output") 292 | os.Exit(1) 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /docs/spec.md: -------------------------------------------------------------------------------- 1 | # Oak programming language 2 | 3 | This is a work-in-progress rough draft of things that will end up in a rough informal language specification. 4 | 5 | ## Syntax 6 | 7 | Oak, like [Ink](https://dotink.co), has automatic comma insertion at end of lines. This means if a comma can be inserted at the end of a line, it will automatically be inserted. 8 | 9 | ``` 10 | program := expr* 11 | 12 | expr := literal | identifier | 13 | assignment | 14 | propertyAccess | 15 | unaryExpr | binaryExpr | 16 | prefixCall | infixCall | 17 | ifExpr | withExpr | 18 | block 19 | 20 | literal := nullLiteral | 21 | numberLiteral | stringLiteral | atomLiteral | boolLiteral | 22 | listLiteral | objectLiteral | 23 | fnLiterael 24 | 25 | nullLiteral := '?' 26 | numberLiteral := \d+ | \d* '.' \d+ 27 | stringLiteral := // single quoted string with standard escape sequences + \x00 syntax 28 | atomLiteral := ':' + identifier 29 | boolLiteral := 'true' | 'false' 30 | listLiteral := '[' ( expr ',' )* ']' // last comma optional 31 | objectLiteral := '{' ( expr ':' expr ',' )* '}' // last comma optional 32 | fnLiteral := 'fn' '(' ( identifier ',' )* (identifier '...')? ')' expr 33 | 34 | identifier := \w_ (\w\d_?!)* | _ 35 | 36 | assignment := ( 37 | identifier [':=' '<-'] expr | 38 | listLiteral [':=' '<-'] expr | 39 | objectLiteral [':=' '<-'] expr 40 | ) 41 | 42 | propertyAccess := identifier ('.' identifier)+ 43 | 44 | unaryExpr := ('!' | '-') expr 45 | binaryExpr := expr (+ - * / % ^ & | > < = >= <= <<) binaryExpr 46 | 47 | prefixCall := expr '(' (expr ',')* ')' 48 | infixCall := expr '|>' prefixCall 49 | 50 | ifExpr := 'if' expr? '{' ifClause* '}' 51 | ifClause := expr '->' expr ',' 52 | 53 | withExpr := 'with' prefixCall fnLiteral 54 | 55 | block := '{' expr+ '}' | '(' expr* ')' 56 | ``` 57 | 58 | ### AST node types 59 | 60 | ``` 61 | nullLiteral 62 | stringLiteral 63 | numberLiteral 64 | boolLiteral 65 | atomLiteral 66 | listLiteral 67 | objectLiteral 68 | fnLiteral 69 | identifier 70 | assignment 71 | propertyAccess 72 | unaryExpr 73 | binaryExpr 74 | fnCall 75 | ifExpr 76 | block 77 | ``` 78 | 79 | ## Builtin functions 80 | 81 | ``` 82 | -- language 83 | import(path) 84 | string(x) 85 | int(x) 86 | float(x) 87 | atom(c) 88 | codepoint(c) 89 | char(n) 90 | type(x) 91 | len(x) 92 | keys(x) 93 | 94 | -- os 95 | args() 96 | env() 97 | time() // returns float 98 | nanotime() // returns int 99 | exit(code) 100 | rand() 101 | srand(length) 102 | wait(duration) 103 | exec(path, args, stdin) // returns stdout, stderr, end events 104 | 105 | ---- I/O interfaces 106 | input() 107 | print() 108 | ls(path) 109 | mkdir(path) 110 | rm(path) 111 | stat(path) 112 | open(path, flags, perm) 113 | close(fd) 114 | read(fd, offset, length) 115 | write(fd, offset, data) 116 | close := listen(host, handler) 117 | req(data) 118 | 119 | -- math 120 | sin(n) 121 | cos(n) 122 | tan(n) 123 | asin(n) 124 | acos(n) 125 | atan(n) 126 | pow(b, n) 127 | log(b, n) 128 | ``` 129 | 130 | ## Code samples 131 | 132 | ```js 133 | // hello world 134 | std.println('Hello, World!') 135 | 136 | // some math 137 | sq := fn(n) n * n 138 | fn sq(n) n * n 139 | fn sq(n) { n * n } // equivalent 140 | 141 | // side-effecting functions 142 | fn say() { std.println('Hi!') } 143 | // if no arguments, () is optiona 144 | fn { std.println('Hi!') } 145 | 146 | // factorial 147 | fn factorial(n) if n <= 1 { 148 | true -> 1 149 | _ -> n * factorial(n - 1) 150 | } 151 | ``` 152 | 153 | ```js 154 | // methods are emulated by pipe notation 155 | scores |> sum() 156 | names |> join(', ') 157 | fn sum(xs...) xs |> reduce(0, fn(a, b) a + b) 158 | oakFiles := fileNames |> filter(fn(name) name |> endsWith?('.oak')) 159 | ``` 160 | 161 | ```js 162 | // "with" keyword just makes the last fn a callback as last arg 163 | with loop(10) fn(n) std.println(n) 164 | with wait(1) fn { 165 | std.println('Done!') 166 | } 167 | with fetch('example.com') fn(resp) { 168 | with resp.json() fn(json) { 169 | std.println(json) 170 | } 171 | } 172 | ``` 173 | 174 | ```js 175 | // raw file read 176 | with open('name.txt') fn(evt) if evt.type { 177 | :error -> std.println(evt.message) 178 | _ -> with read(fd := evt.fd, 0, -1) fn(evt) { 179 | if evt.type { 180 | :error -> std.println(evt.message) 181 | _ -> fmt.printf('file data: {{0}}', evt.data) 182 | } 183 | close(fd) 184 | } 185 | } 186 | 187 | // with stdlib 188 | std := import('std') 189 | fs := import('fs') 190 | with fs.readFile('names.txt') fn(file) if file { 191 | ? -> std.println('[error] could not read file') 192 | _ -> std.println(file) 193 | } 194 | ``` 195 | 196 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thesephist/oak 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/chzyer/readline v1.5.1 7 | golang.org/x/sys v0.6.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 2 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 3 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 4 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 5 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 6 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 7 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 8 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 9 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 10 | -------------------------------------------------------------------------------- /lib.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | //go:embed lib/std.oak 11 | var libstd string 12 | 13 | //go:embed lib/str.oak 14 | var libstr string 15 | 16 | //go:embed lib/math.oak 17 | var libmath string 18 | 19 | //go:embed lib/sort.oak 20 | var libsort string 21 | 22 | //go:embed lib/random.oak 23 | var librandom string 24 | 25 | //go:embed lib/fs.oak 26 | var libfs string 27 | 28 | //go:embed lib/fmt.oak 29 | var libfmt string 30 | 31 | //go:embed lib/json.oak 32 | var libjson string 33 | 34 | //go:embed lib/datetime.oak 35 | var libdatetime string 36 | 37 | //go:embed lib/path.oak 38 | var libpath string 39 | 40 | //go:embed lib/http.oak 41 | var libhttp string 42 | 43 | //go:embed lib/test.oak 44 | var libtest string 45 | 46 | //go:embed lib/debug.oak 47 | var libdebug string 48 | 49 | //go:embed lib/cli.oak 50 | var libcli string 51 | 52 | //go:embed lib/md.oak 53 | var libmd string 54 | 55 | //go:embed lib/crypto.oak 56 | var libcrypto string 57 | 58 | //go:embed lib/syntax.oak 59 | var libsyntax string 60 | 61 | var stdlibs = map[string]string{ 62 | "std": libstd, 63 | "str": libstr, 64 | "math": libmath, 65 | "sort": libsort, 66 | "random": librandom, 67 | "fs": libfs, 68 | "fmt": libfmt, 69 | "json": libjson, 70 | "datetime": libdatetime, 71 | "path": libpath, 72 | "http": libhttp, 73 | "test": libtest, 74 | "debug": libdebug, 75 | "cli": libcli, 76 | "md": libmd, 77 | "crypto": libcrypto, 78 | "syntax": libsyntax, 79 | } 80 | 81 | func isStdLib(name string) bool { 82 | _, ok := stdlibs[name] 83 | return ok 84 | } 85 | 86 | func (c *Context) LoadLib(name string) (Value, *runtimeError) { 87 | program, ok := stdlibs[name] 88 | if !ok { 89 | return nil, &runtimeError{ 90 | reason: fmt.Sprintf("%s is not a valid standard library; could not import", name), 91 | } 92 | } 93 | 94 | if imported, ok := c.eng.importMap[name]; ok { 95 | return ObjectValue(imported.vars), nil 96 | } 97 | 98 | ctx := c.ChildContext(c.rootPath) 99 | ctx.LoadBuiltins() 100 | 101 | ctx.Unlock() 102 | _, err := ctx.Eval(strings.NewReader(program)) 103 | ctx.Lock() 104 | if err != nil { 105 | if runtimeErr, ok := err.(*runtimeError); ok { 106 | return nil, runtimeErr 107 | } else { 108 | return nil, &runtimeError{ 109 | reason: fmt.Sprintf("Error loading %s: %s", name, err.Error()), 110 | } 111 | } 112 | } 113 | 114 | c.eng.importMap[name] = ctx.scope 115 | return ObjectValue(ctx.scope.vars), nil 116 | } 117 | 118 | func (c *Context) loadAllLibs() error { 119 | for libname := range stdlibs { 120 | _, err := c.Eval(strings.NewReader(fmt.Sprintf("%s := import('%s')", libname, libname))) 121 | if err != nil { 122 | return err 123 | } 124 | } 125 | return nil 126 | } 127 | 128 | func (c *Context) mustLoadAllLibs() { 129 | if err := c.loadAllLibs(); err != nil { 130 | fmt.Println(err) 131 | os.Exit(1) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/cli.oak: -------------------------------------------------------------------------------- 1 | // libcli parses command line options and arguments 2 | 3 | { 4 | default: default 5 | slice: slice 6 | join: join 7 | each: each 8 | } := import('std') 9 | { 10 | startsWith?: startsWith? 11 | } := import('str') 12 | 13 | // _maybeOpt checks if a given string is a CLI flag, and if so returns the name 14 | // of the flag. If not, it returns ? 15 | fn _maybeOpt(part) if { 16 | part |> startsWith?('--') -> part |> slice(2) 17 | part |> startsWith?('-') -> part |> slice(1) 18 | _ -> ? 19 | } 20 | 21 | // parseArgv parses command-line arguments of the form 22 | // `./exe verb --flag option arg1 arg2 arg3` 23 | // 24 | // Supports: 25 | // -flag (implied true) 26 | // --flag (implied true) 27 | // -opt val 28 | // --opt val 29 | // and all other values are considered (positional) arguments. Flags respect 30 | // the '--' convention for signaling the start of purely positional arguments. 31 | fn parseArgv(argv) { 32 | // if a flag is in the verb position, amend argv to have verb = ? 33 | if _maybeOpt(default(argv.2, '')) != ? -> { 34 | argv <- slice(argv, 0, 2) |> join([?]) |> join(slice(argv, 2)) 35 | } 36 | 37 | opts := {} 38 | args := [] 39 | 40 | lastOpt := ? 41 | onlyPositional? := false 42 | 43 | argv |> slice(3) |> with each() fn(part) if { 44 | part = ? -> ? 45 | part = '--' -> onlyPositional? <- true 46 | onlyPositional? -> args << part 47 | _ -> if [lastOpt, opt := _maybeOpt(part)] { 48 | // not opt, no prev opt -> positional arg 49 | [?, ?] -> args << part 50 | // not opt, prev opt exists -> flag value 51 | [_, ?] -> { 52 | opts.(lastOpt) := part 53 | lastOpt <- ? 54 | } 55 | // is opt, no prev opt -> queue opt 56 | [?, _] -> lastOpt <- opt 57 | // is opt, prev opt exists -> last opt = true, queue opt 58 | _ -> { 59 | opts.(lastOpt) := true 60 | lastOpt <- opt 61 | } 62 | } 63 | } 64 | 65 | // if flag was queued, mark it as true 66 | if lastOpt != ? -> opts.(lastOpt) := true 67 | 68 | { 69 | exe: argv.0 70 | main: argv.1 71 | verb: argv.2 72 | opts: opts 73 | args: args 74 | } 75 | } 76 | 77 | // parse 78 | fn parse() parseArgv(args()) 79 | 80 | -------------------------------------------------------------------------------- /lib/crypto.oak: -------------------------------------------------------------------------------- 1 | // libcrypto provides utilities for working with cryptographic primitives and 2 | // cryptographically safe sources of randomness. 3 | 4 | { 5 | toHex: toHex 6 | map: map 7 | } := import('std') 8 | { 9 | split: split 10 | } := import('str') 11 | 12 | fn uuid { 13 | ns := srand(16) |> split() |> map(codepoint) 14 | 15 | // uuid v4 version bits 16 | ns.6 := (ns.6 & 15) | 64 17 | ns.8 := (ns.8 & 63) | 128 18 | 19 | // helper 20 | fn x(b) if len(s := toHex(ns.(b))) { 21 | 1 -> '0' << s 22 | _ -> s 23 | } 24 | 25 | x(0) << x(1) << x(2) << x(3) << '-' << 26 | x(4) << x(5) << '-' << 27 | x(6) << x(7) << '-' << 28 | x(8) << x(9) << '-' << 29 | x(10) << x(11) << x(12) << x(13) << x(14) << x(15) 30 | } 31 | 32 | -------------------------------------------------------------------------------- /lib/datetime.oak: -------------------------------------------------------------------------------- 1 | // libdatetime provides utilities for working with dates and UNIX timestamps 2 | // 3 | // In general libdatetime is designed to be correct for dates in the Common 4 | // Era, 0001-01-01T00:00:00Z and forward. This may be extended into the past if 5 | // such behavior is desired, but I haven't hit any such use cases yet. 6 | // 7 | // libdatetime deals with UNIX timestamps, positive and negative, extending 8 | // back to 1 CE and forward until integer overflow, but does not deal with 9 | // millisecond resolution timestamps with the exception of format() and parse() 10 | // which can format and parse milliseconds into and out of ISO8601 datetime 11 | // strings. The library also does not concern itself with time zones, pushing 12 | // that complexity to call sites. 13 | 14 | { 15 | default: default 16 | map: map 17 | take: take 18 | slice: slice 19 | merge: merge 20 | } := import('std') 21 | { 22 | endsWith?: endsWith? 23 | contains?: strContains? 24 | indexOf: strIndexOf 25 | padStart: padStart 26 | padEnd: padEnd 27 | split: split 28 | } := import('str') 29 | { 30 | round: round 31 | } := import('math') 32 | { 33 | format: fmtFormat 34 | } := import('fmt') 35 | 36 | LeapDay := 31 + 28 37 | SecondsPerDay := 86400 38 | DaysPer4Years := 365 * 4 + 1 39 | DaysPer100Years := 25 * DaysPer4Years - 1 40 | DaysPer400Years := DaysPer100Years * 4 + 1 41 | 42 | // our zero time is the year 1 CE, though the Gregorian calendar doesn't extend 43 | // that far into the past, to ensure that we can treat all dates in the Common 44 | // Era correctly without going into negative integer division, and we can take 45 | // advantage of 400-year cycles in the calendar. 46 | ZeroYear := 1 47 | DaysFrom1To1970 := DaysPer400Years * 5 - 365 * 31 - 8 // 8 leap years 48 | 49 | // DaysBeforeMonth.(month) is the number of days in a non-leap calendar year 50 | // _before_ that month, with January = month 1. 51 | DaysBeforeMonth := [ 52 | _ 53 | 0 54 | 31 55 | 31 + 28 56 | 31 + 28 + 31 57 | 31 + 28 + 31 + 30 58 | 31 + 28 + 31 + 30 + 31 59 | 31 + 28 + 31 + 30 + 31 + 30 60 | 31 + 28 + 31 + 30 + 31 + 30 + 31 61 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 62 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 63 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 64 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 65 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31 66 | ] 67 | 68 | // leap? reports whether a calendar year is a leap year 69 | fn leap?(year) year % 4 = 0 & (year % 100 != 0 | year % 400 = 0) 70 | 71 | fn _describeDate(t) { 72 | // only dealing with full days since zero time 73 | d := int((t - t % SecondsPerDay) / SecondsPerDay) + DaysFrom1To1970 74 | // when going negative, we should truncate times into dates in the other 75 | // direction 76 | if t < 0 & t % 86400 != 0 -> d <- d - 1 77 | 78 | n400 := int(d / DaysPer400Years) 79 | d := d - DaysPer400Years * n400 80 | 81 | n100 := int(d / DaysPer100Years) 82 | // 100-year cycles overshoot every 400 years, so we round down 83 | n100 := n100 - int(n100 / 4) 84 | d := d - DaysPer100Years * n100 85 | 86 | n4 := int(d / DaysPer4Years) 87 | d := d - DaysPer4Years * n4 88 | 89 | n := int(d / 365) 90 | // 4-year cycles overshoot every 4 years, so we round down 91 | n := n - int(n / 4) 92 | d := d - 365 * n 93 | 94 | year := ZeroYear + 95 | 400 * n400 + 96 | 100 * n100 + 97 | 4 * n4 + 98 | n 99 | month := 0 100 | day := d 101 | 102 | leapYear? := leap?(year) 103 | if { 104 | leapYear? & day = LeapDay -> { 105 | month <- 2 106 | day <- 29 107 | } 108 | _ -> { 109 | // if after leap day, pull dates forward 1 day 110 | if leapYear? & day > LeapDay -> day <- day - 1 111 | 112 | fn subMonth(m) if day < DaysBeforeMonth.(m + 1) { 113 | true -> m 114 | _ -> subMonth(m + 1) 115 | } 116 | month <- subMonth(1) 117 | day <- day - DaysBeforeMonth.(month) + 1 118 | } 119 | } 120 | 121 | { 122 | year: year 123 | month: month 124 | day: day 125 | } 126 | } 127 | 128 | fn _describeClock(t) { 129 | rem := t % SecondsPerDay 130 | if rem < 0 -> rem <- rem + SecondsPerDay 131 | 132 | hour := int(rem / 3600) 133 | rem := rem % 3600 134 | minute := int(rem / 60) 135 | { 136 | hour: hour 137 | minute: minute 138 | second: rem % 60 139 | } 140 | } 141 | 142 | // describe computes the year, month, day, hour, minute, and second values from 143 | // a UNIX timestamp 144 | fn describe(t) merge( 145 | _describeDate(t) 146 | _describeClock(t) 147 | ) 148 | 149 | // timestamp converts the year, month, day, hour, minute, and second into a 150 | // positive or negative UNIX timestamp 151 | fn timestamp(desc) { 152 | { 153 | year: year 154 | month: month 155 | day: day 156 | hour: hour 157 | minute: minute 158 | second: second 159 | } := desc 160 | 161 | leapYear? := leap?(year) 162 | 163 | year := year - ZeroYear 164 | n400 := int(year / 400), year := year % 400 165 | n100 := int(year / 100), year := year % 100 166 | n4 := int(year / 4), year := year % 4 167 | 168 | daysYearToDate := if leapYear? { 169 | true -> if { 170 | // before leap day 171 | month = 1 172 | month = 2 & day < 29 -> DaysBeforeMonth.(month) + day - 1 173 | // leap day 174 | month = 2 & day = 29 -> 59 175 | // after leap day 176 | _ -> DaysBeforeMonth.(month) + day 177 | } 178 | // if not leap year, we want to account for a previous leap day 179 | _ -> DaysBeforeMonth.(month) + day - 1 180 | } 181 | daysFrom1 := DaysPer400Years * n400 + 182 | DaysPer100Years * n100 + 183 | DaysPer4Years * n4 + 184 | 365 * year + 185 | daysYearToDate 186 | daysFrom1970 := daysFrom1 - DaysFrom1To1970 187 | 188 | daysFrom1970 * SecondsPerDay + 189 | 3600 * hour + 190 | 60 * minute + 191 | second 192 | } 193 | 194 | // format takes a timestamp and returns its ISO8601-compliant date time string. 195 | // tzOffset is the local time zone's offset from UTC, in minutes, and defaults 196 | // to 0 representing UTC. 197 | fn format(t, tzOffset) { 198 | tzOffset := default(tzOffset, 0) 199 | { 200 | year: year 201 | month: month 202 | day: day 203 | hour: hour 204 | minute: minute 205 | second: second 206 | } := describe(t + tzOffset * 60) 207 | 208 | '{{0}}-{{1}}-{{2}}T{{3}}:{{4}}:{{5}}{{6}}{{7}}' |> fmtFormat( 209 | if { 210 | year > 9999 -> year |> string() |> padStart(6, '0') 211 | year < 0 -> '-' << -year |> string() |> padStart(6, '0') 212 | _ -> year |> string() |> padStart(4, '0') 213 | } 214 | month |> string() |> padStart(2, '0') 215 | day |> string() |> padStart(2, '0') 216 | hour |> string() |> padStart(2, '0') 217 | minute |> string() |> padStart(2, '0') 218 | second |> int() |> string() |> padStart(2, '0') 219 | if millis := round((second * 1000) % 1000) { 220 | 0 -> '' 221 | _ -> '.' + millis |> string() 222 | } 223 | if { 224 | tzOffset = 0 -> 'Z' 225 | tzOffset > 0 -> '+' << '{{0}}:{{1}}' |> fmtFormat( 226 | string(int(tzOffset / 60)) |> padStart(2, '0') 227 | string(tzOffset % 60) |> padStart(2, '0') 228 | ) 229 | _ -> '-' << '{{0}}:{{1}}' |> fmtFormat( 230 | string(int(-tzOffset / 60)) |> padStart(2, '0') 231 | string(-tzOffset % 60) |> padStart(2, '0') 232 | ) 233 | } 234 | ) 235 | } 236 | 237 | fn _parseTZOffset(offsetString) if [hh, mm] := offsetString |> split(':') |> map(int) { 238 | // if time offset cannot be parsed, we fail the whole parse 239 | [], [_], [?, _], [_, ?] -> ? 240 | _ -> hh * 60 + mm 241 | } 242 | 243 | // parse takes an ISO8601-compliant date string and returns a time description 244 | fn parse(s) if [date, clock] := s |> split('T') { 245 | [], [_] 246 | [?, _], [_, ?] -> ? 247 | _ -> if [year, month, day] := date |> split('-') |> map(int) { 248 | [], [_], [_, _] 249 | [?, _, _], [_, ?, _], [_, _, ?] -> ? 250 | _ -> if [hour, minute, second] := clock |> take(8) |> split(':') |> map(int) { 251 | [], [_], [_, _] 252 | [?, _, _], [_, ?, _], [_, _, ?] -> ? 253 | _ -> { 254 | // milliseconds and time zones 255 | [_, maybeMillis] := clock |> split('.') |> map(fn(s) s |> take(3)) |> map(int) 256 | tzOffset := if { 257 | clock |> strContains?('+') -> 258 | _parseTZOffset(clock |> slice(clock |> strIndexOf('+') + 1)) 259 | clock |> strContains?('-') -> if parsed := 260 | _parseTZOffset(clock |> slice(clock |> strIndexOf('-') + 1)) { 261 | ? -> ? 262 | _ -> -parsed 263 | } 264 | _ -> 0 265 | } 266 | 267 | if tzOffset != ? -> { 268 | year: year 269 | month: month 270 | day: day 271 | hour: hour 272 | minute: minute 273 | second: second + (maybeMillis |> default(0)) / 1000 274 | tzOffset: tzOffset 275 | } 276 | } 277 | } 278 | } 279 | } 280 | 281 | -------------------------------------------------------------------------------- /lib/debug.oak: -------------------------------------------------------------------------------- 1 | // libdebug contains utilities useful for debugging and inspecting runtime 2 | // values of Oak programs. 3 | 4 | { 5 | println: stdPrintln 6 | default: default 7 | range: range 8 | toHex: toHex 9 | map: map 10 | each: each 11 | some: some 12 | every: every 13 | values: values 14 | reduce: reduce 15 | entries: entries 16 | } := import('std') 17 | { 18 | letter?: letter? 19 | digit?: digit? 20 | join: join 21 | padStart: padStart 22 | } := import('str') 23 | math := import('math') 24 | { 25 | sort!: sort! 26 | } := import('sort') 27 | { 28 | format: format 29 | } := import('fmt') 30 | 31 | // _validIdent? reports if the given string is a valid name for an Oak 32 | // identifier. It's used to check if a string can appear without quotes as an 33 | // object key, or if it must be displayed as a string. 34 | fn _validIdent?(s) s |> every(fn(c, i) if i { 35 | 0 -> letter?(c) | c = '_' | c = '?' | c = '!' 36 | _ -> letter?(c) | digit?(c) | c = '_' | c = '?' | c = '!' 37 | }) 38 | 39 | // _numeral? reports if the given string represents a positive integer, used 40 | // for similar purposes to _validIdent?. 41 | fn _numeral?(s) s |> every(digit?) 42 | 43 | // _primitive? reports whether the given Oak value x is of a primitive or 44 | // function type, or a composite type composed of other Oak values. 45 | fn _primitive?(x) if type(x) { 46 | :null, :empty, :bool, :int, :float, :string, :atom 47 | // functions are considered "primitives" for the purpose of 48 | // inspect-printing because they're printed as `fn { ... }` 49 | :function -> true 50 | _ -> false 51 | } 52 | 53 | // inspect is a utility to pretty-print Oak data structures. Unlike the 54 | // string() builtin (used in std.println), inspect formats its input with 55 | // customizable, nested indentation and spacing for readability in an output 56 | // log or console output. 57 | fn inspect(x, options) { 58 | { 59 | indent: indentUnit 60 | depth: depth 61 | maxLine: maxLine 62 | maxList: maxList 63 | maxObject: maxObject 64 | } := options |> default({}) 65 | 66 | indentUnit := indentUnit |> default(' ') 67 | depth := depth |> default(-1) 68 | maxLine := maxLine |> default(80) 69 | maxList := maxList |> default(16) 70 | maxObject := maxObject |> default(3) 71 | 72 | fn inspectObjectKey(key) if { 73 | _validIdent?(key), _numeral?(key) -> key 74 | _ -> inspectLine(key, -1) 75 | } 76 | 77 | fn inspectAbbreviated(x) if type(x) { 78 | :list -> '[ {{0}} items... ]' |> format(len(x)) 79 | :object -> '{ {{0}} entries... }' |> format(len(x)) 80 | } 81 | 82 | fn inspectLine(x, depth) if type(x) { 83 | :null, :empty, :bool, :int, :float -> string(x) 84 | :string -> '\'' + (x |> map(fn(c) if c { 85 | '\\' -> '\\\\' 86 | '\'' -> '\\\'' 87 | '\n' -> '\\n' 88 | '\r' -> '\\r' 89 | '\f' -> '\\f' 90 | '\t' -> '\\t' 91 | _ -> if { 92 | // hex-format non-printable bytes 93 | codepoint(c) < 32 94 | codepoint(c) > 126 -> '\\x' << toHex(codepoint(c)) |> padStart(2, '0') 95 | _ -> c 96 | } 97 | })) + '\'' 98 | :atom -> if _validIdent?(payload := string(x)) { 99 | true -> ':' + string(payload) 100 | _ -> 'atom({{0}})' |> format(inspectLine(payload)) 101 | } 102 | :function -> 'fn { ... }' 103 | :list -> '[' + (x |> map(fn(y) inspectLine(y, depth)) |> join(', ')) + ']' 104 | :object -> if len(x) { 105 | 0 -> '{}' 106 | _ -> '{ ' + { 107 | entries(x) |> 108 | sort!(0) |> 109 | map(fn(entry) inspectObjectKey(entry.0) + ': ' + inspectLine(entry.1, depth)) |> 110 | join(', ') 111 | } + ' }' 112 | } 113 | } 114 | 115 | fn inspectMulti(x, indent, depth) { 116 | innerIndent := indent + indentUnit 117 | if type(x) { 118 | :list -> x |> reduce('[', fn(lines, item) { 119 | lines << '\n' + innerIndent + inspectAny(item, innerIndent, depth) 120 | }) << '\n' + indent + ']' 121 | :object -> entries(x) |> sort!(0) |> reduce('{', fn(lines, entry) { 122 | lines << '\n' + innerIndent + inspectObjectKey(entry.0) + ': ' + 123 | inspectAny(entry.1, innerIndent, depth) 124 | }) << '\n' + indent + '}' 125 | } 126 | } 127 | 128 | fn inspectAny(x, indent, depth) { 129 | line := inspectLine(x, depth - 1) 130 | overflows? := len(line) + len(indent) > maxLine 131 | if { 132 | _primitive?(x) -> line 133 | depth = 0 -> inspectAbbreviated(x) 134 | overflows? -> inspectMulti(x, indent, depth - 1) 135 | type(x) = :list -> if { 136 | len(x) > maxList 137 | x |> some(fn(y) !_primitive?(y)) -> inspectMulti(x, indent, depth - 1) 138 | _ -> line 139 | } 140 | type(x) = :object -> if { 141 | len(x) > maxObject 142 | x |> values() |> some(fn(y) !_primitive?(y)) -> inspectMulti(x, indent, depth - 1) 143 | _ -> line 144 | } 145 | } 146 | } 147 | 148 | inspectAny(x, '', depth) 149 | } 150 | 151 | // println is a shorthand function to print the output of `inspect`. Note that 152 | // debug.println is not a drop-in replacement for std.println, because 153 | // std.println is variadic, but debug.println takes one argument and one 154 | // (optional) options object. 155 | fn println(x, options) stdPrintln(inspect(x, options)) 156 | 157 | // bar draws a histogram bar as a Unicode string, with 1 character representing 158 | // a value of "1". Very small but non-zero values are rounded up to the 159 | // smallest representable unit, as distinguishing them from zero is usually 160 | // more important at that scale than quantitative correctness. 161 | // 162 | // bar is used to render a histogram in histo. 163 | fn bar(n) { 164 | n := math.max(n * 8, 0) 165 | whole := int(n / 8) 166 | rem := n % 8 167 | graph := range(whole) |> map(fn '█') |> join() + if math.round(rem) { 168 | 0 -> '' 169 | 1 -> '▏' 170 | 2 -> '▎' 171 | 3 -> '▍' 172 | 4 -> '▌' 173 | 5 -> '▋' 174 | 6 -> '▊' 175 | 7 -> '▉' 176 | 8 -> '█' 177 | } 178 | if graph = '' & n > 0 { 179 | true -> '▏' 180 | _ -> graph 181 | } 182 | } 183 | 184 | 185 | // histo draws a histogram from a given list of numbers using Unicode symbols, 186 | // using bar to draw each bar. 187 | fn histo(xs, opts) if len(xs) { 188 | 0 -> '' 189 | _ -> { 190 | opts := opts |> default({}) 191 | min := opts.min |> default(math.min(xs...)) 192 | max := opts.max |> default(math.max(xs...)) 193 | bars := opts.bars |> default(10) |> math.min(len(xs)) |> math.max(1) 194 | label := opts.label |> default(?) 195 | cols := opts.cols |> default(80) 196 | unit := (max - min) / bars 197 | 198 | buckets := range(bars) |> map(fn 0) 199 | xs |> each(fn(x) if x >= min & x < max -> { 200 | i := int((x - min) / unit) 201 | buckets.(i) := buckets.(i) + 1 202 | }) 203 | maxcount := math.max(buckets...) 204 | 205 | labels := buckets |> map(string) 206 | maxlen := math.max(labels |> map(len)...) 207 | if label = :start -> labels := labels |> map(fn(l) l |> padStart(maxlen, ' ')) 208 | buckets |> map(fn(n, i) { 209 | b := n |> math.scale(0, maxcount, 0, cols) |> bar() 210 | if label { 211 | :start -> labels.(i) + ' ' + b 212 | :end -> b + ' ' + labels.(i) 213 | _ -> b 214 | } 215 | }) |> join('\n') 216 | } 217 | } 218 | 219 | -------------------------------------------------------------------------------- /lib/fmt.oak: -------------------------------------------------------------------------------- 1 | // libfmt is the string formatting library for Oak. 2 | 3 | { 4 | println: println 5 | default: default 6 | } := import('std') 7 | 8 | // format returns the format string `raw`, where each substring of the form 9 | // "{{N}}" has been replaced by the Nth value given in the arguments. Values 10 | // may be referenced zero or more times in the format string. 11 | // 12 | // format is ported from Ink's std.format function. 13 | fn format(raw, values...) { 14 | // parser internal state 15 | // 0 -> normal 16 | // 1 -> seen one { 17 | // 2 -> seen two { 18 | // 3 -> seen a valid } 19 | which := 0 20 | // buffer for currently reading key 21 | key := '' 22 | // result build-up buffer 23 | buf := '' 24 | // non-integer keys will key into this dict 25 | value := values.0 |> default({}) 26 | 27 | fn sub(idx) if idx < len(raw) { 28 | true -> { 29 | c := raw.(idx) 30 | 31 | if which { 32 | 0 -> if c { 33 | '{' -> which <- 1 34 | _ -> buf << c 35 | } 36 | 1 -> if c { 37 | '{' -> which <- 2 38 | // if it turns out the earlier brace was not a part of a format 39 | // expression, just backtrack 40 | _ -> { 41 | buf << '{' << c 42 | which <- 0 43 | } 44 | } 45 | 2 -> if c { 46 | '}' -> { 47 | index := int(key) 48 | buf << if { 49 | key = '' -> '' 50 | index = ? -> value.(key) 51 | _ -> values.(index) 52 | } |> string() 53 | key <- '' 54 | which <- 3 55 | } 56 | // ignore spaces in keys 57 | ' ', '\t' -> ? 58 | _ -> key <- key + c 59 | } 60 | 3 -> if c { 61 | '}' -> which <- 0 62 | // ignore invalid inputs -- treat them as nonexistent 63 | _ -> ? 64 | } 65 | } 66 | 67 | sub(idx + 1) 68 | } 69 | _ -> buf 70 | } 71 | 72 | sub(0) 73 | } 74 | 75 | // printf prints the result of format(raw, values...) to output 76 | fn printf(raw, values...) println(format(raw, values...)) 77 | 78 | -------------------------------------------------------------------------------- /lib/fs.oak: -------------------------------------------------------------------------------- 1 | // libfs offers ergonomic filesystem APIs to Oak programs. 2 | // 3 | // It wraps the basic built-in filesystem functions to provide more ergonomic, 4 | // safer, and efficient implementations of basic filesystem tasks like reading 5 | // files and walking a directory tree. 6 | // 7 | // Most functions in libfs are implemented in both synchronous and asynchronous 8 | // variants. Sync variants of functions block, and return the value 9 | // immediately for ease of use. For better performance, we can pass a callback 10 | // to the function to invoke its asynchronous variant. In that case, the 11 | // function will not block; instead, the callback will be called some time 12 | // later with the return value. 13 | 14 | // ReadBufSize is the size of the buffer used to read a file in streaming 15 | // file-read operations in libfs. This may be changed to alter the behavior of 16 | // libfs, but it will affect the behavior of libfs globally in your program. 17 | ReadBufSize := 4096 18 | 19 | fn readFileSync(path) { 20 | evt := open(path, :readonly) 21 | if evt.type { 22 | :error -> ? 23 | _ -> { 24 | fd := evt.fd 25 | fn sub(file, offset) { 26 | evt := read(fd, offset, ReadBufSize) 27 | if evt.type { 28 | :error -> { 29 | close(fd) 30 | ? 31 | } 32 | _ -> if len(evt.data) { 33 | ReadBufSize -> sub( 34 | file << evt.data 35 | offset + ReadBufSize 36 | ) 37 | _ -> { 38 | close(fd) 39 | file << evt.data 40 | } 41 | } 42 | } 43 | } 44 | 45 | sub('', 0) 46 | } 47 | } 48 | } 49 | 50 | fn readFileAsync(path, withFile) with open(path, :readonly) fn(evt) if evt.type { 51 | :error -> withFile(?) 52 | _ -> { 53 | fd := evt.fd 54 | 55 | fn sub(file, offset) with read(fd, offset, ReadBufSize) fn(evt) if evt.type { 56 | :error -> with close(fd) fn { 57 | withFile(?) 58 | } 59 | _ -> if len(evt.data) { 60 | ReadBufSize -> sub( 61 | file << evt.data 62 | offset + ReadBufSize 63 | ) 64 | _ -> with close(fd) fn { 65 | withFile(file << evt.data) 66 | } 67 | } 68 | } 69 | 70 | sub('', 0) 71 | } 72 | } 73 | 74 | // readFile reads the entire contents of a file at `path` and returns the file 75 | // contents as a string if successful, or ? on error. 76 | fn readFile(path, withFile) if withFile { 77 | ? -> readFileSync(path) 78 | _ -> readFileAsync(path, withFile) 79 | } 80 | 81 | fn writeFileSyncWithFlag(path, file, flag) { 82 | evt := open(path, flag) 83 | if evt.type { 84 | :error -> ? 85 | _ -> { 86 | fd := evt.fd 87 | { 88 | evt := write(fd, 0, file) 89 | close(fd) 90 | if evt.type { 91 | :error -> ? 92 | _ -> true 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | fn writeFileAsyncWithFlag(path, file, flag, withEnd) with open(path, flag) fn(evt) if evt.type { 100 | :error -> withEnd(?) 101 | _ -> with write(fd := evt.fd, 0, file) fn(evt) if evt.type { 102 | :error -> with close(fd) fn { 103 | withEnd(?) 104 | } 105 | _ -> with close(fd) fn { 106 | withEnd(true) 107 | } 108 | } 109 | } 110 | 111 | // writeFile writes all data in `file` to a file at `path`, and returns true on 112 | // success and ? on error. If the file does not exist, it will be created. If 113 | // it exists, it will be truncated. 114 | fn writeFile(path, file, withEnd) if withEnd { 115 | ? -> writeFileSyncWithFlag(path, file, :truncate) 116 | _ -> writeFileAsyncWithFlag(path, file, :truncate, withEnd) 117 | } 118 | 119 | // appendFile appends all data in `file` to the end of the file at `path`, and 120 | // returns true on success and ? on error. If the file does not exist, it will 121 | // be created. 122 | fn appendFile(path, file, withEnd) if withEnd { 123 | ? -> writeFileSyncWithFlag(path, file, :append) 124 | _ -> writeFileAsyncWithFlag(path, file, :append, withEnd) 125 | } 126 | 127 | fn statFileSync(path) { 128 | evt := stat(path) 129 | if evt.type { 130 | :error -> ? 131 | _ -> evt.data 132 | } 133 | } 134 | 135 | fn statFileAsync(path, withStat) with stat(path) fn(evt) if evt.type { 136 | :error -> withStat(?) 137 | _ -> withStat(evt.data) 138 | } 139 | 140 | // statFile returns the result of stat() if successful, and ? otherwise. 141 | fn statFile(path, withStat) if withStat { 142 | ? -> statFileSync(path) 143 | _ -> statFileAsync(path, withStat) 144 | } 145 | 146 | fn listFilesSync(path) { 147 | evt := ls(path) 148 | if evt.type { 149 | :error -> ? 150 | _ -> evt.data 151 | } 152 | } 153 | 154 | fn listFilesAsync(path, withFiles) with ls(path) fn(evt) if evt.type { 155 | :error -> withFiles(?) 156 | _ -> withFiles(evt.data) 157 | } 158 | 159 | // listFiles returns a list of files and directories in a directory at `path`. 160 | // If the directory does not exist or is not a directory, or if the read 161 | // failed, it returns ?. 162 | fn listFiles(path, withFiles) if withFiles { 163 | ? -> listFilesSync(path) 164 | _ -> listFilesAsync(path, withFiles) 165 | } 166 | 167 | -------------------------------------------------------------------------------- /lib/json.oak: -------------------------------------------------------------------------------- 1 | // libjson implements a JSON parser and serializer for Oak values 2 | 3 | { 4 | default: default 5 | slice: slice 6 | map: map 7 | } := import('std') 8 | { 9 | space?: space? 10 | join: join 11 | } := import('str') 12 | 13 | // string escape '"' 14 | fn esc(c) if c { 15 | '\t' -> '\\t' 16 | '\n' -> '\\n' 17 | '\r' -> '\\r' 18 | '\f' -> '\\f' 19 | '"' -> '\\"' 20 | '\\' -> '\\\\' 21 | _ -> c 22 | } 23 | 24 | // escapes whole string 25 | fn escape(s) { 26 | max := len(s) 27 | fn sub(i, acc) if i { 28 | max -> acc 29 | _ -> sub(i + 1, acc << esc(s.(i))) 30 | } 31 | sub(0, '') 32 | } 33 | 34 | // serialize takes an Oak value and returns its JSON representation 35 | fn serialize(c) if type(c) { 36 | // do not serialize functions 37 | :null, :empty, :function -> 'null' 38 | :string -> '"' << escape(c) << '"' 39 | :atom -> '"' << string(c) << '"' 40 | :int, :float, :bool -> string(c) 41 | // composite types 42 | :list -> '[' << c |> map(serialize) |> join(',') << ']' 43 | :object -> '{' << keys(c) |> map(fn(k) '"' << escape(k) << '":' << serialize(c.(k))) |> join(',') << '}' 44 | } 45 | 46 | // reader implementation with internal state for parsing 47 | fn Reader(s) { 48 | index := 0 49 | // has there been a parse error? 50 | err? := false 51 | 52 | fn next { 53 | index <- index + 1 54 | default(s.(index - 1), '') 55 | } 56 | fn peek default(s.(index), '') 57 | fn nextWord(n) if index + n > len(s) { 58 | true -> { 59 | index <- len(s) 60 | ? 61 | } 62 | _ -> { 63 | word := s |> slice(index, index + n) 64 | index <- index + n 65 | word 66 | } 67 | } 68 | // fast-forward through whitespace 69 | fn forward { 70 | fn sub if space?(peek()) -> { 71 | index <- index + 1 72 | sub() 73 | } 74 | sub() 75 | } 76 | 77 | { 78 | next: next 79 | peek: peek 80 | forward: forward 81 | nextWord: nextWord 82 | done?: fn() index >= len(s) 83 | err!: fn { 84 | err? <- true 85 | :error 86 | } 87 | err?: fn() err? 88 | } 89 | } 90 | 91 | fn parseNull(r) if r.nextWord(4) { 92 | 'null' -> ? 93 | _ -> r.err!() 94 | } 95 | 96 | fn parseString(r) { 97 | next := r.next 98 | 99 | next() // eat the double quote 100 | 101 | fn sub(acc) if c := next() { 102 | '' -> r.err!() 103 | '\\' -> sub(acc << if c := next() { 104 | 't' -> '\t' 105 | 'n' -> '\n' 106 | 'r' -> '\r' 107 | 'f' -> '\f' 108 | '"' -> '"' 109 | _ -> c 110 | }) 111 | '"' -> acc 112 | _ -> sub(acc << c) 113 | } 114 | sub('') 115 | } 116 | 117 | fn parseNumber(r) { 118 | peek := r.peek 119 | next := r.next 120 | 121 | decimal? := false 122 | negate? := if peek() { 123 | '-' -> { 124 | next() 125 | true 126 | } 127 | _ -> false 128 | } 129 | 130 | fn sub(acc) if peek() { 131 | '.' -> if decimal? { 132 | true -> r.err!() 133 | _ -> { 134 | decimal? <- true 135 | sub(acc << next()) 136 | } 137 | } 138 | '0', '1', '2', '3', '4' 139 | '5', '6', '7', '8', '9' -> sub(acc << next()) 140 | _ -> acc 141 | } 142 | result := sub('') 143 | 144 | if parsed := if decimal? { 145 | true -> float(result) 146 | _ -> int(result) 147 | } { 148 | ? -> :error 149 | _ -> if negate? { 150 | true -> -parsed 151 | _ -> parsed 152 | } 153 | } 154 | } 155 | 156 | fn parseTrue(r) if r.nextWord(4) { 157 | 'true' -> true 158 | _ -> r.err!() 159 | } 160 | 161 | fn parseFalse(r) if r.nextWord(5) { 162 | 'false' -> false 163 | _ -> r.err!() 164 | } 165 | 166 | fn parseList(r) { 167 | err? := r.err? 168 | peek := r.peek 169 | next := r.next 170 | forward := r.forward 171 | 172 | next() // eat the [ 173 | forward() 174 | 175 | fn sub(acc) if err?() { 176 | true -> :error 177 | _ -> if peek() { 178 | '' -> r.err!() 179 | ']' -> { 180 | next() // eat the ] 181 | acc 182 | } 183 | _ -> { 184 | acc << _parseReader(r) 185 | forward() 186 | if peek() = ',' -> next() 187 | 188 | forward() 189 | sub(acc) 190 | } 191 | } 192 | } 193 | sub([]) 194 | } 195 | 196 | fn parseObject(r) { 197 | err? := r.err? 198 | peek := r.peek 199 | next := r.next 200 | forward := r.forward 201 | 202 | next() // eat the { 203 | forward() 204 | 205 | fn sub(acc) if err?() { 206 | true -> :error 207 | _ -> if peek() { 208 | '' -> r.err!() 209 | '}' -> { 210 | next() 211 | acc 212 | } 213 | _ -> { 214 | key := parseString(r) 215 | if !err?() -> { 216 | forward() 217 | if peek() = ':' -> next() 218 | 219 | val := _parseReader(r) 220 | if !err?() -> { 221 | forward() 222 | if peek() = ',' -> next() 223 | 224 | forward() 225 | sub(acc.(key) := val) 226 | } 227 | } 228 | } 229 | } 230 | } 231 | sub({}) 232 | } 233 | 234 | fn _parseReader(r) { 235 | // trim preceding whitespace 236 | r.forward() 237 | 238 | result := if r.peek() { 239 | 'n' -> parseNull(r) 240 | '"' -> parseString(r) 241 | 't' -> parseTrue(r) 242 | 'f' -> parseFalse(r) 243 | '[' -> parseList(r) 244 | '{' -> parseObject(r) 245 | _ -> parseNumber(r) 246 | } 247 | 248 | // if there was a parse error, return :error 249 | if r.err?() { 250 | true -> :error 251 | _ -> result 252 | } 253 | } 254 | 255 | // parse takes a potentially valid JSON string, and returns its Oak 256 | // representation if valid JSON, or :error if the parse fails. 257 | fn parse(s) Reader(s) |> _parseReader() 258 | 259 | -------------------------------------------------------------------------------- /lib/math.oak: -------------------------------------------------------------------------------- 1 | // libmath implements basic arithmetic and algebraic functions 2 | // 3 | // For functions dealing with coordinate pairs and angles, the coordinate plane 4 | // is assumed to be a Cartesian plane with +x to the east and +y to the north, 5 | // where the angle is measured in radians from the +x axis, counterclockwise. 6 | 7 | { 8 | default: default 9 | map: map 10 | reduce: reduce 11 | } := import('std') 12 | { 13 | sort: sort 14 | } := import('sort') 15 | 16 | // Pi, the circle constant 17 | Pi := 3.14159265358979323846264338327950288419716939937510 18 | 19 | // E, the base of the natural logarithm 20 | E := 2.71828182845904523536028747135266249775724709369995 21 | 22 | // sign returns -1 for all negative numbers, and 1 otherwise 23 | fn sign(n) if n >= 0 { 24 | true -> 1 25 | _ -> -1 26 | } 27 | 28 | // abs returns the absolute value of a real number 29 | fn abs(n) if n >= 0 { 30 | true -> n 31 | _ -> -n 32 | } 33 | 34 | // sqrt returns the principal square root of a real number, or ? if the number 35 | // is negative. 36 | fn sqrt(n) if n >= 0 -> pow(n, 0.5) 37 | 38 | // hypot returns the Euclidean distance between two points, equivalent to the 39 | // hypotenuse of a right triangle with the given two points as vertices. 40 | fn hypot(x0, y0, x1, y1) { 41 | if x1 = ? & y1 = ? -> x1 <- y1 <- 0 42 | sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1)) 43 | } 44 | 45 | // scale maps the value x in the range [a, b] to the range [c, d]. If [c, d] 46 | // are not provided, they are assumed to be [0, 1]. x may be outside the range 47 | // [a, b], in which case the value is scaled to be an equal amount outside of 48 | // the range [c, d]. 49 | fn scale(x, a, b, c, d) { 50 | normed := (x - a) / (b - a) 51 | if { 52 | c = ? & d = ? -> normed 53 | _ -> (d - c) * normed + c 54 | } 55 | } 56 | 57 | // bearing returns the point [x', y'] at the other end of a line segment 58 | // starting at (x, y) and extending by distance d at angle t. 59 | fn bearing(x, y, d, t) [ 60 | x + d * cos(t) 61 | y + d * sin(t) 62 | ] 63 | 64 | // orient returns the angle of the line extending from (x0, y0) to (x1, y1). If 65 | // (x1, y1) is not provided, the given coordinate point is assumed to be (x1, 66 | // y1) and (x0, y0) is assumed to be the origin (0, 0). Return values are in 67 | // the range (-Pi, Pi]. This function is more commonly known in the form 68 | // `atan2(y, x)` (note the reversed argument order). 69 | fn orient(x0, y0, x1, y1) { 70 | [x, y] := if x1 = ? & y1 = ? { 71 | true -> [x0, y0] 72 | _ -> [x1 - x0, y1 - y0] 73 | } 74 | if { 75 | x > 0 -> 2 * atan(y / (hypot(x, y) + x)) 76 | x <= 0 & y != 0 -> 2 * atan((hypot(x, y) - x) / y) 77 | x < 0 & y = 0 -> Pi 78 | } 79 | } 80 | 81 | // sum takes a sequence of values and returns their sum 82 | fn sum(xs...) xs |> reduce(0, fn(a, b) a + b) 83 | 84 | // prod takes a sequence of values and returns their product 85 | fn prod(xs...) xs |> reduce(1, fn(a, b) a * b) 86 | 87 | // min returns the minimum value of all given values 88 | fn min(xs...) xs |> reduce(xs.0, fn(acc, n) if n < acc { 89 | true -> n 90 | _ -> acc 91 | }) 92 | 93 | // max returns the maximum value of all given values 94 | fn max(xs...) xs |> reduce(xs.0, fn(acc, n) if n > acc { 95 | true -> n 96 | _ -> acc 97 | }) 98 | 99 | // clamp returns a value bounded by some upper and lower bounds a and b. If the 100 | // given x is between a and b, it is returned as-is; if it is outside the 101 | // bounds, the closer of the two bounds is returned. 102 | fn clamp(x, a, b) if { 103 | x < a -> a 104 | x > b -> b 105 | _ -> x 106 | } 107 | 108 | // mean returns the arithmetic mean, or average, of all given values. If the 109 | // list is empty, mean returns ?. 110 | fn mean(xs) if len(xs) { 111 | 0 -> ? 112 | _ -> sum(xs...) / len(xs) 113 | } 114 | 115 | // median returns the median, or "middle value", of all given values. If there 116 | // is an even number of values given, median computes the mean of the middle 117 | // two values in the list. If the list is empty, median returns ?. 118 | fn median(xs) { 119 | xs := sort(xs) 120 | count := len(xs) 121 | half := int(count / 2) 122 | if 0 { 123 | count -> ? 124 | count % 2 -> (xs.(half - 1) + xs.(half)) / 2 125 | _ -> xs.(half) 126 | } 127 | } 128 | 129 | // stddev returns the population standard deviation computed from the given 130 | // list of values. If the list is empty, stddev returns ?. 131 | fn stddev(xs) if ? != xmean := mean(xs) -> { 132 | xs |> map(fn(x) pow(xmean - x, 2)) |> mean() |> sqrt() 133 | } 134 | 135 | // round takes a number `n` and returns a floating-point number that represents 136 | // `n` round to the nearest `decimals`-th decimal place. For negative values of 137 | // `decimals`, no rounding occurs and `n` is returned exactly. 138 | fn round(n, decimals) { 139 | decimals := int(decimals) |> default(0) 140 | if decimals < 0 { 141 | true -> n 142 | _ -> { 143 | order := pow(10, decimals) 144 | if n >= 0 { 145 | true -> int(n * order + 0.5) / order 146 | _ -> -int(-n * order + 0.5) / order 147 | } 148 | } 149 | } 150 | } 151 | 152 | -------------------------------------------------------------------------------- /lib/path.oak: -------------------------------------------------------------------------------- 1 | // libpath implements utilities for working with UNIX style paths on file 2 | // systems and in URIs 3 | 4 | { 5 | default: default 6 | slice: slice 7 | last: last 8 | filter: filter 9 | reduce: reduce 10 | } := import('std') 11 | { 12 | join: strJoin 13 | split: strSplit 14 | trimEnd: trimEnd 15 | } := import('str') 16 | 17 | // abs? reports whether a path is absolute 18 | fn abs?(path) path.0 = '/' 19 | 20 | // rel? reports whether a path is relative 21 | fn rel?(path) path.0 != '/' 22 | 23 | // internal helper, returns the last occurrence of '/' in a string or 0 if it 24 | // does not appear. 25 | fn _lastSlash(path) if path { 26 | '' -> 0 27 | _ -> { 28 | fn sub(i) if path.(i) { 29 | ?, '/' -> i 30 | _ -> sub(i - 1) 31 | } 32 | sub(len(path) - 1) 33 | } 34 | } 35 | 36 | // dir returns the portion of the a path that represents the directory 37 | // containing it. In effect, this is all but the last part of a path. 38 | fn dir(path) { 39 | path := path |> trimEnd('/') 40 | path |> slice(0, _lastSlash(path)) 41 | } 42 | 43 | // base returns the last element of a path, which is typically the file or 44 | // directory referred to by the path. 45 | fn base(path) { 46 | path := path |> trimEnd('/') 47 | path |> slice(_lastSlash(path) + 1) 48 | } 49 | 50 | // cut returns a [dir, base] pair representing both parts of a path 51 | fn cut(path) { 52 | path := path |> trimEnd('/') 53 | lastSlash := _lastSlash(path) 54 | [ 55 | path |> slice(0, lastSlash) 56 | path |> slice(lastSlash + 1) 57 | ] 58 | } 59 | 60 | // clean returns a path normalized with the following transformations 61 | // 62 | // 1. Remove consecutive slashes not at the beginning 63 | // 2. Remove '.' 64 | // 3. Remove '..' and the (parent) directory right before it, if such parent 65 | // directory is in the path 66 | fn clean(path) { 67 | rooted := path.0 = '/' 68 | cleaned := path |> 69 | strSplit('/') |> 70 | reduce([], fn(stack, part) if part { 71 | // remove consecutive slashes and '.' 72 | '', '.' -> stack 73 | // '..' should pop a dir if available 74 | '..' -> if stack |> last() { 75 | ?, '..' -> stack << part 76 | _ -> stack |> slice(0, len(stack) - 1) 77 | } 78 | _ -> stack << part 79 | }) |> strJoin('/') 80 | if rooted { 81 | true -> '/' << cleaned 82 | _ -> cleaned 83 | } 84 | } 85 | 86 | // join joins multiple paths together into a single valid cleaned path 87 | fn join(parts...) parts |> reduce('', fn(base, path) if base { 88 | // if we simply return `path`, path will be used as `base` next iteration 89 | // which might mutate `path`. 90 | '' -> '' << path 91 | _ -> base << '/' << path 92 | }) |> clean() 93 | 94 | // split returns a list of each element of the path, ignoring the trailing 95 | // slash. If the path is absolute, the first item is an empty string. 96 | fn split(path) if path |> trimEnd('/') { 97 | '' -> [] 98 | _ -> path |> strSplit('/') |> filter(fn(s) s != '') 99 | } 100 | 101 | // resolve takes a path and returns an equivalent cleaned, absolute path, using 102 | // the given base path as the root, or using the current working directory if 103 | // no base path is given. 104 | fn resolve(path, base) if abs?(path) { 105 | true -> clean(path) 106 | _ -> join(base |> default(env().PWD), path) 107 | } 108 | 109 | -------------------------------------------------------------------------------- /lib/random.oak: -------------------------------------------------------------------------------- 1 | // librandom implements utilities for working with pseudorandom sources of 2 | // randomness. 3 | // 4 | // librandom functions source rand() for randomness and are not suitable for 5 | // security-sensitive work. For such code, use srand() for secure randomness or 6 | // the 'crypto' standard library. 7 | 8 | { 9 | Pi: Pi 10 | E: E 11 | sqrt: sqrt 12 | } := import('math') 13 | 14 | // boolean returns either true or false with equal probability 15 | fn boolean rand() > 0.5 16 | 17 | // integer returns an integer in the range [min, max) with uniform probability 18 | fn integer(min, max) number(int(min), int(max)) |> int() 19 | 20 | // number returns a floating point number in the range [min, max) with uniform 21 | // probability 22 | fn number(min, max) { 23 | if max = ? -> [min, max] <- [0, min] 24 | min + rand() * (max - min) 25 | } 26 | 27 | // choice returns an item from the given list, with each item having equal 28 | // probability of being selected on any given call 29 | fn choice(list) list.(integer(0, len(list))) 30 | 31 | // sample from a standard normal distribution: µ = 0, σ = 1 32 | fn normal { 33 | u := 1 - rand() 34 | v := 2 * Pi * rand() 35 | sqrt(-2 * log(E, u)) * cos(v) 36 | } 37 | 38 | -------------------------------------------------------------------------------- /lib/sort.oak: -------------------------------------------------------------------------------- 1 | // libsort implements efficient list sorting algorithms 2 | 3 | { 4 | default: default 5 | identity: id 6 | map: map 7 | clone: clone 8 | } := import('std') 9 | 10 | // sort! sorts items in the list `xs` by each item's `pred` value, using the 11 | // Hoare partitioning strategy. If `pred` is not given, each item is sorted by 12 | // its own value. It mutates the original list for efficiency. If mutation is 13 | // not desired, use sort below. 14 | fn sort!(xs, pred) { 15 | pred := default(pred, id) 16 | 17 | vpred := xs |> map(pred) 18 | 19 | fn partition(xs, lo, hi) { 20 | pivot := vpred.(lo) 21 | fn lsub(i) if vpred.(i) < pivot { 22 | true -> lsub(i + 1) 23 | _ -> i 24 | } 25 | fn rsub(j) if vpred.(j) > pivot { 26 | true -> rsub(j - 1) 27 | _ -> j 28 | } 29 | fn sub(i, j) { 30 | i := lsub(i) 31 | j := rsub(j) 32 | if i < j { 33 | false -> j 34 | _ -> { 35 | tmp := xs.(i) 36 | tmpPred := vpred.(i) 37 | xs.(i) := xs.(j) 38 | xs.(j) := tmp 39 | vpred.(i) := vpred.(j) 40 | vpred.(j) := tmpPred 41 | 42 | sub(i + 1, j - 1) 43 | } 44 | } 45 | } 46 | sub(lo, hi) 47 | } 48 | 49 | fn quicksort(xs, lo, hi) if len(xs) { 50 | 0, 1 -> xs 51 | _ -> if lo < hi { 52 | false -> xs 53 | _ -> { 54 | p := partition(xs, lo, hi) 55 | quicksort(xs, lo, p) 56 | quicksort(xs, p + 1, hi) 57 | } 58 | } 59 | } 60 | 61 | quicksort(xs, 0, len(xs) - 1) 62 | } 63 | 64 | // sort returns a copy of `xs` that is sorted by `pred`, or by each item's 65 | // value if `pred` is not given. If the performance cost of a copy is not 66 | // desirable, use sort!. 67 | fn sort(xs, pred) xs |> clone() |> sort!(pred) 68 | 69 | -------------------------------------------------------------------------------- /lib/str.oak: -------------------------------------------------------------------------------- 1 | // libstr is the core string library for Oak. 2 | // 3 | // It provides a set of utility functions for working with strings and data 4 | // encoded in strings in Oak programs. 5 | 6 | { 7 | default: default 8 | slice: slice 9 | take: take 10 | takeLast: takeLast 11 | reduce: reduce 12 | } := import('std') 13 | 14 | // checkRange is a higher-order function that returns a function `checker`, 15 | // which reports whether a given char is within the range [lo, hi], inclusive. 16 | fn checkRange(lo, hi) fn checker(c) { 17 | p := codepoint(c) 18 | lo <= p & p <= hi 19 | } 20 | 21 | // upper? reports whether a given char is an uppercase ASCII letter 22 | fn upper?(c) c >= 'A' & c <= 'Z' 23 | // lower? reports whether a given char is a lowercase ASCII letter 24 | fn lower?(c) c >= 'a' & c <= 'z' 25 | // digit? reports whether a given char is an ASCII digit 26 | fn digit?(c) c >= '0' & c <= '9' 27 | // space? reports whether a given char is an ASCII whitespace 28 | fn space?(c) if c { 29 | ' ', '\t', '\n', '\r', '\f' -> true 30 | _ -> false 31 | } 32 | // letter? reports whether a given char is an ASCII letter 33 | fn letter?(c) upper?(c) | lower?(c) 34 | // word? reports whether a given char is a letter or a digit 35 | fn word?(c) letter?(c) | digit?(c) 36 | 37 | // join concatenates together a list of strings into a single string, where 38 | // each original string in the list is separated by `joiner`. Joiner is the 39 | // empty string by default. 40 | fn join(strings, joiner) { 41 | joiner := default(joiner, '') 42 | if len(strings) { 43 | 0 -> '' 44 | _ -> strings |> slice(1) |> reduce(strings.0, fn(a, b) a + joiner + b) 45 | } 46 | } 47 | 48 | // startsWith? reports whether a string starts with the substring `prefix`. 49 | fn startsWith?(s, prefix) s |> take(len(prefix)) = prefix 50 | 51 | // endsWith? reports whether a string ends with the substring `suffix`. 52 | fn endsWith?(s, suffix) s |> takeLast(len(suffix)) = suffix 53 | 54 | // _matchesAt? is an internal helper that reports whether a given string `s` 55 | // contains the substring `substr` at index `idx`. It performs this comparison 56 | // efficiently, without unnecessary copying compared to other approaches. 57 | fn _matchesAt?(s, substr, idx) if len(substr) { 58 | 0 -> true 59 | 1 -> s.(idx) = substr 60 | _ -> { 61 | max := len(substr) 62 | fn sub(i) if i { 63 | max -> true 64 | _ -> if s.(idx + i) { 65 | substr.(i) -> sub(i + 1) 66 | _ -> false 67 | } 68 | } 69 | sub(0) 70 | } 71 | } 72 | 73 | // indexOf returns the first index at which the given substring `substr` 74 | // appears in the string `s`. If the substring does not exist, it returns -1. 75 | fn indexOf(s, substr) { 76 | max := len(s) - len(substr) 77 | fn sub(i) if _matchesAt?(s, substr, i) { 78 | true -> i 79 | _ -> if i < max { 80 | true -> sub(i + 1) 81 | _ -> -1 82 | } 83 | } 84 | sub(0) 85 | } 86 | 87 | // rindexOf returns the last index at which the given substring `substr` 88 | // appears in the string `s`. If the substring does not exist, it returns -1. 89 | fn rindexOf(s, substr) { 90 | max := len(s) - len(substr) 91 | fn sub(i) if _matchesAt?(s, substr, i) { 92 | true -> i 93 | _ -> if i > 0 { 94 | true -> sub(i - 1) 95 | _ -> -1 96 | } 97 | } 98 | sub(max) 99 | } 100 | 101 | // contains? reports whether the string `s` contains the substring `substr`. 102 | fn contains?(s, substr) indexOf(s, substr) >= 0 103 | 104 | // cut splits the given string at most once by the given separator substring. 105 | // If the separator is found in s, it returns the substring before and after 106 | // the separator as the list [before, after]. If the separator is not found, it 107 | // returns [s, '']. 108 | fn cut(s, sep) if idx := indexOf(s, sep) { 109 | -1 -> [s, ''] 110 | _ -> [ 111 | s |> slice(0, idx) 112 | s |> slice(idx + len(sep)) 113 | ] 114 | } 115 | 116 | // lower returns a string where any uppercase letter in `s` has been down-cased. 117 | fn lower(s) s |> reduce('', fn(acc, c) if upper?(c) { 118 | true -> acc << char(codepoint(c) + 32) 119 | _ -> acc << c 120 | }) 121 | 122 | // upper returns a string where any lowercase letter in `s` has been up-cased. 123 | fn upper(s) s |> reduce('', fn(acc, c) if lower?(c) { 124 | true -> acc << char(codepoint(c) - 32) 125 | _ -> acc << c 126 | }) 127 | 128 | fn _replaceNonEmpty(s, old, new) { 129 | lold := len(old) 130 | lnew := len(new) 131 | fn sub(acc, i) if _matchesAt?(acc, old, i) { 132 | true -> sub( 133 | slice(acc, 0, i) + new + slice(acc, i + lold) 134 | i + lnew 135 | ) 136 | _ -> if i < len(acc) { 137 | true -> sub(acc, i + 1) 138 | _ -> acc 139 | } 140 | } 141 | sub(s, 0) 142 | } 143 | 144 | // replace returns a string where all occurrences of the substring `old` has 145 | // been replaced by `new` in the string `s`. It does nothing for empty strings. 146 | fn replace(s, old, new) if old { 147 | '' -> s 148 | _ -> _replaceNonEmpty(s, old, new) 149 | } 150 | 151 | fn _splitNonEmpty(s, sep) { 152 | coll := [] 153 | lsep := len(sep) 154 | fn sub(acc, i, last) if _matchesAt?(acc, sep, i) { 155 | true -> { 156 | coll << slice(acc, last, i) 157 | sub(acc, i + lsep, i + lsep) 158 | } 159 | _ -> if i < len(acc) { 160 | true -> sub(acc, i + 1, last) 161 | _ -> coll << slice(acc, last) 162 | } 163 | } 164 | sub(s, 0, 0) 165 | } 166 | 167 | // split splits the string `s` by every occurrence of the substring `sep` in 168 | // it, and returns the result as a list of strings. If `sep` is not specified, 169 | // split returns a list of every character in the string in order. 170 | fn split(s, sep) if sep { 171 | ?, '' -> s |> reduce([], fn(acc, c) acc << c) 172 | _ -> _splitNonEmpty(s, sep) 173 | } 174 | 175 | // helper: repeat the string `pad` until it reaches exactly `n` characters. 176 | fn _extend(pad, n) { 177 | times := int(n / len(pad)) 178 | part := n % len(pad) 179 | 180 | fn sub(base, i) if i { 181 | 0 -> base << slice(pad, 0, part) 182 | _ -> sub(base << pad, i - 1) 183 | } 184 | sub('', times) 185 | } 186 | 187 | // padStart prepends the string s with one or more repetitions of pad until the 188 | // total string is at least n characters long. If len(s) > n, it returns s. 189 | fn padStart(s, n, pad) if len(s) >= n { 190 | true -> s 191 | _ -> _extend(pad, n - len(s)) << s 192 | } 193 | 194 | // padEnd appends one or more repetitions of pad to the string s until the 195 | // total string is at least n characters long. If len(s) > n, it returns s. 196 | fn padEnd(s, n, pad) if len(s) >= n { 197 | true -> s 198 | _ -> s + _extend(pad, n - len(s)) 199 | } 200 | 201 | fn _trimStartSpace(s) { 202 | fn subStart(i) if space?(s.(i)) { 203 | true -> subStart(i + 1) 204 | _ -> i 205 | } 206 | firstNonSpace := subStart(0) 207 | slice(s, firstNonSpace) 208 | } 209 | 210 | fn _trimStartNonEmpty(s, prefix) { 211 | max := len(s) 212 | lpref := len(prefix) 213 | fn sub(i) if i < max { 214 | true -> if _matchesAt?(s, prefix, i) { 215 | true -> sub(i + lpref) 216 | _ -> i 217 | } 218 | _ -> i 219 | } 220 | idx := sub(0) 221 | slice(s, idx) 222 | } 223 | 224 | // trimStart removes any (potentially repeated) occurrences of the string 225 | // `prefix` from the beginning of string `s` 226 | fn trimStart(s, prefix) if prefix { 227 | '' -> s 228 | ? -> _trimStartSpace(s) 229 | _ -> _trimStartNonEmpty(s, prefix) 230 | } 231 | 232 | fn _trimEndSpace(s) { 233 | fn subEnd(i) if space?(s.(i)) { 234 | true -> subEnd(i - 1) 235 | _ -> i 236 | } 237 | lastNonSpace := subEnd(len(s) - 1) 238 | slice(s, 0, lastNonSpace + 1) 239 | } 240 | 241 | fn _trimEndNonEmpty(s, suffix) { 242 | lsuf := len(suffix) 243 | fn sub(i) if i > -1 { 244 | true -> if _matchesAt?(s, suffix, i - lsuf) { 245 | true -> sub(i - lsuf) 246 | _ -> i 247 | } 248 | _ -> i 249 | } 250 | idx := sub(len(s)) 251 | slice(s, 0, idx) 252 | } 253 | 254 | // trimEnd removes any (potentially repeated) occurrences of the string 255 | // `suffix` from the end of string `s` 256 | fn trimEnd(s, suffix) if suffix { 257 | '' -> s 258 | ? -> _trimEndSpace(s) 259 | _ -> _trimEndNonEmpty(s, suffix) 260 | } 261 | 262 | // trim removes any (potentially repeated) ocucrrences of the string `part` 263 | // from either end of the string `s`. If `part` is not specified, trim removes 264 | // all whitespace from either end of the string `s`. 265 | fn trim(s, part) s |> trimStart(part) |> trimEnd(part) 266 | 267 | -------------------------------------------------------------------------------- /lib/test.oak: -------------------------------------------------------------------------------- 1 | // libtest is a unit testing library for Oak 2 | 3 | { 4 | default: default 5 | map: map 6 | each: each 7 | every: every 8 | filter: filter 9 | } := import('std') 10 | { 11 | split: split 12 | join: join 13 | } := import('str') 14 | { 15 | printf: printf 16 | } := import('fmt') 17 | debug := import('debug') 18 | 19 | // new constructs a new test suite, named `title` 20 | // 21 | // Methods: 22 | // 23 | // fn eq(name, result, expect) asserts that a test named `name` returned the 24 | // result `result`, and should expect `expect`. 25 | // fn skip(name, result, expect) ignores the result of this test for reporting 26 | // fn reportFailed() only reports to the console any failed tests 27 | // fn report() reports the result of all tests to the console 28 | // fn exit() exits the program with a non-zero exit code 29 | // if not all tests passed. 30 | fn new(title) { 31 | Tests := [] 32 | Skipped := [] 33 | 34 | fn red(s) '' + s + '' 35 | fn green(s) '' + s + '' 36 | 37 | fn reportTests(tests) { 38 | tests |> each(fn(test) { 39 | { 40 | name: name 41 | passed?: p? 42 | result: result 43 | expect: expect 44 | } := test 45 | 46 | printf( 47 | ' {{ 0 }} {{ 1 }}' 48 | if p? { 49 | true -> green('✔') 50 | _ -> red('✘') 51 | } 52 | name 53 | ) 54 | 55 | fn printIndentedDebug(x, indent) { 56 | debug.inspect(x) |> 57 | split('\n') |> 58 | map(fn(line, i) if i { 0 -> line, _ -> indent + line }) |> 59 | join('\n') 60 | } 61 | 62 | if !p? -> { 63 | printf('\texpected: {{ 0 }}', printIndentedDebug(expect, '\t ')) 64 | printf('\t result: {{ 0 }}', printIndentedDebug(result, '\t ')) 65 | } 66 | }) 67 | } 68 | 69 | fn reportAggregate { 70 | failedTests := Tests |> filter(fn(t) !t.passed?) 71 | if len(failedTests) { 72 | 0 -> printf('All {{ 0 }} tests passed.', len(Tests)) 73 | _ -> printf('{{ 0 }} / {{ 1 }} tests passed.', len(Tests) - len(failedTests), len(Tests)) 74 | } 75 | if skipped := len(Skipped) { 76 | 0 -> ? 77 | _ -> printf('{{ 0 }} tests skipped.', skipped) 78 | } 79 | } 80 | 81 | self := { 82 | eq: fn(name, result, expect) Tests << { 83 | name: name 84 | passed?: result = expect 85 | result: result 86 | expect: expect 87 | } 88 | approx: fn(name, result, expect, epsilon) { 89 | epsilon := epsilon |> default(0.00000001) 90 | fn similar?(a, b) if type(a) { 91 | :list -> a |> every(fn(_, i) similar?(a.(i), b.(i))) 92 | :object -> a |> keys() |> every(fn(k) similar?(a.(k), b.(k))) 93 | _ -> a > b - epsilon & a < b + epsilon 94 | } 95 | Tests << { 96 | name: name 97 | passed?: similar?(result, expect) 98 | result: result 99 | expect: expect 100 | } 101 | } 102 | assert: fn(name, result) self.eq(name, result, true) 103 | skip: fn(name, result, expect) Skipped << { name: name } 104 | reportFailed: fn { 105 | if failedTests := Tests |> filter(fn(t) !t.passed?) { 106 | [] -> ? 107 | _ -> { 108 | printf('Failed {{ 0 }} tests:', title) 109 | failedTests |> reportTests() 110 | } 111 | } 112 | reportAggregate() 113 | } 114 | report: fn { 115 | printf('{{ 0 }} tests:', title) 116 | Tests |> reportTests() 117 | reportAggregate() 118 | } 119 | exit: fn { 120 | exit(if Tests |> filter(fn(t) !t.passed?) { 121 | [] -> 0 122 | _ -> 1 123 | }) 124 | } 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | 5 | func main() { 6 | if runPackFile() { 7 | return 8 | } 9 | 10 | if len(os.Args) > 1 { 11 | arg := os.Args[1] 12 | if isCommand := performCommandIfExists(arg); !isCommand { 13 | runFile(arg) 14 | } 15 | return 16 | } 17 | 18 | if isStdinReadable() { 19 | runStdin() 20 | return 21 | } 22 | 23 | runRepl() 24 | } 25 | -------------------------------------------------------------------------------- /samples/fib.oak: -------------------------------------------------------------------------------- 1 | // fibonacci sequence 2 | 3 | { 4 | println: println 5 | range: range 6 | map: map 7 | } := import('std') 8 | 9 | fn fibNaive(n) if n <= 1 { 10 | true -> 1 11 | _ -> fibNaive(n - 1) + fibNaive(n - 2) 12 | } 13 | 14 | fn fibFast(n) { 15 | fn sub(n, a, b) if n <= 1 { 16 | true -> b 17 | _ -> sub(n - 1, b, a + b) 18 | } 19 | sub(n, 1, 1) 20 | } 21 | 22 | println('naive:', range(20) |> map(fibNaive)) 23 | println('fast:', range(20) |> map(fibFast)) 24 | 25 | -------------------------------------------------------------------------------- /samples/fileserver.oak: -------------------------------------------------------------------------------- 1 | // basic file server using libhttp 2 | 3 | std := import('std') 4 | fmt := import('fmt') 5 | http := import('http') 6 | 7 | Port := 9990 8 | 9 | server := http.Server() 10 | 11 | with server.route('/hello/:name') fn(params) fn(req, end) if req.method { 12 | 'GET' -> end({ 13 | status: 200 14 | body: fmt.format('Hello, {{ 0 }}!', std.default(params.name, 'World')) 15 | }) 16 | _ -> end(http.MethodNotAllowed) 17 | } 18 | 19 | with server.route('/*staticPath') fn(params) { 20 | http.handleStatic('./' + params.staticPath) 21 | } 22 | 23 | with server.route('/') fn(params) fn(req, end) if req.method { 24 | 'GET' -> end({ 25 | status: 200 26 | body: 'Welcome to Oak!' 27 | }) 28 | _ -> end(http.MethodNotAllowed) 29 | } 30 | 31 | // start server 32 | server.start(Port) 33 | fmt.printf('Static server running at port {{ 0 }}', Port) 34 | 35 | -------------------------------------------------------------------------------- /samples/fizzbuzz.oak: -------------------------------------------------------------------------------- 1 | // fizzbuzz 2 | 3 | std := import('std') 4 | 5 | fn fizzbuzz(n) if [n % 3, n % 5] { 6 | [0, 0] -> 'FizzBuzz' 7 | [0, _] -> 'Fizz' 8 | [_, 0] -> 'Buzz' 9 | _ -> string(n) 10 | } 11 | 12 | std.range(1, 101) |> std.each(fn(n) { 13 | std.println(fizzbuzz(n)) 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /samples/fs.oak: -------------------------------------------------------------------------------- 1 | // more user-friendly filesystem APIs 2 | 3 | { 4 | println: println 5 | } := import('std') 6 | { 7 | upper: upper 8 | lower: lower 9 | } := import('str') 10 | { 11 | readFile: readFile 12 | writeFile: writeFile 13 | } := import('fs') 14 | 15 | println('Reading README') 16 | if readme := readFile('./README.md') { 17 | ? -> println('sync read: failed') 18 | _ -> println(upper(readme)) 19 | } 20 | with readFile('./README.md') fn(file) if file { 21 | ? -> println('async read: failed') 22 | _ -> println(lower(file)) 23 | } 24 | 25 | println('Writing trash file to /tmp/trash') 26 | Trash := 'So trashy!\n' 27 | if writeFile('/tmp/trash-sync', Trash) { 28 | ? -> println('sync write: failed') 29 | } 30 | with writeFile('/tmp/trash-async', Trash) fn(end) if end { 31 | ? -> println('async write: failed') 32 | } 33 | 34 | -------------------------------------------------------------------------------- /samples/hello.oak: -------------------------------------------------------------------------------- 1 | // The first Oak program 2 | 3 | std := { 4 | println: fn(x) print(string(x) + '\n') 5 | } 6 | 7 | fn say() std.println('Hello, World!') 8 | fn bye() { 9 | std.println('Goodbye!') 10 | } 11 | 12 | // basic line comment 13 | if 1 + 2 { // another comment! 14 | 3 -> say() 15 | } 16 | 17 | bye() 18 | 19 | -------------------------------------------------------------------------------- /samples/lists.oak: -------------------------------------------------------------------------------- 1 | // exercising list iterator functions 2 | 3 | { 4 | println: println 5 | 6 | range: range 7 | slice: slice 8 | reverse: reverse 9 | map: map 10 | each: each 11 | filter: filter 12 | reduce: reduce 13 | } := import('std') 14 | 15 | { 16 | printf: printf 17 | } := import('fmt') 18 | 19 | nums := range(1, 11) 20 | 21 | // slice 22 | printf('slice: {{ 0 }}', nums |> slice(3, 7)) 23 | 24 | // reverse 25 | printf('reverse: {{ 0 }}', nums |> reverse()) 26 | 27 | // map 28 | printf('map: {{ 0 }}', nums |> map(fn(n) n * n)) 29 | 30 | // each 31 | println('each:') 32 | nums |> each(fn(n) println(n)) 33 | 34 | // filter 35 | printf('filter: {{ 0 }}', nums |> filter(fn even?(n) n % 2 = 0)) 36 | 37 | // reduce (sum) 38 | sum := fn(xs) xs |> reduce(0, fn(a, b) a + b) 39 | printf('reduce (sum): {{ 0 }}', sum(nums)) 40 | 41 | -------------------------------------------------------------------------------- /samples/math.oak: -------------------------------------------------------------------------------- 1 | // exercising math functions 2 | 3 | { 4 | println: println 5 | } := import('std') 6 | 7 | { 8 | printf: printf 9 | } := import('fmt') 10 | 11 | { 12 | sum: sum 13 | min: min 14 | max: max 15 | } := import('math') 16 | 17 | { 18 | sort: sort 19 | } := import('sort') 20 | 21 | nums := [1, 2, 6, 35, -24, 100, -2] 22 | 23 | printf('sum: {{ 0 }}', sum(nums...)) 24 | printf('min: {{ 0 }}', min(nums...)) 25 | printf('max: {{ 0 }}', max(nums...)) 26 | 27 | // sorting 28 | printf('sorted: {{ 0 }}', sort(nums)) 29 | 30 | -------------------------------------------------------------------------------- /samples/raw-fs-async.oak: -------------------------------------------------------------------------------- 1 | // Raw file operations with the asynchronous native file API 2 | // creates a readme.lower.md that's an all-lowercase version of the README 3 | 4 | { 5 | println: println 6 | } := import('std') 7 | { 8 | printf: printf 9 | } := import('fmt') 10 | { 11 | lower: lower 12 | } := import('str') 13 | 14 | ReadBuf := 4096 15 | 16 | // write the readme.lower 17 | fn writeLower(text, done) with open('./readme.lower.md') fn(evt) if evt.type { 18 | :error -> printf('Could not open readme: {{ 0 }}', evt.error) 19 | _ -> with write(fd := evt.fd, 0, lower(text)) fn(evt) { 20 | if evt.type { 21 | :error -> printf('Could not write readme: {{ 0 }}', evt.error) 22 | } 23 | close(fd, done) 24 | } 25 | } 26 | 27 | // read the README 28 | with open('./README.md') fn(evt) if evt.type { 29 | :error -> printf('Could not open README: {{ 0 }}', evt.error) 30 | _ -> with read(fd := evt.fd, 0, ReadBuf) fn(evt) { 31 | if evt.type { 32 | :error -> printf('Could not read README: {{ 0 }}', evt.error) 33 | _ -> with writeLower(evt.data) fn { 34 | println('Done!') 35 | } 36 | } 37 | close(fd) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /samples/raw-fs-sync.oak: -------------------------------------------------------------------------------- 1 | // Raw file operations with the synchronous native file API 2 | // creates a readme.lower.md that's an all-lowercase version of the README 3 | 4 | { 5 | println: println 6 | } := import('std') 7 | { 8 | printf: printf 9 | } := import('fmt') 10 | { 11 | lower: lower 12 | } := import('str') 13 | 14 | ReadBuf := 4096 15 | 16 | // write the readme.lower 17 | fn writeLower(text) { 18 | lowerFile := open('./readme.lower.md') 19 | if lowerFile.type { 20 | :error -> printf('Could not open readme: {{ 0 }}', lowerFile.error) 21 | _ -> { 22 | { fd: fd } := lowerFile 23 | writeResult := write(fd, 0, lower(text)) 24 | if writeResult.type { 25 | :error -> printf('Could not write readme: {{ 0 }}', writeResult.error) 26 | } 27 | close(fd) 28 | 29 | println('Done!') 30 | } 31 | } 32 | } 33 | 34 | // read the README 35 | readmeFile := open('./README.md') 36 | if readmeFile.type { 37 | :error -> printf('Could not open README: {{ 0 }}', readmeFile.error) 38 | _ -> { 39 | { fd: fd } := readmeFile 40 | readResult := read(fd, 0, ReadBuf) 41 | if readResult.type { 42 | :error -> printf('Could not read README: {{ 0 }}', readResult.error) 43 | _ -> writeLower(readResult.data) 44 | } 45 | close(fd) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /samples/strings.oak: -------------------------------------------------------------------------------- 1 | // exercising string functions 2 | 3 | { 4 | println: println 5 | } := import('std') 6 | { 7 | join: join 8 | } := import('str') 9 | 10 | println(['one', 'two', 'three'] |> join(', ')) 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/tailcall.oak: -------------------------------------------------------------------------------- 1 | // tail call elimination test 2 | 3 | { 4 | println: println 5 | } := import('std') 6 | 7 | // just counts up from 0, prints every 100k 8 | fn countUp(max) { 9 | fn sub(i) if i { 10 | max -> ? 11 | _ -> if i % 100000 { 12 | 0 -> { 13 | println(i) 14 | sub(i + 1) 15 | } 16 | _ -> sub(i + 1) 17 | } 18 | } 19 | 20 | sub(0) 21 | } 22 | 23 | countUp(1000000) 24 | 25 | -------------------------------------------------------------------------------- /test/cli.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | str := import('str') 3 | cli := import('cli') 4 | 5 | fn run(t) { 6 | // parseArgv 7 | { 8 | // helper so I can call like p('--opts=1 arg1 arg2') 9 | fn p(args) cli.parseArgv(['/bin/oak', 'main.oak'] |> 10 | std.join(args |> str.split(' ') |> std.filter(fn(s) s != ''))) 11 | 12 | 'no args' |> t.eq( 13 | p('') 14 | { 15 | exe: '/bin/oak' 16 | main: 'main.oak' 17 | verb: ? 18 | opts: {} 19 | args: [] 20 | } 21 | ) 22 | 'complex args' |> t.eq( 23 | p('compile --static -O3 --ignore-ub --author thesephist -time today ./a.oak b.oak -- --c.oak') 24 | { 25 | exe: '/bin/oak' 26 | main: 'main.oak' 27 | verb: 'compile' 28 | opts: { 29 | 'O3': true 30 | 'static': true 31 | 'ignore-ub': true 32 | 'author': 'thesephist' 33 | 'time': 'today' 34 | } 35 | args: [ 36 | './a.oak' 37 | 'b.oak' 38 | '--c.oak' 39 | ] 40 | } 41 | ) 42 | 43 | // edge cases 44 | 'position arg followed by flag' |> t.eq( 45 | p(' do-stuff --flag -- pos1 pos2') 46 | { 47 | exe: _ 48 | main: _ 49 | verb: 'do-stuff' 50 | opts: { 51 | flag: true 52 | } 53 | args: ['pos1', 'pos2'] 54 | } 55 | ) 56 | 'no verb, flag in verb position' |> t.eq( 57 | p('--do-thing 2,3 arg') 58 | { 59 | exe: _ 60 | main: _ 61 | verb: ? 62 | opts: { 63 | 'do-thing': '2,3' 64 | } 65 | args: ['arg'] 66 | } 67 | ) 68 | 'no verb, no flag value' |> t.eq( 69 | p('-do-thing') 70 | { 71 | exe: _ 72 | main: _ 73 | verb: ? 74 | opts: { 75 | 'do-thing': true 76 | } 77 | args: [] 78 | } 79 | ) 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /test/crypto.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | str := import('str') 3 | crypto := import('crypto') 4 | 5 | fn run(t) { 6 | // uuid 7 | { 8 | { uuid: uuid } := crypto 9 | 10 | // behavior tests 11 | uuids := std.range(200) |> std.map(uuid) 12 | 13 | // every char should be a hex character or '-' 14 | 'uuid() validity, hexadecimal char set' |> t.assert( 15 | uuids |> with std.every() fn(u) { 16 | u |> str.split() |> with std.every() fn(c) if c { 17 | 'a', 'b', 'c', 'd', 'e', 'f', '-' -> true 18 | _ -> str.digit?(c) 19 | } 20 | } 21 | ) 22 | 23 | // (sort-of) test for uniqueness 24 | 'uuid() validity, rare collisions' |> t.assert( 25 | uuids |> with std.every() fn(u) !(crypto.uuid() |> std.contains?(u)) 26 | ) 27 | 28 | // correct length and formatting 29 | 'uuid() validity, correct string formatting' |> t.assert( 30 | uuids |> with std.every() fn(u) str.split(u) = [ 31 | _, _, _, _, _, _, _, _, '-' 32 | _, _, _, _, '-' 33 | _, _, _, _, '-' 34 | _, _, _, _, '-' 35 | _, _, _, _, _, _, _, _, _, _, _, _ 36 | ] 37 | ) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /test/datetime.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | datetime := import('datetime') 3 | 4 | // helper to generate datetime descriptions 5 | fn T(y, m, d, h, min, s) { 6 | year: y 7 | month: m 8 | day: d 9 | hour: h |> std.default(0) 10 | minute: min |> std.default(0) 11 | second: s |> std.default(0) 12 | } 13 | 14 | Conversions := [ 15 | ['UNIX zero', 0, T(1970, 1, 1), '1970-01-01T00:00:00Z'] 16 | // guess this date ¯\_(ツ)_/¯ 17 | ['October 15, 1998', 908417643, T(1998, 10, 15, 2, 14, 3), '1998-10-15T02:14:03Z'] 18 | // when I moved to the US 19 | ['Big Move', 1248917025.875, T(2009, 7, 30, 1, 23, 45.875), '2009-07-30T01:23:45.875Z'] 20 | // Y2K 21 | ['start of 2000', 946684800, T(2000, 1, 1), '2000-01-01T00:00:00Z'] 22 | // Zero CE 23 | ['January 1, year 1', -62135596800, T(1, 1, 1), '0001-01-01T00:00:00Z'] 24 | // fall of the Western Roman Empire 25 | ['September 4, 476', -47124720000, T(476, 9, 4), '0476-09-04T00:00:00Z'] 26 | // US Declaration of Independence 27 | ['July 4, 1776', -6106060800, T(1776, 7, 4), '1776-07-04T00:00:00Z'] 28 | // Steve Jobs's iPhone announcement 29 | ['January 9, 2007 9:41AM PST', 1168360860, T(2007, 1, 9, 16, 41, 0), '2007-01-09T16:41:00Z'] 30 | 31 | // other strategic dates 32 | ['normal leap year, before Feb', 1074297600, T(2004, 1, 17), '2004-01-17T00:00:00Z'] 33 | ['normal leap year, leap day', 1078012800, T(2004, 2, 29), '2004-02-29T00:00:00Z'] 34 | ['normal leap year, after Feb', 1103587200, T(2004, 12, 21), '2004-12-21T00:00:00Z'] 35 | ['non-leap year (100y)', -5346518400, T(1800, 7, 30), '1800-07-30T00:00:00Z'] 36 | ['non-non-leap year (400y)', 951782400, T(2000, 2, 29), '2000-02-29T00:00:00Z'] 37 | ['far past', -29808864000, T(1025, 5, 25), '1025-05-25T00:00:00Z'] 38 | ['far future', 64055059200, T(3999, 10, 29), '3999-10-29T00:00:00Z'] 39 | ['last day of year before non-non-leap year', 946598400, T(1999, 12, 31), '1999-12-31T00:00:00Z'] 40 | ['last day of year of non-non-leap year', 978220800, T(2000, 12, 31), '2000-12-31T00:00:00Z'] 41 | ['first day of year following leap year', 1483228800, T(2017, 1, 1), '2017-01-01T00:00:00Z'] 42 | 43 | // other strategic times 44 | ['zero hour, minute, second of year', 1609459200, T(2021, 1, 1, 0, 0, 0), '2021-01-01T00:00:00Z'] 45 | ['last hour, minute, second of year', 1640995199, T(2021, 12, 31, 23, 59, 59), '2021-12-31T23:59:59Z'] 46 | ] 47 | 48 | fn run(t) { 49 | // describe, timestamp 50 | { 51 | { 52 | describe: describe 53 | timestamp: timestamp 54 | } := datetime 55 | 56 | Conversions |> with std.each() fn(spec) { 57 | [name, secs, description] := spec 58 | t.eq('describe ' << name 59 | describe(secs), description) 60 | t.eq('timestamp ' << name 61 | timestamp(description), secs) 62 | } 63 | 64 | // round-trip tests on 2k generated times, spaced 71 days and 161 65 | // seconds apart, starting from Jan 1, 1871 (arbitrary picks). 66 | 'random round-trip describe/timestamp' |> t.assert( 67 | std.range(2000) |> 68 | std.map(fn(i) - 3124137600 + 71 * 86400 * i + 161 * i) |> 69 | std.every(fn(secs) secs |> describe() |> timestamp() = secs) 70 | ) 71 | } 72 | 73 | // format, parse 74 | { 75 | { 76 | format: format 77 | parse: parse 78 | timestamp: timestamp 79 | } := datetime 80 | 81 | Conversions |> with std.each() fn(spec) { 82 | [name, secs, description, iso] := spec 83 | t.eq('format ' << name 84 | format(secs), iso) 85 | t.eq('parse ' << name 86 | parse(iso), std.merge({ tzOffset: 0 }, description)) 87 | } 88 | 89 | // edge cases, with tzOffset = 0 ("Z") 90 | 'parse with milliseconds' |> t.eq( 91 | parse('2023-10-21T12:34:56.529Z') 92 | { 93 | year: 2023, month: 10, day: 21 94 | hour: 12, minute: 34, second: 56.529 95 | tzOffset: 0 96 | } 97 | ) 98 | 'format with time zone offset = 0 converts to Z' |> t.eq( 99 | format(946771200) // 2000-01-02T00:00:00+00:00 100 | '2000-01-02T00:00:00Z' 101 | ) 102 | 103 | // parse with time zone offsets 104 | 'parse with time zone offset = 0' |> t.eq( 105 | parse('2000-01-02T00:00:00+00:00') 106 | { 107 | year: 2000, month: 1, day: 2 108 | hour: 0, minute: 0, second: 0 109 | tzOffset: 0 110 | } 111 | ) 112 | 'parse with time zone offset > 0' |> t.eq( 113 | parse('2000-01-02T00:00:00+04:15') 114 | { 115 | year: 2000, month: 1, day: 2 116 | hour: 0, minute: 0, second: 0 117 | tzOffset: 4 * 60 + 15 118 | } 119 | ) 120 | 'parse with time zone offset > 0 and millis' |> t.eq( 121 | parse('2000-01-02T00:00:00.123+04:15') 122 | { 123 | year: 2000, month: 1, day: 2 124 | hour: 0, minute: 0, second: 0.123 125 | tzOffset: 4 * 60 + 15 126 | } 127 | ) 128 | 'parse with time zone offset < 0' |> t.eq( 129 | parse('2000-01-02T00:00:00-04:15') 130 | { 131 | year: 2000, month: 1, day: 2 132 | hour: 0, minute: 0, second: 0 133 | tzOffset: - (4 * 60 + 15) 134 | } 135 | ) 136 | 'parse with time zone offset < 0 and millis' |> t.eq( 137 | parse('2000-01-02T00:00:00.456-04:15') 138 | { 139 | year: 2000, month: 1, day: 2 140 | hour: 0, minute: 0, second: 0.456 141 | tzOffset: - (4 * 60 + 15) 142 | } 143 | ) 144 | 145 | // parse errors 146 | 'parse with nonsense string returns ?' |> t.eq( 147 | parse('2021-nonsense') 148 | ? 149 | ) 150 | 'parse with missing time returns ?' |> t.eq( 151 | parse('2000-01-02') 152 | ? 153 | ) 154 | 'parse with malformed date returns ?' |> t.eq( 155 | parse('20000102T00:00:00Z') 156 | ? 157 | ) 158 | 'parse with malformed time returns ?' |> t.eq( 159 | parse('2000-01-02T123456Z') 160 | ? 161 | ) 162 | 'parse with malformed tzOffset returns ?' |> t.eq( 163 | parse('2000-01-02T00:00:00-0z:00') 164 | ? 165 | ) 166 | 167 | // format with time zone offsets 168 | 'format with time zone offset = 0' |> t.eq( 169 | { 170 | year: 2000, month: 1, day: 2 171 | hour: 0, minute: 0, second: 0 172 | } |> timestamp() |> format(0) 173 | '2000-01-02T00:00:00Z' 174 | ) 175 | 'format with time zone offset > 0' |> t.eq( 176 | { 177 | year: 2000, month: 1, day: 2 178 | hour: 0, minute: 0, second: 0 179 | } |> timestamp() |> format(4 * 60 + 15) 180 | '2000-01-02T04:15:00+04:15' 181 | ) 182 | 'format with time zone offset > 0 and millis' |> t.eq( 183 | { 184 | year: 2000, month: 1, day: 2 185 | hour: 0, minute: 0, second: 0.123 186 | } |> timestamp() |> format(4 * 60 + 15) 187 | '2000-01-02T04:15:00.123+04:15' 188 | ) 189 | 'format with time zone offset < 0' |> t.eq( 190 | { 191 | year: 2000, month: 1, day: 2 192 | hour: 0, minute: 0, second: 0 193 | } |> timestamp() |> format(- (4 * 60 + 15)) 194 | '2000-01-01T19:45:00-04:15' 195 | ) 196 | 'format with time zone offset < 0 and millis' |> t.eq( 197 | { 198 | year: 2000, month: 1, day: 2 199 | hour: 0, minute: 0, second: 0.456 200 | } |> timestamp() |> format(- (4 * 60 + 15)) 201 | '2000-01-01T19:45:00.456-04:15' 202 | ) 203 | 204 | // round-trip tests on 200 generated times, spaced 701 days and 2161 205 | // seconds apart, starting from Jan 1, 1871 (arbitrary picks). 206 | 'random round-trip format/parse' |> t.assert( 207 | std.range(200) |> 208 | std.map(fn(i) - 3124137600 + 701 * 86400 * i + 2161 * i) |> 209 | std.every(fn(secs) secs |> format() |> parse() |> datetime.timestamp() = secs) 210 | ) 211 | } 212 | 213 | // leap? 214 | { 215 | leap? := datetime.leap? 216 | 217 | 'multiples of 4' |> t.eq( 218 | [-2024, -52, 48, 1040, 1440, 1972, 2024] |> std.every(leap?) 219 | true 220 | ) 221 | 'multiples of 100' |> t.eq( 222 | [-2100, -300, 200, 1300, 1900, 2100] |> std.some(leap?) 223 | false 224 | ) 225 | 'multiples of 400' |> t.eq( 226 | [-2400, -400, 400, 1600, 2000, 2400] |> std.every(leap?) 227 | true 228 | ) 229 | 'non-leap years' |> t.eq( 230 | [-333, -17, 1, 210, 627, 2013, 2021] |> std.some(leap?) 231 | false 232 | ) 233 | } 234 | } 235 | 236 | -------------------------------------------------------------------------------- /test/fmt.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | fmt := import('fmt') 3 | 4 | fn run(t) { 5 | // format 6 | { 7 | f := fmt.format 8 | 9 | 'format empty string' |> t.eq( 10 | '' |> f() 11 | '' 12 | ) 13 | 'format string literally' |> t.eq( 14 | 'String literal' |> f() 15 | 'String literal' 16 | ) 17 | '1 interpolated variable' |> t.eq( 18 | 'One {{ 0 }} Three' |> f('Two') 19 | 'One Two Three' 20 | ) 21 | 'interpolation as whole string' |> t.eq( 22 | '{{ 0 }}' |> f('First!') 23 | 'First!' 24 | ) 25 | 'multiple interpolated variables' |> t.eq( 26 | 'One {{ 0 }} Three {{ 2 }}::{{ 1 }}' |> f('Two', 'Four', 'Five') 27 | 'One Two Three Five::Four' 28 | ) 29 | 'a variable interpolated multiple times' |> t.eq( 30 | 'One {{ 0 }} -- Two {{ 0 }}' |> f('hi') 31 | 'One hi -- Two hi' 32 | ) 33 | 'non-string interpolated variables' |> t.eq( 34 | 'abc {{ 0 }} xyz {{ 1 }}' |> f(:atoms, 100) 35 | 'abc atoms xyz 100' 36 | ) 37 | 'composite interpolated values' |> t.eq( 38 | 'debug -> {{ 0 }}' |> f({ name: 'Linus' }) 39 | 'debug -> {name: \'Linus\'}' 40 | ) 41 | 'non-given variables show as ?' |> t.eq( 42 | 'Hello, {{ 0 }}!' |> f() 43 | 'Hello, ?!' 44 | ) 45 | 'non-given indices become empty' |> t.eq( 46 | 'Hello, {{ }}!' |> f('World!') 47 | 'Hello, !' 48 | ) 49 | 'interpolation without surrounding space' |> t.eq( 50 | '{{1}}, {{0}}!' |> f('World', 'Hello') 51 | 'Hello, World!' 52 | ) 53 | 'interpolation with extra surrounding space' |> t.eq( 54 | '{{ 1 }}, {{\t0\t}}!' |> f('World', 'Hello') 55 | 'Hello, World!' 56 | ) 57 | 'formatting in \'{}\' to escape it' |> t.eq( 58 | 'Format using \'{{0}}\'' |> f('{}') 59 | 'Format using \'{}\'' 60 | ) 61 | 'formatting in format strings do not cause weird behavior' |> t.eq( 62 | '1 {{ 0 }} 2 {{ 1 }}' |> f('{{', '{{ 0 }}') 63 | '1 {{ 2 {{ 0 }}' 64 | ) 65 | 66 | // named keys 67 | '1 named variable' |> t.eq( 68 | 'One {{ two }} Three' |> f({ two: 'Two' }) 69 | 'One Two Three' 70 | ) 71 | 'multiple named variables' |> t.eq( 72 | 'One {{ two }} Three {{ five }}::{{ four }}' |> f({ 73 | two: 'Two' 74 | four: 'Four' 75 | five: 'Five' 76 | }) 77 | 'One Two Three Five::Four' 78 | ) 79 | 'a named variable interpolated multiple times' |> t.eq( 80 | 'One {{ x }} -- Two {{ x }}' |> f({ x: 'hi' }) 81 | 'One hi -- Two hi' 82 | ) 83 | 'non-string named variables' |> t.eq( 84 | 'abc {{ atom }} xyz {{ number }}' |> f({ 85 | atom: :atoms 86 | number: 100 87 | }) 88 | 'abc atoms xyz 100' 89 | ) 90 | 'composite named variable values' |> t.eq( 91 | 'debug -> {{ user }}' |> f({ 92 | user: { name: 'Linus' } 93 | }) 94 | 'debug -> {name: \'Linus\'}' 95 | ) 96 | 'non-given named variables show as ?' |> t.eq( 97 | 'Hello, {{ name }}!' |> f({}) 98 | 'Hello, ?!' 99 | ) 100 | 'named variables with no given values show as ?' |> t.eq( 101 | 'Hello, {{ name }}!' |> f() 102 | 'Hello, ?!' 103 | ) 104 | 'named variables without surrounding space' |> t.eq( 105 | '{{greeting}}, {{name}}!' |> f({ 106 | greeting: 'Hello' 107 | name: 'World' 108 | }) 109 | 'Hello, World!' 110 | ) 111 | 'named variables with extra surrounding space' |> t.eq( 112 | '{{ greeting }}, {{\tname\t}}!' |> f({ 113 | greeting: 'Hello' 114 | name: 'World' 115 | }) 116 | 'Hello, World!' 117 | ) 118 | 'mixed use of numbered and named variable interpolation' |> t.eq( 119 | '{{ name }} is an {{ desc }} {{ 1 }} {{ 2 }}.' |> f( 120 | { name: 'Oak', desc: :expressive } 121 | 'programming' 122 | 'language' 123 | ) 124 | 'Oak is an expressive programming language.' 125 | ) 126 | 127 | // failure cases 128 | 'using only one brace pair is a no-op' |> t.eq( 129 | 'Hello, { 0 }!' |> f('World!') 130 | 'Hello, { 0 }!' 131 | ) 132 | 'incomplete interpolation opening' |> t.eq( 133 | 'Hello, { 0 }}!' |> f('World!') 134 | 'Hello, { 0 }}!' 135 | ) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/generative/datetime.test.oak: -------------------------------------------------------------------------------- 1 | // Generative tests for datetime timestamp-related functions 2 | // 3 | // This script generates 500k random UNIX timestamps from a very large 4 | // historical range (999 CE - 2500 CE) and ensures the "round-trip behavior": 5 | // it validates that for each date, UNIX timestamp |> describe |> timestamp is 6 | // idempotent. 7 | 8 | { 9 | println: println 10 | range: range 11 | map: map 12 | each: each 13 | } := import('std') 14 | random := import('random') 15 | { 16 | printf: printf 17 | } := import('fmt') 18 | { 19 | SecondsPerDay: SecondsPerDay 20 | describe: describe 21 | timestamp: timestamp 22 | format: format 23 | } := import('datetime') 24 | 25 | Start := -30610224000 26 | End := 16725225600 27 | 28 | 'Generative tests on datetime.(describe, timestamp, format) 29 | \tfrom {{0}} to {{1}}' |> printf(format(Start), format(End)) 30 | 31 | range(500000) |> 32 | map(fn() random.integer(Start, End)) |> 33 | with each() fn(stamp, i) if timestamp(describe(stamp)) { 34 | stamp -> ? 35 | _ -> { 36 | formatted := stamp |> format() 37 | derivedTimestamp := stamp |> describe() |> timestamp() 38 | '#{{0}} did not match: {{1}} {{2}} != {{3}} ({{4}}d off)' |> printf( 39 | i 40 | formatted 41 | stamp 42 | derivedTimestamp 43 | (stamp - derivedTimestamp) / float(SecondsPerDay) 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/generative/random.normal.test.oak: -------------------------------------------------------------------------------- 1 | // Generative tests for statistical correctness of random.normal, which should 2 | // sample from a standard normal distribution 3 | // 4 | // renders a histogram and computes the mean and stddev of the generated 5 | // samples, and in the process also stress-tests debug.histo. 6 | 7 | { 8 | println: println 9 | default: default 10 | range: range 11 | map: map 12 | each: each 13 | loop: loop 14 | filter: filter 15 | } := import('std') 16 | { 17 | join: join 18 | } := import('str') 19 | math := import('math') 20 | fmt := import('fmt') 21 | { 22 | normal: normal 23 | } := import('random') 24 | debug := import('debug') 25 | 26 | N := 100000 27 | xs := range(N) |> map(normal) 28 | debug.histo(xs, { 29 | min: -5 30 | max: 5 31 | bars: 50 32 | label: :end 33 | }) |> println() 34 | fmt.printf('µ = {{0}}, σ = {{1}}' 35 | math.mean(xs) 36 | math.stddev(xs)) 37 | 38 | -------------------------------------------------------------------------------- /test/generative/randomness.test.oak: -------------------------------------------------------------------------------- 1 | // Generative tests for uniformity of randomness sources, rand() and srand() 2 | // 3 | // Also tests some derived randomness sources like random.(boolean, integer, number) 4 | 5 | { 6 | println: println 7 | range: range 8 | map: map 9 | filter: filter 10 | } := import('std') 11 | { 12 | split: split 13 | } := import('str') 14 | { 15 | round: round 16 | } := import('math') 17 | random := import('random') 18 | { 19 | printf: printf 20 | } := import('fmt') 21 | 22 | N := 100000 23 | 24 | 'Generative tests on random.(boolean, integer, number) and srand() 25 | \tchecking distribution uniformity' |> printf() 26 | 27 | fn printPcts(counts) { 28 | counts |> 29 | map(fn(xs) round(len(xs) / N * 100, 4)) |> 30 | with map() fn(pct) printf('{{0}}%\t', pct) 31 | } 32 | 33 | { 34 | println('# boolean()') 35 | bools := range(N) |> map(random.boolean) 36 | [ 37 | bools |> filter(fn(b) b) 38 | bools |> filter(fn(b) !b) 39 | ] |> printPcts() 40 | } 41 | 42 | { 43 | println('# integer(100, 200)') 44 | ints := range(N) |> map(fn() random.integer(100, 200)) 45 | [ 46 | ints |> filter(fn(i) i < 120) 47 | ints |> filter(fn(i) i >= 120 & i < 140) 48 | ints |> filter(fn(i) i >= 140 & i < 160) 49 | ints |> filter(fn(i) i >= 160 & i < 180) 50 | ints |> filter(fn(i) i >= 180) 51 | ] |> printPcts() 52 | } 53 | 54 | { 55 | println('# number(-0.5, 0.5)') 56 | floats := range(N) |> map(fn() random.number(-0.5, 0.5)) 57 | [ 58 | floats |> filter(fn(i) i < -0.3) 59 | floats |> filter(fn(i) i >= -0.3 & i < -0.1) 60 | floats |> filter(fn(i) i >= -0.1 & i < 0.1) 61 | floats |> filter(fn(i) i >= 0.1 & i < 0.3) 62 | floats |> filter(fn(i) i >= 0.3) 63 | ] |> printPcts() 64 | } 65 | 66 | { 67 | println('# choice([:a, :b, :b])') 68 | choices := range(N) |> map(fn() random.choice([:a, :b, :b])) 69 | [ 70 | choices |> filter(fn(x) x = :a) 71 | choices |> filter(fn(x) x = :b) 72 | ] |> printPcts() 73 | } 74 | 75 | { 76 | println('# srand(N)') 77 | // not using N because N may be too large to generate N bytes of securely 78 | // random data 79 | SN := 10000 80 | bytes := srand(SN) |> split() |> map(codepoint) 81 | [ 82 | bytes |> filter(fn(b) b < 32) 83 | bytes |> filter(fn(b) b >= 32 & b < 64) 84 | bytes |> filter(fn(b) b >= 64 & b < 96) 85 | bytes |> filter(fn(b) b >= 96 & b < 128) 86 | bytes |> filter(fn(b) b >= 128 & b < 160) 87 | bytes |> filter(fn(b) b >= 160 & b < 192) 88 | bytes |> filter(fn(b) b >= 192 & b < 224) 89 | bytes |> filter(fn(b) b >= 224) 90 | ] |> 91 | map(fn(xs) round(len(xs) / SN * 100, 4)) |> 92 | with map() fn(pct) printf('{{0}}%\t', pct) 93 | } 94 | 95 | -------------------------------------------------------------------------------- /test/http.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | fmt := import('fmt') 3 | http := import('http') 4 | 5 | fn run(t) { 6 | // query string encodings 7 | { 8 | { 9 | queryEncode: queryEncode 10 | queryDecode: queryDecode 11 | } := http 12 | 13 | [ 14 | // basic 15 | [{}, ''] 16 | [{ a: 'bcd' }, 'a=bcd'] 17 | [ 18 | { results: -24.5, max_tokens: 100 } 19 | 'max_tokens=100&results=-24.5' 20 | { results: '-24.5', max_tokens: '100' } 21 | ] 22 | [ 23 | { query: 'climate change', show_inactives: false } 24 | 'query=climate%20change&show_inactives=false' 25 | { query: 'climate change', show_inactives: 'false' } 26 | ] 27 | 28 | // with nulls, functions, atoms 29 | [ 30 | { empty: '', gone: ?, xyz: 200 } 31 | 'empty=&xyz=200' 32 | { empty: '', xyz: '200' } 33 | ] 34 | [ 35 | { adder: fn(a, b) a + b, type: :fn } 36 | 'type=fn' 37 | { type: 'fn' } 38 | ] 39 | 40 | // with lists and objects 41 | [ 42 | { numbers: [1, 2, 3, 10], count: 4 } 43 | 'count=4&numbers=%5B1%2C2%2C3%2C10%5D' 44 | { numbers: '[1,2,3,10]', count: '4' } 45 | ] 46 | [ 47 | { advancedSearch: [:coupe, '>600', { make: 'Ferrari' }] } 48 | 'advancedSearch=%5B%22coupe%22%2C%22%3E600%22%2C%7B%22make%22%3A%22Ferrari%22%7D%5D' 49 | { advancedSearch: '["coupe",">600",{"make":"Ferrari"}]' } 50 | ] 51 | 52 | // with complex percent-encoding keys and values 53 | [{ 'hey? there': 'how are you?' }, 'hey%3F%20there=how%20are%20you%3F'] 54 | ] |> with std.each() fn(spec) { 55 | [params, encoded, decoded] := spec 56 | decoded := decoded |> std.default(params) 57 | 58 | 'queryEncode "{{0}}"' |> fmt.format(params) |> 59 | t.eq(queryEncode(params), encoded) 60 | 'queryDecode "{{0}}"' |> fmt.format(encoded) |> 61 | t.eq(queryDecode(encoded), decoded) 62 | } 63 | } 64 | 65 | // percent encodings 66 | { 67 | { 68 | percentEncode: encode 69 | percentEncodeURI: encodeURI 70 | percentDecode: decode 71 | } := http 72 | 73 | [ 74 | // basic strings 75 | ['', ''] 76 | ['oaklang.org', 'oaklang.org'] 77 | // numbers 78 | ['(123) 456-7890', '(123)%20456-7890'] 79 | // space ' ' 80 | ['Linus Lee', 'Linus%20Lee'] 81 | 82 | // decoding plus '+' into space ' ' 83 | ['Linus+Lee', 'Linus%2BLee', 'Linus+Lee', :encode] 84 | ['Linus+Lee', 'Linus%2BLee', 'Linus%2BLee', :decode] 85 | ['Linus Lee', 'Linus+Lee', 'Linus+Lee', :decode] 86 | // percent sign 87 | ['A%20B', 'A%2520B'] 88 | ['20% 30%', '20%25%2030%25'] 89 | // special characters that are never escaped 90 | ['-_.!~*\'()', '-_.!~*\'()', '-_.!~*\'()'] 91 | // special characters that are only escaped in URI components 92 | // 93 | // NOTE: this test case excludes the escaped character '+', because 94 | // it's handled specially during decoding in case it represents the 95 | // space character ' '. See above and below instead for that case. 96 | [';,/?:@&=$', '%3B%2C%2F%3F%3A%40%26%3D%24', ';,/?:@&=$'] 97 | 98 | // simple full URL 99 | [ 100 | 'https://thesephist.com/?q=linus' 101 | 'https%3A%2F%2Fthesephist.com%2F%3Fq%3Dlinus' 102 | 'https://thesephist.com/?q=linus' 103 | ] 104 | // complex full URL 105 | [ 106 | 'http://username:password@www.example.com:80/path/to/file.php?foo=316&bar=this+has+spaces#anchor' 107 | 'http%3A%2F%2Fusername%3Apassword%40www.example.com%3A80%2Fpath%2Fto%2Ffile.php%3Ffoo%3D316%26bar%3Dthis%2Bhas%2Bspaces%23anchor' 108 | 'http://username:password@www.example.com:80/path/to/file.php?foo=316&bar=this+has+spaces#anchor' 109 | :encode 110 | ] 111 | [ 112 | 'http://username:password@www.example.com:80/path/to/file.php?foo=316&bar=this has spaces#anchor' 113 | 'http%3A%2F%2Fusername%3Apassword%40www.example.com%3A80%2Fpath%2Fto%2Ffile.php%3Ffoo%3D316%26bar%3Dthis%20has%20spaces%23anchor' 114 | 'http://username:password@www.example.com:80/path/to/file.php?foo=316&bar=this+has+spaces#anchor' 115 | :decode 116 | ] 117 | ] |> with std.each() fn(spec) { 118 | [plain, encoded, uriEncoded, ty] := spec 119 | 120 | uriEncoded := uriEncoded |> std.default(encoded) 121 | ty := ty |> std.default(_) 122 | 123 | if ty = :encode -> { 124 | 'percentEncode "{{0}}"' |> fmt.format(plain) |> 125 | t.eq(encode(plain), encoded) 126 | 'percentEncodeURI "{{0}}"' |> fmt.format(plain) |> 127 | t.eq(encodeURI(plain), uriEncoded) 128 | } 129 | if ty = :decode -> { 130 | 'percentDecode "{{0}}"' |> fmt.format(encoded) |> 131 | t.eq(decode(encoded), plain) 132 | 'percentDecode from URI "{{0}}"' |> fmt.format(uriEncoded) |> 133 | t.eq(decode(uriEncoded), plain) 134 | } 135 | } 136 | } 137 | 138 | // MIME smoke tests 139 | { 140 | { 141 | mimeForPath: mime 142 | } := http 143 | 144 | [ 145 | ['some-unknown-file', 'application/octet-stream'] 146 | 147 | ['index.html', 'text/html; charset=utf-8'] 148 | ['style.css', 'text/css; charset=utf-8'] 149 | ['script.js', 'application/javascript; charset=utf-8'] 150 | 151 | ['image.jpg', 'image/jpeg'] 152 | ['image.jpeg', 'image/jpeg'] 153 | ['image.png', 'image/png'] 154 | ['image.gif', 'image/gif'] 155 | ['image.svg', 'image/svg+xml'] 156 | 157 | ['file.pdf', 'application/pdf'] 158 | ['file.zip', 'application/zip'] 159 | ] |> with std.each() fn(spec) { 160 | [path, mimeType] := spec 161 | 'mimeForPath({{0}})' |> fmt.format(path) |> 162 | t.eq(mime(path), mimeType) 163 | } 164 | } 165 | } 166 | 167 | -------------------------------------------------------------------------------- /test/json.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | fmt := import('fmt') 3 | json := import('json') 4 | 5 | fn run(t) { 6 | // serialize 7 | { 8 | ser := json.serialize 9 | 10 | 'null' |> t.eq( 11 | ser(?) 12 | 'null' 13 | ) 14 | 'empty' |> t.eq( 15 | ser(_) 16 | 'null' 17 | ) 18 | 'empty string' |> t.eq( 19 | ser('') 20 | '""' 21 | ) 22 | 'ordinary string' |> t.eq( 23 | ser('world') 24 | '"world"' 25 | ) 26 | 'string with escapes' |> t.eq( 27 | ser('es"c \\a"pe\nme\t') 28 | '"es\\"c \\\\a\\"pe\\nme\\t"' 29 | ) 30 | 'atoms' |> t.eq( 31 | ser(':_atom_THIS') 32 | '":_atom_THIS"' 33 | ) 34 | 'true' |> t.eq( 35 | ser(true) 36 | 'true' 37 | ) 38 | 'false' |> t.eq( 39 | ser(false) 40 | 'false' 41 | ) 42 | 'integer' |> t.eq( 43 | ser(12) 44 | '12' 45 | ) 46 | 'decimal number' |> t.eq( 47 | ser(3.14) 48 | '3.14' 49 | ) 50 | 'negative number' |> t.eq( 51 | ser(-2.4142) 52 | '-2.4142' 53 | ) 54 | 'function => null' |> t.eq( 55 | ser(fn {}) 56 | 'null' 57 | ) 58 | 'empty list' |> t.eq( 59 | ser([]) 60 | '[]' 61 | ) 62 | 'empty object' |> t.eq( 63 | ser({}) 64 | '{}' 65 | ) 66 | 'ordinary list' |> t.eq( 67 | ser([10, 20, 1, 0, 'hi', :zero]) 68 | '[10,20,1,0,"hi","zero"]' 69 | ) 70 | 'nested list' |> t.eq( 71 | ser([10, [20, 30], [?, _], []]) 72 | '[10,[20,30],[null,null],[]]' 73 | ) 74 | 'ordinary object' |> t.assert( 75 | [ 76 | // object keys, and hence serialized JSON keys, are not 77 | // guaranteed to be in a deterministic order 78 | '{"cd":-4.251,"a":"b"}' 79 | '{"a":"b","cd":-4.251}' 80 | ] |> std.contains?(ser({ a: 'b', cd: -4.251 })) 81 | ) 82 | 'nested object' |> t.assert( 83 | { 84 | serialized := ser([ 85 | 'a' 86 | true 87 | { c: 'd', 'e"': 32.14 } 88 | ['f', {}, ?, -42] 89 | ]) 90 | [ 91 | '["a",true,{"c":"d","e\\"":32.14},["f",{},null,-42]]' 92 | '["a",true,{"e\\"":32.14,"c":"d"},["f",{},null,-42]]' 93 | ] |> std.contains?(serialized) 94 | } 95 | ) 96 | } 97 | 98 | // parse 99 | { 100 | p := json.parse 101 | 102 | 'empty string or whitespace' |> t.eq( 103 | ['', '\n', ' \t '] |> std.map(p) 104 | [:error, :error, :error] 105 | ) 106 | 'null, true, false' |> t.eq( 107 | ['null', 'true', 'false'] |> std.map(p) 108 | [?, true, false] 109 | ) 110 | 'invalid JSON, nearly-keywords' |> t.eq( 111 | ['nul', 'truu', 'fals '] |> std.map(p) 112 | [:error, :error, :error] 113 | ) 114 | 'empty string' |> t.eq( 115 | p('""') 116 | '' 117 | ) 118 | 'ordinary string' |> t.eq( 119 | p('"thing 1 thing 2"') 120 | 'thing 1 thing 2' 121 | ) 122 | 'escaped string' |> t.eq( 123 | p('"es\\"c \\\\a\\"pe\\nme\\t"') 124 | 'es"c \\a"pe\nme\t' 125 | ) 126 | 'interrupted string' |> t.eq( 127 | p('"my"what"') 128 | 'my' 129 | ) 130 | 'ordinary number' |> t.eq( 131 | p('420') 132 | 420 133 | ) 134 | 'negative number' |> t.eq( 135 | p('-69') 136 | -69 137 | ) 138 | 'decimal number' |> t.eq( 139 | p('-59.413') 140 | -59.413 141 | ) 142 | 'interrupted number' |> t.eq( 143 | p('10.1-2') 144 | 10.1 145 | ) 146 | 'invalid number' |> t.eq( 147 | p('1.2.3') 148 | :error 149 | ) 150 | 'empty list' |> t.eq( 151 | ['[]', '[\n]', '[ ]'] |> std.map(p) 152 | [[], [], []] 153 | ) 154 | 'empty object' |> t.eq( 155 | ['{}', '{\n}', '{ }'] |> std.map(p) 156 | [{}, {}, {}] 157 | ) 158 | 'ordinary list' |> t.eq( 159 | p('[1, "two", 30]') 160 | [1, 'two', 30] 161 | ) 162 | 'nested list' |> t.eq( 163 | p('[1, [2, [3]], [4, "five"]]') 164 | [1, [2, [3]], [4, 'five']] 165 | ) 166 | 'ordinary object' |> t.eq( 167 | p('{"a": "bee", "c": [10, 20]}') 168 | { a: 'bee', c: [10, 20] } 169 | ) 170 | 'nested object' |> t.eq( 171 | p('{"a": {"Key": "Value"}}') 172 | { a: { Key: 'Value' } } 173 | ) 174 | 175 | // malformed JSONs that should not parse 176 | } 177 | 178 | // round-trip tests 179 | { 180 | { 181 | serialize: ser 182 | parse: par 183 | } := json 184 | 185 | targets := [ 186 | ? 187 | 100 188 | 'something \n\t\r wild' 189 | { a: 'b', c: -4.251, d: [10, 20, ?] } 190 | [ 191 | 'a', true 192 | { c: 'd', e: 32.14 } 193 | ['f', {}, (), -42] 194 | ] 195 | { 196 | ser: 'de' 197 | '\\': 3 198 | 'esc\\': 'back\\slash' 199 | apple: 'dessert' 200 | x: ['train', false, 'car', true, { x: ['y', 'z'] }] 201 | 32: 'thirty-two' 202 | nothing: ? 203 | } 204 | ] 205 | 206 | targets |> std.each(fn(target) { 207 | fmt.format('2x round-trip {{ 0 }}...', string(target) |> std.take(20)) |> t.eq( 208 | target |> ser() |> par() |> ser() |> par() 209 | target 210 | ) 211 | }) 212 | } 213 | } 214 | 215 | -------------------------------------------------------------------------------- /test/main.oak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oak 2 | // standard library tests 3 | 4 | std := import('std') 5 | test := import('test') 6 | runners := import('runners') 7 | 8 | t := test.new('Oak stdlib') 9 | 10 | runners.Runners |> with std.each() fn(name) { 11 | runner := import(name + '.test') 12 | runner.run(t) 13 | } 14 | 15 | t.reportFailed() 16 | t.exit() 17 | 18 | -------------------------------------------------------------------------------- /test/math.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | math := import('math') 3 | fmt := import('fmt') 4 | 5 | fn run(t) { 6 | // sign, abs 7 | { 8 | { 9 | sign: sign 10 | abs: abs 11 | } := math 12 | 13 | 'sign of 0 and positive numbers' |> t.eq( 14 | [1, 2, 10, 1000] |> std.map(sign) 15 | [1, 1, 1, 1] 16 | ) 17 | 'sign of negative numbers' |> t.eq( 18 | [-1, -2, -10, -1000] |> std.map(sign) 19 | [-1, -1, -1, -1] 20 | ) 21 | 22 | 'absolute value of numbers' |> t.eq( 23 | [-1000, -10, -2, 1, 0, 1, 2, 10, 1000] |> std.map(abs) 24 | [1000, 10, 2, 1, 0, 1, 2, 10, 1000] 25 | ) 26 | } 27 | 28 | // sqrt, hypot 29 | { 30 | { 31 | sqrt: sqrt 32 | hypot: hypot 33 | } := math 34 | 35 | 'sqrt(0)' |> t.eq(sqrt(0), 0) 36 | 'sqrt(1)' |> t.eq(sqrt(1), 1) 37 | 'sqrt(negative number)' |> t.eq(sqrt(-10), ?) 38 | [2, 10, 100, 225, 1000] |> with std.each() fn(n) { 39 | 'sqrt({{0}}) <> pow({{0}}, 0.5)' |> fmt.format(n) |> 40 | t.approx(sqrt(n), pow(n, 0.5)) 41 | } 42 | 43 | Triples := [ 44 | [0, 0, 0] 45 | [3, 4, 5] 46 | [5, 12, 13] 47 | ] 48 | 49 | // single-point 50 | Triples |> with std.each() fn(pt) { 51 | [a, b, c] := pt 52 | 53 | 'hypot({{0}}, {{1}}) = {{2}}' |> 54 | fmt.format(pt...) |> 55 | t.approx(hypot(a, b), c) 56 | 57 | 'hypot(-{{0}}, -{{1}}) = {{2}}' |> 58 | fmt.format(pt...) |> 59 | t.approx(hypot(-a, -b), c) 60 | } 61 | 62 | // double-point 63 | Triples |> with std.each() fn(pt) { 64 | [a0, b0] := [-2, -4] 65 | [a, b, c] := pt 66 | 67 | 'hypot({{0}}, {{1}}, {{2}}, {{3}}) = {{4}}' |> 68 | fmt.format(a0, b0, a0 + a, b0 + b, c) |> 69 | t.approx(hypot(a0, b0, a0 + a, b0 + b), c) 70 | 71 | 'hypot({{0}}, {{1}}, {{2}}, {{3}}) = {{4}}' |> 72 | fmt.format(a0, b0, a0 - a, b0 - b, c) |> 73 | t.approx(hypot(a0, b0, a0 - a, b0 - b), c) 74 | } 75 | } 76 | 77 | // scale 78 | { 79 | { scale: scale } := math 80 | 81 | 'scale t.eq(scale(-10, 0, 10), -1) 82 | 'scale min to [0, 1]' |> t.eq(scale(0, 0, 10), 0) 83 | 'scale max to [0, 1]' |> t.eq(scale(10, 0, 10), 1) 84 | 'scale mid to [0, 1]' |> t.eq(scale(5, 0, 10), 0.5) 85 | 'scale >max to [0, 1]' |> t.eq(scale(20, 0, 10), 2) 86 | 87 | 'scale t.eq(scale(10, 0, -10), -1) 88 | 'scale min to [0, 1] in range < 0' |> t.eq(scale(0, 0, -10), 0) 89 | 'scale max to [0, 1] in range < 0' |> t.eq(scale(-10, 0, -10), 1) 90 | 'scale mid to [0, 1] in range < 0' |> t.eq(scale(-5, 0, -10), 0.5) 91 | 'scale >max to [0, 1] in range < 0' |> t.eq(scale(-20, 0, -10), 2) 92 | 93 | 'scale t.eq(scale(-10, 0, 10, 50, 100), 0) 94 | 'scale min to [50, 100]' |> t.eq(scale(0, 0, 10, 50, 100), 50) 95 | 'scale max to [50, 100]' |> t.eq(scale(10, 0, 10, 50, 100), 100) 96 | 'scale mid to [50, 100]' |> t.eq(scale(5, 0, 10, 50, 100), 75) 97 | 'scale >max to [50, 100]' |> t.eq(scale(20, 0, 10, 50, 100), 150) 98 | 99 | 'scale t.eq(scale(-10, 0, 10, -50, -100), 0) 100 | 'scale min to [-50, -100]' |> t.eq(scale(0, 0, 10, -50, -100), -50) 101 | 'scale max to [-50, -100]' |> t.eq(scale(10, 0, 10, -50, -100), -100) 102 | 'scale mid to [-50, -100]' |> t.eq(scale(5, 0, 10, -50, -100), -75) 103 | 'scale >max to [-50, -100]' |> t.eq(scale(20, 0, 10, -50, -100), -150) 104 | 105 | 'scale to 0' |> t.eq(scale(7, 0, 10, 0, 0), 0) 106 | 'scale to singularity' |> t.eq(scale(7, 0, 10, 12, 12), 12) 107 | } 108 | 109 | // bearing, orient 110 | { 111 | { 112 | bearing: bearing 113 | orient: orient 114 | } := math 115 | 116 | fn angle(t) t * math.Pi / 180 117 | 118 | [ 119 | ['east', 0, [10, 0]] 120 | ['north', 90, [0, 10]] 121 | ['west', 180, [-10, 0]] 122 | ['south', -90, [0, -10]] 123 | ['30deg', 30, [8.66025403, 5]] 124 | ['60deg', 60, [5, 8.66025403]] 125 | ] |> with std.each() fn(spec) { 126 | [dir, th, pt] := spec 127 | [px, py] := pt 128 | 129 | [ 130 | [0, 0] 131 | [2, 5] 132 | [-2, 5] 133 | [-2, -5] 134 | ] |> with std.each() fn(origin) { 135 | [ox, oy] := origin 136 | 'bearing {{0}} from ({{1}}, {{2}})' |> 137 | fmt.format(dir, ox, oy) |> 138 | t.approx(bearing(ox, oy, 10, angle(th)), [ox + px, oy + py]) 139 | } 140 | } 141 | } 142 | 143 | // sum, prod 144 | { 145 | { 146 | sum: sum 147 | prod: prod 148 | } := math 149 | 150 | 'sum of nothing' |> t.eq(sum(), 0) 151 | 'sum of 1' |> t.eq(sum(42), 42) 152 | 'sum of many' |> t.eq(sum(std.range(100)...), 4950) 153 | 154 | 'prod of nothing' |> t.eq(prod(), 1) 155 | 'prod of 1' |> t.eq(prod(42), 42) 156 | 'prod of many' |> t.eq(prod(std.range(1, 11)...), 3628800) 157 | } 158 | 159 | // min, max, clamp 160 | { 161 | { 162 | min: min 163 | max: max 164 | clamp: clamp 165 | } := math 166 | 167 | 'min of empty' |> t.eq(min(), ?) 168 | 'max of empty' |> t.eq(max(), ?) 169 | 170 | 'min of list of 1' |> t.eq(min(-30), -30) 171 | 'max of list of 1' |> t.eq(max(100), 100) 172 | 173 | list := [39, 254, 5, -2, 0, 3] 174 | 'min of list' |> t.eq(min(list...), -2) 175 | 'max of list' |> t.eq(max(list...), 254) 176 | 177 | same := std.range(10) |> std.map(fn 2) 178 | 'min of same' |> t.eq(min(same...), 2) 179 | 'max of same' |> t.eq(max(same...), 2) 180 | 181 | 'clamp when x < a' |> t.eq(clamp(5, 10, 20), 10) 182 | 'clamp when x = a' |> t.eq(clamp(10, 10, 20), 10) 183 | 'clamp when a < x < b' |> t.eq(clamp(12, 10, 20), 12) 184 | 'clamp when x = b' |> t.eq(clamp(20, 10, 20), 20) 185 | 'clamp when x > b' |> t.eq(clamp(50, 10, 20), 20) 186 | 187 | 'clamp string x < a' |> t.eq(clamp('a', 'e', 'g'), 'e') 188 | 'clamp string x = a' |> t.eq(clamp('e', 'e', 'g'), 'e') 189 | 'clamp string a < x < b' |> t.eq(clamp('f', 'e', 'g'), 'f') 190 | 'clamp string x = b' |> t.eq(clamp('g', 'e', 'g'), 'g') 191 | 'clamp string x > b' |> t.eq(clamp('s', 'e', 'g'), 'g') 192 | } 193 | 194 | // mean, median, stddev 195 | { 196 | { 197 | mean: mean 198 | median: median 199 | stddev: stddev 200 | } := math 201 | 202 | 'mean of empty' |> t.eq(mean([]), ?) 203 | 'mean of 1' |> t.eq(mean([10]), 10) 204 | 'mean of many' |> t.eq(mean([1, 3, 5, 7, 12, 20]), 8) 205 | 206 | 'median of empty' |> t.eq(median([]), ?) 207 | 'median of list of 1' |> t.eq(median([10]), 10) 208 | 'median of list of 2' |> t.eq(median([1, 4]), 2.5) 209 | 'median of odd-numbered list' |> t.eq(median([1, 3, 5]), 3) 210 | 'median of even-numbered list' |> t.eq(median([1, 3, 5, 7]), 4) 211 | 'median of unsorted list' |> t.eq(median([7, 1, 5, 3]), 4) 212 | 213 | 'stddev of empty' |> t.eq(stddev([]), ?) 214 | 'stddev of equal samples' |> t.approx(stddev([3, 3, 3, 3, 3]), 0) 215 | 'stddev of many' |> t.approx(stddev([1.5, 2.5, 2.5, 2.75, 3.25, 4.75]), 0.9868932735) 216 | } 217 | 218 | // rounding 219 | { 220 | { 221 | round: round 222 | } := math 223 | 224 | 'round 0' |> t.eq(round(0), 0.0) 225 | [2, 100, -49] |> with std.each() fn(n) { 226 | fmt.format('round integer {{0}}', n) |> t.eq( 227 | round(n) 228 | float(n) 229 | ) 230 | } 231 | 232 | // table test 233 | decimals := [-5, 0, 1, 2, 4, 10] 234 | [ 235 | [2.5, [2.5, 3.0, 2.5, 2.5, 2.5, 2.5]] 236 | [-2.5, [-2.5, -3.0, -2.5, -2.5, -2.5, -2.5]] 237 | [3.141592, [3.141592, 3.0, 3.1, 3.14, 3.1416, 3.141592]] 238 | [0.0021828, [0.0021828, 0.0, 0.0, 0.0, 0.0022, 0.0021828]] 239 | [-694.20108, [-694.20108, -694.0, -694.2, -694.2, -694.2011, -694.20108]] 240 | ] |> with std.each() fn(spec) { 241 | [value, results] := spec 242 | results |> with std.each() fn(result, i) { 243 | decimal := decimals.(i) 244 | fmt.format('round {{0}} to {{1}} places => {{2}}', value, decimal, result) |> t.eq( 245 | round(value, decimal) 246 | result 247 | ) 248 | } 249 | } 250 | } 251 | } 252 | 253 | -------------------------------------------------------------------------------- /test/path.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | path := import('path') 3 | 4 | fn run(t) { 5 | // abs?, rel? 6 | { 7 | { abs?: abs?, rel?: rel? } := path 8 | 9 | 'empty absolute path' |> t.eq( 10 | [abs?('/'), rel?('/')] 11 | [true, false] 12 | ) 13 | 'empty relative path' |> t.eq( 14 | [abs?(''), rel?('')] 15 | [false, true] 16 | ) 17 | 'absolute path' |> t.eq( 18 | [ 19 | abs?('/tmp/src/test.oak') 20 | rel?('/tmp/src/test.oak') 21 | ] 22 | [true, false] 23 | ) 24 | 'relative path' |> t.eq( 25 | [ 26 | abs?('./src/test.oak') 27 | rel?('./src/test.oak') 28 | ] 29 | [false, true] 30 | ) 31 | } 32 | 33 | // path components 34 | { 35 | { dir: dir, base: base, cut: cut } := path 36 | 37 | fn allMatch(name, path, expectDir, expectBase) { 38 | (name + ' with dir/base') |> t.eq( 39 | [dir(path), base(path)] 40 | [expectDir, expectBase] 41 | ) 42 | (name + ' with cut') |> t.eq( 43 | cut(path) 44 | [expectDir, expectBase] 45 | ) 46 | } 47 | 48 | 'empty path' |> allMatch( 49 | '' 50 | '', '' 51 | ) 52 | 'empty absolute path' |> allMatch( 53 | '/' 54 | '', '' 55 | ) 56 | 'relative path' |> allMatch( 57 | './src/plugins/test.oak' 58 | './src/plugins', 'test.oak' 59 | ) 60 | 'absolute path' |> allMatch( 61 | '/home/thesephist/src/oak/README.md' 62 | '/home/thesephist/src/oak', 'README.md' 63 | ) 64 | 'path ending with /' |> allMatch( 65 | 'editor/plugins/' 66 | 'editor', 'plugins' 67 | ) 68 | 'path ending with multiple ///' |> allMatch( 69 | 'editor/plugins///' 70 | 'editor', 'plugins' 71 | ) 72 | } 73 | 74 | // clean path 75 | { 76 | clean := path.clean 77 | 78 | 'empty path' |> t.eq( 79 | clean('') 80 | '' 81 | ) 82 | 83 | 'root path /' |> t.eq( 84 | clean('/') 85 | '/' 86 | ) 87 | 'dot at root' |> t.eq( 88 | clean('/./') 89 | '/' 90 | ) 91 | 'slash dot' |> t.eq( 92 | clean('./') 93 | '' 94 | ) 95 | 'dot slash at root' |> t.eq( 96 | clean('/./') 97 | '/' 98 | ) 99 | 100 | 'remove trailing slash(es)' |> t.eq( 101 | clean('./iphone-13/') 102 | 'iphone-13' 103 | ) 104 | 'remove consecutive slashes' |> t.eq( 105 | clean('abc//def/b.c/') 106 | 'abc/def/b.c' 107 | ) 108 | 'remove "."' |> t.eq( 109 | clean('./hello/world/./pic.jpg') 110 | 'hello/world/pic.jpg' 111 | ) 112 | 'remove ".." where possible' |> t.eq( 113 | clean('../magic/a/../pocket..dir/x/y/../../x.gif') 114 | '../magic/pocket..dir/x.gif' 115 | ) 116 | 'do not collapse consecutive sequences of ".."' |> t.eq( 117 | clean('../../x/../../') 118 | '../../..' 119 | ) 120 | 'correctly clean consecutive sequences of "." and ".."' |> t.eq( 121 | clean('.././../one/two/./../three/.././four') 122 | '../../one/four' 123 | ) 124 | } 125 | 126 | // joining, splitting, resolve 127 | { 128 | { 129 | join: join 130 | split: split 131 | resolve: resolve 132 | } := path 133 | 134 | 'join no paths' |> t.eq( 135 | join() 136 | '' 137 | ) 138 | 'join 1 path' |> t.eq( 139 | join('../abc') 140 | '../abc' 141 | ) 142 | 'join 2 paths' |> t.eq( 143 | join('../abc', '/def') 144 | '../abc/def' 145 | ) 146 | 'join multiple paths' |> t.eq( 147 | join('../abc', '/def', 'ghi', '../xyz.jpg') 148 | '../abc/def/xyz.jpg' 149 | ) 150 | 151 | 'split empty path' |> t.eq( 152 | split('') 153 | [] 154 | ) 155 | 'split /' |> t.eq( 156 | split('/') 157 | [] 158 | ) 159 | 'split long path' |> t.eq( 160 | split('../abc//def/ghi/../xyz.jpg') 161 | ['..', 'abc', 'def', 'ghi', '..', 'xyz.jpg'] 162 | ) 163 | 164 | 'resolve to /' |> t.eq( 165 | resolve('./src/editor.ts', '/') |> path.clean() 166 | '/src/editor.ts' 167 | ) 168 | 'resolve to base' |> t.eq( 169 | resolve('./src/editor.ts', '/home/thesephist') 170 | '/home/thesephist/src/editor.ts' 171 | ) 172 | 'resolve absolute path cleans up path' |> t.eq( 173 | resolve('/var/../etc/./nginx.default', '/var/log/nginx') 174 | '/etc/nginx.default' 175 | ) 176 | 'resolve absolute path is a no-op for clean paths' |> t.eq( 177 | resolve('/etc/nginx.default', '/var/log/nginx') 178 | '/etc/nginx.default' 179 | ) 180 | } 181 | } 182 | 183 | -------------------------------------------------------------------------------- /test/random.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | random := import('random') 3 | 4 | fn run(t) { 5 | N := 200 6 | 7 | // bool, int, float 8 | // int, float take either (max) with default min = 0 or (min, max) 9 | { 10 | 'boolean() returns either true or false' |> t.assert( 11 | std.range(N) |> std.map(random.boolean) |> 12 | with std.every() fn(x) type(x) = :bool 13 | ) 14 | 15 | 'integer() returns an int' |> t.assert( 16 | std.range(N) |> std.map(fn() random.integer(10)) |> 17 | with std.every() fn(x) type(x) = :int 18 | ) 19 | 'all integer() are in range of (_, max)' |> t.assert( 20 | std.range(N) |> std.map(fn() random.integer(10)) |> 21 | with std.every() fn(i) i >= 0 & i < 10 22 | ) 23 | 'all integer() are in range of (min, max)' |> t.assert( 24 | std.range(N) |> std.map(fn() random.integer(5, 10)) |> 25 | with std.every() fn(i) i >= 5 & i < 10 26 | ) 27 | 'all integer() are in range of (min, max), min < 0' |> t.assert( 28 | std.range(N) |> std.map(fn() random.integer(-10, 10)) |> 29 | with std.every() fn(i) i >= -10 & i < 10 30 | ) 31 | 32 | 'number() returns a float' |> t.assert( 33 | std.range(N) |> std.map(fn() random.number(10)) |> 34 | with std.every() fn(x) type(x) = :float 35 | ) 36 | 'all number() are in range of (_, max)' |> t.assert( 37 | std.range(N) |> std.map(fn() random.number(2.5)) |> 38 | with std.every() fn(i) i >= 0 & i < 2.5 39 | ) 40 | 'all number() are in range of (min, max)' |> t.assert( 41 | std.range(N) |> std.map(fn() random.number(1, 1.5)) |> 42 | with std.every() fn(i) i >= 1 & i < 1.5 43 | ) 44 | 'all number() are in range of (min, max), min < 0' |> t.assert( 45 | std.range(N) |> std.map(fn() random.number(-0.5, 0.5)) |> 46 | with std.every() fn(i) i >= -0.5 & i < 0.5 47 | ) 48 | } 49 | 50 | // random choice 51 | { 52 | choice := random.choice 53 | 54 | 'choice empty list = ?' |> t.eq(choice([]), ?) 55 | 'choice list of 1' |> t.assert( 56 | std.range(N) |> std.map(fn() choice([:abc])) |> 57 | with std.every() fn(x) x = :abc 58 | ) 59 | 'choice list of many' |> t.assert( 60 | std.range(N) |> std.map(fn() choice([1, 10, 100])) |> 61 | with std.every() fn(x) x = 1 | x = 10 | x = 100 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/runners.oak: -------------------------------------------------------------------------------- 1 | // test runners run by test/main 2 | // 3 | // this file is split from test/main in part so that when standard library 4 | // tests are run with `oak build`, it also exercises the bundler's static 5 | // import analysis logic properly (in all three cases: standard library 6 | // modules, statically imported modules, and dynamically included modules). 7 | 8 | { 9 | slice: slice 10 | filter: filter 11 | contains?: contains? 12 | } := import('std') 13 | 14 | // allow selection of tests to run with CLI arguments 15 | UserSpecifiedRunners := if len(args()) > 2 { 16 | true -> args() |> slice(2) 17 | _ -> [_] // matches any runner 18 | } 19 | 20 | // when adding a stdlib here, also add to Makefile/test-js to get JS coverage 21 | Runners := [ 22 | 'std' 23 | 'str' 24 | 'math' 25 | 'sort' 26 | 'random' 27 | 'fmt' 28 | 'json' 29 | 'datetime' 30 | 'path' 31 | 'http' 32 | 'debug' 33 | 'cli' 34 | 'md' 35 | 'crypto' 36 | 'syntax' 37 | ] |> with filter() fn(name) UserSpecifiedRunners |> contains?(name) 38 | 39 | -------------------------------------------------------------------------------- /test/sort.test.oak: -------------------------------------------------------------------------------- 1 | std := import('std') 2 | str := import('str') 3 | libsort := import('sort') 4 | 5 | fn run(t) { 6 | id := std.identity 7 | 8 | { 9 | sort!: sort! 10 | sort: sort 11 | } := libsort 12 | 13 | // without predicates 14 | 'sort! empty list' |> t.eq( 15 | sort!([]) 16 | [] 17 | ) 18 | 'sort! small list' |> t.eq( 19 | sort!([1, 40, 20, -4]) 20 | [-4, 1, 20, 40] 21 | ) 22 | 'sort! list with dups' |> t.eq( 23 | sort!([1, 40, 20, -4, 20]) 24 | [-4, 1, 20, 20, 40] 25 | ) 26 | 27 | 'sort! mutates list' |> t.eq( 28 | { 29 | arr := [1, 2, 20, 3, -3] 30 | sort!(arr) 31 | arr 32 | } 33 | [-3, 1, 2, 3, 20] 34 | ) 35 | 'sort does not mutate list' |> t.eq( 36 | { 37 | arr := [1, 2, 20, 3, -3] 38 | sort(arr) 39 | arr 40 | } 41 | [1, 2, 20, 3, -3] 42 | ) 43 | 'sort! mutates string' |> t.eq( 44 | { 45 | arr := 'alphabet soup' 46 | sort!(arr) 47 | arr 48 | } 49 | ' aabehloppstu' 50 | ) 51 | 'sort does not mutate string' |> t.eq( 52 | { 53 | arr := 'alphabet soup' 54 | sort(arr) 55 | arr 56 | } 57 | 'alphabet soup' 58 | ) 59 | 60 | // various types 61 | 'sort numbers' |> t.eq( 62 | sort([11, 0, 13, 7, 2, 3, 5]) 63 | [0, 2, 3, 5, 7, 11, 13] 64 | ) 65 | 'sort strings, incl. empty string' |> t.eq( 66 | sort(['abc', '', 'xyz', '', 'linus']) 67 | ['', '', 'abc', 'linus', 'xyz'] 68 | ) 69 | 70 | // with predicates 71 | fn o(n) { key: n } 72 | fn get(o) o.key 73 | 74 | 'sort! empty list by predicate' |> t.eq( 75 | sort!([], get) 76 | [] 77 | ) 78 | 'sort! small list by predicate' |> t.eq( 79 | sort!([o(1), o(40), o(20), o(-4)], get) 80 | [o(-4), o(1), o(20), o(40)] 81 | ) 82 | 'sort! list with dups by predicate' |> t.eq( 83 | sort!([o(1), o(40), o(20), o(-4), o(20)], get) 84 | [o(-4), o(1), o(20), o(20), o(40)] 85 | ) 86 | 87 | // non-function keys 88 | 'sort! list by atom predicate' |> t.eq( 89 | sort!([o(1), o(40), o(20), o(-4)], :key) 90 | [o(-4), o(1), o(20), o(40)] 91 | ) 92 | 'sort! list by string predicate' |> t.eq( 93 | sort!([o(1), o(40), o(20), o(-4)], 'key') 94 | [o(-4), o(1), o(20), o(40)] 95 | ) 96 | 'sort! list by int predicate' |> t.eq( 97 | sort!([ 98 | [:d, 3, -2] 99 | [:c, 10, 5] 100 | [:a, 1, 10] 101 | [:b, 2, 40] 102 | ], 1) 103 | [ 104 | [:a, 1, 10] 105 | [:b, 2, 40] 106 | [:d, 3, -2] 107 | [:c, 10, 5] 108 | ] 109 | ) 110 | 111 | // mutation 112 | 'sort! mutates arg by predicate' |> t.eq( 113 | { 114 | arr := [o(1), o(2), o(20), o(3), o(-3)] 115 | sort!(arr, get) 116 | arr 117 | } 118 | [o(-3), o(1), o(2), o(3), o(20)] 119 | ) 120 | 'sort does not mutate arg by predicate' |> t.eq( 121 | { 122 | arr := [o(1), o(2), o(20), o(3), o(-3)] 123 | sort(arr, get) 124 | arr 125 | } 126 | [o(1), o(2), o(20), o(3), o(-3)] 127 | ) 128 | 129 | // sort strings 130 | 'sort! empty string' |> t.eq( 131 | sort!('') 132 | '' 133 | ) 134 | 'sort! string' |> t.eq( 135 | sort!('abacadbcab') 136 | 'aaaabbbccd' 137 | ) 138 | 'sort! string by predicate' |> t.eq( 139 | sort!('AAEEbbcgz', str.lower) 140 | 'AAbbcEEgz' 141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /tools/oak.vim: -------------------------------------------------------------------------------- 1 | " place this in the init path (.vimrc) 2 | " autocmd BufNewFile,BufRead *.oak set filetype=oak 3 | 4 | if exists("b:current_syntax") 5 | finish 6 | endif 7 | 8 | " oak syntax definition for vi/vim 9 | syntax sync fromstart 10 | 11 | " prefer hard tabs 12 | set noexpandtab 13 | 14 | " operators 15 | syntax match oakOp "\v\~" 16 | syntax match oakOp "\v\+" 17 | syntax match oakOp "\v\-" 18 | syntax match oakOp "\v\*" 19 | syntax match oakOp "\v\/" 20 | syntax match oakOp "\v\%" 21 | 22 | syntax match oakOp "\v\&" 23 | syntax match oakOp "\v\|" 24 | syntax match oakOp "\v\^" 25 | 26 | syntax match oakOp "\v\>" 27 | syntax match oakOp "\v\<" 28 | syntax match oakOp "\v\=" 29 | syntax match oakOp "\v\>\=" 30 | syntax match oakOp "\v\<\=" 31 | syntax match oakOp "\v\!\=" 32 | syntax match oakOp "\v\." 33 | syntax match oakOp "\v\:\=" 34 | syntax match oakOp "\v\<\-" 35 | syntax match oakOp "\v\<\<" 36 | highlight link oakOp Operator 37 | 38 | " match 39 | syntax keyword oakMatch if 40 | syntax match oakMatch "\v\-\>" 41 | highlight link oakMatch Conditional 42 | 43 | " functions 44 | syntax keyword oakFunction fn 45 | syntax keyword oakFunction with 46 | syntax match oakFunction "\v\|\>" 47 | highlight link oakFunction Type 48 | 49 | " bools 50 | syntax keyword oakBool true false 51 | highlight link oakBool Boolean 52 | 53 | " constants 54 | syntax keyword oakConst _ 55 | highlight link oakConst Constant 56 | 57 | " atoms 58 | syntax match oakAtom "\v:[A-Za-z_!][A-Za-z0-9_!?]*" 59 | highlight link oakAtom Special 60 | 61 | " numbers should be consumed first by identifiers, so comes before 62 | syntax match oakNumber "\v\d+" 63 | syntax match oakNumber "\v\d+\.\d+" 64 | highlight link oakNumber Number 65 | 66 | " functions 67 | syntax match oakFnCall "\v[A-Za-z_!][A-Za-z0-9_!?]*\(" contains=oakFunctionName,oakBuiltin 68 | 69 | " identifiers 70 | syntax match oakFunctionName "\v[A-Za-z_][A-Za-z0-9_!?]*" contained 71 | highlight link oakFunctionName Identifier 72 | 73 | syntax keyword oakBuiltin import contained 74 | syntax keyword oakBuiltin string contained 75 | syntax keyword oakBuiltin int contained 76 | syntax keyword oakBuiltin float contained 77 | syntax keyword oakBuiltin atom contained 78 | syntax keyword oakBuiltin codepoint contained 79 | syntax keyword oakBuiltin char contained 80 | syntax keyword oakBuiltin type contained 81 | syntax keyword oakBuiltin len contained 82 | syntax keyword oakBuiltin keys contained 83 | 84 | syntax keyword oakBuiltin args contained 85 | syntax keyword oakBuiltin env contained 86 | syntax keyword oakBuiltin time contained 87 | syntax keyword oakBuiltin nanotime contained 88 | syntax keyword oakBuiltin exit contained 89 | syntax keyword oakBuiltin rand contained 90 | syntax keyword oakBuiltin wait contained 91 | syntax keyword oakBuiltin exec contained 92 | 93 | syntax keyword oakBuiltin input contained 94 | syntax keyword oakBuiltin print contained 95 | syntax keyword oakBuiltin ls contained 96 | syntax keyword oakBuiltin mkdir contained 97 | syntax keyword oakBuiltin rm contained 98 | syntax keyword oakBuiltin stat contained 99 | syntax keyword oakBuiltin open contained 100 | syntax keyword oakBuiltin close contained 101 | syntax keyword oakBuiltin read contained 102 | syntax keyword oakBuiltin write contained 103 | syntax keyword oakBuiltin listen contained 104 | syntax keyword oakBuiltin req contained 105 | 106 | syntax keyword oakBuiltin sin contained 107 | syntax keyword oakBuiltin cos contained 108 | syntax keyword oakBuiltin tan contained 109 | syntax keyword oakBuiltin asin contained 110 | syntax keyword oakBuiltin acos contained 111 | syntax keyword oakBuiltin atan contained 112 | syntax keyword oakBuiltin pow contained 113 | syntax keyword oakBuiltin log contained 114 | highlight link oakBuiltin Keyword 115 | 116 | " strings 117 | syntax region oakString start=/\v'/ skip=/\v(\\.|\r|\n)/ end=/\v'/ 118 | highlight link oakString String 119 | 120 | " comment 121 | " -- block 122 | syntax region oakComment start=/\v\/\*/ skip=/\v(\\.|\r|\n)/ end=/\v\*\// contains=oakTodo 123 | highlight link oakComment Comment 124 | " -- line-ending comment 125 | syntax match oakLineComment "\v\/\/.*" contains=oakTodo 126 | highlight link oakLineComment Comment 127 | " -- shebang, highlighted as comment 128 | syntax match oakShebangComment "\v^#!.*" 129 | highlight link oakShebangComment Comment 130 | " -- TODO in comments 131 | syntax match oakTodo "\v(TODO\(.*\)|TODO)" contained 132 | syntax match oakTodo "\v(NOTE\(.*\)|NOTE)" contained 133 | syntax match oakTodo "\v(XXX\(.*\)|XXX)" contained 134 | syntax keyword oakTodo XXX contained 135 | highlight link oakTodo Todo 136 | 137 | " syntax-based code folds 138 | syntax region oakExpressionList start="(" end=")" transparent fold 139 | syntax region oakMatchExpression start="{" end="}" transparent fold 140 | syntax region oakComposite start="\v\[" end="\v\]" transparent fold 141 | set foldmethod=syntax 142 | 143 | let b:current_syntax = "oak" 144 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | # oaklang.org 2 | 3 | The [oaklang.org](https://oaklang.org) website hosts documentation, updates, releases, and an overview of the Oak programming language project. The website is a partly dynamic web application written entirely in Oak. 4 | 5 | ## Architecture 6 | 7 | // TODO 8 | 9 | ## Structure 10 | 11 | - Index 12 | - Examples 13 | - HTTP echo server 14 | - File I/O 15 | - Fibonacci 16 | - The project + rationale + history 17 | - Download links proxied via dynamic API Backends to `oaklang.org/download` 18 | - Docs + guides + tooling documentation, following `golang.org` 19 | - "How to write Oak programs" `https://golang.org/doc/code` 20 | - Blog (updates + releases) 21 | - Playground (powerful but simple online IDE) 22 | - Source link -> github.com/thesephist/oak 23 | 24 | -------------------------------------------------------------------------------- /www/content/fib-perf.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Oak performance I: Fibonacci faster 3 | date: 2022-03-11T19:37:28-05:00 4 | --- 5 | 6 | I'm in a benchmarking mood today. So I implemented a (naive, not-memoized) Fibonacci function in both Oak and Python (the language it's probably most comparable to in performance) and ran them through some measurements. This post is a pretty casual look at how they both did. 7 | 8 | Here's the Oak version of the program. I call `print()` directly here to avoid any overhead of importing the standard library on every run of the program (though it's probably within the margin of error). I picked the number `34` sort of by trial-and-error, to make sure it was small enough for me to run the benchmarks many times but big enough that the program ran long enough to allow for consistent measurements. 9 | 10 | ```oak 11 | fn fib(n) if n { 12 | 0, 1 -> 1 13 | _ -> fib(n - 1) + fib(n - 2) 14 | } 15 | 16 | print(string(fib(34)) + '\n') 17 | ``` 18 | 19 | Here's the same program in Python, which I ran with Python 3.9.10 on my MacBook Pro. It's almost a direct port of the Oak program. 20 | 21 | ```py 22 | def fib(n): 23 | if n == 0 or n == 1: 24 | return 1 25 | else: 26 | return fib(n - 1) + fib(n - 2) 27 | 28 | print(fib(34)) 29 | ``` 30 | 31 | I also made a Node.js version compiled from the Oak implementation, with `oak build --entry fib.oak -o out.js --web`. 32 | 33 | I measured 10 runs of each program with the magic of [Hyperfine](https://github.com/sharkdp/hyperfine). Here's the (abbreviated) output: 34 | 35 | ``` 36 | Benchmark 1: node out.js 37 | Time (mean ± σ): 259.4 ms ± 1.9 ms [User: 252.6 ms, System: 11.7 ms] 38 | 39 | Benchmark 2: python3 fib.py 40 | Time (mean ± σ): 2.441 s ± 0.042 s [User: 2.421 s, System: 0.011 s] 41 | 42 | Benchmark 3: oak fib.oak 43 | Time (mean ± σ): 13.536 s ± 0.047 s [User: 14.767 s, System: 0.729 s] 44 | 45 | Summary 46 | 'node out.js' ran 47 | 9.41 ± 0.18 times faster than 'python3 fib.py' 48 | 52.19 ± 0.43 times faster than 'oak fib.oak' 49 | ``` 50 | 51 | The gap between Python and native Oak isn't too surprising -- Oak is generally 4-5 times slower than Python on numerical code, so this looks right. But I was quite surprised to see just how much faster the Node.js version ran. V8 is _very, very good_ at optimizing simple number-crunching code, and it shows clearly here. 52 | 53 | -------------------------------------------------------------------------------- /www/content/notebook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Oak Notebook: an experiment in dynamic, interactive documents 3 | date: 2022-03-14T07:34:17-05:00 4 | --- 5 | 6 | On a whim this week, I decided to build a little experiment from some ideas I had simmering in my mind about a way to write documents with dynamic, interactive components interleaved into prose. I've published the experiment under the name **[Oak Notebook](https://thesephist.github.io/x-oak-notebook/)**, but it's not really a complete project or even a complete idea, so much as just a playground onto which I've thrown some things that seemed interesting. 7 | 8 | !html

9 | A demo of me scrolling through the Oak Notebook demo website 11 |

12 | 13 | I'll quote at length from the demo site itself, since that's the best way I know of explaining what Oak Notebook is: 14 | 15 | >Oak Notebook is an experimental tool for creating [dynamic documents](https://thesephist.com/posts/notation/#dynamic-notation) with Markdown and [Oak](https://oaklang.org/). It's both a way of writing documents with interactive, programmable "panels" for explaining and exploring complex ideas, and a "compiler" script that transforms such Markdown documents into HTML web pages. It's a bit like [MDX](https://mdxjs.com/), if MDX was focused specifically on input widgets and interactive exploration of information. 16 | > 17 | >I was inspired initially by [Streamlit](https://docs.streamlit.io/library/api-reference) and [Bret Victor's Scrubbing Calculator](http://worrydream.com/ScrubbingCalculator/) to explore ideas in this space, but what's here isn't a specific re-implementation of either of those concepts, and takes further inspirations from other products, experiments, and prior art. 18 | > 19 | >Oak Notebook provides a way to embed these interactive panels into documents written in Markdown without worrying about styling user interfaces or managing rich user input components. 20 | 21 | I'm hoping that Oak Notebook will become a tool that I'll reach for to build little visualizations and interactive explorations to help me understand and think through situations; hopefully, in that process, Oak Notebook will improve slowly but surely to fit my use cases more and more. I also think this domain of interactive, dynamic documents is fascinating as a space for research, but it's always felt far too broad and deep for me to wade into comfortably. It's very difficult to even try to define an alphabet for the primitive components of dynamic data displays. Oak Notebook is a small and casual start to me cautiously exploring this domain of ideas with my favorite little toy language. 22 | 23 | There are lots of question to answer, even for something as basic as what's on the demo site today. On the interface design side, some questions I face are: 24 | 25 | - How do you clearly communicate what parts of the interactive embeds are inputs to be played with, and which are simply output display? 26 | - How tightly should interactivity be woven into prose? Should authors be able to embed arbitrary inline components right into prose? Should dynamic pieces be offset from normal paragraphs in some way? 27 | - Can we have an "alphabet" of basic input components from which more complex and situationally apt input mechanisms (rotatable knobs, color picker, 2D grid, text selection, time series input) can be built? 28 | - How can we make ["running a program backwards" with automatic differentiation of programs](https://alpha.trycarbide.com/) easier to use for building explorable explanations? 29 | - How can we make interactive embeds programmable without letting the complexity of these mini-programs get out of hand and turn into fully-fledged pieces of software to maintain? 30 | - If there are errors in the program that runs each embed, how should they be reported to the user? 31 | 32 | On the technical implementation side, some questions I face are: 33 | 34 | - How can we make these dynamic documents easier to build and share? Can we minimize the toolchain requirements? Simplify the APIs? 35 | - How might we take inherently computationally expensive simulations, like DL inference or long-running algorithms, and make them smooth to interact with? Can we use dynamic programming or caching to have the interface move faster than the underlying computation? 36 | 37 | At a higher level, here are some guiding principles I used to help direct my brainstorming, quoting again from the demo site: 38 | 39 | >**Words first.** Dynamic documents are still fundamentally documents, and in the spirit of [literate programming](https://en.wikipedia.org/wiki/Literate_programming), I think language is the most flexible and versatile tool we have to communicate ideas. Other dynamic, interactive components should augment prose rather than dominate them on the page. 40 | > 41 | >**Direct representation, direct manipulation.** When designing visualizations, we should favor representing quantities and concepts directly using shapes, graphs, animated motion, or whatever else is best suited for the idea that needs to be communicated. Likewise, when building input components, we should establish as direct a connection as possible between the user's manipulation of the controls and the data being fed into the simulation or visualization -- motion in time for time-series input, a rotating knob for angle input, a slider for size or magnitude, and so on. 42 | > 43 | >**Embrace programming and programmability.** What makes programming challenging is not the notation or syntax, but the toolchain and mental models that are often required to build software. I believe that with simple build steps and a well-designed API, the ability to express dynamic ideas with the full power of a general-purpose programming language can be a gift. 44 | > 45 | >**Composability.** Oak Notebook should come with a versatile set of inputs and widgets out of the box, but it should be easy and idiomatic to compose or combine these primitive building blocks together to make larger input components or reusable widgets that fit a given problem, like a color input with R/G/B controls or a reusable label for displaying dates. 46 | 47 | In particular, I think "words first" is a good grounding principle whenever I think about making documents more interactive or smarter or amenable to machine understanding. Text documents are the skeleton to which authors can add color and texture with data displays and interactive explorations, but if we try to collapse narratives and ideas in prose down to simple knobs and graphs, the whole house collapses. 48 | 49 | At a technical level, the Oak Notebook compiler is a [single Oak script that packs a serious punch](/highlight/https://raw.githubusercontent.com/thesephist/x-oak-notebook/da1513475af03504782040a448cc395c74b6263a/compile.oak?start=17&end=122). It parses Markdown using Oak's `md` standard library module. It calls out to `oak cat` to syntax highlight any Oak code blocks. It also shells out to child processes of `oak build --web` to compile a dynamically generated Oak program to JavaScript that runs on the Oak Notebook page. It's a great testament to the hackability of Oak and Oak's self-hosted tools, and I had a lot of fun writing the script. 50 | 51 | I don't really know where this Oak Notebook project will go. It may go nowhere, and become a little tool I occasionally reach for to make fun demos. I think it would be pretty cool if it can become a part of the standard Oak toolchain -- that would be one step on the path to making Oak itself an ["erector set" for thinking](https://twitter.com/jessmartin/status/1451198781702111250). Perhaps it would even become a starting for more interesting and creative work on how we can write and share living, programmable documents to communicate bigger ideas better. 52 | 53 | -------------------------------------------------------------------------------- /www/content/times.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building a personal time zone CLI with Oak 3 | date: 2022-03-20T00:50:30-05:00 4 | --- 5 | 6 | `times` is a little command-line utility I wrote to show me dates and times in all the time zones I care about quickly. It's the next in a series of [toy Oak programs](/posts/ackermann/) I've been writing when I get bored, but what sets it apart is that I've used Oak's new [`oak pack` feature](/posts/pack/) to install it as a stand-alone executable on my Mac, so I can call it with `times` anywhere, and share it with my friends, too. 7 | 8 | `times` takes no input or command-line arguments, and does exactly one thing: print this list: 9 | 10 | ``` 11 | $ times 12 | US Eastern 0:52 3/20 -4h 13 | US Pacific 21:52 3/19 -7h 14 | Korea 13:52 3/20 +9h 15 | Ukraine 6:52 3/20 +2h 16 | France 5:52 3/20 +1h 17 | Germany 5:52 3/20 +1h 18 | UK 4:52 3/20 +0h 19 | ``` 20 | 21 | Though you can't see it here in the copy-pasted output, the terminal output also color-codes different parts of this table. The utility doesn't really interoperate with any other tools, and it's not particularly configurable, so it doesn't quite fit neatly into the UNIX philosophy. But hey — if I want to change the time format or add a new time zone, I can just dig into the Oak source code, re-compile, and re-install, all in under a minute. 22 | 23 | Especially after shipping `oak pack`, I think Oak is turning out to be a really pleasant way for me to customize my desktop working environment with little utilities like this. Not only does it produce simple self-contained programs, but each program doesn't need too many dependencies, because the standard library contains most of the functionalities I routinely need in my tools, from date and time formatting to Markdown compilation. I've always maintained that even though Oak is a general purpose programming language, it isn't trying to fill every niche. It's trying to be a [tool for building personal tools and projects](/posts/why/), and I'm excited about the way Oak has been moving towards that vision steadily over time. 24 | 25 | --- 26 | 27 | Here's the full `times.oak` program, for sake of completeness: 28 | 29 | ```oak 30 | std := import('std') 31 | str := import('str') 32 | math := import('math') 33 | fmt := import('fmt') 34 | datetime := import('datetime') 35 | 36 | Zones := [ 37 | { name: 'US Eastern', offset: -4 } // Daylight Saving Time 38 | { name: 'US Pacific', offset: -7 } // Daylight Saving Time 39 | { name: 'Korea', offset: 9 } 40 | { name: 'Ukraine', offset: 2 } 41 | { name: 'France', offset: 1 } 42 | { name: 'Germany', offset: 1 } 43 | { name: 'UK', offset: 0 } 44 | ] 45 | 46 | Now := int(time()) 47 | MaxNameLen := math.max(Zones |> std.map(:name) |> std.map(len)...) 48 | 49 | fn yellow(s) '\x1b[0;33m' << s << '\x1b[0;0m' 50 | fn gray(s) '\x1b[0;90m' << s << '\x1b[0;0m' 51 | 52 | Zones |> with std.each() fn(zone) { 53 | { 54 | month: month 55 | day: day 56 | hour: hour 57 | minute: minute 58 | second: second 59 | } := datetime.describe(Now + zone.offset * 3600) 60 | fmt.printf( 61 | '{{0}} {{1}} {{2}}' 62 | zone.name |> str.padStart(MaxNameLen, ' ') 63 | fmt.format( 64 | '{{0}}:{{1}} {{2}}/{{3}}' 65 | hour |> string() |> str.padStart(2, ' ') 66 | minute |> string() |> str.padStart(2, '0') 67 | month 68 | day 69 | ) |> yellow() 70 | if zone.offset < 0 { 71 | true -> '-' + string(math.abs(zone.offset)) + 'h' 72 | _ -> '+' + string(zone.offset) + 'h' 73 | } |> gray() 74 | ) 75 | } 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /www/oaklang.org.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=oaklang.org 3 | ConditionPathExists=/home/oak-user/go/bin/oak 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | User=oak-user 9 | LimitNOFILE=1024 10 | PermissionsStartOnly=true 11 | 12 | Restart=on-failure 13 | RestartSec=100ms 14 | StartLimitIntervalSec=60 15 | 16 | WorkingDirectory=/home/oak-user/src/oak/www 17 | ExecStart=/home/oak-user/go/bin/oak ./src/main.oak 18 | 19 | # make sure log directory exists and owned by syslog 20 | PermissionsStartOnly=true 21 | ExecStartPre=/bin/mkdir -p /var/log/oaklang.org 22 | ExecStartPre=/bin/chown syslog:adm /var/log/oaklang.org 23 | ExecStartPre=/bin/chmod 755 /var/log/oaklang.org 24 | StandardOutput=syslog 25 | StandardError=syslog 26 | SyslogIdentifier=oaklang.org 27 | 28 | [Install] 29 | WantedBy=multi-user.target 30 | -------------------------------------------------------------------------------- /www/src/app.js.oak: -------------------------------------------------------------------------------- 1 | // oaklang.org website 2 | 3 | { 4 | each: each 5 | } := import('std') 6 | { 7 | contains?: contains? 8 | } := import('str') 9 | 10 | UA := navigator.userAgent 11 | 12 | // open right download instruction section based on OS 13 | document.querySelectorAll('.try-details') |> 14 | Array.from() |> 15 | each(fn(el) el.open := false) 16 | if { 17 | UA |> contains?('Macintosh') 18 | UA |> contains?('iPhone') 19 | UA |> contains?('iPad') -> document.querySelector('.try-details.os-macos').open := true 20 | UA |> contains?('Linux') -> document.querySelector('.try-details.os-linux').open := true 21 | _ -> document.querySelector('.try-details.os-other').open := true 22 | } 23 | 24 | -------------------------------------------------------------------------------- /www/src/gen.oak: -------------------------------------------------------------------------------- 1 | // static site generator for oaklang.org 2 | 3 | { 4 | println: println 5 | default: default 6 | map: map 7 | each: each 8 | take: take 9 | slice: slice 10 | filter: filter 11 | compact: compact 12 | reverse: reverse 13 | fromEntries: fromEntries 14 | } := import('std') 15 | { 16 | cut: cut 17 | trim: trim 18 | join: join 19 | split: split 20 | indexOf: indexOf 21 | trimEnd: trimEnd 22 | endsWith?: endsWith? 23 | } := import('str') 24 | sort := import('sort') 25 | fs := import('fs') 26 | fmt := import('fmt') 27 | datetime := import('datetime') 28 | path := import('path') 29 | md := import('md') 30 | 31 | OakExec := args().0 32 | 33 | // printlog wraps println with the [www/gen] command scope 34 | fn printlog(xs...) println('[www/gen]', xs...) 35 | 36 | // for every library file, generate a syntax-highlighted source page 37 | with fs.readFile('./www/tpl/lib.html') fn(tplFile) if tplFile { 38 | ? -> printlog('Could not read template') 39 | _ -> with fs.listFiles('./lib') fn(files) files |> with each() fn(file) if file.name.0 != '.' -> { 40 | with exec(OakExec, ['cat', path.join('./lib', file.name), '--html'], '') fn(evt) if evt.type { 41 | :error -> printlog('Could not syntax-highlight', file.name) 42 | _ -> { 43 | highlightedFile := evt.stdout 44 | 45 | pageName := file.name |> trimEnd('.oak') + '.html' 46 | pagePath := path.join('./www/static/lib', pageName) 47 | page := tplFile |> fmt.format({ 48 | name: file.name 49 | content: highlightedFile 50 | }) 51 | with fs.writeFile(pagePath, page) fn { 52 | printlog('Generated lib', file.name) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | // generate a post page for every post in ./content, and generate a listing 60 | // page of every post on the site 61 | with fs.listFiles('./www/content') fn(files) { 62 | fn dateString(iso, fmtString) { 63 | fmtString := fmtString |> default('{{ day }} {{ monthName }} {{ year }}') 64 | Months := [ 65 | _ 66 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep' 67 | 'Oct', 'Nov', 'Dec' 68 | ] 69 | if date := datetime.parse(iso) { 70 | ? -> { 71 | printlog('Invalid date:', iso) 72 | '' 73 | } 74 | _ -> { 75 | date.monthName := Months.(date.month) 76 | date.shortYear := date.year % 100 77 | fmtString |> fmt.format(date) 78 | } 79 | } 80 | } 81 | 82 | fn parsePostFile(postFile) { 83 | meta := if postFile |> take(4) { 84 | '---\n' -> { 85 | endFrontMatter := postFile |> indexOf('\n---') 86 | frontMatter := postFile |> slice(4, endFrontMatter) 87 | 88 | postFile <- postFile |> slice(endFrontMatter + 4) 89 | frontMatter |> 90 | split('\n') |> 91 | map(fn(line) line |> cut(':') |> map(fn(s) trim(s))) |> 92 | fromEntries() 93 | } 94 | _ -> {} 95 | } 96 | 97 | meta.content := md.parse(postFile) |> map(fn(block) if { 98 | // syntax highlight any Oak code blocks using `oak cat --html --stdin` 99 | block.tag = :pre & block.children.(0).lang = 'oak' -> { 100 | evt := exec(OakExec, ['cat', '--html', '--stdin'], block.children.(0).children.0) 101 | if evt.type { 102 | :error -> printlog('Could not syntax-highlight blog: ', meta.title) 103 | _ -> block.children.(0).children.0 := { 104 | tag: :rawHTML 105 | children: [evt.stdout] 106 | } 107 | } 108 | block 109 | } 110 | _ -> block 111 | }) |> md.compile() 112 | } 113 | 114 | postNames := files |> 115 | filter(fn(file) file.name.0 != '.') |> 116 | filter(fn(file) endsWith?(file.name, '.md')) |> 117 | map(fn(file) file.name |> trimEnd('.md')) 118 | 119 | // NOTE: all posts are read and compiled synchronously, but in practice 120 | // this is not a performance bottleneck ... yet. 121 | posts := postNames |> map(fn(postName) { 122 | postPath := './www/content/{{0}}.md' |> fmt.format(postName) 123 | pagePath := './www/static/posts/{{0}}.html' |> fmt.format(postName) 124 | if postFile := fs.readFile(postPath) { 125 | ? -> { 126 | printlog('Could not read post:', postName) 127 | ? 128 | } 129 | _ -> { 130 | post := parsePostFile(postFile) 131 | post.name := postName 132 | post.srcPath := postPath 133 | post.destPath := pagePath 134 | post.dateString := dateString(post.date) 135 | post.shortDateString := dateString(post.date, '{{ day }} {{ monthName }} \'{{ shortYear }}') 136 | } 137 | } 138 | }) |> compact() |> filter(fn(post) post.draft != 'true') 139 | 140 | // generate and write post list 141 | listTemplate := fs.readFile('./www/tpl/list.html') 142 | renderedList := listTemplate |> fmt.format({ 143 | content: posts |> 144 | sort.sort(:date) |> 145 | reverse() |> 146 | map(fn(post) '
  • {{ title }} {{ shortDateString }}
  • ' |> fmt.format(post)) |> 147 | join('\n') 148 | }) 149 | listPath := './www/static/posts/index.html' 150 | with fs.writeFile(listPath, renderedList) fn(res) if res { 151 | ? -> printlog('Could not write file', listPath) 152 | _ -> printlog('Generated', listPath) 153 | } 154 | 155 | // generate and write all posts 156 | postTemplate := fs.readFile('./www/tpl/post.html') 157 | posts |> with each() fn(post) { 158 | renderedPost := postTemplate |> fmt.format(post) 159 | with fs.writeFile(post.destPath, renderedPost) fn(res) if res { 160 | ? -> printlog('Could not write file', post.destPath) 161 | _ -> printlog('Generated post', post.name) 162 | } 163 | } 164 | } 165 | 166 | -------------------------------------------------------------------------------- /www/src/highlight.js.oak: -------------------------------------------------------------------------------- 1 | // oak highlight proxy 2 | 3 | { 4 | map: map 5 | take: take 6 | slice: slice 7 | } := import('std') 8 | { 9 | cut: cut 10 | join: join 11 | split: split 12 | trimStart: trimStart 13 | } := import('str') 14 | fmt := import('fmt') 15 | 16 | Form := document.querySelector('form') 17 | URLInput := document.querySelector('input') 18 | SubmitButton := document.querySelector('form button') 19 | 20 | fn inputURLToProxyPath(url) { 21 | parts := url |> trimStart('https://') |> split('/') 22 | if parts |> take(4) { 23 | ['github.com', _, _, 'blob'] -> { 24 | [_, user, repo] := parts 25 | branchAndFilePath := parts |> slice(4) |> join('/') 26 | 27 | // determine if there's a #LXX-LYY marker selecting line numbers in 28 | // the GitHub link 29 | [branchAndFilePath, hash] := branchAndFilePath |> cut('#') 30 | [startOffset, endOffset] := hash |> split('-') |> map(fn(s) { 31 | if s.0 = 'L' & ? != lineNo := int(s |> slice(1)) -> lineNo 32 | }) 33 | 34 | proxyPath := '/highlight/https://raw.githubusercontent.com/{{ user }}/{{ repo }}/{{ branchAndFilePath }}' |> 35 | fmt.format({ user: user, repo: repo, branchAndFilePath: branchAndFilePath }) 36 | if startOffset != ? & endOffset != ? { 37 | true -> proxyPath + '?start={{0}}&end={{1}}' |> fmt.format(startOffset, endOffset) 38 | _ -> proxyPath 39 | } 40 | } 41 | _ -> '/highlight/' + url 42 | } 43 | } 44 | 45 | with Form.addEventListener('submit') fn(evt) { 46 | evt.preventDefault() 47 | window.location.href := inputURLToProxyPath(URLInput.value) 48 | } 49 | 50 | -------------------------------------------------------------------------------- /www/src/main.oak: -------------------------------------------------------------------------------- 1 | // oaklang.org 2 | // 3 | // This server handles all static and dynamic API requests to oaklang.org, the 4 | // main website for the Oak programming language. 5 | 6 | { 7 | map: map 8 | each: each 9 | slice: slice 10 | merge: merge 11 | entries: entries 12 | } := import('std') 13 | { 14 | split: split 15 | join: join 16 | replace: replace 17 | endsWith?: endsWith? 18 | } := import('str') 19 | { 20 | format: format 21 | printf: printf 22 | } := import('fmt') 23 | fs := import('fs') 24 | fmt := import('fmt') 25 | path := import('path') 26 | http := import('http') 27 | 28 | Port := 9898 29 | OakExec := args().0 30 | HighlightTemplate := fs.readFile('./tpl/highlight.html') 31 | HighlightEmbedTemplate := fs.readFile('./tpl/highlight-embed.html') 32 | 33 | server := http.Server() 34 | 35 | { 36 | // explicit static paths 37 | '/': './static/index.html' 38 | '/lib/': './static/lib/index.html' 39 | '/posts/': './static/posts/index.html' 40 | '/highlight/': './static/highlight.html' 41 | '/favicon.ico': './static/favicon.ico' 42 | } |> entries() |> with each() fn(entry) { 43 | [reqPath, fsPath] := entry 44 | with server.route(reqPath) fn(params) fn(req, end) if req.method { 45 | 'GET' -> with fs.readFile(fsPath) fn(file) if file { 46 | ? -> end(http.NotFound) 47 | _ -> end({ 48 | status: 200 49 | headers: { 50 | 'Content-Type': http.mimeForPath(fsPath) 51 | } 52 | body: file 53 | }) 54 | } 55 | _ -> end(http.MethodNotAllowed) 56 | } 57 | } 58 | 59 | ['lib', 'posts'] |> with each() fn(listName) { 60 | with server.route('/{{0}}/:itemName' |> format(listName)) fn(params) fn(req, end) if req.method { 61 | 'GET' -> with fs.readFile(path.join('./static', listName, params.itemName + '.html')) fn(file) if file { 62 | ? -> end(http.NotFound) 63 | _ -> end({ 64 | status: 200 65 | headers: { 66 | 'Content-Type': http.MimeTypes.html 67 | } 68 | body: file 69 | }) 70 | } 71 | _ -> end(http.MethodNotAllowed) 72 | } 73 | } 74 | 75 | with server.route('/highlight/*proxyPath') fn(params) fn(request, end) if request.method { 76 | 'GET' -> { 77 | // URL processing seems to collapse double-slashes, so restore it 78 | proxyPath := params.proxyPath |> 79 | replace('http:/', 'http://') |> replace('https:/', 'https://') 80 | errorHeaders := { 81 | 'Content-Type': http.MimeTypes.txt 82 | } 83 | 84 | queryParams := '?' + merge({}, params, { start: _, end: _, embed: _, proxyPath: _ }) |> 85 | entries() |> 86 | map(fn(pair) http.percentEncode(pair.0) + '=' + http.percentEncode(pair.1)) |> 87 | join('&') 88 | if queryParams = '?' -> queryParams <- '' 89 | proxyURL := proxyPath + queryParams 90 | 91 | if proxyPath |> endsWith?('.oak') { 92 | false -> end({ 93 | status: 400 94 | headers: errorHeaders 95 | body: 'cannot proxy resources not ending in .oak' 96 | }) 97 | _ -> with req({ url: proxyURL }) fn(evt) if { 98 | evt.type = :error 99 | evt.resp.status != 200 -> end({ 100 | status: 500 101 | headers: errorHeaders 102 | body: 'could not fetch ' + proxyURL 103 | }) 104 | _ -> { 105 | sourceFile := evt.resp.body 106 | 107 | sliced? := false 108 | { 109 | start: startOffset 110 | end: endOffset 111 | } := params 112 | if int(startOffset) != ? & int(endOffset) != ? -> { 113 | sliced? <- true 114 | sourceFile <- sourceFile |> 115 | split('\n') |> 116 | // start - 1 because offsets are 0-based but line 117 | // numbers are 1-based indexes; NOT end - 1 because 118 | // end line number is inclusive, not exclusive 119 | slice(int(startOffset) - 1, int(endOffset)) |> 120 | join('\n') 121 | } 122 | 123 | with exec(OakExec, ['cat', '--html', '--stdin'], sourceFile) fn(evt) if evt.type { 124 | :error -> end({ 125 | status: 500 126 | headers: errorHeaders 127 | body: 'could not syntax highlight ' + proxyURL 128 | }) 129 | _ -> end({ 130 | status: 200 131 | headers: { 132 | 'Content-Type': http.MimeTypes.html 133 | } 134 | body: if params.embed { 135 | ? -> HighlightTemplate 136 | _ -> HighlightEmbedTemplate 137 | } |> fmt.format({ 138 | href: proxyURL 139 | name: path.base(proxyPath) 140 | heading: path.base(proxyPath) + if { 141 | sliced? -> ' L{{0}} - {{1}}' |> 142 | fmt.format(startOffset, endOffset) 143 | _ -> '' 144 | } 145 | content: evt.stdout 146 | }) 147 | }) 148 | } 149 | } 150 | } 151 | } 152 | } 153 | _ -> end(http.MethodNotAllowed) 154 | } 155 | 156 | with server.route('/*staticPath') fn(params) { 157 | http.handleStatic('./static/' + params.staticPath) 158 | } 159 | 160 | server.start(Port) 161 | printf('oaklang.org running on port {{0}}', Port) 162 | 163 | -------------------------------------------------------------------------------- /www/static/css/lib.css: -------------------------------------------------------------------------------- 1 | article.stdlib pre, 2 | article.stdlib code { 3 | background: var(--primary-bg); 4 | padding: 0; 5 | line-height: 1.375em; 6 | } 7 | 8 | td { 9 | line-height: 1.5em; 10 | padding: 4px 0; 11 | vertical-align: top; 12 | } 13 | 14 | td:first-child { 15 | padding-right: 2ch; 16 | font-weight: bold; 17 | } 18 | 19 | /* syntax highlighting */ 20 | 21 | .oak-comment { 22 | color: #8b95a1; 23 | } 24 | 25 | .oak-assign, 26 | .oak-nonlocalAssign, 27 | .oak-branchArrow, 28 | .oak-pushArrow, 29 | .oak-exclam, 30 | .oak-ifKeyword, 31 | .oak-plus, 32 | .oak-minus, 33 | .oak-times, 34 | .oak-divide, 35 | .oak-modulus, 36 | .oak-xor, 37 | .oak-and, 38 | .oak-or, 39 | .oak-greater, 40 | .oak-less, 41 | .oak-eq, 42 | .oak-geq, 43 | .oak-leq, 44 | .oak-neq { 45 | color: #cf222e; 46 | } 47 | 48 | .oak-pipeArrow, 49 | .oak-ellipsis, 50 | .oak-fnKeyword { 51 | color: #234fcd; 52 | } 53 | 54 | .oak-qmark, 55 | .oak-underscore, 56 | .oak-trueLiteral, 57 | .oak-falseLiteral { 58 | color: #764cc5; 59 | } 60 | 61 | .oak-withKeyword, 62 | .oak-numberLiteral { 63 | color: #07997f; 64 | } 65 | 66 | .oak-fnName { 67 | color: #50910b; 68 | } 69 | 70 | .oak-stringLiteral { 71 | color: #cf940c; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /www/static/highlight.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Highlight proxy | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 57 | 58 | 59 |
    60 |
    61 |
    62 | Oak 63 |
    64 | 70 |
    71 |
    72 |
    73 |
    74 |

    Oak highlight proxy

    75 |

    The highlight proxy syntax highlights Oak source code found 76 | elsewhere on the Web, for example on GitHub, using the same 77 | interface used to show the standard library source 78 | on this website.

    79 |

    The highlight proxy can also highlight just a portion of a 80 | larger source file, by specifing a range of lines using either 81 | start=N&end=M 82 | query parameters, or using line ranges specified in GitHub file 83 | URLs.

    84 |

    For an example, try pasting in the URL to this "Hello World" Oak program.

    86 |
    87 | 91 | 92 |
    93 |
    94 |
    95 |
    96 |
    97 |
    98 |
    99 |
    100 | - Linus 101 |
    102 |
    103 |
    104 | 105 | 106 | -------------------------------------------------------------------------------- /www/static/img/oak-http-latency-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/oak/21db2d8394f4ad64dfa1198e24bbf516285499fc/www/static/img/oak-http-latency-plot.png -------------------------------------------------------------------------------- /www/static/img/oak-http-throughput-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/oak/21db2d8394f4ad64dfa1198e24bbf516285499fc/www/static/img/oak-http-throughput-plot.png -------------------------------------------------------------------------------- /www/static/img/oak-node-hyperfine-bench.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/oak/21db2d8394f4ad64dfa1198e24bbf516285499fc/www/static/img/oak-node-hyperfine-bench.jpg -------------------------------------------------------------------------------- /www/static/img/oak-notebook-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/oak/21db2d8394f4ad64dfa1198e24bbf516285499fc/www/static/img/oak-notebook-demo.gif -------------------------------------------------------------------------------- /www/static/lib/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Standard library | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 | Oak 17 |
    18 | 24 |
    25 |
    26 |
    27 |
    28 |

    Standard library

    29 |

    Oak comes with a growing standard library for quickly writing 30 | scripts and building small apps. The standard library also serves 31 | as an example of what idiomatic Oak code looks like.

    32 |

    Oak's standard library is covered by the same stability 33 | guarantee as the rest of the language — after Oak 1.0, documented 34 | APIs in the standard library will not have breaking changes.

    35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
    stdcore functions, iteration, and control flow
    strfunctions for working with characters and strings
    mathmathematical constants and operators, algebraic functions, and utilities for working in 2D/3D
    sortlist sorting algorithms; currently quicksort
    randomfunctions for working with insecure, pseudorandom sources of randomness
    fsergonomic filesystem APIs for reading and writing to files and directories
    fmtformatted printing using "{{ N }}"-style format strings
    jsonJSON parser and serializer for Oak values
    datetimeutilities for working with human-readable dates and UNIX timestamps
    pathfunctions for working with UNIX style paths in file systems and URIs
    httpa toolkit for writing HTTP servers, routers, and services
    testbasic unit testing library, used by Oak's language and standard library tests
    debugtools for inspecting runtime values, including a pretty-printer for structured data
    clia toolkit for writing command-line Oak apps, including a parser for CLI arguments
    mdMarkdown parsing and rendering
    cryptoutilities for working with cryptographic primitives and cryptographically safe sources of randomness
    syntaxtokenizer, parser, and code formatter for the Oak language
    105 |
    106 |
    107 |
    108 |
    109 |
    110 |
    111 |
    112 | - Linus 113 |
    114 |
    115 |
    116 | 117 | 118 | -------------------------------------------------------------------------------- /www/static/lib/random.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | random.oak | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 | Oak 17 |
    18 | 24 |
    25 |
    26 |
    27 |
    28 |

    random.oak

    29 |

    30 | ← Standard library 31 | See on GitHub ↗ 32 |

    33 |
    // librandom implements utilities for working with pseudorandom sources of
    34 | // randomness.
    35 | //
    36 | // librandom functions source rand() for randomness and are not suitable for
    37 | // security-sensitive work. For such code, use srand() for secure randomness or
    38 | // the 'crypto' standard library.
    39 | 
    40 | {
    41 | 	Pi: Pi
    42 | 	E: E
    43 | 	sqrt: sqrt
    44 | } := import('math')
    45 | 
    46 | // boolean returns either true or false with equal probability
    47 | fn boolean rand() > 0.5
    48 | 
    49 | // integer returns an integer in the range [min, max) with uniform probability
    50 | fn integer(min, max) number(int(min), int(max)) |> int()
    51 | 
    52 | // number returns a floating point number in the range [min, max) with uniform
    53 | // probability
    54 | fn number(min, max) {
    55 | 	if max = ? -> [min, max] <- [0, min]
    56 | 	min + rand() * (max - min)
    57 | }
    58 | 
    59 | // choice returns an item from the given list, with each item having equal
    60 | // probability of being selected on any given call
    61 | fn choice(list) list.(integer(0, len(list)))
    62 | 
    63 | // sample from a standard normal distribution: µ = 0, σ = 1
    64 | fn normal {
    65 | 	u := 1 - rand()
    66 | 	v := 2 * Pi * rand()
    67 | 	sqrt(-2 * log(E, u)) * cos(v)
    68 | }
    69 | 
    70 | 
    71 |
    72 |
    73 |
    74 |
    75 |
    76 |
    77 |
    78 | - Linus 79 |
    80 |
    81 |
    82 | 83 | -------------------------------------------------------------------------------- /www/static/oak/codecols.oak: -------------------------------------------------------------------------------- 1 | // codecols designed after Rasmus Andersson's linelen_hist.sh 2 | // https://gist.github.com/rsms/36bda3b5c8ab83d951e45ed788a184f4 3 | 4 | { 5 | println: println 6 | default: default 7 | map: map 8 | stdin: stdin 9 | range: range 10 | filter: filter 11 | reduce: reduce 12 | append: append 13 | values: values 14 | identity: identity 15 | partition: partition 16 | } := import('std') 17 | { 18 | split: split 19 | join: join 20 | padEnd: padEnd 21 | } := import('str') 22 | sort := import('sort') 23 | math := import('math') 24 | cli := import('cli') 25 | 26 | // adjust libcli for (1) oak pack and (2) no verb in this CLI 27 | argv := ['_exe', '_main'] |> append(args()) 28 | Cli := cli.parseArgv(argv) 29 | 30 | if Cli.opts.h = true | Cli.opts.help = true -> { 31 | println('codecols counts columns in source code given to its standard input. 32 | 33 | Usage 34 | codecols [options] < your/code/*.c 35 | cat *.c | codecols [options] 36 | 37 | Options 38 | --max-cols, -c Maximum number of columsn of code to display in the 39 | output table 40 | --histo-width, -w If the column counts are high enough that the histogram 41 | must be scaled down to fit on a terminal screen, the 42 | bars will be scaled such that the longest one is this 43 | long. 60 by default.') 44 | exit(0) 45 | } 46 | 47 | // histo returns a histogram bar of a given length 48 | fn histo(n) { 49 | whole := int(n / 8) 50 | rem := n % 8 51 | graph := range(whole) |> map(fn '█') |> join() + if int(rem) { 52 | 0 -> '' 53 | 1 -> '▏' 54 | 2 -> '▎' 55 | 3 -> '▍' 56 | 4 -> '▌' 57 | 5 -> '▋' 58 | 6 -> '▊' 59 | 7 -> '▉' 60 | } 61 | if graph = '' & n > 0 { 62 | true -> '▏' 63 | _ -> graph 64 | } 65 | } 66 | 67 | // list of number of non-zero column counts 68 | cols := stdin() |> 69 | split('\n') |> 70 | filter(fn(s) s != '') |> 71 | // round up to the nearest even number 72 | map(fn(line) len(line) + len(line) % 2) 73 | // same data as above, but in frequency map 74 | freqs := cols |> 75 | sort.sort!() |> 76 | partition(identity) |> 77 | reduce({}, fn(freq, ns) freq.(ns.0) := len(ns)) 78 | 79 | min := 0 80 | max := math.max(keys(freqs) |> map(int)...) 81 | maxCount := math.max(values(freqs)...) 82 | maxHisto := int(Cli.opts.'histo-width' |> default(Cli.opts.w)) |> 83 | default(60) |> 84 | math.min(maxCount) 85 | maxListedCols := int(Cli.opts.'max-cols' |> default(Cli.opts.c)) |> 86 | default(max) 87 | 88 | colWidth := math.max( 89 | len('cols') 90 | len(string(max)) 91 | ) 92 | countWidth := math.max( 93 | len('count') 94 | len(string(maxCount)) 95 | ) 96 | 97 | println( 98 | 'cols' |> padEnd(colWidth, ' ') 99 | 'count' |> padEnd(countWidth, ' ') 100 | ) 101 | range(2, maxListedCols + 1, 2) |> map( 102 | fn(n) println( 103 | string(n) |> padEnd(colWidth, ' ') 104 | freqs.(n) |> default(0) |> string() |> padEnd(countWidth, ' ') 105 | histo(freqs.(n) |> default(0) |> math.scale(0, maxCount, 0, maxHisto * 8)) 106 | ) 107 | ) 108 | println('average columns per line:', math.mean(cols) |> math.round(2)) 109 | -------------------------------------------------------------------------------- /www/static/posts/fib-perf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oak performance I: Fibonacci faster | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 | Oak 17 |
    18 | 24 |
    25 |
    26 |
    27 |
    28 |

    Oak performance I: Fibonacci faster

    29 |

    30 | ← Posts 31 | 11 Mar 2022 32 |

    33 |

    I'm in a benchmarking mood today. So I implemented a (naive, not-memoized) Fibonacci function in both Oak and Python (the language it's probably most comparable to in performance) and ran them through some measurements. This post is a pretty casual look at how they both did.

    Here's the Oak version of the program. I call print() directly here to avoid any overhead of importing the standard library on every run of the program (though it's probably within the margin of error). I picked the number 34 sort of by trial-and-error, to make sure it was small enough for me to run the benchmarks many times but big enough that the program ran long enough to allow for consistent measurements.

    fn fib(n) if n {
    34 |     0, 1 -> 1
    35 |     _ -> fib(n - 1) + fib(n - 2)
    36 | }
    37 | 
    38 | print(string(fib(34)) + '\n')

    Here's the same program in Python, which I ran with Python 3.9.10 on my MacBook Pro. It's almost a direct port of the Oak program.

    def fib(n):
    39 |     if n == 0 or n == 1:
    40 |         return 1
    41 |     else:
    42 |         return fib(n - 1) + fib(n - 2)
    43 | 
    44 | print(fib(34))

    I also made a Node.js version compiled from the Oak implementation, with oak build --entry fib.oak -o out.js --web.

    I measured 10 runs of each program with the magic of Hyperfine. Here's the (abbreviated) output:

    Benchmark 1: node out.js
    45 |   Time (mean ± σ):     259.4 ms ±   1.9 ms    [User: 252.6 ms, System: 11.7 ms]
    46 | 
    47 | Benchmark 2: python3 fib.py
    48 |   Time (mean ± σ):      2.441 s ±  0.042 s    [User: 2.421 s, System: 0.011 s]
    49 | 
    50 | Benchmark 3: oak fib.oak
    51 |   Time (mean ± σ):     13.536 s ±  0.047 s    [User: 14.767 s, System: 0.729 s]
    52 | 
    53 | Summary
    54 |   'node out.js' ran
    55 |     9.41 ± 0.18 times faster than 'python3 fib.py'
    56 |    52.19 ± 0.43 times faster than 'oak fib.oak'

    The gap between Python and native Oak isn't too surprising -- Oak is generally 4-5 times slower than Python on numerical code, so this looks right. But I was quite surprised to see just how much faster the Node.js version ran. V8 is very, very good at optimizing simple number-crunching code, and it shows clearly here.

    57 |
    58 |
    59 |
    60 |
    61 |
    62 |
    63 |
    64 | - Linus 65 |
    66 |
    67 |
    68 | 69 | -------------------------------------------------------------------------------- /www/static/posts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blog | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
    13 |
    14 |
    15 | Oak 16 |
    17 | 23 |
    24 |
    25 |
    26 |
    27 |

    Oak blog

    28 |

    Most of my personal writing lives at thesephist.com. 29 | On this blog, I share technical stories and deep-dives about Oak and projects built with Oak. 30 | This blog, like the rest of this website, is 31 | generated by an Oak program. 32 |

    33 | 47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 | - Linus 55 |
    56 |
    57 |
    58 | 59 | -------------------------------------------------------------------------------- /www/tpl/highlight-embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ name }} | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
    {{ content }}
    16 |
    17 |
    18 | 19 | -------------------------------------------------------------------------------- /www/tpl/highlight.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ name }} | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 | Oak 17 |
    18 | 24 |
    25 |
    26 |
    27 | 34 |
    35 |
    36 |
    37 |
    38 |
    39 |
    40 | - Linus 41 |
    42 |
    43 |
    44 | 45 | -------------------------------------------------------------------------------- /www/tpl/lib.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ name }} | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 | Oak 17 |
    18 | 24 |
    25 |
    26 |
    27 | 35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 | - Linus 42 |
    43 |
    44 |
    45 | 46 | -------------------------------------------------------------------------------- /www/tpl/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blog | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
    13 |
    14 |
    15 | Oak 16 |
    17 | 23 |
    24 |
    25 |
    26 |
    27 |

    Oak blog

    28 |

    Most of my personal writing lives at thesephist.com. 29 | On this blog, I share technical stories and deep-dives about Oak and projects built with Oak. 30 | This blog, like the rest of this website, is 31 | generated by an Oak program. 32 |

    33 |
      34 | {{ content }} 35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 |
    43 | - Linus 44 |
    45 |
    46 |
    47 | 48 | -------------------------------------------------------------------------------- /www/tpl/post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} | Oak programming language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 | Oak 17 |
    18 | 24 |
    25 |
    26 |
    27 |
    28 |

    {{ title }}

    29 |

    30 | ← Posts 31 | {{ dateString }} 32 |

    33 | {{ content }} 34 |
    35 |
    36 | 45 | 46 | --------------------------------------------------------------------------------