From dd0be120e4dcb909b2d3708d7d1d475326daa51d Mon Sep 17 00:00:00 2001 From: SpectCOW <126259962+SpcFORK@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:08:20 -0400 Subject: [PATCH 1/4] Update README.md --- README.md | 315 +++++++++++++++++++----------------------------------- 1 file changed, 109 insertions(+), 206 deletions(-) diff --git a/README.md b/README.md index 7ff6a9b..78c602f 100644 --- a/README.md +++ b/README.md @@ -1,224 +1,127 @@ -# Oak 🌳 +# Oak Programming Language Documentation -[![Build Status](https://travis-ci.com/thesephist/oak.svg?branch=main)](https://app.travis-ci.com/thesephist/oak) +## Syntax -**Oak** is an expressive, dynamically typed programming language. It takes the best parts of my experience with [Ink](https://dotink.co/), and adds what I missed and removes what didn't work to get a language that feels just as small and simple, but much more ergonomic and capable. +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. -Here's an example Oak program. +```go +program := expr* -```js -std := import('std') +expr := literal | identifier | + assignment | + propertyAccess | + unaryExpr | binaryExpr | + prefixCall | infixCall | + ifExpr | withExpr | + block -fn fizzbuzz(n) if [n % 3, n % 5] { - [0, 0] -> 'FizzBuzz' - [0, _] -> 'Fizz' - [_, 0] -> 'Buzz' - _ -> string(n) -} +literal := nullLiteral | + numberLiteral | stringLiteral | atomLiteral | boolLiteral | + listLiteral | objectLiteral | + fnLiterael -std.range(1, 101) |> std.each(fn(n) { - std.println(fizzbuzz(n)) -}) -``` - -Oak has good support for asynchronous I/O. Here's how you read a file and print it. - -```js -std := import('std') -fs := import('fs') - -with fs.readFile('./file.txt') fn(file) if file { - ? -> std.println('Could not read file!') - _ -> print(file) -} -``` - -Oak also has a pragmatic [standard library](https://oaklang.org/lib/) that comes built into the `oak` executable. For example, there's a built-in HTTP server and router in the `http` library. - -```js -std := import('std') -fmt := import('fmt') -http := import('http') - -server := http.Server() -with server.route('/hello/:name') fn(params) { - fn(req, end) if req.method { - 'GET' -> end({ - status: 200 - body: fmt.format('Hello, {{ 0 }}!' - std.default(params.name, 'World')) - }) - _ -> end(http.MethodNotAllowed) - } -} -server.start(9999) -``` - -## Install - -On macOS, Oak can be installed through [Homebrew](https://brew.sh/). - -```sh -brew install oak -``` - -On other platforms or macOS without Homebrew, check the [installation guide](https://oaklang.org/#start) on the Oak website to install the Oak CLI or build it from source. - -## Overview - -Oak has 7 primitive and 3 complex types. - -```js -? // null, also "()" -_ // "empty" value, equal to anything -1, 2, 3 // integers -3.14 // floats -true // booleans -'hello' // strings -:error // atoms - -[1, :number] // list -{ a: 'hello' } // objects -fn(a, b) a + b // functions -``` - -These types mostly behave as you'd expect. Some notable details: - -- There is no implicit type casting between any types, except during arithmetic operations when ints may be cast up to floats. -- Both ints and floats are full 64-bit. -- Strings are mutable byte arrays, also used for arbitrary data storage in memory, like in Lua. For immutable strings, use atoms. -- Lists are backed by a vector data structure -- appending and indexing is cheap, but cloning is not -- For lists and objects, equality is defined as deep equality. There is no identity equality in Oak. - -We define a function in Oak with the `fn` keyword. A name is optional, and if given, will define that function in that scope. If there are no arguments, the `()` may be omitted. +nullLiteral := '?' +numberLiteral := \d+ | \d* '.' \d+ +stringLiteral := // single quoted string with standard escape sequences + \x00 syntax +atomLiteral := ':' + identifier +boolLiteral := 'true' | 'false' +listLiteral := '[' ( expr ',' )* ']' // last comma optional +objectLiteral := '{' ( expr ':' expr ',' )* '}' // last comma optional +fnLiteral := 'fn' '(' ( identifier ',' )* (identifier '...')? ')' expr -```js -fn double(n) 2 * n -fn speak { - println('Hello!') -} -``` - -Besides the normal set of arithmetic operators, Oak has a few strange operators. - -- The **assignment operator** `:=` binds values on the right side to names on the left, potentially by destructuring an object or list. For example: - - ```js - a := 1 // a is 1 - [b, c] := [2, 3] // b is 2, c is 3 - d := double(a) // d is 2 - ``` -- The **nonlocal assignment operator** `<-` binds values on the right side to names on the left, but only when those variables already exist. If the variable doesn't exist in the current scope, the operator ascends up parent scopes until it reaches the global scope to find the last scope where that name was bound. - - ```js - n := 10 - m := 20 - { - n <- 30 - m := 40 - } - n // 30 - m // 20 - ``` -- The **push operator** `<<` pushes values onto the end of a string or a list, mutating it, and returns the changed string or list. - - ```js - str := 'Hello ' - str << 'World!' // 'Hello World!' - - list := [1, 2, 3] - list << 4 - list << 5 << 6 // [1, 2, 3, 4, 5, 6] - ``` -- The **pipe operator** `|>` takes a value on the left and makes it the first argument to a function call on the right. - - ```js - // print 2n for every prime n in range [0, 10) - range(10) |> filter(prime?) |> - each(double) |> each(println) - - // adding numbers - fn add(a, b) a + b - 10 |> add(20) |> add(3) // 33 - ``` - -Oak uses one main construct for control flow -- the `if` match expression. Unlike a traditional `if` expression, which can only test for truthy and falsy values, Oak's `if` acts like a sophisticated switch-case, comparing values until the right match is reached. - -```js -fn pluralize(word, count) if count { - 1 -> word - 2 -> 'a pair of ' + word - _ -> word + 's' -} -``` - -This match expression, combined with safe tail recursion, makes Oak Turing-complete. - -Lastly, because callback-based asynchronous concurrency is common in Oak, there's special syntax sugar, the `with` expression, to help. The `with` syntax sugar de-sugars like this. +identifier := \w_ (\w\d_?!)* | _ -```js -with readFile('./path') fn(file) { - println(file) -} +assignment := ( + identifier [':=' '<-'] expr | + listLiteral [':=' '<-'] expr | + objectLiteral [':=' '<-'] expr +) -// desugars to -readFile('./path', fn(file) { - println(file) -}) -``` +propertyAccess := identifier ('.' identifier)+ -For a more detailed description of the language, see the [work-in-progress language spec](docs/spec.md). +unaryExpr := ('!' | '-') expr +binaryExpr := expr (+ - * / % ^ & | > < = >= <= <<) binaryExpr -### Builds and deployment +prefixCall := expr '(' (expr ',')* ')' +infixCall := expr '|>' prefixCall -While the Oak interpreter can run programs and modules directly from source code on the file system, Oak also offers a build tool, `oak build`, which can _bundle_ an Oak program distributed across many files into a single "bundle" source file. `oak build` can also cross-compile Oak bundles into JavaScript bundles, to run in the browser or in JavaScript environments like Node.js and Deno. This allows Oak programs to be deployed and distributed as single-file programs, both on the server and in the browser. +ifExpr := 'if' expr? '{' ifClause* '}' +ifClause := expr '->' expr ',' -To build a new bundle, we can simply pass an "entrypoint" to the program. +withExpr := 'with' prefixCall fnLiteral -```sh -oak build --entry src/main.oak --output dist/bundle.oak +block := '{' expr+ '}' | '(' expr* ')' ``` -Compiling to JavaScript works similarly, but with the `--web` flag, which turns on JavaScript cross-compilation. - -```sh -oak build --entry src/app.js.oak --output dist/bundle.js --web +### AST node types + +```c +nullLiteral +stringLiteral +numberLiteral +boolLiteral +atomLiteral +listLiteral +objectLiteral +fnLiteral +identifier +assignment +propertyAccess +unaryExpr +binaryExpr +fnCall +ifExpr +block ``` -The bundler and compiler are built on top of my past work with the [September](https://github.com/thesephist/september) toolchain for Ink, but slightly re-architected to support bundling and multiple compilation targets. In the future, the goal of `oak build` is to become a lightly optimizing compiler and potentially help yield an `oak compile` command that could package the interpreter and an Oak bundle into a single executable binary. For more information on `oak build`, see `oak help build`. - -### Performance - -As of September 2021, Oak is about 5-6x slower than Python 3.9 on pure function call and number-crunching overhead (assessed by a basic `fib(30)` benchmark). These figures are worst-case estimates -- because Oak's data structures are far simpler than Python's, the ratios start to go down on more realistic complex programs. But nonetheless, this gives a good estimate of the kind of performance (or, currently, the lack thereof) you can expect from Oak programs. It's not fast, though anecdotally it's fast enough for me to have few complaints for most of my use cases. - -Runtime performance is not currently my primary concern; my primary concern is implementing a correct and pleasant interpreter that's fast _enough_ for me to write real apps with. Only when speed becomes a problem for software I built with Oak will I really invest much more in speed. I think being as fast as Python and Ruby is a good goal, long-term. Those languages run in production and receive continuous investments into performance tuning, but are far more complex. Oak is much simpler, but it's also just me. I think it evens out the difference. - -There are several immediately actionable things we can do to speed up Oak programs' runtime performance, though none are under works today. In order of increasing implementation complexity: - -1. Basic compiler optimization techniques applied to the abstract syntax tree, like constant folding and propagation. -2. A thorough audit of the interpreter's memory allocation profile and a memory optimization pass (and the same for L1/L2 cache misses). -3. A bytecode VM that executes Oak compiled down to more compact and efficient bytecode rather than a syntax tree-walking interpreter. - -## Development - -Oak (ab)uses GNU Make to run development workflows and tasks. - -- `make run` compiles and runs the Oak binary, which opens an interactive REPL -- `make fmt` or `make f` runs the `oak fmt` code formatter over any _files with unstaged changes in the git repository_. This is equivalent to running `oak fmt --changes --fix`. -- `make tests` or `make t` runs the Go test suite for the Oak language and interpreter -- `make test-oak` or `make tk` runs the Oak test suite, which tests the standard libraries -- `make test-bundle` runs the Oak test suite, bundled using `oak build` -- `make test-js` runs the Oak test suite on the system's Node.js, compiled using `oak build --web` -- `make build` generates release builds of Oak for various operating systems; `make build-` builds for a specific OS -- `make install` installs the Oak interpreter on your `$GOPATH` as `oak`, and re-installs Oak's vim syntax file -- `make site` builds an Oak bundle for the [oaklang.org](https://oaklang.org/) website, amd `make site-w` does it on every file save -- `make site-gen` rebuilds the statically generated parts of the Oak website, like the standard library documentation - -To try Oak by building from source, clone the repository and run `make install` (or simply `go build .`). - -## Unit and generative tests - -The Oak repository so far as two kinds of tests: unit tests and generative/fuzz tests. **Unit tests** are just what they sound like -- tests validated with assertions -- and are built on the `libtest` Oak library with the exception of Go tests in `eval_test.go`. **Generative tests** include fuzz tests, and are tests that run some pre-defined behavior of functions through a much larger body of procedurally generated set of inputs, for validating behavior that's difficult to validate manually like correctness of parsers and `libdatetime`'s date/time conversion algorithms. - -Both sets of tests are written and run entirely in the "userland" of Oak, without invoking the interpreter separately. Unit tests live in `./test` and are run with `./test/main.oak`; generative tests are in `test/generative`, and can be run manually. - +## Language Functions + +- `import(path)`: Imports a module located at the specified `path`. +- `string(x)`: Converts the argument `x` to a string. +- `int(x)`: Converts the argument `x` to an integer. +- `float(x)`: Converts the argument `x` to a floating-point number. +- `atom(c)`: Creates an atom with the specified character `c`. +- `codepoint(c)`: Returns the Unicode code point of the character `c`. +- `char(n)`: Converts the Unicode code point `n` to a character. +- `type(x)`: Returns the type of the argument `x`. +- `len(x)`: Returns the length of the argument `x`. +- `keys(x)`: Returns an array of keys of the argument `x`. + +## OS Functions + +- `args()`: Returns command-line arguments as an array of strings. +- `env()`: Returns the environment variables as an object. +- `time()`: Returns the current time as a float. +- `nanotime()`: Returns the current time in nanoseconds as an integer. +- `exit(code)`: Exits the program with the specified exit code. +- `rand()`: Generates a random floating-point number between 0 and 1. +- `srand(length)`: Seeds the random number generator with the specified length. +- `wait(duration)`: Pauses the program execution for the specified duration. +- `exec(path, args, stdin)`: Executes a command specified by `path` with the given `args` and optional standard input `stdin`. Returns stdout, stderr, and end events. + +## I/O Interfaces + +- `input()`: Reads input from the standard input. +- `print()`: Writes output to the standard output. +- `ls(path)`: Lists files and directories in the specified path. +- `mkdir(path)`: Creates a directory at the specified path. +- `rm(path)`: Removes the file or directory at the specified path. +- `stat(path)`: Retrieves file or directory information at the specified path. +- `open(path, flags, perm)`: Opens a file at the specified path with the given flags and permissions. +- `close(fd)`: Closes the file descriptor `fd`. +- `read(fd, offset, length)`: Reads data from the file descriptor `fd` starting at the specified `offset` and reading `length` bytes. +- `write(fd, offset, data)`: Writes data to the file descriptor `fd` starting at the specified `offset`. +- `close := listen(host, handler)`: Listens for incoming connections on the specified `host` and handles them with the provided `handler` function. +- `req(data)`: Sends an HTTP request with the provided data. + + ```go + // Req syntax: + // --- + + req({ + url: '' + method: 'GET' + headers: {} + body: _ + }) + ``` From f661ae61e7f90cf9c0ebe853fae6e73130d25840 Mon Sep 17 00:00:00 2001 From: SpectCOW <126259962+SpcFORK@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:09:08 -0400 Subject: [PATCH 2/4] Update spec.md --- docs/spec.md | 176 ++++++++++++++++----------------------------------- 1 file changed, 54 insertions(+), 122 deletions(-) diff --git a/docs/spec.md b/docs/spec.md index fe643b0..78c602f 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -1,12 +1,10 @@ -# Oak programming language - -This is a work-in-progress rough draft of things that will end up in a rough informal language specification. +# Oak Programming Language Documentation ## Syntax 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. -``` +```go program := expr* expr := literal | identifier | @@ -57,7 +55,7 @@ block := '{' expr+ '}' | '(' expr* ')' ### AST node types -``` +```c nullLiteral stringLiteral numberLiteral @@ -76,120 +74,54 @@ ifExpr block ``` -## Builtin functions - -``` --- language -import(path) -string(x) -int(x) -float(x) -atom(c) -codepoint(c) -char(n) -type(x) -len(x) -keys(x) - --- os -args() -env() -time() // returns float -nanotime() // returns int -exit(code) -rand() -srand(length) -wait(duration) -exec(path, args, stdin) // returns stdout, stderr, end events - ----- I/O interfaces -input() -print() -ls(path) -mkdir(path) -rm(path) -stat(path) -open(path, flags, perm) -close(fd) -read(fd, offset, length) -write(fd, offset, data) -close := listen(host, handler) -req(data) - --- math -sin(n) -cos(n) -tan(n) -asin(n) -acos(n) -atan(n) -pow(b, n) -log(b, n) -``` - -## Code samples - -```js -// hello world -std.println('Hello, World!') - -// some math -sq := fn(n) n * n -fn sq(n) n * n -fn sq(n) { n * n } // equivalent - -// side-effecting functions -fn say() { std.println('Hi!') } -// if no arguments, () is optiona -fn { std.println('Hi!') } - -// factorial -fn factorial(n) if n <= 1 { - true -> 1 - _ -> n * factorial(n - 1) -} -``` - -```js -// methods are emulated by pipe notation -scores |> sum() -names |> join(', ') -fn sum(xs...) xs |> reduce(0, fn(a, b) a + b) -oakFiles := fileNames |> filter(fn(name) name |> endsWith?('.oak')) -``` - -```js -// "with" keyword just makes the last fn a callback as last arg -with loop(10) fn(n) std.println(n) -with wait(1) fn { - std.println('Done!') -} -with fetch('example.com') fn(resp) { - with resp.json() fn(json) { - std.println(json) - } -} -``` - -```js -// raw file read -with open('name.txt') fn(evt) if evt.type { - :error -> std.println(evt.message) - _ -> with read(fd := evt.fd, 0, -1) fn(evt) { - if evt.type { - :error -> std.println(evt.message) - _ -> fmt.printf('file data: {{0}}', evt.data) - } - close(fd) - } -} - -// with stdlib -std := import('std') -fs := import('fs') -with fs.readFile('names.txt') fn(file) if file { - ? -> std.println('[error] could not read file') - _ -> std.println(file) -} -``` - +## Language Functions + +- `import(path)`: Imports a module located at the specified `path`. +- `string(x)`: Converts the argument `x` to a string. +- `int(x)`: Converts the argument `x` to an integer. +- `float(x)`: Converts the argument `x` to a floating-point number. +- `atom(c)`: Creates an atom with the specified character `c`. +- `codepoint(c)`: Returns the Unicode code point of the character `c`. +- `char(n)`: Converts the Unicode code point `n` to a character. +- `type(x)`: Returns the type of the argument `x`. +- `len(x)`: Returns the length of the argument `x`. +- `keys(x)`: Returns an array of keys of the argument `x`. + +## OS Functions + +- `args()`: Returns command-line arguments as an array of strings. +- `env()`: Returns the environment variables as an object. +- `time()`: Returns the current time as a float. +- `nanotime()`: Returns the current time in nanoseconds as an integer. +- `exit(code)`: Exits the program with the specified exit code. +- `rand()`: Generates a random floating-point number between 0 and 1. +- `srand(length)`: Seeds the random number generator with the specified length. +- `wait(duration)`: Pauses the program execution for the specified duration. +- `exec(path, args, stdin)`: Executes a command specified by `path` with the given `args` and optional standard input `stdin`. Returns stdout, stderr, and end events. + +## I/O Interfaces + +- `input()`: Reads input from the standard input. +- `print()`: Writes output to the standard output. +- `ls(path)`: Lists files and directories in the specified path. +- `mkdir(path)`: Creates a directory at the specified path. +- `rm(path)`: Removes the file or directory at the specified path. +- `stat(path)`: Retrieves file or directory information at the specified path. +- `open(path, flags, perm)`: Opens a file at the specified path with the given flags and permissions. +- `close(fd)`: Closes the file descriptor `fd`. +- `read(fd, offset, length)`: Reads data from the file descriptor `fd` starting at the specified `offset` and reading `length` bytes. +- `write(fd, offset, data)`: Writes data to the file descriptor `fd` starting at the specified `offset`. +- `close := listen(host, handler)`: Listens for incoming connections on the specified `host` and handles them with the provided `handler` function. +- `req(data)`: Sends an HTTP request with the provided data. + + ```go + // Req syntax: + // --- + + req({ + url: '' + method: 'GET' + headers: {} + body: _ + }) + ``` From 808dbc7ab9e79e61f474cf2bffd00f6d1cb5ce74 Mon Sep 17 00:00:00 2001 From: SpectCOW <126259962+SpcFORK@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:10:09 -0400 Subject: [PATCH 3/4] Update README.md --- README.md | 314 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 205 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 78c602f..add1a95 100644 --- a/README.md +++ b/README.md @@ -1,127 +1,223 @@ -# Oak Programming Language Documentation +# Oak 🌳 -## Syntax +[![Build Status](https://travis-ci.com/thesephist/oak.svg?branch=main)](https://app.travis-ci.com/thesephist/oak) -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. +**Oak** is an expressive, dynamically typed programming language. It takes the best parts of my experience with [Ink](https://dotink.co/), and adds what I missed and removes what didn't work to get a language that feels just as small and simple, but much more ergonomic and capable. -```go -program := expr* +Here's an example Oak program. -expr := literal | identifier | - assignment | - propertyAccess | - unaryExpr | binaryExpr | - prefixCall | infixCall | - ifExpr | withExpr | - block +```js +std := import('std') -literal := nullLiteral | - numberLiteral | stringLiteral | atomLiteral | boolLiteral | - listLiteral | objectLiteral | - fnLiterael +fn fizzbuzz(n) if [n % 3, n % 5] { + [0, 0] -> 'FizzBuzz' + [0, _] -> 'Fizz' + [_, 0] -> 'Buzz' + _ -> string(n) +} -nullLiteral := '?' -numberLiteral := \d+ | \d* '.' \d+ -stringLiteral := // single quoted string with standard escape sequences + \x00 syntax -atomLiteral := ':' + identifier -boolLiteral := 'true' | 'false' -listLiteral := '[' ( expr ',' )* ']' // last comma optional -objectLiteral := '{' ( expr ':' expr ',' )* '}' // last comma optional -fnLiteral := 'fn' '(' ( identifier ',' )* (identifier '...')? ')' expr +std.range(1, 101) |> std.each(fn(n) { + std.println(fizzbuzz(n)) +}) +``` + +Oak has good support for asynchronous I/O. Here's how you read a file and print it. + +```js +std := import('std') +fs := import('fs') + +with fs.readFile('./file.txt') fn(file) if file { + ? -> std.println('Could not read file!') + _ -> print(file) +} +``` + +Oak also has a pragmatic [standard library](https://oaklang.org/lib/) that comes built into the `oak` executable. For example, there's a built-in HTTP server and router in the `http` library. + +```js +std := import('std') +fmt := import('fmt') +http := import('http') + +server := http.Server() +with server.route('/hello/:name') fn(params) { + fn(req, end) if req.method { + 'GET' -> end({ + status: 200 + body: fmt.format('Hello, {{ 0 }}!' + std.default(params.name, 'World')) + }) + _ -> end(http.MethodNotAllowed) + } +} +server.start(9999) +``` + +## Install + +On macOS, Oak can be installed through [Homebrew](https://brew.sh/). + +```sh +brew install oak +``` + +On other platforms or macOS without Homebrew, check the [installation guide](https://oaklang.org/#start) on the Oak website to install the Oak CLI or build it from source. -identifier := \w_ (\w\d_?!)* | _ +## Overview -assignment := ( - identifier [':=' '<-'] expr | - listLiteral [':=' '<-'] expr | - objectLiteral [':=' '<-'] expr -) +Oak has 7 primitive and 3 complex types. -propertyAccess := identifier ('.' identifier)+ +```js +? // null, also "()" +_ // "empty" value, equal to anything +1, 2, 3 // integers +3.14 // floats +true // booleans +'hello' // strings +:error // atoms -unaryExpr := ('!' | '-') expr -binaryExpr := expr (+ - * / % ^ & | > < = >= <= <<) binaryExpr +[1, :number] // list +{ a: 'hello' } // objects +fn(a, b) a + b // functions +``` -prefixCall := expr '(' (expr ',')* ')' -infixCall := expr '|>' prefixCall +These types mostly behave as you'd expect. Some notable details: -ifExpr := 'if' expr? '{' ifClause* '}' -ifClause := expr '->' expr ',' +- There is no implicit type casting between any types, except during arithmetic operations when ints may be cast up to floats. +- Both ints and floats are full 64-bit. +- Strings are mutable byte arrays, also used for arbitrary data storage in memory, like in Lua. For immutable strings, use atoms. +- Lists are backed by a vector data structure -- appending and indexing is cheap, but cloning is not +- For lists and objects, equality is defined as deep equality. There is no identity equality in Oak. -withExpr := 'with' prefixCall fnLiteral +We define a function in Oak with the `fn` keyword. A name is optional, and if given, will define that function in that scope. If there are no arguments, the `()` may be omitted. + +```js +fn double(n) 2 * n +fn speak { + println('Hello!') +} +``` -block := '{' expr+ '}' | '(' expr* ')' +Besides the normal set of arithmetic operators, Oak has a few strange operators. + +- The **assignment operator** `:=` binds values on the right side to names on the left, potentially by destructuring an object or list. For example: + + ```js + a := 1 // a is 1 + [b, c] := [2, 3] // b is 2, c is 3 + d := double(a) // d is 2 + ``` +- The **nonlocal assignment operator** `<-` binds values on the right side to names on the left, but only when those variables already exist. If the variable doesn't exist in the current scope, the operator ascends up parent scopes until it reaches the global scope to find the last scope where that name was bound. + + ```js + n := 10 + m := 20 + { + n <- 30 + m := 40 + } + n // 30 + m // 20 + ``` +- The **push operator** `<<` pushes values onto the end of a string or a list, mutating it, and returns the changed string or list. + + ```js + str := 'Hello ' + str << 'World!' // 'Hello World!' + + list := [1, 2, 3] + list << 4 + list << 5 << 6 // [1, 2, 3, 4, 5, 6] + ``` +- The **pipe operator** `|>` takes a value on the left and makes it the first argument to a function call on the right. + + ```js + // print 2n for every prime n in range [0, 10) + range(10) |> filter(prime?) |> + each(double) |> each(println) + + // adding numbers + fn add(a, b) a + b + 10 |> add(20) |> add(3) // 33 + ``` + +Oak uses one main construct for control flow -- the `if` match expression. Unlike a traditional `if` expression, which can only test for truthy and falsy values, Oak's `if` acts like a sophisticated switch-case, comparing values until the right match is reached. + +```js +fn pluralize(word, count) if count { + 1 -> word + 2 -> 'a pair of ' + word + _ -> word + 's' +} ``` -### AST node types - -```c -nullLiteral -stringLiteral -numberLiteral -boolLiteral -atomLiteral -listLiteral -objectLiteral -fnLiteral -identifier -assignment -propertyAccess -unaryExpr -binaryExpr -fnCall -ifExpr -block +This match expression, combined with safe tail recursion, makes Oak Turing-complete. + +Lastly, because callback-based asynchronous concurrency is common in Oak, there's special syntax sugar, the `with` expression, to help. The `with` syntax sugar de-sugars like this. + +```js +with readFile('./path') fn(file) { + println(file) +} + +// desugars to +readFile('./path', fn(file) { + println(file) +}) ``` -## Language Functions - -- `import(path)`: Imports a module located at the specified `path`. -- `string(x)`: Converts the argument `x` to a string. -- `int(x)`: Converts the argument `x` to an integer. -- `float(x)`: Converts the argument `x` to a floating-point number. -- `atom(c)`: Creates an atom with the specified character `c`. -- `codepoint(c)`: Returns the Unicode code point of the character `c`. -- `char(n)`: Converts the Unicode code point `n` to a character. -- `type(x)`: Returns the type of the argument `x`. -- `len(x)`: Returns the length of the argument `x`. -- `keys(x)`: Returns an array of keys of the argument `x`. - -## OS Functions - -- `args()`: Returns command-line arguments as an array of strings. -- `env()`: Returns the environment variables as an object. -- `time()`: Returns the current time as a float. -- `nanotime()`: Returns the current time in nanoseconds as an integer. -- `exit(code)`: Exits the program with the specified exit code. -- `rand()`: Generates a random floating-point number between 0 and 1. -- `srand(length)`: Seeds the random number generator with the specified length. -- `wait(duration)`: Pauses the program execution for the specified duration. -- `exec(path, args, stdin)`: Executes a command specified by `path` with the given `args` and optional standard input `stdin`. Returns stdout, stderr, and end events. - -## I/O Interfaces - -- `input()`: Reads input from the standard input. -- `print()`: Writes output to the standard output. -- `ls(path)`: Lists files and directories in the specified path. -- `mkdir(path)`: Creates a directory at the specified path. -- `rm(path)`: Removes the file or directory at the specified path. -- `stat(path)`: Retrieves file or directory information at the specified path. -- `open(path, flags, perm)`: Opens a file at the specified path with the given flags and permissions. -- `close(fd)`: Closes the file descriptor `fd`. -- `read(fd, offset, length)`: Reads data from the file descriptor `fd` starting at the specified `offset` and reading `length` bytes. -- `write(fd, offset, data)`: Writes data to the file descriptor `fd` starting at the specified `offset`. -- `close := listen(host, handler)`: Listens for incoming connections on the specified `host` and handles them with the provided `handler` function. -- `req(data)`: Sends an HTTP request with the provided data. - - ```go - // Req syntax: - // --- - - req({ - url: '' - method: 'GET' - headers: {} - body: _ - }) - ``` +For a more detailed description of the language, see the [work-in-progress language spec](docs/spec.md). + +### Builds and deployment + +While the Oak interpreter can run programs and modules directly from source code on the file system, Oak also offers a build tool, `oak build`, which can _bundle_ an Oak program distributed across many files into a single "bundle" source file. `oak build` can also cross-compile Oak bundles into JavaScript bundles, to run in the browser or in JavaScript environments like Node.js and Deno. This allows Oak programs to be deployed and distributed as single-file programs, both on the server and in the browser. + +To build a new bundle, we can simply pass an "entrypoint" to the program. + +```sh +oak build --entry src/main.oak --output dist/bundle.oak +``` + +Compiling to JavaScript works similarly, but with the `--web` flag, which turns on JavaScript cross-compilation. + +```sh +oak build --entry src/app.js.oak --output dist/bundle.js --web +``` + +The bundler and compiler are built on top of my past work with the [September](https://github.com/thesephist/september) toolchain for Ink, but slightly re-architected to support bundling and multiple compilation targets. In the future, the goal of `oak build` is to become a lightly optimizing compiler and potentially help yield an `oak compile` command that could package the interpreter and an Oak bundle into a single executable binary. For more information on `oak build`, see `oak help build`. + +### Performance + +As of September 2021, Oak is about 5-6x slower than Python 3.9 on pure function call and number-crunching overhead (assessed by a basic `fib(30)` benchmark). These figures are worst-case estimates -- because Oak's data structures are far simpler than Python's, the ratios start to go down on more realistic complex programs. But nonetheless, this gives a good estimate of the kind of performance (or, currently, the lack thereof) you can expect from Oak programs. It's not fast, though anecdotally it's fast enough for me to have few complaints for most of my use cases. + +Runtime performance is not currently my primary concern; my primary concern is implementing a correct and pleasant interpreter that's fast _enough_ for me to write real apps with. Only when speed becomes a problem for software I built with Oak will I really invest much more in speed. I think being as fast as Python and Ruby is a good goal, long-term. Those languages run in production and receive continuous investments into performance tuning, but are far more complex. Oak is much simpler, but it's also just me. I think it evens out the difference. + +There are several immediately actionable things we can do to speed up Oak programs' runtime performance, though none are under works today. In order of increasing implementation complexity: + +1. Basic compiler optimization techniques applied to the abstract syntax tree, like constant folding and propagation. +2. A thorough audit of the interpreter's memory allocation profile and a memory optimization pass (and the same for L1/L2 cache misses). +3. A bytecode VM that executes Oak compiled down to more compact and efficient bytecode rather than a syntax tree-walking interpreter. + +## Development + +Oak (ab)uses GNU Make to run development workflows and tasks. + +- `make run` compiles and runs the Oak binary, which opens an interactive REPL +- `make fmt` or `make f` runs the `oak fmt` code formatter over any _files with unstaged changes in the git repository_. This is equivalent to running `oak fmt --changes --fix`. +- `make tests` or `make t` runs the Go test suite for the Oak language and interpreter +- `make test-oak` or `make tk` runs the Oak test suite, which tests the standard libraries +- `make test-bundle` runs the Oak test suite, bundled using `oak build` +- `make test-js` runs the Oak test suite on the system's Node.js, compiled using `oak build --web` +- `make build` generates release builds of Oak for various operating systems; `make build-` builds for a specific OS +- `make install` installs the Oak interpreter on your `$GOPATH` as `oak`, and re-installs Oak's vim syntax file +- `make site` builds an Oak bundle for the [oaklang.org](https://oaklang.org/) website, amd `make site-w` does it on every file save +- `make site-gen` rebuilds the statically generated parts of the Oak website, like the standard library documentation + +To try Oak by building from source, clone the repository and run `make install` (or simply `go build .`). + +## Unit and generative tests + +The Oak repository so far as two kinds of tests: unit tests and generative/fuzz tests. **Unit tests** are just what they sound like -- tests validated with assertions -- and are built on the `libtest` Oak library with the exception of Go tests in `eval_test.go`. **Generative tests** include fuzz tests, and are tests that run some pre-defined behavior of functions through a much larger body of procedurally generated set of inputs, for validating behavior that's difficult to validate manually like correctness of parsers and `libdatetime`'s date/time conversion algorithms. + +Both sets of tests are written and run entirely in the "userland" of Oak, without invoking the interpreter separately. Unit tests live in `./test` and are run with `./test/main.oak`; generative tests are in `test/generative`, and can be run manually. From 313ca3be4ac28a1d5d658b931fb8fb121d86fe6e Mon Sep 17 00:00:00 2001 From: SpectCOW <126259962+SpcFORK@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:19:16 -0400 Subject: [PATCH 4/4] Update spec.md --- docs/spec.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/spec.md b/docs/spec.md index 78c602f..e61f266 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -125,3 +125,16 @@ block body: _ }) ``` + +# Math Functions +- Trigonometric functions + - `sin(n)`: Calculates the sine of the angle `n`. + - `cos(n)`: Calculates the cosine of the angle `n`. + - `tan(n)`: Calculates the tangent of the angle `n`. +- Inverse Trigonometric functions + - `asin(n)`: Calculates the arcsine of the value `n`. + - `acos(n)`: Calculates the arccosine of the value `n`. + - `atan(n)`: Calculates the arctangent of the value `n`. +- Power and logarithmic functions + - `pow(b, n)`: Raises the base `b` to the power of `n`. + - `log(b, n)`: Calculates the logarithm of `n` with base `b`.