release 0.1.0
This commit is contained in:
commit
30d94536a9
90 changed files with 7722 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
.vscode
|
||||||
|
.envrc
|
||||||
1217
Cargo.lock
generated
Normal file
1217
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
40
Cargo.toml
Normal file
40
Cargo.toml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[package]
|
||||||
|
name = "owa-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
repository = "https://git.ruject.fun/RuJect/owa-rs"
|
||||||
|
description = "A Lisp-like programming language implemented in Rust"
|
||||||
|
license = "Unlicense"
|
||||||
|
readme = "README.md"
|
||||||
|
categories = ["compilers", "parsing", "development-tools", "data-structures"]
|
||||||
|
keywords = [
|
||||||
|
"owa",
|
||||||
|
"lisp",
|
||||||
|
"s-expression",
|
||||||
|
"language",
|
||||||
|
"interpreter",
|
||||||
|
"functional",
|
||||||
|
"persistent",
|
||||||
|
"immutable",
|
||||||
|
"ast",
|
||||||
|
"parser",
|
||||||
|
"runtime",
|
||||||
|
"rpds",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
archery = "1.2.2"
|
||||||
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
|
dirs = "6.0.0"
|
||||||
|
educe = "0.6.0"
|
||||||
|
libffi = "5.1.0"
|
||||||
|
libloading = "0.9.0"
|
||||||
|
nom = "8.0.0"
|
||||||
|
num-traits = "0.2.19"
|
||||||
|
ordered-float = { version = "5.3.0", features = ["std"] }
|
||||||
|
paste = "1.0.15"
|
||||||
|
reedline = "0.47.0"
|
||||||
|
rpds = "1.2.0"
|
||||||
|
test-case = "3.3.1"
|
||||||
|
tracing = { version = "0.1.44", features = ["release_max_level_off"] }
|
||||||
|
tracing-subscriber = "0.3.23"
|
||||||
24
LICENSE
Normal file
24
LICENSE
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <https://unlicense.org>
|
||||||
329
README.md
Normal file
329
README.md
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
# OWA-RS
|
||||||
|
|
||||||
|
A Lisp-inspired programming language written in Rust. This is a personal learning project, so expect rough edges and experimental design.
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [OWA-RS](#owa-rs)
|
||||||
|
- [Content](#content)
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Syntax 📝](#syntax-)
|
||||||
|
- [Literals](#literals)
|
||||||
|
- [Collections](#collections)
|
||||||
|
- [Special Forms](#special-forms)
|
||||||
|
- [Comments](#comments)
|
||||||
|
- [Annotations](#annotations)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Quick Start 🛠️](#quick-start-️)
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [Build](#build)
|
||||||
|
- [Run a file](#run-a-file)
|
||||||
|
- [Run directly without owu](#run-directly-without-owu)
|
||||||
|
- [Flags](#flags)
|
||||||
|
- [Architecture 🏗️](#architecture-️)
|
||||||
|
- [Rust core](#rust-core)
|
||||||
|
- [Modular standard library](#modular-standard-library)
|
||||||
|
- [Built-in API 🔧](#built-in-api-)
|
||||||
|
- [Core operations](#core-operations)
|
||||||
|
- [Built-in namespaces](#built-in-namespaces)
|
||||||
|
- [Standard Library 📚](#standard-library-)
|
||||||
|
- [Core Library (`modules/core/`)](#core-library-modulescore)
|
||||||
|
- [Standard Library (`modules/std/`)](#standard-library-modulesstd)
|
||||||
|
- [owu (`modules/owu/`)](#owu-modulesowu)
|
||||||
|
- [Performance ⚡](#performance-)
|
||||||
|
- [Examples 🌟](#examples-)
|
||||||
|
- [Hello World](#hello-world)
|
||||||
|
- [Using Functions from Core](#using-functions-from-core)
|
||||||
|
- [Working with Collections](#working-with-collections)
|
||||||
|
- [Development 🛠️](#development-️)
|
||||||
|
- [Running Tests](#running-tests)
|
||||||
|
- [Building Documentation](#building-documentation)
|
||||||
|
- [TODOs 📋](#todos-)
|
||||||
|
- [High Priority](#high-priority)
|
||||||
|
- [Medium Priority](#medium-priority)
|
||||||
|
- [Lower Priority](#lower-priority)
|
||||||
|
- [Completed](#completed)
|
||||||
|
- [Contributing 🤝](#contributing-)
|
||||||
|
- [License 📄](#license-)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
OWA-RS is designed as a lightweight Lisp-like interpreter with:
|
||||||
|
|
||||||
|
- a small Rust runtime,
|
||||||
|
- a modular standard library,
|
||||||
|
- a simple CLI wrapper (owu),
|
||||||
|
- persistent immutable collections via `rpds`.
|
||||||
|
|
||||||
|
> ⚠️ This is a **personal project** for learning and experimentation. It's my very first Rust project, so expect some rough edges. Use at your own risk!
|
||||||
|
|
||||||
|
## Syntax 📝
|
||||||
|
|
||||||
|
OWA-RS uses a familiar Lisp-style syntax with nested expressions.
|
||||||
|
|
||||||
|
### Literals
|
||||||
|
|
||||||
|
- Integers: `42`, `-7`
|
||||||
|
- Floats: `3.14`, `-0.5`
|
||||||
|
- Strings: `"hello world"`
|
||||||
|
- Keywords: `:key`, `:value`
|
||||||
|
- Symbols: `variable-name`, `function?`
|
||||||
|
|
||||||
|
### Collections
|
||||||
|
|
||||||
|
- Lists / Calls: `(function arg1 arg2)`
|
||||||
|
- Vectors: `[1 2 3]`
|
||||||
|
- Maps: `{:key "value" :other 42}`
|
||||||
|
- Sets: `#{"a" "b" "c"}`
|
||||||
|
|
||||||
|
### Special Forms
|
||||||
|
|
||||||
|
- Lambdas: `(lambda [args] body)`
|
||||||
|
- Macros: `(macro [args] body)`
|
||||||
|
- Quotes: `'expression`
|
||||||
|
- Unquotes: `$expression`
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
|
||||||
|
Comments start with `;;` and continue to the end of the line.
|
||||||
|
|
||||||
|
### Annotations
|
||||||
|
|
||||||
|
Annotations are placed in angle brackets (`<annotation>value`) and can contain any value that will be computedduring linting (⚠️ currently not implemented).
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```owa
|
||||||
|
(seq
|
||||||
|
;; Define a function
|
||||||
|
(def greet (lambda [name]
|
||||||
|
(str.concat "Hello, " name "!")))
|
||||||
|
|
||||||
|
;; Call it
|
||||||
|
(trace (greet "World")))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start 🛠️
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Rust 1.70+ (for building from source)
|
||||||
|
- Cargo
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/RuJect/owa-rs.git
|
||||||
|
cd owa-rs
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run a file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./target/release/owa-rs run hello.owa
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run directly without owu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./target/release/owa-rs --no-owu hello.owa
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
When running via owu, these flags are available:
|
||||||
|
|
||||||
|
- `--no-builtins`: skip Rust built-ins
|
||||||
|
- `--no-core`: skip loading core library
|
||||||
|
- `--no-std`: skip loading standard library
|
||||||
|
- `--debug`: enable debug mode
|
||||||
|
- `--test`: run in test mode
|
||||||
|
|
||||||
|
## Architecture 🏗️
|
||||||
|
|
||||||
|
OWA-RS follows a **Parse → Interpret** pipeline with modular design:
|
||||||
|
|
||||||
|
### Rust core
|
||||||
|
|
||||||
|
- `src/parser/`: source code → AST
|
||||||
|
- `src/core/interpreter.rs`: evaluates AST nodes
|
||||||
|
- `src/core/scope.rs`: manages variables and closures
|
||||||
|
- `src/builtins/`: runtime primitives and host operations
|
||||||
|
|
||||||
|
### Modular standard library
|
||||||
|
|
||||||
|
- `modules/core/`: core language utilities, types, math, error handling, loops, tests
|
||||||
|
- `modules/std/`: higher-level helpers such as `path` and `platform`
|
||||||
|
- `modules/owu/`: command-line interface and `run` command support
|
||||||
|
|
||||||
|
The runtime stays intentionally small, while higher-level language features live in library code.
|
||||||
|
|
||||||
|
## Built-in API 🔧
|
||||||
|
|
||||||
|
OWA-RS follows a **minimalist principle** for Rust built-ins: the runtime only implements fundamental primitives and avoids domain/application logic. The result is a small, focused runtime with powerful libraries built on top.
|
||||||
|
|
||||||
|
### Core operations
|
||||||
|
|
||||||
|
- Language: `def`, `lambda`, `macro`, `seq`, `trace`, `apply`, `lookup`, `include`
|
||||||
|
- Scope: `scope`, `set!`, `unset!`, `exec`
|
||||||
|
- Type inspection: `typeof`, `cmp`
|
||||||
|
- Flow: `return`, `break`, `continue` (via `flow` namespace)
|
||||||
|
- Errors: `throw`, `catch` (via `errors` namespace)
|
||||||
|
|
||||||
|
### Built-in namespaces
|
||||||
|
|
||||||
|
- `math`: arithmetic and numeric helpers
|
||||||
|
- `str`: string construction and serialization
|
||||||
|
- `vec`: vector creation and folding
|
||||||
|
- `map`: maps and lookup
|
||||||
|
- `set`: set operations
|
||||||
|
- `cond`: conditional helpers like `if-eq` and `match`
|
||||||
|
- `platform`: environment, CLI parsing, debug flags
|
||||||
|
|
||||||
|
## Standard Library 📚
|
||||||
|
|
||||||
|
The standard library is organized in modules:
|
||||||
|
|
||||||
|
### Core Library (`modules/core/`)
|
||||||
|
|
||||||
|
Essential functionality available by default when running through owu:
|
||||||
|
|
||||||
|
- `assert.owa`: Assertion utilities for testing
|
||||||
|
- `types/`: Type checking and predicates, including:
|
||||||
|
- `main.owa`: Core type utilities and predicates
|
||||||
|
- `bool.owa`, `str.owa`, `vec.owa`, `map.owa`, `set.owa`, `null.owa`, `option.owa`: Type-specific operations
|
||||||
|
- `cmp.owa`: Comparison operations
|
||||||
|
- `math.owa`: Arithmetic operations
|
||||||
|
- `error.owa`: Error handling
|
||||||
|
- `loop.owa`: Looping constructs
|
||||||
|
- `test.owa`: Testing framework
|
||||||
|
|
||||||
|
### Standard Library (`modules/std/`)
|
||||||
|
|
||||||
|
Additional utilities:
|
||||||
|
|
||||||
|
- `platform.owa`: Platform-specific operations
|
||||||
|
|
||||||
|
### owu (`modules/owu/`)
|
||||||
|
|
||||||
|
The OWA Utility tool:
|
||||||
|
|
||||||
|
- `cli/`: Command-line interface framework
|
||||||
|
- `cli/run.owa`: The `run` command handler
|
||||||
|
|
||||||
|
## Performance ⚡
|
||||||
|
|
||||||
|
OWA-RS is a tree-walking interpreter with reasonable performance for its simplicity. Rust's persistent data structures (via rpds) provide efficient immutable collections.
|
||||||
|
|
||||||
|
For performance-critical workloads, consider:
|
||||||
|
|
||||||
|
- Writing hot paths in Rust as built-ins
|
||||||
|
- Avoiding deeply nested function calls
|
||||||
|
- Using tail recursion where possible
|
||||||
|
|
||||||
|
See `benchmarks/` for sample performance characteristics.
|
||||||
|
|
||||||
|
## Examples 🌟
|
||||||
|
|
||||||
|
### Hello World
|
||||||
|
|
||||||
|
```owa
|
||||||
|
(trace "Hello, OWA-RS!")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Functions from Core
|
||||||
|
|
||||||
|
```owa
|
||||||
|
(def fib (lambda [n]
|
||||||
|
(if (>= n 2)
|
||||||
|
(+ (fib (- n 1)) (fib (- n 2)))
|
||||||
|
n)))
|
||||||
|
|
||||||
|
(trace (fib 10))
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with: `./target/release/owa-rs run fib.owa`
|
||||||
|
|
||||||
|
### Working with Collections
|
||||||
|
|
||||||
|
```owa
|
||||||
|
(def numbers [1 2 3 4 5])
|
||||||
|
(trace (vec.len numbers))
|
||||||
|
(trace (vec.first numbers))
|
||||||
|
|
||||||
|
(def config {:name "OWA" :version "0.1.0"})
|
||||||
|
(trace (map.get config :name))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development 🛠️
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
The core library has a built-in testing framework:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./target/release/owa-rs run --test modules/core/tests/main.owa
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building Documentation
|
||||||
|
|
||||||
|
To understand the codebase:
|
||||||
|
|
||||||
|
- `src/core/interpreter.rs` - Main evaluation logic
|
||||||
|
- `src/builtins/` - Rust built-in implementations
|
||||||
|
- `modules/core/` - OWA standard library code
|
||||||
|
- `modules/owu/cli/` - Command-line interface implementation
|
||||||
|
|
||||||
|
## TODOs 📋
|
||||||
|
|
||||||
|
### High Priority
|
||||||
|
|
||||||
|
- [ ] Add proper error reporting with location information
|
||||||
|
- [ ] Improve debugging/tracing utilities
|
||||||
|
- [ ] Add REPL mode for interactive development
|
||||||
|
- [ ] Implement tail call optimization
|
||||||
|
|
||||||
|
### Medium Priority
|
||||||
|
|
||||||
|
- [ ] Pattern matching enhancements
|
||||||
|
- [x] Destructuring in set!/def
|
||||||
|
- [ ] More comprehensive std library
|
||||||
|
- [ ] Package manager / module repository
|
||||||
|
- [ ] Performance profiling tools
|
||||||
|
|
||||||
|
### Lower Priority
|
||||||
|
|
||||||
|
- [ ] Concurrency support
|
||||||
|
- [x] FFI (Foreign Function Interface)
|
||||||
|
- [ ] File system operations (fs module)
|
||||||
|
- [ ] Random number generation
|
||||||
|
- [ ] Compilation to bytecode
|
||||||
|
- [ ] Lint phase
|
||||||
|
|
||||||
|
### Completed
|
||||||
|
|
||||||
|
- [x] Annotations
|
||||||
|
- [x] Separate core and std libraries
|
||||||
|
- [x] CLI framework with commands
|
||||||
|
- [x] Testing framework
|
||||||
|
- [x] Error handling macros
|
||||||
|
|
||||||
|
## Contributing 🤝
|
||||||
|
|
||||||
|
This is a learning project exploring language design and implementation. Contributions are welcome!
|
||||||
|
|
||||||
|
Areas where help is appreciated:
|
||||||
|
|
||||||
|
- Standard library expansion
|
||||||
|
- Bug fixes and optimizations
|
||||||
|
- Documentation and examples
|
||||||
|
- Testing and benchmarking
|
||||||
|
|
||||||
|
## License 📄
|
||||||
|
|
||||||
|
Unlicense License - see LICENSE file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️ and lots of ☕ as a Rust learning project!
|
||||||
17
benchmarks/bench.ps1
Normal file
17
benchmarks/bench.ps1
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
$fileDirname = $PSScriptRoot
|
||||||
|
$owaCmd = ".\target\release\owa-rs.exe"
|
||||||
|
|
||||||
|
for ($i = 1; $i -le 1; $i++) {
|
||||||
|
$pythonFile = "$fileDirname/case$i/main.py"
|
||||||
|
$owaFile = "$fileDirname/case$i/main.owa"
|
||||||
|
$noOwuFile = "$fileDirname/case$i/no_owu.owa"
|
||||||
|
Write-Host "Running case #$i"
|
||||||
|
hyperfine `
|
||||||
|
--warmup 5 `
|
||||||
|
--min-runs 50 `
|
||||||
|
--show-output `
|
||||||
|
--export-markdown "$fileDirname/case$i/result.md" `
|
||||||
|
"python3.14 $pythonFile" `
|
||||||
|
"$owaCmd run $owaFile" `
|
||||||
|
"$owaCmd --no-owu $noOwuFile"
|
||||||
|
}
|
||||||
18
benchmarks/bench.sh
Normal file
18
benchmarks/bench.sh
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
file_dirname="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
owa_cmd="./target/release/owa-rs"
|
||||||
|
|
||||||
|
for i in $(seq 1 1); do
|
||||||
|
python_file="${file_dirname}/case${i}/main.py"
|
||||||
|
owa_file="${file_dirname}/case${i}/main.owa"
|
||||||
|
no_owu_file="${file_dirname}/case${i}/no_owu.owa"
|
||||||
|
echo "Running case #${i}"
|
||||||
|
hyperfine \
|
||||||
|
--warmup 5 \
|
||||||
|
--min-runs 50 \
|
||||||
|
--show-output \
|
||||||
|
--export-markdown "${file_dirname}/case${i}/result.md" \
|
||||||
|
"python3 $python_file" \
|
||||||
|
"$owa_cmd run $owa_file" \
|
||||||
|
"$owa_cmd --no-owu $no_owu_file"
|
||||||
|
done
|
||||||
8
benchmarks/case1/main.owa
Normal file
8
benchmarks/case1/main.owa
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
(seq
|
||||||
|
(def fib (lambda [n]
|
||||||
|
(match n
|
||||||
|
(0 0)
|
||||||
|
(1 1)
|
||||||
|
(_ (+ (this (- n 1)) (this (- n 2)))))))
|
||||||
|
(trace (fib 12))
|
||||||
|
)
|
||||||
9
benchmarks/case1/main.py
Normal file
9
benchmarks/case1/main.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
def fib(n):
|
||||||
|
if n == 0:
|
||||||
|
return 0
|
||||||
|
if n == 1:
|
||||||
|
return 1
|
||||||
|
return fib(n - 1) + fib(n - 2)
|
||||||
|
|
||||||
|
|
||||||
|
print(fib(12))
|
||||||
8
benchmarks/case1/no_owu.owa
Normal file
8
benchmarks/case1/no_owu.owa
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
(builtins.seq
|
||||||
|
(builtins.def fib (builtins.lambda [n]
|
||||||
|
(builtins.cond.match n
|
||||||
|
(0 0)
|
||||||
|
(1 1)
|
||||||
|
(_ (builtins.math.add (this (builtins.math.sub n 1)) (this (builtins.math.sub n 2)))))))
|
||||||
|
(builtins.trace (fib 12))
|
||||||
|
)
|
||||||
5
benchmarks/case1/result.md
Normal file
5
benchmarks/case1/result.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||||
|
|:---|---:|---:|---:|---:|
|
||||||
|
| `python3.14 D:\Tools\Projects\owa-rs\benchmarks/case1/main.py` | 40.5 ± 0.7 | 39.5 | 43.1 | 2.38 ± 0.36 |
|
||||||
|
| `.\target\release\owa-rs.exe run D:\Tools\Projects\owa-rs\benchmarks/case1/main.owa` | 53.3 ± 1.2 | 51.5 | 57.6 | 3.14 ± 0.48 |
|
||||||
|
| `.\target\release\owa-rs.exe --no-owu D:\Tools\Projects\owa-rs\benchmarks/case1/no_owu.owa` | 17.0 ± 2.6 | 15.3 | 37.5 | 1.00 |
|
||||||
175
flake.lock
generated
Normal file
175
flake.lock
generated
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776497206,
|
||||||
|
"narHash": "sha256-Em+RSdFnwyyKPGUBFtQYtVjm+1UvIc9gOR91Y22zlzg=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "df2295365fb081fe0745449762a771290782c22d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"naersk",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752475459,
|
||||||
|
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"naersk": {
|
||||||
|
"inputs": {
|
||||||
|
"fenix": "fenix_2",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776200608,
|
||||||
|
"narHash": "sha256-broZ6RFQr4Fv0wT73gGmzNX14A43TmTFF8g4wDKlNss=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"rev": "8b23250ab45c2a38cd91031aee26478ca4d0a28e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752077645,
|
||||||
|
"narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776169885,
|
||||||
|
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"naersk": "naersk",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776441750,
|
||||||
|
"narHash": "sha256-1rVfG+mj8R4ze+lSYCa4iAv7FzrB03Cprtxmd1MfZak=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "251df518d73abb5c5d573c4d5d266a3edae9ca5a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src_2": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752428706,
|
||||||
|
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
48
flake.nix
Normal file
48
flake.nix
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
naersk.url = "github:nix-community/naersk";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
nixpkgs,
|
||||||
|
fenix,
|
||||||
|
flake-utils,
|
||||||
|
naersk,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system: let
|
||||||
|
pkgs = (import nixpkgs) {
|
||||||
|
inherit system;
|
||||||
|
overlays = [fenix.overlays.default];
|
||||||
|
};
|
||||||
|
naersk' = pkgs.callPackage naersk {};
|
||||||
|
in {
|
||||||
|
defaultPackage = naersk'.buildPackage {
|
||||||
|
src = ./.;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
hyperfine
|
||||||
|
python3
|
||||||
|
alejandra
|
||||||
|
rust-analyzer
|
||||||
|
(pkgs.fenix.stable.withComponents [
|
||||||
|
"cargo"
|
||||||
|
"clippy"
|
||||||
|
"rust-src"
|
||||||
|
"rustc"
|
||||||
|
"rustfmt"
|
||||||
|
])
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
17
modules/core/assert.owa
Normal file
17
modules/core/assert.owa
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
(namespace assert
|
||||||
|
(defmacro ok! [] (if (bool.and $%&)
|
||||||
|
:true
|
||||||
|
(throw! "assertion failed")))
|
||||||
|
|
||||||
|
(defmacro not! [] (if (bool.and $%&)
|
||||||
|
(throw! "assertion failed")
|
||||||
|
:true))
|
||||||
|
|
||||||
|
(defmacro eq! [] (if-eq $(%&)
|
||||||
|
:true
|
||||||
|
(throw! "assertion failed")))
|
||||||
|
|
||||||
|
(defmacro nq! [] (if-eq $(%&)
|
||||||
|
(throw! "assertion failed")
|
||||||
|
:true))
|
||||||
|
)
|
||||||
10
modules/core/ast.owa
Normal file
10
modules/core/ast.owa
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
(namespace ast
|
||||||
|
(defmacro pack! [v]
|
||||||
|
(builtins.ast.pack $v))
|
||||||
|
|
||||||
|
(defmacro value! [v]
|
||||||
|
(map.get (ast.pack! $v) :data))
|
||||||
|
|
||||||
|
(defmacro type! [v]
|
||||||
|
(map.get (ast.pack! $v) :type))
|
||||||
|
)
|
||||||
12
modules/core/branch.owa
Normal file
12
modules/core/branch.owa
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
(seq
|
||||||
|
;; builtins
|
||||||
|
(def if-eq builtins.cond.if_eq)
|
||||||
|
(def match builtins.cond.match)
|
||||||
|
(def if-has builtins.cond.if_has)
|
||||||
|
|
||||||
|
(defmacro if [cond then]
|
||||||
|
(if-eq (:false $cond) (seq $%&) (seq $then)))
|
||||||
|
|
||||||
|
(defmacro unless [cond then]
|
||||||
|
(if-eq (:false $cond) (seq $then) (seq $%&)))
|
||||||
|
)
|
||||||
25
modules/core/cmp.owa
Normal file
25
modules/core/cmp.owa
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
(seq
|
||||||
|
(defmacro eq? <Bool>[]
|
||||||
|
(if-eq ($%%) :true :false))
|
||||||
|
(defmacro nq? <Bool>[]
|
||||||
|
(if-eq ($%%) :false :true))
|
||||||
|
|
||||||
|
(fn cmp <Keyword>[left right]
|
||||||
|
(match (builtins.cmp left right)
|
||||||
|
(-1 :less)
|
||||||
|
(0 :equal)
|
||||||
|
(1 :greater)))
|
||||||
|
|
||||||
|
|
||||||
|
(fn lt? <Bool>[left right]
|
||||||
|
(eq? (cmp left right) :less))
|
||||||
|
|
||||||
|
(fn lte? <Bool>[left right]
|
||||||
|
(bool.or (lt? left right) (eq? left right)))
|
||||||
|
|
||||||
|
(fn gt? <Bool>[left right]
|
||||||
|
(eq? (cmp left right) :greater))
|
||||||
|
|
||||||
|
(fn gte? <Bool>[left right]
|
||||||
|
(bool.or (gt? left right) (eq? left right)))
|
||||||
|
)
|
||||||
22
modules/core/error.owa
Normal file
22
modules/core/error.owa
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
(seq
|
||||||
|
(fn throw! []
|
||||||
|
(throw.runtime_error! (str.join "" %%)))
|
||||||
|
(fn panic! [v]
|
||||||
|
(throw.panic! v))
|
||||||
|
(fn unreachable! []
|
||||||
|
(throw! "Reached unreachable code"))
|
||||||
|
(fn not_implemented! []
|
||||||
|
(throw! "Not implemented"))
|
||||||
|
(def try builtins.errors.try)
|
||||||
|
|
||||||
|
(defmacro catch [pattern then]
|
||||||
|
(lambda [msg type] (if-eq (type $pattern)
|
||||||
|
$then
|
||||||
|
:null)))
|
||||||
|
|
||||||
|
(namespace throw
|
||||||
|
(def panic! builtins.errors.throw_panic)
|
||||||
|
(def type_error! builtins.errors.throw_type_error)
|
||||||
|
(def arity_error! builtins.errors.throw_arity_error)
|
||||||
|
(def runtime_error! builtins.errors.throw_runtime_error)
|
||||||
|
))
|
||||||
32
modules/core/ffi.owa
Normal file
32
modules/core/ffi.owa
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
(namespace ffi
|
||||||
|
(def unload builtins.ffi.unload)
|
||||||
|
|
||||||
|
(def ctypes [
|
||||||
|
:i8 :i16 :i32 :i64
|
||||||
|
:u8 :u16 :u32 :u64
|
||||||
|
:f32 :f64
|
||||||
|
:ptr
|
||||||
|
:str
|
||||||
|
])
|
||||||
|
|
||||||
|
(defmacro extern [name libname]
|
||||||
|
(namespace $name
|
||||||
|
(builtins.ffi.load $libname)
|
||||||
|
(fn call [symbol signature args]
|
||||||
|
(builtins.ffi.call $libname symbol signature args)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(defmacro native [name libname]
|
||||||
|
(namespace $name
|
||||||
|
(builtins.ffi.load $libname)
|
||||||
|
(defmacro fn [name symbol args ret]
|
||||||
|
(def '$name #(builtins.ffi.call $libname '$symbol ['$args '$ret] %&))
|
||||||
|
)
|
||||||
|
$%&
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(extern ucrtbase "ucrtbase.dll")
|
||||||
|
)
|
||||||
8
modules/core/lambda.owa
Normal file
8
modules/core/lambda.owa
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
(seq
|
||||||
|
;; builtins
|
||||||
|
(def apply builtins.apply)
|
||||||
|
(def lambda builtins.lambda)
|
||||||
|
|
||||||
|
(defmacro fn [name params body]
|
||||||
|
(def $name (lambda $params $body)))
|
||||||
|
)
|
||||||
23
modules/core/loop.owa
Normal file
23
modules/core/loop.owa
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
(seq
|
||||||
|
(def break builtins.flow.break)
|
||||||
|
(def continue builtins.flow.continue)
|
||||||
|
(def loop builtins.flow.loop)
|
||||||
|
|
||||||
|
(defmacro return []
|
||||||
|
(builtins.flow.return $%% null))
|
||||||
|
|
||||||
|
(defmacro while [cond] (loop
|
||||||
|
(if (eq? $cond :false) (break) :null)
|
||||||
|
$%&))
|
||||||
|
|
||||||
|
(defmacro do-while [cond] (loop
|
||||||
|
$%&
|
||||||
|
(if (eq? $cond :false) (break) :null)))
|
||||||
|
|
||||||
|
(defmacro for [var iter] (vec.map
|
||||||
|
(lambda [x] (seq
|
||||||
|
(def $var x)
|
||||||
|
$%&))
|
||||||
|
(vec.from $iter)
|
||||||
|
))
|
||||||
|
)
|
||||||
25
modules/core/main.owa
Normal file
25
modules/core/main.owa
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
(builtins.seq
|
||||||
|
(builtins.def include builtins.include)
|
||||||
|
|
||||||
|
(include "scope")
|
||||||
|
(include "branch")
|
||||||
|
(include "lambda")
|
||||||
|
|
||||||
|
(def exec builtins.exec)
|
||||||
|
(def trace builtins.trace)
|
||||||
|
(def typeof builtins.typeof)
|
||||||
|
|
||||||
|
(fn identity [x] x)
|
||||||
|
|
||||||
|
(include "types")
|
||||||
|
(include "assert")
|
||||||
|
(include "ast")
|
||||||
|
(include "cmp")
|
||||||
|
(include "error")
|
||||||
|
(include "ffi")
|
||||||
|
(include "loop")
|
||||||
|
(include "math")
|
||||||
|
(include "test")
|
||||||
|
|
||||||
|
(test.space "core" (include "tests/main"))
|
||||||
|
)
|
||||||
14
modules/core/math.owa
Normal file
14
modules/core/math.owa
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
(seq
|
||||||
|
(def + builtins.math.add)
|
||||||
|
(def - builtins.math.sub)
|
||||||
|
(def * builtins.math.mul)
|
||||||
|
(def / builtins.math.div)
|
||||||
|
(def % builtins.math.mod)
|
||||||
|
(def ** builtins.math.pow)
|
||||||
|
|
||||||
|
(fn ++ <Number>[<Number>x]
|
||||||
|
(+ x 1))
|
||||||
|
|
||||||
|
(fn -- <Number>[<Number>x]
|
||||||
|
(- x 1))
|
||||||
|
)
|
||||||
22
modules/core/scope.owa
Normal file
22
modules/core/scope.owa
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
(builtins.seq
|
||||||
|
;; builtins
|
||||||
|
(builtins.def def builtins.def)
|
||||||
|
(def lookup builtins.lookup)
|
||||||
|
(def set! builtins.set!)
|
||||||
|
(def scope builtins.scope)
|
||||||
|
|
||||||
|
;; macro
|
||||||
|
(def macro builtins.macro)
|
||||||
|
(def defmacro (macro [name params body]
|
||||||
|
(def $name (macro $params $body))))
|
||||||
|
|
||||||
|
;; flow
|
||||||
|
(defmacro seq []
|
||||||
|
(builtins.seq :null $%&))
|
||||||
|
(defmacro namespace [name] (def $name (scope $%&)))
|
||||||
|
|
||||||
|
;; other
|
||||||
|
(defmacro mut! [v fn] (seq
|
||||||
|
(set! (ast.value! $v) ($fn $v $%&)))
|
||||||
|
)
|
||||||
|
)
|
||||||
16
modules/core/test.owa
Normal file
16
modules/core/test.owa
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
(namespace test
|
||||||
|
(def target (lookup __test__ null))
|
||||||
|
|
||||||
|
(defmacro if-target [then]
|
||||||
|
(if-eq (test.target null) :null (seq $%%)))
|
||||||
|
|
||||||
|
(defmacro case [name] (test.if-target
|
||||||
|
(builtins.errors.try
|
||||||
|
(scope $%& (trace "Running test \"" $name "\": OK ✅"))
|
||||||
|
#(trace "Running test \"" $name "\": FAILED ❌ (" %2 ": " %1 ")"))))
|
||||||
|
|
||||||
|
(defmacro space [name] (if
|
||||||
|
(bool.or (eq? test.target $name) (eq? test.target "."))
|
||||||
|
(namespace $name (seq $%&))
|
||||||
|
))
|
||||||
|
)
|
||||||
38
modules/core/tests/bool.owa
Normal file
38
modules/core/tests/bool.owa
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
(namespace tests.bool
|
||||||
|
(test.case "bool.bool?"
|
||||||
|
(assert.ok! (bool? true))
|
||||||
|
(assert.ok! (bool? false))
|
||||||
|
(assert.not! (bool? 1))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "bool.not"
|
||||||
|
(assert.eq! (bool.not true) false)
|
||||||
|
(assert.eq! (bool.not false) true)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "bool.and"
|
||||||
|
(assert.ok! (bool.and true true))
|
||||||
|
(assert.not! (bool.and true false))
|
||||||
|
(assert.not! (bool.and false true))
|
||||||
|
(assert.not! (bool.and false false))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "bool.or"
|
||||||
|
(assert.ok! (bool.or true true))
|
||||||
|
(assert.ok! (bool.or true false))
|
||||||
|
(assert.ok! (bool.or false true))
|
||||||
|
(assert.not! (bool.or false false))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "bool.nand"
|
||||||
|
(assert.not! (bool.nand true true))
|
||||||
|
(assert.ok! (bool.nand true false))
|
||||||
|
(assert.ok! (bool.nand false true))
|
||||||
|
(assert.ok! (bool.nand false false))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "bool.new"
|
||||||
|
(assert.eq! (bool.new true) true)
|
||||||
|
(assert.eq! (bool.new false) false)
|
||||||
|
)
|
||||||
|
)
|
||||||
41
modules/core/tests/cmp.owa
Normal file
41
modules/core/tests/cmp.owa
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
(namespace tests.cmp
|
||||||
|
(test.case "cmp.eq?"
|
||||||
|
(assert.ok! (eq? 1 1))
|
||||||
|
(assert.not! (eq? 1 2))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "cmp.nq?"
|
||||||
|
(assert.ok! (nq? 1 2))
|
||||||
|
(assert.not! (nq? 1 1))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "cmp.cmp"
|
||||||
|
(assert.eq! (cmp 1 2) :less)
|
||||||
|
(assert.eq! (cmp 2 1) :greater)
|
||||||
|
(assert.eq! (cmp 1 1) :equal)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "cmp.lt?"
|
||||||
|
(assert.ok! (lt? 1 2))
|
||||||
|
(assert.not! (lt? 2 1))
|
||||||
|
(assert.not! (lt? 1 1))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "cmp.lte?"
|
||||||
|
(assert.ok! (lte? 1 2))
|
||||||
|
(assert.ok! (lte? 1 1))
|
||||||
|
(assert.not! (lte? 2 1))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "cmp.gt?"
|
||||||
|
(assert.ok! (gt? 2 1))
|
||||||
|
(assert.not! (gt? 1 2))
|
||||||
|
(assert.not! (gt? 1 1))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "cmp.gte?"
|
||||||
|
(assert.ok! (gte? 2 1))
|
||||||
|
(assert.ok! (gte? 1 1))
|
||||||
|
(assert.not! (gte? 1 2))
|
||||||
|
)
|
||||||
|
)
|
||||||
42
modules/core/tests/loop.owa
Normal file
42
modules/core/tests/loop.owa
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
(namespace tests.loop
|
||||||
|
(test.case "while.basic-true"
|
||||||
|
(def counter 0)
|
||||||
|
(while (eq? counter 0)
|
||||||
|
(set! counter 1))
|
||||||
|
(assert.eq! counter 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "while.condition-false"
|
||||||
|
(def counter 5)
|
||||||
|
(while (lt? counter 0)
|
||||||
|
(set! counter (+ counter 1)))
|
||||||
|
(assert.eq! counter 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "while.counting-loop"
|
||||||
|
(def n 0)
|
||||||
|
(while (lt? n 10)
|
||||||
|
(set! n (+ n 1)))
|
||||||
|
(assert.eq! n 10)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "while.loop-condition-reached"
|
||||||
|
(def i 0)
|
||||||
|
(while (lt? i 5)
|
||||||
|
(set! i (+ i 1)))
|
||||||
|
(assert.eq! i 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "while.nested"
|
||||||
|
(def outer 0)
|
||||||
|
(def inner 0)
|
||||||
|
(def total 0)
|
||||||
|
(while (lt? outer 3)
|
||||||
|
(set! inner 0)
|
||||||
|
(while (lt? inner 2)
|
||||||
|
(set! total (+ total 1))
|
||||||
|
(set! inner (+ inner 1)))
|
||||||
|
(set! outer (+ outer 1)))
|
||||||
|
(assert.eq! total 6)
|
||||||
|
)
|
||||||
|
)
|
||||||
14
modules/core/tests/main.owa
Normal file
14
modules/core/tests/main.owa
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
(seq
|
||||||
|
(include "vec")
|
||||||
|
(include "str")
|
||||||
|
(include "set")
|
||||||
|
(include "map")
|
||||||
|
(include "bool")
|
||||||
|
(include "option")
|
||||||
|
(include "scope")
|
||||||
|
(include "null")
|
||||||
|
(include "cmp")
|
||||||
|
(include "math")
|
||||||
|
(include "types")
|
||||||
|
(include "loop")
|
||||||
|
)
|
||||||
83
modules/core/tests/map.owa
Normal file
83
modules/core/tests/map.owa
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
(namespace tests.map
|
||||||
|
(test.case "map.len"
|
||||||
|
(assert.eq! (map.len (map.new)) 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.empty?"
|
||||||
|
(assert.ok! (map.empty? (map.new)))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.is-empty?"
|
||||||
|
(assert.ok! (map.is-empty? (map.new)))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.get"
|
||||||
|
(seq
|
||||||
|
(def m (map.new :a 1 :b 2))
|
||||||
|
(assert.eq! (map.get m :a) 1)
|
||||||
|
(assert.eq! (map.get m :b) 2)
|
||||||
|
(assert.eq! (map.get m :c) null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.put"
|
||||||
|
(seq
|
||||||
|
(def m (map.new :a 1))
|
||||||
|
(def m2 (map.put m :b 2))
|
||||||
|
(assert.eq! (map.len m2) 2)
|
||||||
|
(assert.eq! (map.get m2 :b) 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.has-key?"
|
||||||
|
(seq
|
||||||
|
(def m (map.new :a 1 :b 2))
|
||||||
|
(assert.ok! (map.has-key? m :a))
|
||||||
|
(assert.not! (map.has-key? m :c))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.has-value?"
|
||||||
|
(seq
|
||||||
|
(def m (map.new :a 1 :b 2))
|
||||||
|
(assert.ok! (map.has-value? m 1))
|
||||||
|
(assert.not! (map.has-value? m 5))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.keys"
|
||||||
|
(seq
|
||||||
|
(def m (map.new :a 1 :b 2))
|
||||||
|
(def ks (map.keys m))
|
||||||
|
(assert.eq! (vec.len ks) 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.values"
|
||||||
|
(seq
|
||||||
|
(def m (map.new :a 1 :b 2))
|
||||||
|
(def vs (map.values m))
|
||||||
|
(assert.eq! (vec.len vs) 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.merge"
|
||||||
|
(seq
|
||||||
|
(def m1 (map.new :a 1 :b 2))
|
||||||
|
(def m2 (map.new :b 20 :c 3))
|
||||||
|
(def m3 (map.merge m1 m2))
|
||||||
|
(assert.eq! (map.get m3 :a) 1)
|
||||||
|
(assert.eq! (map.get m3 :b) 20)
|
||||||
|
(assert.eq! (map.get m3 :c) 3)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "map.dissoc"
|
||||||
|
(seq
|
||||||
|
(def m (map.new :a 1 :b 2 :c 3))
|
||||||
|
(def m2 (map.dissoc m :b))
|
||||||
|
(assert.eq! (map.len m2) 2)
|
||||||
|
(assert.not! (map.has-key? m2 :b))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
40
modules/core/tests/math.owa
Normal file
40
modules/core/tests/math.owa
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
(namespace tests.math
|
||||||
|
(test.case "math.add"
|
||||||
|
(assert.eq! (+ 1 2) 3)
|
||||||
|
(assert.eq! (+ 0 0) 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "math.sub"
|
||||||
|
(assert.eq! (- 5 3) 2)
|
||||||
|
(assert.eq! (- 1 1) 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "math.mul"
|
||||||
|
(assert.eq! (* 3 4) 12)
|
||||||
|
(assert.eq! (* 0 5) 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "math.div"
|
||||||
|
(assert.eq! (/ 10 2) 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "math.mod"
|
||||||
|
(assert.eq! (% 10 3) 1)
|
||||||
|
(assert.eq! (% 5 5) 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "math.pow"
|
||||||
|
(assert.eq! (** 2 3) 8)
|
||||||
|
(assert.eq! (** 5 2) 25)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "math.++"
|
||||||
|
(assert.eq! (++ 5) 6)
|
||||||
|
(assert.eq! (++ 0) 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "math.--"
|
||||||
|
(assert.eq! (-- 5) 4)
|
||||||
|
(assert.eq! (-- 0) -1)
|
||||||
|
)
|
||||||
|
)
|
||||||
8
modules/core/tests/null.owa
Normal file
8
modules/core/tests/null.owa
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
(namespace tests.null
|
||||||
|
(test.case "null.null?"
|
||||||
|
(assert.ok! (null? null))
|
||||||
|
(assert.ok! (null? :nil))
|
||||||
|
(assert.not! (null? 0))
|
||||||
|
(assert.not! (null? false))
|
||||||
|
)
|
||||||
|
)
|
||||||
33
modules/core/tests/option.owa
Normal file
33
modules/core/tests/option.owa
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
(namespace tests.option
|
||||||
|
(test.case "option.ok"
|
||||||
|
(seq
|
||||||
|
(def opt (option.ok 42))
|
||||||
|
(assert.ok! (option.ok? opt))
|
||||||
|
(assert.eq! (vec.get opt 1) 42)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "option.err"
|
||||||
|
(seq
|
||||||
|
(def opt (option.err "error"))
|
||||||
|
(assert.ok! (option.err? opt))
|
||||||
|
(assert.eq! (vec.get opt 1) "error")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "option.ok?"
|
||||||
|
(assert.ok! (option.ok? (option.ok 1)))
|
||||||
|
(assert.not! (option.ok? (option.err "x")))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "option.err?"
|
||||||
|
(assert.ok! (option.err? (option.err "x")))
|
||||||
|
(assert.not! (option.err? (option.ok 1)))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "option.option?"
|
||||||
|
(assert.ok! (option? (option.ok 1)))
|
||||||
|
(assert.ok! (option? (option.err "x")))
|
||||||
|
(assert.not! (option? [1 2]))
|
||||||
|
)
|
||||||
|
)
|
||||||
78
modules/core/tests/scope.owa
Normal file
78
modules/core/tests/scope.owa
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
(namespace tests.scopes
|
||||||
|
(test.case "def"
|
||||||
|
(def x 1)
|
||||||
|
(assert.eq! x 1)
|
||||||
|
|
||||||
|
(def [y z] [1 2])
|
||||||
|
(assert.eq! y 1)
|
||||||
|
(assert.eq! z 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "def bounded"
|
||||||
|
(def x 1)
|
||||||
|
(try
|
||||||
|
(def x 2)
|
||||||
|
(catch :Panic (return))
|
||||||
|
)
|
||||||
|
(unreachable!)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set!"
|
||||||
|
(def [x y] [1 1])
|
||||||
|
(set! x 2)
|
||||||
|
(assert.eq! x 2)
|
||||||
|
|
||||||
|
(set! [x y] [3 4])
|
||||||
|
(assert.eq! x 3)
|
||||||
|
(assert.eq! y 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set! unbounded"
|
||||||
|
(try
|
||||||
|
(set! x 2)
|
||||||
|
(catch :UnboundVariable (return))
|
||||||
|
)
|
||||||
|
(unreachable!)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "lookup"
|
||||||
|
(def x 1)
|
||||||
|
(assert.eq! (lookup x null) 1)
|
||||||
|
(assert.eq! (lookup y null) null)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "inheritance"
|
||||||
|
(scope
|
||||||
|
(def x 1)
|
||||||
|
(scope
|
||||||
|
(assert.eq! x 1)
|
||||||
|
(set! x 2)
|
||||||
|
)
|
||||||
|
(assert.eq! x 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "shadowing"
|
||||||
|
(scope
|
||||||
|
(def x 100)
|
||||||
|
(scope
|
||||||
|
(def x 1)
|
||||||
|
)
|
||||||
|
(assert.eq! x 100)
|
||||||
|
)
|
||||||
|
(assert.eq! (lookup x null) null)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "leakage"
|
||||||
|
(scope (def x 1))
|
||||||
|
(assert.eq! (lookup x null) null)
|
||||||
|
|
||||||
|
(scope (def y 2))
|
||||||
|
(assert.eq! (lookup y null) null)
|
||||||
|
|
||||||
|
(scope
|
||||||
|
(scope (def z 1))
|
||||||
|
(assert.eq! (lookup z null) null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
44
modules/core/tests/set.owa
Normal file
44
modules/core/tests/set.owa
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
(namespace tests.set
|
||||||
|
(test.case "set.empty?"
|
||||||
|
(assert.ok! (set.empty? (set.new)))
|
||||||
|
(assert.not! (set.empty? (set.new 1)))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set.len"
|
||||||
|
(assert.eq! (set.len (set.new)) 0)
|
||||||
|
(assert.eq! (set.len (set.new 1)) 1)
|
||||||
|
(assert.eq! (set.len (set.new 1 2 3)) 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set.has"
|
||||||
|
(assert.ok! (set.has (set.new 1 2 3) 2))
|
||||||
|
(assert.not! (set.has (set.new 1 2 3) 5))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set.add"
|
||||||
|
(assert.ok! (set.has (set.add (set.new 1 2) 3) 3))
|
||||||
|
(assert.eq! (set.len (set.add (set.new 1 2) 3)) 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set.remove"
|
||||||
|
(assert.not! (set.has (set.remove (set.new 1 2 3) 2) 2))
|
||||||
|
(assert.eq! (set.len (set.remove (set.new 1 2 3) 2)) 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set.union"
|
||||||
|
(assert.eq! (set.len (set.union (set.new 1 2) (set.new 2 3))) 3)
|
||||||
|
(assert.ok! (set.has (set.union (set.new 1 2) (set.new 2 3)) 1))
|
||||||
|
(assert.ok! (set.has (set.union (set.new 1 2) (set.new 2 3)) 3))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set.intersection"
|
||||||
|
(assert.eq! (set.len (set.intersection (set.new 1 2 3) (set.new 2 3 4))) 2)
|
||||||
|
(assert.ok! (set.has (set.intersection (set.new 1 2 3) (set.new 2 3 4)) 2))
|
||||||
|
(assert.ok! (set.has (set.intersection (set.new 1 2 3) (set.new 2 3 4)) 3))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "set.difference"
|
||||||
|
(assert.eq! (set.len (set.difference (set.new 1 2 3) (set.new 2 3))) 1)
|
||||||
|
(assert.ok! (set.has (set.difference (set.new 1 2 3) (set.new 2 3)) 1))
|
||||||
|
)
|
||||||
|
)
|
||||||
67
modules/core/tests/str.owa
Normal file
67
modules/core/tests/str.owa
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
(namespace tests.str
|
||||||
|
(test.case "str.len"
|
||||||
|
(assert.eq! (str.len "") 0)
|
||||||
|
(assert.eq! (str.len "hello") 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.empty?"
|
||||||
|
(assert.ok! (str.empty? ""))
|
||||||
|
(assert.not! (str.empty? "a"))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.is-empty?"
|
||||||
|
(assert.ok! (str.is-empty? ""))
|
||||||
|
(assert.not! (str.is-empty? "a"))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.first"
|
||||||
|
(assert.eq! (str.first "hello") "h")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.last"
|
||||||
|
(assert.eq! (str.last "hello") "o")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.concat"
|
||||||
|
(assert.eq! (str.concat "hello" " " "world") "hello world")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.substring"
|
||||||
|
(assert.eq! (str.substring "hello" 0 3) "hel")
|
||||||
|
(assert.eq! (str.substring "hello" 1 4) "ell")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.take"
|
||||||
|
(assert.eq! (str.take "hello" 3) "hel")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.drop"
|
||||||
|
(assert.eq! (str.drop "hello" 2) "llo")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.reverse"
|
||||||
|
(assert.eq! (str.reverse "hello") "olleh")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.starts-with?"
|
||||||
|
(assert.ok! (str.starts-with? "hello" "hel"))
|
||||||
|
(assert.not! (str.starts-with? "hello" "xyz"))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.ends-with?"
|
||||||
|
(assert.ok! (str.ends-with? "hello" "lo"))
|
||||||
|
(assert.not! (str.ends-with? "hello" "xyz"))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.split"
|
||||||
|
(assert.eq! (str.split "hello world" " ") ["hello" "world"])
|
||||||
|
(assert.eq! (str.split "hello world" "w") ["hello " "orld"])
|
||||||
|
(assert.eq! (str.split "hello world" "x") ["hello world"])
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "str.join"
|
||||||
|
(assert.eq! (str.join " " ["hello" "world"]) "hello world")
|
||||||
|
(assert.eq! (str.join "w" ["hello " "orld"]) "hello world")
|
||||||
|
(assert.eq! (str.join "x" ["hello world"]) "hello world")
|
||||||
|
)
|
||||||
|
)
|
||||||
53
modules/core/tests/types.owa
Normal file
53
modules/core/tests/types.owa
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
(namespace tests.types
|
||||||
|
(test.case "types.is?"
|
||||||
|
(assert.ok! (is? 42 :int))
|
||||||
|
(assert.ok! (is? "hello" :str))
|
||||||
|
(assert.ok! (is? [] :vec))
|
||||||
|
(assert.not! (is? 42 :str))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "types.guard"
|
||||||
|
(seq
|
||||||
|
(def int-guard (guard :int))
|
||||||
|
(assert.ok! (int-guard 42))
|
||||||
|
(assert.not! (int-guard "42"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "types.kw?"
|
||||||
|
(assert.ok! (kw? :keyword))
|
||||||
|
(assert.not! (kw? "not-keyword"))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "types.type?"
|
||||||
|
(assert.ok! (type? :int))
|
||||||
|
(assert.ok! (type? :str))
|
||||||
|
(assert.not! (type? :unknown))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "types.str?"
|
||||||
|
(assert.ok! (str? "hello"))
|
||||||
|
(assert.not! (str? 42))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "types.vec?"
|
||||||
|
(assert.ok! (vec? []))
|
||||||
|
(assert.ok! (vec? [1 2 3]))
|
||||||
|
(assert.not! (vec? 42))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "types.bool?"
|
||||||
|
(assert.ok! (bool? true))
|
||||||
|
(assert.ok! (bool? false))
|
||||||
|
(assert.not! (bool? 1))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "types.null?"
|
||||||
|
(assert.ok! (null? null))
|
||||||
|
(assert.not! (null? 0))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "types.map.is?"
|
||||||
|
(assert.ok! (map.is? (map.new)))
|
||||||
|
)
|
||||||
|
)
|
||||||
91
modules/core/tests/vec.owa
Normal file
91
modules/core/tests/vec.owa
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
(namespace tests.vec
|
||||||
|
(test.case "vec.empty?"
|
||||||
|
(assert.ok! (vec.empty? []))
|
||||||
|
(assert.not! (vec.empty? [1]))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.len"
|
||||||
|
(assert.eq! (vec.len []) 0)
|
||||||
|
(assert.eq! (vec.len [1]) 1)
|
||||||
|
(assert.eq! (vec.len [1 2 3]) 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.first"
|
||||||
|
(assert.eq! (vec.first [1 2 3]) 1)
|
||||||
|
(assert.eq! (vec.first ["a"]) "a")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.last"
|
||||||
|
(assert.eq! (vec.last [1 2 3]) 3)
|
||||||
|
(assert.eq! (vec.last ["x"]) "x")
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.get"
|
||||||
|
(assert.eq! (vec.get [10 20 30] 0) 10)
|
||||||
|
(assert.eq! (vec.get [10 20 30] 1) 20)
|
||||||
|
(assert.eq! (vec.get [10 20 30] 2) 30)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.map"
|
||||||
|
(assert.eq!
|
||||||
|
(vec.map (lambda [x _] (+ x 1)) [1 2 3])
|
||||||
|
[2 3 4])
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.filter"
|
||||||
|
(assert.eq!
|
||||||
|
(vec.filter (lambda [x _] (gt? x 1)) [1 2 3])
|
||||||
|
[2 3])
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.has"
|
||||||
|
(assert.ok! (vec.has [1 2 3] 2))
|
||||||
|
(assert.not! (vec.has [1 2 3] 5))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.includes?"
|
||||||
|
(assert.ok! (vec.includes? [1 2 3] 2))
|
||||||
|
(assert.not! (vec.includes? [1 2 3] 5))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.find"
|
||||||
|
(assert.eq!
|
||||||
|
(vec.find (lambda [x _] (gt? x 2)) [1 2 3 4])
|
||||||
|
3)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.reverse"
|
||||||
|
(assert.eq! (vec.reverse [1 2 3]) [3 2 1])
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.take"
|
||||||
|
(assert.eq! (vec.take [1 2 3 4 5] 3) [1 2 3])
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.drop"
|
||||||
|
(assert.eq! (vec.drop [1 2 3 4 5] 2) [3 4 5])
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.uniq"
|
||||||
|
(assert.eq! (vec.uniq [1 2 2 3 3 3]) [1 2 3])
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.every?"
|
||||||
|
(assert.ok! (vec.every? (lambda [x _] (gt? x 0)) [1 2 3]))
|
||||||
|
(assert.not! (vec.every? (lambda [x _] (gt? x 1)) [1 2 3]))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.some?"
|
||||||
|
(assert.ok! (vec.some? (lambda [x _] (gt? x 2)) [1 2 3]))
|
||||||
|
(assert.not! (vec.some? (lambda [x _] (gt? x 5)) [1 2 3]))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.count"
|
||||||
|
(assert.eq! (vec.count (lambda [x _] (gt? x 1)) [1 2 3]) 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "vec.sum"
|
||||||
|
(assert.eq! (vec.sum [1 2 3]) 6)
|
||||||
|
(assert.eq! (vec.sum []) 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
23
modules/core/types/bool.owa
Normal file
23
modules/core/types/bool.owa
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
(seq
|
||||||
|
(def true :true)
|
||||||
|
(def false :false)
|
||||||
|
(fn bool? [x]
|
||||||
|
(bool.or (eq? x true) (eq? x false)))
|
||||||
|
|
||||||
|
(namespace bool
|
||||||
|
(fn new [x]
|
||||||
|
(eq? x true))
|
||||||
|
(def __call__ new)
|
||||||
|
|
||||||
|
(defmacro or []
|
||||||
|
(if-eq (false $%%) false true))
|
||||||
|
|
||||||
|
(defmacro and []
|
||||||
|
(if-has false ($%%) false true))
|
||||||
|
|
||||||
|
(defmacro nand []
|
||||||
|
(if-has false ($%%) true false))
|
||||||
|
|
||||||
|
(fn not [x]
|
||||||
|
(if-eq (x true) false true))
|
||||||
|
))
|
||||||
39
modules/core/types/main.owa
Normal file
39
modules/core/types/main.owa
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
(seq
|
||||||
|
(def types [
|
||||||
|
:symbol
|
||||||
|
:keyword
|
||||||
|
:str
|
||||||
|
:float
|
||||||
|
:int
|
||||||
|
:vec
|
||||||
|
:call
|
||||||
|
:set
|
||||||
|
:map
|
||||||
|
:quote
|
||||||
|
:lambda
|
||||||
|
:macro
|
||||||
|
])
|
||||||
|
|
||||||
|
(fn is? [x type]
|
||||||
|
(eq? (typeof x) type))
|
||||||
|
(fn guard [type]
|
||||||
|
(lambda [x] (is? x type)))
|
||||||
|
|
||||||
|
(def kw? (guard :keyword))
|
||||||
|
(def kw builtins.str.kw)
|
||||||
|
|
||||||
|
(include "bool")
|
||||||
|
(include "map")
|
||||||
|
(include "null")
|
||||||
|
(include "option")
|
||||||
|
(include "set")
|
||||||
|
(include "str")
|
||||||
|
(include "struct")
|
||||||
|
(include "vec")
|
||||||
|
|
||||||
|
(def vec? (guard :vec))
|
||||||
|
(def map? (guard :map))
|
||||||
|
|
||||||
|
(fn type? [x]
|
||||||
|
(vec.has types x))
|
||||||
|
)
|
||||||
94
modules/core/types/map.owa
Normal file
94
modules/core/types/map.owa
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
(namespace map
|
||||||
|
(def type :map)
|
||||||
|
(def is? (guard type))
|
||||||
|
(def new builtins.map.new)
|
||||||
|
(def __call__ new)
|
||||||
|
|
||||||
|
(fn keys [m]
|
||||||
|
(vec.map #(vec.first %1) (vec.from m)))
|
||||||
|
|
||||||
|
(fn values [m]
|
||||||
|
(vec.map #(vec.last %1) (vec.from m)))
|
||||||
|
|
||||||
|
(fn len [m]
|
||||||
|
(vec.len (vec.from m)))
|
||||||
|
|
||||||
|
(fn empty? [m]
|
||||||
|
(eq? (len m) 0))
|
||||||
|
|
||||||
|
(fn is-empty? [m]
|
||||||
|
(empty? m))
|
||||||
|
|
||||||
|
(fn get [m key]
|
||||||
|
(vec.fold
|
||||||
|
(lambda [result pair _]
|
||||||
|
(if (eq? (vec.get pair 0) key)
|
||||||
|
(vec.get pair 1)
|
||||||
|
result))
|
||||||
|
null
|
||||||
|
(vec.from m)))
|
||||||
|
|
||||||
|
(fn put [m key value]
|
||||||
|
(apply apply new (vec.from m) [[key value]]))
|
||||||
|
|
||||||
|
(fn assoc [m key value]
|
||||||
|
(put m key value))
|
||||||
|
|
||||||
|
(fn dissoc [m key]
|
||||||
|
(apply apply new (vec.filter
|
||||||
|
(lambda [pair _] (nq?
|
||||||
|
(vec.get pair 0)
|
||||||
|
key
|
||||||
|
))
|
||||||
|
(vec.from m))
|
||||||
|
))
|
||||||
|
|
||||||
|
(fn has-key? [m key]
|
||||||
|
(if (null? (get m key))
|
||||||
|
false
|
||||||
|
true))
|
||||||
|
|
||||||
|
(fn has-value? [m value]
|
||||||
|
(vec.fold
|
||||||
|
(lambda [result pair _]
|
||||||
|
(seq
|
||||||
|
(def k (vec.get pair 0))
|
||||||
|
(def v (vec.get pair 1))
|
||||||
|
(bool.or result (eq? v value))))
|
||||||
|
false
|
||||||
|
(vec.from m)))
|
||||||
|
|
||||||
|
(fn map [callback m]
|
||||||
|
(apply new (vec.apply-flat (vec.map
|
||||||
|
(lambda [pair _]
|
||||||
|
(seq
|
||||||
|
(def k (vec.get pair 0))
|
||||||
|
(def v (vec.get pair 1))
|
||||||
|
(def new-v (callback v k))
|
||||||
|
[k new-v]))
|
||||||
|
(vec.from m)))))
|
||||||
|
|
||||||
|
(fn filter [callback m]
|
||||||
|
(apply new (vec.apply-flat (vec.filter
|
||||||
|
(lambda [pair _]
|
||||||
|
(seq
|
||||||
|
(def k (vec.get pair 0))
|
||||||
|
(def v (vec.get pair 1))
|
||||||
|
(callback v k)))
|
||||||
|
(vec.from m)))))
|
||||||
|
|
||||||
|
(fn merge [m1 m2]
|
||||||
|
(apply apply new (vec.from m1) (vec.from m2)))
|
||||||
|
|
||||||
|
(fn merge-all []
|
||||||
|
(vec.fold
|
||||||
|
(lambda [acc m _]
|
||||||
|
(merge acc m))
|
||||||
|
(new)
|
||||||
|
%%))
|
||||||
|
|
||||||
|
(fn conj [m other]
|
||||||
|
(if (map.is? other)
|
||||||
|
(merge m other)
|
||||||
|
m))
|
||||||
|
)
|
||||||
5
modules/core/types/null.owa
Normal file
5
modules/core/types/null.owa
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
(seq
|
||||||
|
(def null :null)
|
||||||
|
(fn null? [x]
|
||||||
|
(bool.or (eq? x null) (eq? x :nil)))
|
||||||
|
)
|
||||||
26
modules/core/types/option.owa
Normal file
26
modules/core/types/option.owa
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
(seq
|
||||||
|
(namespace option
|
||||||
|
(fn ok <Option>[value]
|
||||||
|
(vec :ok value))
|
||||||
|
|
||||||
|
(fn ok? <Bool>[x]
|
||||||
|
(bool.and
|
||||||
|
(vec? x)
|
||||||
|
(eq? (vec.first x) :ok)))
|
||||||
|
|
||||||
|
(fn err <Option>[value]
|
||||||
|
(vec :err value))
|
||||||
|
|
||||||
|
(fn err? <Bool>[x]
|
||||||
|
(bool.and
|
||||||
|
(vec? x)
|
||||||
|
(eq? (vec.first x) :err)))
|
||||||
|
)
|
||||||
|
|
||||||
|
(fn option? [x]
|
||||||
|
(bool.and
|
||||||
|
(vec? x)
|
||||||
|
(bool.or
|
||||||
|
(eq? (vec.first x) :err)
|
||||||
|
(eq? (vec.first x) :ok))))
|
||||||
|
)
|
||||||
60
modules/core/types/set.owa
Normal file
60
modules/core/types/set.owa
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
(seq
|
||||||
|
(namespace set
|
||||||
|
(def type :set)
|
||||||
|
(def is? (guard type))
|
||||||
|
(fn new [] (apply from %%))
|
||||||
|
(def __call__ new)
|
||||||
|
|
||||||
|
(def fold builtins.set.fold)
|
||||||
|
(def from builtins.set.from)
|
||||||
|
|
||||||
|
(def empty #{})
|
||||||
|
(fn empty? <Bool>[s]
|
||||||
|
(eq? s empty))
|
||||||
|
|
||||||
|
(fn len <Int>[<Set>s]
|
||||||
|
(fold (lambda [acc _] (+ acc 1)) 0 s))
|
||||||
|
|
||||||
|
(fn has <Bool>[<Set>s item]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem]
|
||||||
|
(bool.or acc (eq? elem item)))
|
||||||
|
false
|
||||||
|
s))
|
||||||
|
|
||||||
|
(fn add <Set>[<Set>s item]
|
||||||
|
(from s [item]))
|
||||||
|
|
||||||
|
(fn remove <Set>[<Set>s item]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem]
|
||||||
|
(if (eq? elem item)
|
||||||
|
acc
|
||||||
|
(from acc elem)))
|
||||||
|
#{}
|
||||||
|
s))
|
||||||
|
|
||||||
|
(fn union <Set>[]
|
||||||
|
(apply from %%))
|
||||||
|
|
||||||
|
(fn intersection <Set>[<Set>s1 <Set>s2]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem]
|
||||||
|
(if (has s2 elem)
|
||||||
|
(add acc elem)
|
||||||
|
acc))
|
||||||
|
#{}
|
||||||
|
s1))
|
||||||
|
|
||||||
|
(fn difference <Set>[<Set>s1 <Set>s2]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem]
|
||||||
|
(if (has s2 elem)
|
||||||
|
acc
|
||||||
|
(add acc elem)))
|
||||||
|
#{}
|
||||||
|
s1))
|
||||||
|
)
|
||||||
|
|
||||||
|
(def set? (guard set.type))
|
||||||
|
)
|
||||||
76
modules/core/types/str.owa
Normal file
76
modules/core/types/str.owa
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
(seq
|
||||||
|
(namespace str
|
||||||
|
(def type :str)
|
||||||
|
(def new builtins.str.new)
|
||||||
|
(def __call__ new)
|
||||||
|
|
||||||
|
(fn len <Int>[<Str>s]
|
||||||
|
(vec.len (vec.from s)))
|
||||||
|
|
||||||
|
(fn concat <Str>[]
|
||||||
|
(apply new %%))
|
||||||
|
|
||||||
|
(fn empty? <Bool>[<Str>s]
|
||||||
|
(eq? s ""))
|
||||||
|
|
||||||
|
(fn is-empty? <Bool>[<Str>s]
|
||||||
|
(empty? s))
|
||||||
|
|
||||||
|
(fn chars-at <Str>[<Str>s <Int>idx]
|
||||||
|
(vec.get (vec.from s) idx))
|
||||||
|
|
||||||
|
(fn first <Str>[<Str>s]
|
||||||
|
(chars-at s 0))
|
||||||
|
|
||||||
|
(fn last <Str>[<Str>s]
|
||||||
|
(chars-at s (-- (len s))))
|
||||||
|
|
||||||
|
(fn substring <Str>[<Str>s <Int>start <Int>end]
|
||||||
|
(apply concat (vec.slice (vec.from s) start end)))
|
||||||
|
|
||||||
|
(fn substr <Str>[<Str>s <Int>start <Int>len-sub]
|
||||||
|
(substring s start (+ start len-sub)))
|
||||||
|
|
||||||
|
(fn take <Str>[<Str>s <Int>n]
|
||||||
|
(apply concat (vec.take (vec.from s) n)))
|
||||||
|
|
||||||
|
(fn drop <Str>[<Str>s <Int>n]
|
||||||
|
(apply concat (vec.drop (vec.from s) n)))
|
||||||
|
|
||||||
|
(fn reverse <Str>[<Str>s]
|
||||||
|
(apply concat (vec.reverse (vec.from s))))
|
||||||
|
|
||||||
|
(fn starts-with? <Bool>[<Str>s <Str>prefix]
|
||||||
|
(eq? (substring s 0 (len prefix)) prefix))
|
||||||
|
|
||||||
|
(fn ends-with? <Bool>[<Str>s <Str>suffix]
|
||||||
|
(eq? (substring s (- (len s) (len suffix)) (len s)) suffix))
|
||||||
|
|
||||||
|
(fn split <Vec>[<Str>s <Str>sep] (seq
|
||||||
|
(vec.fold
|
||||||
|
(lambda [acc elem _]
|
||||||
|
(if-eq (elem sep)
|
||||||
|
(vec.conj acc "")
|
||||||
|
(vec.conj (vec.take acc (- (len acc) 1))
|
||||||
|
(str.concat (vec.last acc) elem)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
[""]
|
||||||
|
(vec.from s)
|
||||||
|
)))
|
||||||
|
|
||||||
|
(fn join <Str>[<Str>sep <Vec>strs]
|
||||||
|
(if (vec.empty? strs)
|
||||||
|
""
|
||||||
|
(vec.fold
|
||||||
|
(lambda [acc elem _]
|
||||||
|
(new acc sep elem))
|
||||||
|
(vec.first strs)
|
||||||
|
(vec.tail strs))))
|
||||||
|
|
||||||
|
(fn map <Str>[callback self]
|
||||||
|
(join "" (vec.map callback (vec.from self))))
|
||||||
|
)
|
||||||
|
|
||||||
|
(def str? (guard str.type))
|
||||||
|
)
|
||||||
22
modules/core/types/struct.owa
Normal file
22
modules/core/types/struct.owa
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
(seq
|
||||||
|
(defmacro struct [name fields] (seq
|
||||||
|
(for field $fields
|
||||||
|
(if (bool.not (kw? %1))
|
||||||
|
(throw! "Struct fields must be keywords"))
|
||||||
|
)
|
||||||
|
(def $name {
|
||||||
|
:__struct_type__ true
|
||||||
|
:new (lambda [data] (vec.map
|
||||||
|
#(if (map.has-key? data %1)
|
||||||
|
map.get data %1
|
||||||
|
(throw! "Missing field: " %1)
|
||||||
|
)
|
||||||
|
$fields))
|
||||||
|
})
|
||||||
|
))
|
||||||
|
|
||||||
|
(fn struct-type? [v]
|
||||||
|
(if (bool.and (map? v) (map.get v :__struct_type__))
|
||||||
|
true
|
||||||
|
false))
|
||||||
|
)
|
||||||
301
modules/core/types/vec.owa
Normal file
301
modules/core/types/vec.owa
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
(namespace vec
|
||||||
|
(def type :vec)
|
||||||
|
(def is? (guard type))
|
||||||
|
(def new builtins.vec.new)
|
||||||
|
(def __call__ new)
|
||||||
|
|
||||||
|
(def fold builtins.vec.fold)
|
||||||
|
(def from builtins.vec.from)
|
||||||
|
|
||||||
|
;; Empty vector
|
||||||
|
(def empty [])
|
||||||
|
|
||||||
|
;; Guard to check if a value is a empty vector
|
||||||
|
(fn empty? [<Vec>v]
|
||||||
|
(eq? v empty))
|
||||||
|
|
||||||
|
;; Get the count of items in a vector
|
||||||
|
(fn len [<Vec>v]
|
||||||
|
(fold (lambda [acc _] (+ acc 1)) 0 v))
|
||||||
|
|
||||||
|
;; Append an item to the end of a vector
|
||||||
|
(fn conj [<Vec>v item]
|
||||||
|
(apply new v [item]))
|
||||||
|
|
||||||
|
;; Check if a vector contains an item
|
||||||
|
(fn has [<Vec>v item]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem _]
|
||||||
|
(bool.or acc (eq? elem item)))
|
||||||
|
false
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Get an item from a vector by index
|
||||||
|
(fn get [<Vec>v idx]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem index]
|
||||||
|
(if (eq? index idx)
|
||||||
|
elem
|
||||||
|
result))
|
||||||
|
null
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Update an item in a vector
|
||||||
|
;; If the index is out of bounds, the original vector is returned
|
||||||
|
(fn assoc [<Vec>v <Int>index item]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem idx]
|
||||||
|
(if (eq? idx index)
|
||||||
|
(conj acc item)
|
||||||
|
(conj acc elem)))
|
||||||
|
[]
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Remove an item from a vector by index
|
||||||
|
;; If the index is out of bounds, the original vector is returned
|
||||||
|
(fn remove [<Vec>v <Int>index]
|
||||||
|
(filter
|
||||||
|
(lambda [elem idx]
|
||||||
|
(nq? idx index))
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Reverse a vector
|
||||||
|
(fn reverse [<Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem _]
|
||||||
|
(apply merge elem acc))
|
||||||
|
[]
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Flatten a vector of vectors into a single vector
|
||||||
|
(fn flat [<Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem _]
|
||||||
|
(apply conj [acc elem]))
|
||||||
|
[]
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Get the first item in a vector
|
||||||
|
(fn first [<Vec>v]
|
||||||
|
(get v 0))
|
||||||
|
|
||||||
|
;; Get the last item in a vector
|
||||||
|
(fn last [<Vec>v]
|
||||||
|
(get v (-- (len v))))
|
||||||
|
|
||||||
|
;; Get all items except the first
|
||||||
|
(fn tail [<Vec>v]
|
||||||
|
(filter
|
||||||
|
(lambda [elem idx]
|
||||||
|
(nq? idx 0))
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Get the first n items from a vector
|
||||||
|
(fn take [<Vec>v <Int>n]
|
||||||
|
(filter
|
||||||
|
(lambda [elem idx]
|
||||||
|
(lt? idx n))
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Get all items except the first n
|
||||||
|
(fn drop [<Vec>v n]
|
||||||
|
(filter
|
||||||
|
(lambda [elem idx]
|
||||||
|
(gte? idx n))
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Get all items except the last
|
||||||
|
(fn drop-last [<Vec>v]
|
||||||
|
(filter
|
||||||
|
(lambda [elem idx]
|
||||||
|
(nq? idx (-- (len v))))
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn enumerate [<Vec>v]
|
||||||
|
(map
|
||||||
|
(lambda [elem idx]
|
||||||
|
(new [idx elem]))
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn zip [v1 v2]
|
||||||
|
(map
|
||||||
|
(lambda [elem idx]
|
||||||
|
(new elem (get v2 idx)))
|
||||||
|
v1))
|
||||||
|
|
||||||
|
(fn slice [<Vec>v start end]
|
||||||
|
(filter
|
||||||
|
(lambda [elem idx]
|
||||||
|
(bool.and (gte? idx start) (lt? idx end)))
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn includes? [<Vec>v item]
|
||||||
|
(has v item))
|
||||||
|
|
||||||
|
(fn index-of [<Vec>v item]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem idx]
|
||||||
|
(if (null? result)
|
||||||
|
(if (eq? elem item)
|
||||||
|
idx
|
||||||
|
result)
|
||||||
|
result))
|
||||||
|
null
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn join-str [sep v]
|
||||||
|
(if (empty? v)
|
||||||
|
""
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem _]
|
||||||
|
(str.new acc sep elem))
|
||||||
|
(first v)
|
||||||
|
(tail v))))
|
||||||
|
|
||||||
|
;; Map a callback to each item in a vector
|
||||||
|
(fn map [callback <Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem index]
|
||||||
|
(apply conj [acc (callback elem index)]))
|
||||||
|
[]
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Filter a vector by a callback
|
||||||
|
(fn filter [callback <Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem index]
|
||||||
|
(if (callback elem index)
|
||||||
|
(apply conj [acc elem])
|
||||||
|
acc))
|
||||||
|
[]
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Find the first item that matches the callback
|
||||||
|
(fn find [callback <Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem index]
|
||||||
|
(if (null? result)
|
||||||
|
(if (callback elem index)
|
||||||
|
elem
|
||||||
|
result)
|
||||||
|
result))
|
||||||
|
null
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Check if every item in a vector matches the callback
|
||||||
|
;; TODO: Break at first non-match for better performance
|
||||||
|
(fn every? <Bool>[callback <Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem idx]
|
||||||
|
(bool.and result (callback elem idx)))
|
||||||
|
true
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Check if any item in a vector matches the callback
|
||||||
|
;; TODO: Break at first match for better performance
|
||||||
|
(fn some? <Bool>[callback <Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem idx]
|
||||||
|
(bool.or result (callback elem idx)))
|
||||||
|
false
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Reduce a vector into a single value
|
||||||
|
(fn reduce <Bool>[callback initial <Vec>v]
|
||||||
|
(fold callback initial v))
|
||||||
|
|
||||||
|
;; Count the number of items that match the callback
|
||||||
|
(fn count [callback v]
|
||||||
|
(fold
|
||||||
|
(lambda [count elem idx]
|
||||||
|
(if (callback elem idx)
|
||||||
|
(+ count 1)
|
||||||
|
count))
|
||||||
|
0
|
||||||
|
v))
|
||||||
|
|
||||||
|
;; Merge all vectors into a single vector
|
||||||
|
(fn merge []
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem _]
|
||||||
|
(apply conj [acc elem]))
|
||||||
|
[]
|
||||||
|
%%))
|
||||||
|
|
||||||
|
(fn max-by [comparator v]
|
||||||
|
(fold
|
||||||
|
(lambda [current elem _]
|
||||||
|
(if (null? current)
|
||||||
|
elem
|
||||||
|
(if (eq? (comparator elem current) :greater)
|
||||||
|
elem
|
||||||
|
current)))
|
||||||
|
null
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn min-by [comparator v]
|
||||||
|
(fold
|
||||||
|
(lambda [current elem _]
|
||||||
|
(if (null? current)
|
||||||
|
elem
|
||||||
|
(if (eq? (comparator elem current) :less)
|
||||||
|
elem
|
||||||
|
current)))
|
||||||
|
null
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn uniq [<Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem _]
|
||||||
|
(if (has result elem)
|
||||||
|
result
|
||||||
|
(conj result elem)))
|
||||||
|
[]
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn partition [callback v]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem idx]
|
||||||
|
(seq
|
||||||
|
(def group (if (callback elem idx) 0 1))
|
||||||
|
(def current-part (get result group))
|
||||||
|
(assoc result group (conj current-part elem))))
|
||||||
|
[[] []]
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn group-by [key-fn v]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem idx]
|
||||||
|
(seq
|
||||||
|
(def key (key-fn elem idx))
|
||||||
|
(def group (map.get result key))
|
||||||
|
(def new-group (if (null? group) [] group))
|
||||||
|
(map.put result key (conj new-group elem))))
|
||||||
|
(map.new)
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn flatten [<Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [result elem _]
|
||||||
|
(if (vec.is? elem)
|
||||||
|
(conj result (flatten elem))
|
||||||
|
(conj result elem)))
|
||||||
|
[]
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn distinct [<Vec>v]
|
||||||
|
(uniq v))
|
||||||
|
|
||||||
|
(fn compact [<Vec>v]
|
||||||
|
(filter
|
||||||
|
(lambda [elem _]
|
||||||
|
(not (null? elem)))
|
||||||
|
v))
|
||||||
|
|
||||||
|
(fn sum [<Vec>v]
|
||||||
|
(fold
|
||||||
|
(lambda [acc elem _]
|
||||||
|
(+ acc elem))
|
||||||
|
0
|
||||||
|
v))
|
||||||
|
)
|
||||||
19
modules/owu/cli/main.owa
Normal file
19
modules/owu/cli/main.owa
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
(seq
|
||||||
|
(def cmd (vec.first argv))
|
||||||
|
(def args (vec.tail argv))
|
||||||
|
|
||||||
|
(if (bool.or (null? cmd) (str.starts-with? cmd "-")) (seq
|
||||||
|
(trace "Error: No command specified")
|
||||||
|
(return 1)
|
||||||
|
))
|
||||||
|
|
||||||
|
(try
|
||||||
|
(match cmd
|
||||||
|
("run" (include "run"))
|
||||||
|
(_ (trace "Error: Command '" cmd "' not found"))
|
||||||
|
)
|
||||||
|
(lambda [msg type] (seq
|
||||||
|
(trace type ": " msg)
|
||||||
|
(return 1)))
|
||||||
|
)
|
||||||
|
)
|
||||||
43
modules/owu/cli/run.owa
Normal file
43
modules/owu/cli/run.owa
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
(seq
|
||||||
|
(def target (vec.find #(bool.not (str.starts-with? %1 "-")) (vec.reverse args)))
|
||||||
|
(if (null? target) (seq
|
||||||
|
(trace "No target specified")
|
||||||
|
(return)
|
||||||
|
))
|
||||||
|
|
||||||
|
(fn parse-arg [args name] (seq
|
||||||
|
(def idx (vec.index-of args name))
|
||||||
|
(if (null? idx)
|
||||||
|
null
|
||||||
|
(vec.get args (+ idx 1))
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
(def no-builtins? (vec.has args "--no-builtins"))
|
||||||
|
(def no-core? (vec.has args "--no-core"))
|
||||||
|
(def no-std? (vec.has args "--no-std"))
|
||||||
|
(def is-debug? (vec.has args "--debug"))
|
||||||
|
(def test-target (parse-arg args "--test"))
|
||||||
|
|
||||||
|
(exec
|
||||||
|
{
|
||||||
|
:__runtime__ :owu
|
||||||
|
:__dir__ ""
|
||||||
|
:__main__ target
|
||||||
|
:__test__ test-target
|
||||||
|
}
|
||||||
|
(seq
|
||||||
|
(unless no-builtins?
|
||||||
|
(def builtins builtins))
|
||||||
|
|
||||||
|
(unless (bool.or no-builtins? no-core?)
|
||||||
|
(include (str.concat modules-dir "/core")))
|
||||||
|
|
||||||
|
(unless (bool.or no-builtins? no-core? no-std?)
|
||||||
|
(include (str.concat modules-dir "/std")))
|
||||||
|
|
||||||
|
(def __is_debug__ is-debug?)
|
||||||
|
)
|
||||||
|
target
|
||||||
|
)
|
||||||
|
)
|
||||||
15
modules/owu/main.owa
Normal file
15
modules/owu/main.owa
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
(builtins.seq
|
||||||
|
(builtins.include "../core")
|
||||||
|
(builtins.include "../std")
|
||||||
|
(namespace owu
|
||||||
|
(def argv (platform.args))
|
||||||
|
(def modules-dir (str.concat __dir__ "/.."))
|
||||||
|
|
||||||
|
(if (eq? (vec.len argv) 0) (seq
|
||||||
|
(return 1)
|
||||||
|
))
|
||||||
|
|
||||||
|
(include "cli")
|
||||||
|
(return 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
6
modules/std/main.owa
Normal file
6
modules/std/main.owa
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
(seq
|
||||||
|
(include "platform")
|
||||||
|
(include "path")
|
||||||
|
|
||||||
|
(test.space "std" (include "tests/main"))
|
||||||
|
)
|
||||||
222
modules/std/path.owa
Normal file
222
modules/std/path.owa
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
(namespace path
|
||||||
|
;; Platform-specific path separator.
|
||||||
|
(def sep (match platform.os-family
|
||||||
|
(:windows "\\")
|
||||||
|
(_ "/")
|
||||||
|
))
|
||||||
|
|
||||||
|
;; Internal: converts "/" to "\" on Windows; no-op on other platforms.
|
||||||
|
(fn norm-seps <Str>[<Str>p]
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (str.map (lambda [c _] (if (eq? c "/") "\\" c)) p))
|
||||||
|
(_ p)))
|
||||||
|
|
||||||
|
;; Returns the root prefix of a path.
|
||||||
|
;;
|
||||||
|
;; Unix — "/" for paths starting with "/", "" for relative.
|
||||||
|
;; Windows — "\" for UNC/rooted paths ("\" or "/" prefix)
|
||||||
|
;; "X:\" for drive-letter absolute paths ("X:\" or "X:/")
|
||||||
|
;; "" for all relative paths ("X:path" is relative on Windows)
|
||||||
|
(fn root <Str>[<Str>p]
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows
|
||||||
|
(if (bool.or (str.starts-with? p "\\") (str.starts-with? p "/"))
|
||||||
|
"\\"
|
||||||
|
(if (bool.and (gte? (str.len p) 3)
|
||||||
|
(bool.and (eq? (str.chars-at p 1) ":")
|
||||||
|
(bool.or (eq? (str.chars-at p 2) "\\")
|
||||||
|
(eq? (str.chars-at p 2) "/"))))
|
||||||
|
(str.concat (str.take p 2) "\\")
|
||||||
|
"")))
|
||||||
|
(_ (if (str.starts-with? p "/") "/" ""))
|
||||||
|
))
|
||||||
|
|
||||||
|
;; Returns true if the path is absolute (has a non-empty root prefix).
|
||||||
|
(fn absolute? <Bool>[<Str>p]
|
||||||
|
(bool.not (str.empty? (root p))))
|
||||||
|
|
||||||
|
;; Returns true if the path is relative (has no root prefix).
|
||||||
|
(fn relative? <Bool>[<Str>p]
|
||||||
|
(bool.not (absolute? p)))
|
||||||
|
|
||||||
|
;; Splits a path into its string components using the platform separator.
|
||||||
|
;; On Windows "/" is also accepted as a separator.
|
||||||
|
;; The root prefix (if any) is preserved as the first component so that
|
||||||
|
;; split + join is a round-trip operation.
|
||||||
|
(fn split <Vec>[<Str>p]
|
||||||
|
(if (str.empty? p)
|
||||||
|
[]
|
||||||
|
(str.split (norm-seps p) sep)))
|
||||||
|
|
||||||
|
;; Joins path components with the platform separator.
|
||||||
|
(fn join <Str>[<Vec>p]
|
||||||
|
(str.join sep p))
|
||||||
|
|
||||||
|
;; Removes "." components, resolves ".." components, and returns the
|
||||||
|
;; canonical form of the path using the platform separator.
|
||||||
|
;;
|
||||||
|
;; Bug fixes vs. previous version:
|
||||||
|
;; • absolute paths that collapse entirely return the root (e.g. "/" not "")
|
||||||
|
;; • Windows drive-letter paths no longer get a spurious leading "\"
|
||||||
|
(fn normalize <Str>[<Str>p]
|
||||||
|
(seq
|
||||||
|
(def r (root p))
|
||||||
|
;; Work only on the non-root body so that root components never
|
||||||
|
;; participate in ".." resolution.
|
||||||
|
(def body (norm-seps (str.drop p (str.len r))))
|
||||||
|
(def parts (if (str.empty? body) [] (str.split body sep)))
|
||||||
|
(def normalized [])
|
||||||
|
(for part parts
|
||||||
|
(if (bool.or (eq? part ".") (str.empty? part)) (continue))
|
||||||
|
(set! normalized (if (eq? part "..")
|
||||||
|
(vec.drop-last normalized)
|
||||||
|
(vec.conj normalized part))))
|
||||||
|
(def result (join normalized))
|
||||||
|
(if (str.empty? r)
|
||||||
|
result
|
||||||
|
(if (str.empty? result)
|
||||||
|
r
|
||||||
|
(str.concat r result)))))
|
||||||
|
|
||||||
|
;; Returns the parent directory of the path.
|
||||||
|
;; • Paths directly under the root return the root string.
|
||||||
|
;; • Top-level relative paths (e.g. "a") return "".
|
||||||
|
;;
|
||||||
|
;; Bug fix vs. previous version: returns sep-based root, not hardcoded "/".
|
||||||
|
(fn parent <Str>[<Str>p] (seq
|
||||||
|
(def norm (normalize p))
|
||||||
|
(def r (root norm))
|
||||||
|
(def body (str.drop norm (str.len r)))
|
||||||
|
(def parts (if (str.empty? body)
|
||||||
|
[]
|
||||||
|
(vec.filter
|
||||||
|
(lambda [part _] (bool.not (str.empty? part)))
|
||||||
|
(str.split body sep))))
|
||||||
|
(def parent-parts (vec.drop-last parts))
|
||||||
|
(if (vec.empty? parent-parts)
|
||||||
|
r
|
||||||
|
(str.concat r (join parent-parts)))))
|
||||||
|
|
||||||
|
;; Returns the last component of the path (filename or final directory name).
|
||||||
|
;; Returns "" for paths that end with a separator.
|
||||||
|
(fn basename <Str>[<Str>p] (seq
|
||||||
|
(def parts (split p))
|
||||||
|
(if (vec.empty? parts) "" (vec.last parts))))
|
||||||
|
|
||||||
|
;; Returns the file extension including the leading dot, or "" if none.
|
||||||
|
;;
|
||||||
|
;; Rules:
|
||||||
|
;; • Hidden files whose name starts with "." have no extension.
|
||||||
|
;; • A trailing dot (e.g. "file.") is not considered an extension.
|
||||||
|
;; • Only the last dot in the basename is used (e.g. "a.tar.gz" → ".gz").
|
||||||
|
;;
|
||||||
|
;; Examples:
|
||||||
|
;; "file.txt" → ".txt"
|
||||||
|
;; "archive.tar.gz" → ".gz"
|
||||||
|
;; "file" → ""
|
||||||
|
;; ".hidden" → ""
|
||||||
|
;; "file." → ""
|
||||||
|
(fn ext <Str>[<Str>p] (seq
|
||||||
|
(def base (basename p))
|
||||||
|
(def dot-idx (vec.fold
|
||||||
|
(lambda [acc elem idx]
|
||||||
|
(if (eq? elem ".") idx acc))
|
||||||
|
-1
|
||||||
|
(vec.from base)))
|
||||||
|
(if (bool.or (lte? dot-idx 0) (eq? dot-idx (-- (str.len base))))
|
||||||
|
""
|
||||||
|
(str.drop base dot-idx))))
|
||||||
|
|
||||||
|
;; Returns the filename without its extension.
|
||||||
|
;;
|
||||||
|
;; Examples:
|
||||||
|
;; "file.txt" → "file"
|
||||||
|
;; "archive.tar.gz" → "archive.tar"
|
||||||
|
;; "file" → "file"
|
||||||
|
;; ".hidden" → ".hidden"
|
||||||
|
(fn stem <Str>[<Str>p] (seq
|
||||||
|
(def base (basename p))
|
||||||
|
(def e (ext p))
|
||||||
|
(if (str.empty? e)
|
||||||
|
base
|
||||||
|
(str.take base (- (str.len base) (str.len e))))))
|
||||||
|
|
||||||
|
;; Returns the path with its extension replaced by new-ext.
|
||||||
|
;; Pass "" as new-ext to strip the extension entirely.
|
||||||
|
;; The directory prefix and stem are preserved unchanged.
|
||||||
|
;;
|
||||||
|
;; Examples:
|
||||||
|
;; (with-ext "file.txt" ".md") → "file.md"
|
||||||
|
;; (with-ext "file.txt" "") → "file"
|
||||||
|
;; (with-ext "file" ".txt") → "file.txt"
|
||||||
|
(fn with-ext <Str>[<Str>p <Str>new-ext] (seq
|
||||||
|
(def e (ext p))
|
||||||
|
(def without-ext
|
||||||
|
(if (str.empty? e)
|
||||||
|
p
|
||||||
|
(str.take p (- (str.len p) (str.len e)))))
|
||||||
|
(str.concat without-ext new-ext)))
|
||||||
|
|
||||||
|
;; Resolves path relative to base and returns the normalized result.
|
||||||
|
;; If path is already absolute it is returned normalized, ignoring base.
|
||||||
|
;; Otherwise base + sep + path is normalized.
|
||||||
|
;;
|
||||||
|
;; Examples (Unix):
|
||||||
|
;; (resolve "/base" "file.txt") → "/base/file.txt"
|
||||||
|
;; (resolve "/base/dir" "../file.txt") → "/base/file.txt"
|
||||||
|
;; (resolve "/other" "/abs") → "/abs"
|
||||||
|
(fn resolve <Str>[<Str>base <Str>p]
|
||||||
|
(if (absolute? p)
|
||||||
|
(normalize p)
|
||||||
|
(normalize (str.concat base sep p))))
|
||||||
|
|
||||||
|
;; Computes the relative path from base to target.
|
||||||
|
;;
|
||||||
|
;; • Returns "." when base and target are the same path.
|
||||||
|
;; • When the roots differ (e.g. different Windows drive letters)
|
||||||
|
;; the normalized target is returned unchanged.
|
||||||
|
;;
|
||||||
|
;; Examples (Unix):
|
||||||
|
;; (relative-to "/a/b" "/a/b/c/d") → "c/d"
|
||||||
|
;; (relative-to "/a/b" "/a/c") → "../c"
|
||||||
|
;; (relative-to "/a/b/c" "/a") → "../.."
|
||||||
|
;; (relative-to "/a/b" "/a/b") → "."
|
||||||
|
(fn relative-to <Str>[<Str>base <Str>target] (seq
|
||||||
|
(def norm-base (normalize base))
|
||||||
|
(def norm-target (normalize target))
|
||||||
|
(def base-r (root norm-base))
|
||||||
|
(def target-r (root norm-target))
|
||||||
|
(if (nq? base-r target-r)
|
||||||
|
norm-target
|
||||||
|
(seq
|
||||||
|
(def base-body (str.drop norm-base (str.len base-r)))
|
||||||
|
(def target-body (str.drop norm-target (str.len target-r)))
|
||||||
|
(fn parts-of [body]
|
||||||
|
(if (str.empty? body)
|
||||||
|
[]
|
||||||
|
(vec.filter
|
||||||
|
(lambda [part _] (bool.not (str.empty? part)))
|
||||||
|
(str.split body sep))))
|
||||||
|
(def base-parts (parts-of base-body))
|
||||||
|
(def target-parts (parts-of target-body))
|
||||||
|
(def base-len (vec.len base-parts))
|
||||||
|
(def target-len (vec.len target-parts))
|
||||||
|
;; Find length of the common prefix.
|
||||||
|
(def i 0)
|
||||||
|
(while (bool.and (lt? i base-len)
|
||||||
|
(bool.and (lt? i target-len)
|
||||||
|
(eq? (vec.get base-parts i)
|
||||||
|
(vec.get target-parts i))))
|
||||||
|
(set! i (++ i)))
|
||||||
|
;; One ".." per remaining component in base, then the remaining target components.
|
||||||
|
(def ups (vec.map (lambda [_e _i] "..") (vec.drop base-parts i)))
|
||||||
|
(def remaining (vec.drop target-parts i))
|
||||||
|
(def result-parts
|
||||||
|
(vec.fold
|
||||||
|
(lambda [acc elem _] (vec.conj acc elem))
|
||||||
|
ups
|
||||||
|
remaining))
|
||||||
|
(if (vec.empty? result-parts)
|
||||||
|
"."
|
||||||
|
(join result-parts))))))
|
||||||
|
)
|
||||||
55
modules/std/platform.owa
Normal file
55
modules/std/platform.owa
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
(namespace platform
|
||||||
|
(def os-name builtins.platform.os-name)
|
||||||
|
(def os-family builtins.platform.os-family)
|
||||||
|
(def arch builtins.platform.arch)
|
||||||
|
(def argv builtins.platform.argv)
|
||||||
|
(def executable #(vec.get (argv) 0))
|
||||||
|
(def args #(vec.slice (argv) 1 (vec.len (argv))))
|
||||||
|
|
||||||
|
(def debug? (lookup __is_debug__ false))
|
||||||
|
|
||||||
|
(def os-names [
|
||||||
|
:linux
|
||||||
|
:windows
|
||||||
|
:macos
|
||||||
|
:android
|
||||||
|
:ios
|
||||||
|
:openbsd
|
||||||
|
:freebsd
|
||||||
|
:netbsd
|
||||||
|
:wasi
|
||||||
|
:hermit
|
||||||
|
:aix
|
||||||
|
:apple
|
||||||
|
:dragonfly
|
||||||
|
:emscripten
|
||||||
|
:espidf
|
||||||
|
:fortanix
|
||||||
|
:uefi
|
||||||
|
:fuchsia
|
||||||
|
:haiku
|
||||||
|
:hermit
|
||||||
|
:watchos
|
||||||
|
:visionos
|
||||||
|
:tvos
|
||||||
|
:horizon
|
||||||
|
:hurd
|
||||||
|
:illumos
|
||||||
|
:l4re
|
||||||
|
:nto
|
||||||
|
:redox
|
||||||
|
:solaris
|
||||||
|
:solid_asp3
|
||||||
|
:vexos
|
||||||
|
:vita
|
||||||
|
:vxworks
|
||||||
|
:xous
|
||||||
|
])
|
||||||
|
|
||||||
|
(def os-families [
|
||||||
|
:unix
|
||||||
|
:windows
|
||||||
|
:itron
|
||||||
|
:wasm
|
||||||
|
])
|
||||||
|
)
|
||||||
3
modules/std/tests/main.owa
Normal file
3
modules/std/tests/main.owa
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
(seq
|
||||||
|
(include "path")
|
||||||
|
)
|
||||||
286
modules/std/tests/path.owa
Normal file
286
modules/std/tests/path.owa
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
(namespace test.path
|
||||||
|
(test.case "path.sep"
|
||||||
|
(assert.eq! path.sep (match platform.os-family (:windows "\\") (_ "/")))
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.root"
|
||||||
|
;; Relative paths and empty string have no root on all platforms.
|
||||||
|
(assert.eq! (path.root "relative") "")
|
||||||
|
(assert.eq! (path.root "") "")
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
;; Rooted by backslash or forward-slash → canonical "\"
|
||||||
|
(assert.eq! (path.root "\\a\\b") "\\")
|
||||||
|
(assert.eq! (path.root "/a/b") "\\")
|
||||||
|
;; Drive-letter absolute ("X:\" or "X:/")
|
||||||
|
(assert.eq! (path.root "C:\\a\\b") "C:\\")
|
||||||
|
(assert.eq! (path.root "C:/a/b") "C:\\")
|
||||||
|
(assert.eq! (path.root "C:\\") "C:\\")
|
||||||
|
;; Drive letter WITHOUT a separator is relative on Windows
|
||||||
|
(assert.eq! (path.root "C:") "")
|
||||||
|
(assert.eq! (path.root "C:relative") "")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.root "/") "/")
|
||||||
|
(assert.eq! (path.root "/a/b") "/")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.absolute?"
|
||||||
|
;; "/" is recognised as absolute on all platforms.
|
||||||
|
(assert.ok! (path.absolute? "/absolute/unix"))
|
||||||
|
(assert.ok! (path.absolute? "/"))
|
||||||
|
(assert.not! (path.absolute? "relative/unix"))
|
||||||
|
(assert.not! (path.absolute? "./relative/unix"))
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
;; Drive-letter prefix (both separators)
|
||||||
|
(assert.ok! (path.absolute? "C:\\absolute\\windows"))
|
||||||
|
(assert.ok! (path.absolute? "C:/absolute/windows"))
|
||||||
|
;; UNC / rooted backslash
|
||||||
|
(assert.ok! (path.absolute? "\\absolute\\unc"))
|
||||||
|
;; Relative Windows paths
|
||||||
|
(assert.not! (path.absolute? "relative\\windows"))
|
||||||
|
(assert.not! (path.absolute? ".\\relative\\windows"))
|
||||||
|
;; Drive letter without separator is relative
|
||||||
|
(assert.not! (path.absolute? "C:relative"))
|
||||||
|
))
|
||||||
|
(_ (void))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.relative?"
|
||||||
|
(assert.ok! (path.relative? "relative/unix"))
|
||||||
|
(assert.not! (path.relative? "/absolute/unix"))
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
(assert.ok! (path.relative? "relative\\windows"))
|
||||||
|
(assert.not! (path.relative? "C:\\absolute\\windows"))
|
||||||
|
(assert.not! (path.relative? "\\absolute\\unc"))
|
||||||
|
(assert.ok! (path.relative? "C:relative"))
|
||||||
|
))
|
||||||
|
(_ (void))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.split"
|
||||||
|
(assert.eq! (path.split "") [])
|
||||||
|
(assert.eq! (path.split "a") ["a"])
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
(assert.eq! (path.split "a\\b\\c") ["a" "b" "c"])
|
||||||
|
;; Windows also accepts "/" as separator
|
||||||
|
(assert.eq! (path.split "a/b/c") ["a" "b" "c"])
|
||||||
|
;; Drive-letter prefix is kept as the first component (round-trip with join)
|
||||||
|
(assert.eq! (path.split "C:\\a\\b") ["C:" "a" "b"])
|
||||||
|
(assert.eq! (path.split "C:/a/b") ["C:" "a" "b"])
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.split "a/b/c") ["a" "b" "c"])
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.join"
|
||||||
|
(assert.eq! (path.join []) "")
|
||||||
|
(assert.eq! (path.join ["a"]) "a")
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
(assert.eq! (path.join ["a" "b" "c"]) "a\\b\\c")
|
||||||
|
(assert.eq! (path.join ["C:" "a" "b"]) "C:\\a\\b")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.join ["a" "b" "c"]) (str.concat "a" path.sep "b" path.sep "c"))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.normalize"
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
;; Dot component is stripped
|
||||||
|
(assert.eq! (path.normalize "a\\.\\b") (path.join ["a" "b"]))
|
||||||
|
;; Double-dot collapses parent
|
||||||
|
(assert.eq! (path.normalize "a\\..\\b") "b")
|
||||||
|
;; Rooted-backslash absolute path
|
||||||
|
(assert.eq! (path.normalize "\\a\\..\\b") (str.concat path.sep "b"))
|
||||||
|
;; Bug fix: absolute path collapsing to root returns the root, not ""
|
||||||
|
(assert.eq! (path.normalize "\\") "\\")
|
||||||
|
(assert.eq! (path.normalize "\\a\\..") "\\")
|
||||||
|
;; Drive-letter paths: no spurious leading "\" added
|
||||||
|
(assert.eq! (path.normalize "C:\\a\\..\\b") "C:\\b")
|
||||||
|
(assert.eq! (path.normalize "C:\\") "C:\\")
|
||||||
|
(assert.eq! (path.normalize "C:\\a\\b\\..\\..") "C:\\")
|
||||||
|
;; Windows normalises "/" separators as well
|
||||||
|
(assert.eq! (path.normalize "a/./b") (path.join ["a" "b"]))
|
||||||
|
(assert.eq! (path.normalize "C:/a/../b") "C:\\b")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
;; Dot component is stripped
|
||||||
|
(assert.eq! (path.normalize "a/./b") (path.join ["a" "b"]))
|
||||||
|
;; Double-dot collapses parent
|
||||||
|
(assert.eq! (path.normalize "a/../b") "b")
|
||||||
|
;; Rooted absolute path
|
||||||
|
(assert.eq! (path.normalize "/a/../b") (str.concat path.sep "b"))
|
||||||
|
;; Bug fix: absolute path collapsing to root returns "/", not ""
|
||||||
|
(assert.eq! (path.normalize "/") "/")
|
||||||
|
(assert.eq! (path.normalize "/a/..") "/")
|
||||||
|
(assert.eq! (path.normalize "/a/b/../..") "/")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.parent"
|
||||||
|
;; Top-level relative path always returns "" on every platform.
|
||||||
|
(assert.eq! (path.parent "a") "")
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
(assert.eq! (path.parent "a\\b\\c") "a\\b")
|
||||||
|
(assert.eq! (path.parent "a\\b") "a")
|
||||||
|
;; Bug fix: rooted paths return "\" not hardcoded "/"
|
||||||
|
(assert.eq! (path.parent "\\") "\\")
|
||||||
|
(assert.eq! (path.parent "\\..\\a\\.\\..\\") "\\")
|
||||||
|
;; Drive-letter paths
|
||||||
|
(assert.eq! (path.parent "C:\\") "C:\\")
|
||||||
|
(assert.eq! (path.parent "C:\\a") "C:\\")
|
||||||
|
(assert.eq! (path.parent "C:\\a\\b") "C:\\a")
|
||||||
|
(assert.eq! (path.parent "C:\\a\\b\\c") "C:\\a\\b")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.parent "a/b/c") "a/b")
|
||||||
|
(assert.eq! (path.parent "a/b") "a")
|
||||||
|
(assert.eq! (path.parent "/") "/")
|
||||||
|
(assert.eq! (path.parent "/../a/./../") "/")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.basename"
|
||||||
|
(assert.eq! (path.basename "file.txt") "file.txt")
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
(assert.eq! (path.basename "a\\b\\c") "c")
|
||||||
|
;; Trailing separator → empty basename
|
||||||
|
(assert.eq! (path.basename "a\\b\\") "")
|
||||||
|
(assert.eq! (path.basename "C:\\Users\\file.txt") "file.txt")
|
||||||
|
;; Windows also handles "/" separator
|
||||||
|
(assert.eq! (path.basename "a/b/c") "c")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.basename "a/b/c") "c")
|
||||||
|
(assert.eq! (path.basename "a/b/") "")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.ext"
|
||||||
|
;; Basic cases work on all platforms (basename is a simple name)
|
||||||
|
(assert.eq! (path.ext "file.txt") ".txt")
|
||||||
|
(assert.eq! (path.ext "file") "")
|
||||||
|
(assert.eq! (path.ext ".hidden") "")
|
||||||
|
(assert.eq! (path.ext "archive.tar.gz") ".gz")
|
||||||
|
(assert.eq! (path.ext "file.") "")
|
||||||
|
(assert.eq! (path.ext "") "")
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
(assert.eq! (path.ext "C:\\Users\\file.txt") ".txt")
|
||||||
|
(assert.eq! (path.ext "a\\b\\no-ext") "")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.ext "a/b/file.txt") ".txt")
|
||||||
|
(assert.eq! (path.ext "a/b/no-ext") "")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.stem"
|
||||||
|
(assert.eq! (path.stem "file.txt") "file")
|
||||||
|
(assert.eq! (path.stem "file") "file")
|
||||||
|
(assert.eq! (path.stem ".hidden") ".hidden")
|
||||||
|
(assert.eq! (path.stem "archive.tar.gz") "archive.tar")
|
||||||
|
(assert.eq! (path.stem "") "")
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
(assert.eq! (path.stem "C:\\Users\\file.txt") "file")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.stem "a/b/file.txt") "file")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.with-ext"
|
||||||
|
(assert.eq! (path.with-ext "file.txt" ".md") "file.md")
|
||||||
|
(assert.eq! (path.with-ext "file.txt" "") "file")
|
||||||
|
(assert.eq! (path.with-ext "file" ".txt") "file.txt")
|
||||||
|
(assert.eq! (path.with-ext "archive.tar.gz" ".bz2") "archive.tar.bz2")
|
||||||
|
;; Hidden file: no extension → new-ext is appended
|
||||||
|
(assert.eq! (path.with-ext ".hidden" ".txt") ".hidden.txt")
|
||||||
|
;; Directory prefix is preserved
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
(assert.eq! (path.with-ext "C:\\a\\file.txt" ".md") "C:\\a\\file.md")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.with-ext "a/b/file.txt" ".md") "a/b/file.md")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.resolve"
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
;; Relative path is joined to base and normalized
|
||||||
|
(assert.eq! (path.resolve "C:\\base" "file.txt") "C:\\base\\file.txt")
|
||||||
|
;; ".." is resolved against base
|
||||||
|
(assert.eq! (path.resolve "C:\\base\\dir" "..\\file.txt") "C:\\base\\file.txt")
|
||||||
|
;; Absolute path ignores base
|
||||||
|
(assert.eq! (path.resolve "C:\\other" "C:\\absolute") "C:\\absolute")
|
||||||
|
;; Deep traversal
|
||||||
|
(assert.eq! (path.resolve "C:\\a\\b\\c" "..\\..\\d") "C:\\a\\d")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.resolve "/base" "file.txt") "/base/file.txt")
|
||||||
|
(assert.eq! (path.resolve "/base/dir" "../file.txt") "/base/file.txt")
|
||||||
|
;; Absolute path ignores base
|
||||||
|
(assert.eq! (path.resolve "/other" "/absolute") "/absolute")
|
||||||
|
;; Deep traversal
|
||||||
|
(assert.eq! (path.resolve "/a/b/c" "../../d") "/a/d")
|
||||||
|
;; Relative base + relative path
|
||||||
|
(assert.eq! (path.resolve "base/dir" "file.txt") "base/dir/file.txt")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(test.case "path.relative-to"
|
||||||
|
(match platform.os-family
|
||||||
|
(:windows (seq
|
||||||
|
;; Child path
|
||||||
|
(assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\b\\c\\d") "c\\d")
|
||||||
|
;; Sibling path
|
||||||
|
(assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\c") "..\\c")
|
||||||
|
;; Ancestor path
|
||||||
|
(assert.eq! (path.relative-to "C:\\a\\b\\c" "C:\\a") "..\\..")
|
||||||
|
;; Same path → "."
|
||||||
|
(assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\b") ".")
|
||||||
|
;; From root
|
||||||
|
(assert.eq! (path.relative-to "C:\\" "C:\\a\\b") "a\\b")
|
||||||
|
;; Different drives → return normalized target unchanged
|
||||||
|
(assert.eq! (path.relative-to "C:\\a" "D:\\b") "D:\\b")
|
||||||
|
))
|
||||||
|
(_ (seq
|
||||||
|
(assert.eq! (path.relative-to "/a/b" "/a/b/c/d") "c/d")
|
||||||
|
(assert.eq! (path.relative-to "/a/b" "/a/c") "../c")
|
||||||
|
(assert.eq! (path.relative-to "/a/b/c" "/a") "../..")
|
||||||
|
;; Same path → "."
|
||||||
|
(assert.eq! (path.relative-to "/a/b" "/a/b") ".")
|
||||||
|
;; From root
|
||||||
|
(assert.eq! (path.relative-to "/" "/a/b") "a/b")
|
||||||
|
;; Relative paths
|
||||||
|
(assert.eq! (path.relative-to "a/b" "a/b/c") "c")
|
||||||
|
(assert.eq! (path.relative-to "a/b" "a/c") "../c")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
14
src/builtins/ast.rs
Normal file
14
src/builtins/ast.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use crate::core::{OwaValue, Scope, except_arity};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_pack() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|_, _, args| {
|
||||||
|
let [arg] = except_arity(args)?;
|
||||||
|
Ok(arg.pack().into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([("pack", owa_pack())])
|
||||||
|
}
|
||||||
97
src/builtins/cond.rs
Normal file
97
src/builtins/cond.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
use crate::{
|
||||||
|
core::{OwaError, OwaValue, Scope, except_arity, except_arity_min},
|
||||||
|
parser::ast::OwaAst,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_if_eq() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let [cond_ast, then_ast, else_ast] = except_arity(args)?;
|
||||||
|
let cond_ast = cond_ast.as_call()?;
|
||||||
|
|
||||||
|
let mut result = true;
|
||||||
|
let mut prev = None;
|
||||||
|
for arg in cond_ast {
|
||||||
|
if !result {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Condition args are evaluated as values (control-flow here is a programming error)
|
||||||
|
let arg = interpreter.eval_value(arg, scope_id)?;
|
||||||
|
if let Some(prev) = prev {
|
||||||
|
result = prev == arg;
|
||||||
|
}
|
||||||
|
prev = Some(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if result {
|
||||||
|
// Branch body — propagate Completion (break/continue/return pass through)
|
||||||
|
interpreter.eval(then_ast, scope_id)
|
||||||
|
} else {
|
||||||
|
interpreter.eval(else_ast, scope_id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_if_has() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let [target_ast, cond_ast, then_ast, else_ast] = except_arity(args)?;
|
||||||
|
let target_ast = interpreter.eval_value(target_ast, scope_id)?;
|
||||||
|
let items_ast = cond_ast.as_call()?;
|
||||||
|
|
||||||
|
for arg in items_ast {
|
||||||
|
let arg = interpreter.eval_value(arg, scope_id)?;
|
||||||
|
if arg == target_ast {
|
||||||
|
return interpreter.eval(then_ast, scope_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interpreter.eval(else_ast, scope_id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_match() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let ([value_ast], branches) = except_arity_min(args)?;
|
||||||
|
// Evaluate the matched value
|
||||||
|
let value = interpreter.eval_value(value_ast, scope_id)?;
|
||||||
|
let mut default = None;
|
||||||
|
|
||||||
|
for branch in branches {
|
||||||
|
let branch = branch.as_call()?;
|
||||||
|
|
||||||
|
let [pattern_ast, expr_ast] = except_arity(branch)?;
|
||||||
|
if pattern_ast == &OwaAst::sym("_") {
|
||||||
|
default = Some(expr_ast);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate pattern as a value
|
||||||
|
let pattern = interpreter.eval_value(pattern_ast, scope_id)?;
|
||||||
|
|
||||||
|
if pattern == value {
|
||||||
|
// Branch body — propagate Completion
|
||||||
|
return interpreter.eval(expr_ast, scope_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default.map_or_else(
|
||||||
|
|| {
|
||||||
|
Err(OwaError::RuntimeError(OwaValue::Str(
|
||||||
|
"Unmatched pattern".into(),
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
|default| interpreter.eval(default, scope_id),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("if_eq", owa_if_eq()),
|
||||||
|
("if_has", owa_if_has()),
|
||||||
|
("match", owa_match()),
|
||||||
|
])
|
||||||
|
}
|
||||||
83
src/builtins/errors.rs
Normal file
83
src/builtins/errors.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
use crate::{
|
||||||
|
core::{Completion, OwaError, OwaValue, Scope, except_arity, except_arity_min},
|
||||||
|
parser::ast::OwaAst,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_throw_panic() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [message] = except_arity(args)?;
|
||||||
|
let message = message.as_str()?;
|
||||||
|
Err(OwaError::Panic(message.to_string()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_throw_type_error() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [expected, got] = except_arity(args)?;
|
||||||
|
let expected = expected.as_str()?;
|
||||||
|
let got = got.as_str()?;
|
||||||
|
Err(OwaError::TypeError(expected.to_string(), got.to_string()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_throw_arity_error() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [expected, got] = except_arity(args)?;
|
||||||
|
let expected = expected.as_int()?;
|
||||||
|
let got = got.as_int()?;
|
||||||
|
Err(OwaError::ArityError(expected as usize, got as usize))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_throw_runtime_error() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [value] = except_arity(args)?;
|
||||||
|
Err(OwaError::RuntimeError(value.clone()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_try_catch() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let ([try_expr], catch_exprs) = except_arity_min(args)?;
|
||||||
|
|
||||||
|
// Normalise both signal channels first so that control-flow signals
|
||||||
|
// (e.g. `break` inside try) are never mistaken for real errors.
|
||||||
|
match Completion::from_result(interpreter.eval(try_expr, scope_id)) {
|
||||||
|
// Propagate any completion (Value, Break, Continue, Return) unchanged
|
||||||
|
Ok(completion) => Ok(completion),
|
||||||
|
// Signal errors that escaped normalisation are also propagated
|
||||||
|
Err(OwaError::Signal(f)) => Ok(f.into()),
|
||||||
|
Err(err) => {
|
||||||
|
for catch_expr in catch_exprs {
|
||||||
|
let catch_call = OwaAst::call([
|
||||||
|
catch_expr.clone(),
|
||||||
|
OwaAst::str(err.message()),
|
||||||
|
OwaAst::kw(err.type_name()),
|
||||||
|
]);
|
||||||
|
// Run catch handler; propagate control flow if it emits any
|
||||||
|
match Completion::from_result(interpreter.eval(&catch_call, scope_id))? {
|
||||||
|
Completion::Value(_) => {} // discard value
|
||||||
|
c => return Ok(c), // propagate Break/Continue/Return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(OwaError::Panic(format!("Unhandled error: {err}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("throw_panic", owa_throw_panic()),
|
||||||
|
("throw_type_error", owa_throw_type_error()),
|
||||||
|
("throw_arity_error", owa_throw_arity_error()),
|
||||||
|
("throw_runtime_error", owa_throw_runtime_error()),
|
||||||
|
("try", owa_try_catch()),
|
||||||
|
])
|
||||||
|
}
|
||||||
299
src/builtins/ffi.rs
Normal file
299
src/builtins/ffi.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
ffi::{CStr, CString, c_char, c_void},
|
||||||
|
sync::{Mutex, OnceLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use libffi::middle::{Arg, Cif, CodePtr, Type};
|
||||||
|
use libloading::Library;
|
||||||
|
|
||||||
|
use crate::core::{OwaError, OwaValue, Scope, except_arity};
|
||||||
|
|
||||||
|
static LIBRARIES: OnceLock<Mutex<HashMap<String, Library>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn libraries() -> &'static Mutex<HashMap<String, Library>> {
|
||||||
|
LIBRARIES.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_err(msg: impl AsRef<str>) -> OwaError {
|
||||||
|
OwaError::RuntimeError(OwaValue::str(msg.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kw_to_ffi_type(kw: &str) -> Result<Type, OwaError> {
|
||||||
|
Ok(match kw {
|
||||||
|
"void" => Type::void(),
|
||||||
|
"ptr" | "str" => Type::pointer(), // both are just pointers at the ABI level
|
||||||
|
"i8" => Type::i8(),
|
||||||
|
"i16" => Type::i16(),
|
||||||
|
"i32" => Type::i32(),
|
||||||
|
"i64" => Type::i64(),
|
||||||
|
"u8" => Type::u8(),
|
||||||
|
"u16" => Type::u16(),
|
||||||
|
"u32" => Type::u32(),
|
||||||
|
"u64" => Type::u64(),
|
||||||
|
"f32" => Type::f32(),
|
||||||
|
"f64" => Type::f64(),
|
||||||
|
_ => return Err(runtime_err(format!("Unknown FFI type keyword: :{kw}"))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// libffi expects a pointer-to-argument for each arg (void**). We box every
|
||||||
|
// value so the address stays stable regardless of Vec reallocations.
|
||||||
|
enum ArgBacking {
|
||||||
|
I8(Box<i8>),
|
||||||
|
I16(Box<i16>),
|
||||||
|
I32(Box<i32>),
|
||||||
|
I64(Box<i64>),
|
||||||
|
U8(Box<u8>),
|
||||||
|
U16(Box<u16>),
|
||||||
|
U32(Box<u32>),
|
||||||
|
U64(Box<u64>),
|
||||||
|
F32(Box<f32>),
|
||||||
|
F64(Box<f64>),
|
||||||
|
Ptr(Box<*const c_void>),
|
||||||
|
// _cstring keeps the allocation alive; ptr is a boxed *const c_char into it
|
||||||
|
Str {
|
||||||
|
_cstring: Box<CString>,
|
||||||
|
ptr: Box<*const c_char>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArgBacking {
|
||||||
|
fn to_ffi_arg(&self) -> Arg<'_> {
|
||||||
|
match self {
|
||||||
|
Self::I8(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::I16(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::I32(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::I64(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::U8(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::U16(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::U32(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::U64(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::F32(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::F64(v) => Arg::new(v.as_ref()),
|
||||||
|
Self::Ptr(p) => Arg::new(p.as_ref()),
|
||||||
|
Self::Str { ptr, .. } => Arg::new(ptr.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn prepare_backing(type_kw: &str, value: &OwaValue) -> Result<ArgBacking, OwaError> {
|
||||||
|
Ok(match type_kw {
|
||||||
|
"i8" => ArgBacking::I8(Box::new(value.as_int()? as i8)),
|
||||||
|
"i16" => ArgBacking::I16(Box::new(value.as_int()? as i16)),
|
||||||
|
"i32" => ArgBacking::I32(Box::new(value.as_int()? as i32)),
|
||||||
|
"i64" => ArgBacking::I64(Box::new(value.as_int()?)),
|
||||||
|
"u8" => ArgBacking::U8(Box::new(value.as_int()? as u8)),
|
||||||
|
"u16" => ArgBacking::U16(Box::new(value.as_int()? as u16)),
|
||||||
|
"u32" => ArgBacking::U32(Box::new(value.as_int()? as u32)),
|
||||||
|
"u64" => ArgBacking::U64(Box::new(value.as_int()? as u64)),
|
||||||
|
"f32" => {
|
||||||
|
let v = match value {
|
||||||
|
OwaValue::Float(f) => f.0 as f32,
|
||||||
|
OwaValue::Int(i) => *i as f32,
|
||||||
|
_ => {
|
||||||
|
return Err(OwaError::TypeError(
|
||||||
|
"float or int".into(),
|
||||||
|
value.type_name().into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ArgBacking::F32(Box::new(v))
|
||||||
|
}
|
||||||
|
"f64" => {
|
||||||
|
let v = match value {
|
||||||
|
OwaValue::Float(f) => f.0,
|
||||||
|
OwaValue::Int(i) => *i as f64,
|
||||||
|
_ => {
|
||||||
|
return Err(OwaError::TypeError(
|
||||||
|
"float or int".into(),
|
||||||
|
value.type_name().into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ArgBacking::F64(Box::new(v))
|
||||||
|
}
|
||||||
|
"ptr" => {
|
||||||
|
let raw = value.as_int()? as *const c_void;
|
||||||
|
ArgBacking::Ptr(Box::new(raw))
|
||||||
|
}
|
||||||
|
"str" => {
|
||||||
|
let s = value.as_str()?;
|
||||||
|
let cstring = Box::new(
|
||||||
|
CString::new(s.as_bytes())
|
||||||
|
.map_err(|e| runtime_err(format!("Invalid string for FFI: {e}")))?,
|
||||||
|
);
|
||||||
|
// box the pointer separately so libffi can dereference it once
|
||||||
|
let ptr = Box::new(cstring.as_ptr());
|
||||||
|
ArgBacking::Str {
|
||||||
|
_cstring: cstring,
|
||||||
|
ptr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err(runtime_err(format!("Unknown FFI type keyword: :{type_kw}"))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Safety: caller must ensure func_ptr matches the cif signature
|
||||||
|
#[allow(clippy::cast_lossless, clippy::cast_possible_wrap)]
|
||||||
|
unsafe fn dispatch_call(
|
||||||
|
cif: &Cif,
|
||||||
|
func_ptr: CodePtr,
|
||||||
|
ffi_args: &[Arg<'_>],
|
||||||
|
return_type: &str,
|
||||||
|
) -> Result<OwaValue, OwaError> {
|
||||||
|
Ok(match return_type {
|
||||||
|
"void" => {
|
||||||
|
// libffi doesn't write the return slot for FFI_TYPE_VOID, so () is fine
|
||||||
|
unsafe { cif.call::<()>(func_ptr, ffi_args) };
|
||||||
|
OwaValue::int(0)
|
||||||
|
}
|
||||||
|
"i8" => OwaValue::int(unsafe { cif.call::<i8>(func_ptr, ffi_args) } as i64),
|
||||||
|
"i16" => OwaValue::int(unsafe { cif.call::<i16>(func_ptr, ffi_args) } as i64),
|
||||||
|
"i32" => OwaValue::int(unsafe { cif.call::<i32>(func_ptr, ffi_args) } as i64),
|
||||||
|
"i64" => OwaValue::int(unsafe { cif.call::<i64>(func_ptr, ffi_args) }),
|
||||||
|
"u8" => OwaValue::int(unsafe { cif.call::<u8>(func_ptr, ffi_args) } as i64),
|
||||||
|
"u16" => OwaValue::int(unsafe { cif.call::<u16>(func_ptr, ffi_args) } as i64),
|
||||||
|
"u32" => OwaValue::int(unsafe { cif.call::<u32>(func_ptr, ffi_args) } as i64),
|
||||||
|
"u64" => OwaValue::int(unsafe { cif.call::<u64>(func_ptr, ffi_args) } as i64),
|
||||||
|
"f32" => OwaValue::float(unsafe { cif.call::<f32>(func_ptr, ffi_args) } as f64),
|
||||||
|
"f64" => OwaValue::float(unsafe { cif.call::<f64>(func_ptr, ffi_args) }),
|
||||||
|
"ptr" => OwaValue::int(unsafe { cif.call::<usize>(func_ptr, ffi_args) } as i64),
|
||||||
|
"str" => {
|
||||||
|
let raw = unsafe { cif.call::<*const c_char>(func_ptr, ffi_args) };
|
||||||
|
if raw.is_null() {
|
||||||
|
OwaValue::str("")
|
||||||
|
} else {
|
||||||
|
let cstr = unsafe { CStr::from_ptr(raw) };
|
||||||
|
OwaValue::str(cstr.to_string_lossy().as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(runtime_err(format!(
|
||||||
|
"Unknown FFI return type: :{return_type}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_load() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [libname] = except_arity(args)?;
|
||||||
|
let libname = libname.clone();
|
||||||
|
let libname_str = libname.as_str()?;
|
||||||
|
|
||||||
|
if libraries()
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| runtime_err("Failed to acquire library registry lock"))?
|
||||||
|
.contains_key(libname_str.as_ref())
|
||||||
|
{
|
||||||
|
return Ok(libname.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the library without holding the lock to avoid unnecessary contention
|
||||||
|
let lib = unsafe {
|
||||||
|
Library::new(libname_str.as_ref())
|
||||||
|
.map_err(|e| runtime_err(format!("Failed to load '{libname_str}': {e}")))?
|
||||||
|
};
|
||||||
|
|
||||||
|
libraries()
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| runtime_err("Failed to acquire library registry lock"))?
|
||||||
|
.insert(libname_str.to_string(), lib);
|
||||||
|
Ok(libname.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_unload() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [libname] = except_arity(args)?;
|
||||||
|
let libname = libname.clone();
|
||||||
|
let libname_str = libname.as_str()?;
|
||||||
|
|
||||||
|
libraries()
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| runtime_err("Failed to acquire library registry lock"))?
|
||||||
|
.remove(libname_str.as_ref());
|
||||||
|
Ok(libname.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// (ffi.call libname funcname [[param-type ...] return-type] [args ...])
|
||||||
|
//
|
||||||
|
// Supported types: :void :ptr :str :i8 :i16 :i32 :i64 :u8 :u16 :u32 :u64 :f32 :f64
|
||||||
|
// :str — takes OwaValue::Str, passes const char*
|
||||||
|
// :ptr — takes OwaValue::Int as a raw address
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_call() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [libname, funcname, signature, call_args] = except_arity(args)?;
|
||||||
|
let libname = libname.as_str()?;
|
||||||
|
let funcname = funcname.as_str()?;
|
||||||
|
let signature = signature.as_vec()?;
|
||||||
|
let call_args = call_args.as_vec()?;
|
||||||
|
|
||||||
|
if signature.len() != 2 {
|
||||||
|
return Err(runtime_err(
|
||||||
|
"Signature must be [[param-type ...] return-type]",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let param_types: Vec<_> = signature[0]
|
||||||
|
.as_vec()?
|
||||||
|
.iter()
|
||||||
|
.map(OwaValue::as_kw)
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
let return_type = signature[1].as_kw()?;
|
||||||
|
|
||||||
|
if param_types.len() != call_args.len() {
|
||||||
|
return Err(OwaError::ArityError(param_types.len(), call_args.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ffi_param_types: Vec<Type> = param_types
|
||||||
|
.iter()
|
||||||
|
.map(|kw| kw_to_ffi_type(kw.as_ref()))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
let cif = Cif::new(ffi_param_types, kw_to_ffi_type(return_type.as_ref())?);
|
||||||
|
|
||||||
|
// grab the fn pointer while holding the lock, then release before calling
|
||||||
|
let func_ptr: CodePtr = unsafe {
|
||||||
|
let libs = libraries()
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| runtime_err("Failed to acquire library registry lock"))?;
|
||||||
|
let ptr = {
|
||||||
|
let sym: libloading::Symbol<unsafe extern "C" fn()> = libs
|
||||||
|
.get(libname.as_ref())
|
||||||
|
.ok_or_else(|| runtime_err(format!("Library not loaded: '{libname}'")))?
|
||||||
|
.get(funcname.as_bytes())
|
||||||
|
.map_err(|e| {
|
||||||
|
runtime_err(format!("Symbol '{funcname}' not found in '{libname}': {e}"))
|
||||||
|
})?;
|
||||||
|
// fn ptr -> usize -> *mut c_void is the portable cast in Rust
|
||||||
|
CodePtr(*sym as usize as *mut c_void)
|
||||||
|
}; // sym (and its borrow of libs) dropped here
|
||||||
|
drop(libs);
|
||||||
|
ptr
|
||||||
|
};
|
||||||
|
|
||||||
|
let backings: Vec<ArgBacking> = param_types
|
||||||
|
.iter()
|
||||||
|
.zip(call_args.iter())
|
||||||
|
.map(|(kw, val)| prepare_backing(kw.as_ref(), val))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
let ffi_args: Vec<Arg<'_>> = backings.iter().map(ArgBacking::to_ffi_arg).collect();
|
||||||
|
|
||||||
|
unsafe { dispatch_call(&cif, func_ptr, &ffi_args, return_type.as_ref()) }.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("load", owa_load()),
|
||||||
|
("unload", owa_unload()),
|
||||||
|
("call", owa_call()),
|
||||||
|
])
|
||||||
|
}
|
||||||
51
src/builtins/flow.rs
Normal file
51
src/builtins/flow.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use crate::core::{Completion, OwaError, OwaValue, Scope, special_names};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_break() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, _| Ok(Completion::Break))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_continue() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, _| Ok(Completion::Continue))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_return() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
args.first().map_or_else(
|
||||||
|
|| Err(OwaError::ArityMinError(1, 0)),
|
||||||
|
|result| Ok(Completion::Return(result.clone())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_loop() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
'outer: loop {
|
||||||
|
for arg in args {
|
||||||
|
// Normalise both signal channels: Ok(Completion::*) from direct
|
||||||
|
// calls and Err(Signal(...)) from nested argument evaluation.
|
||||||
|
match Completion::from_result(interpreter.eval(arg, scope_id))? {
|
||||||
|
Completion::Value(_) => {}
|
||||||
|
Completion::Break => break 'outer,
|
||||||
|
Completion::Continue => continue 'outer,
|
||||||
|
Completion::Return(v) => return Ok(Completion::Value(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OwaValue::kw(special_names::KW_NULL).into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("break", owa_break()),
|
||||||
|
("continue", owa_continue()),
|
||||||
|
("return", owa_return()),
|
||||||
|
("loop", owa_loop()),
|
||||||
|
])
|
||||||
|
}
|
||||||
14
src/builtins/map.rs
Normal file
14
src/builtins/map.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use crate::core::{OwaValue, Scope};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_map() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let value = OwaValue::map(args.iter().cloned());
|
||||||
|
Ok(value.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([("new", owa_map())])
|
||||||
|
}
|
||||||
83
src/builtins/math.rs
Normal file
83
src/builtins/math.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
#![allow(clippy::cast_precision_loss)]
|
||||||
|
use crate::core::{OwaError, OwaValue, Scope, except_arity};
|
||||||
|
|
||||||
|
macro_rules! math_binary_op {
|
||||||
|
($fn_name:ident, $int_op:tt, $float_op:tt) => {
|
||||||
|
#[must_use]
|
||||||
|
pub fn $fn_name() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args: &[OwaValue]| {
|
||||||
|
match args {
|
||||||
|
[arg] => {
|
||||||
|
match (arg) {
|
||||||
|
OwaValue::Int(a) => Ok(OwaValue::Int(0 $int_op a).into()),
|
||||||
|
OwaValue::Float(a) => Ok(OwaValue::float(0.0 $float_op a.as_ref()).into()),
|
||||||
|
_ => Err(OwaError::TypeError("number".into(), arg.type_name().into())),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[first, rest @ ..] => {
|
||||||
|
let mut first = first.clone();
|
||||||
|
for arg in rest {
|
||||||
|
first = match (&first, arg) {
|
||||||
|
(OwaValue::Int(a), OwaValue::Int(b)) => OwaValue::Int(*a $int_op b),
|
||||||
|
(OwaValue::Float(a), OwaValue::Float(b)) => OwaValue::Float(*a $float_op b),
|
||||||
|
(OwaValue::Int(a), OwaValue::Float(b)) => OwaValue::float((*a as f64) $float_op b.as_ref()),
|
||||||
|
(OwaValue::Float(a), OwaValue::Int(b)) => OwaValue::Float(*a $float_op (*b as f64)),
|
||||||
|
_ => return Err(OwaError::TypeError("number".into(), arg.type_name().into())),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(first.into())
|
||||||
|
},
|
||||||
|
_ => Err(OwaError::ArityMinError(1, args.len())),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! math_binary_op_method {
|
||||||
|
($fn_name:ident, $int_method:ident, $float_method:ident) => {
|
||||||
|
#[must_use]
|
||||||
|
pub fn $fn_name() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args: &[OwaValue]| {
|
||||||
|
let [value1, value2] = except_arity(args)?;
|
||||||
|
match (value1, value2) {
|
||||||
|
(OwaValue::Int(a), OwaValue::Int(b)) => {
|
||||||
|
Ok(OwaValue::Int(a.pow((*b).try_into().unwrap_or(0))).into())
|
||||||
|
}
|
||||||
|
(OwaValue::Float(a), OwaValue::Float(b)) => {
|
||||||
|
Ok(OwaValue::float(a.powf(**b)).into())
|
||||||
|
}
|
||||||
|
(OwaValue::Int(a), OwaValue::Float(b)) => {
|
||||||
|
Ok(OwaValue::float((*a as f64).powf(**b)).into())
|
||||||
|
}
|
||||||
|
(OwaValue::Float(a), OwaValue::Int(b)) => {
|
||||||
|
Ok(OwaValue::float(a.powf(*b as f64)).into())
|
||||||
|
}
|
||||||
|
_ => Err(OwaError::TypeError(
|
||||||
|
"number".into(),
|
||||||
|
value1.type_name().into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
math_binary_op!(owa_add, +, +);
|
||||||
|
math_binary_op!(owa_sub, -, -);
|
||||||
|
math_binary_op!(owa_mul, *, *);
|
||||||
|
math_binary_op!(owa_div, /, /);
|
||||||
|
math_binary_op!(owa_mod, %, %);
|
||||||
|
math_binary_op_method!(owa_pow, pow, powf);
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("add", owa_add()),
|
||||||
|
("sub", owa_sub()),
|
||||||
|
("mul", owa_mul()),
|
||||||
|
("div", owa_div()),
|
||||||
|
("mod", owa_mod()),
|
||||||
|
("pow", owa_pow()),
|
||||||
|
])
|
||||||
|
}
|
||||||
324
src/builtins/mod.rs
Normal file
324
src/builtins/mod.rs
Normal file
|
|
@ -0,0 +1,324 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rpds::VectorSync;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
core::{Completion, Interpreter, OwaError, OwaValue, Scope, except_arity, except_arity_min},
|
||||||
|
parser::ast::OwaAst,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod ast;
|
||||||
|
pub mod cond;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod ffi;
|
||||||
|
pub mod flow;
|
||||||
|
pub mod map;
|
||||||
|
pub mod math;
|
||||||
|
pub mod platform;
|
||||||
|
pub mod set;
|
||||||
|
pub mod str;
|
||||||
|
pub mod vec;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_apply() -> OwaValue {
|
||||||
|
OwaValue::native_function(|interpreter, _, args| {
|
||||||
|
let ([first], rest) = except_arity_min(args)?;
|
||||||
|
let calle = first.as_callable()?;
|
||||||
|
let mut flat_args: Vec<OwaValue> = Vec::with_capacity(args.len());
|
||||||
|
for arg in rest {
|
||||||
|
match arg {
|
||||||
|
OwaValue::Vec(vec) => flat_args.extend(vec.iter().cloned()),
|
||||||
|
_ => flat_args.push(arg.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calle.call(interpreter, flat_args.as_slice())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_cmp() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [left, right] = except_arity(args)?;
|
||||||
|
let result = match left.cmp(right) {
|
||||||
|
std::cmp::Ordering::Less => OwaValue::int(-1),
|
||||||
|
std::cmp::Ordering::Equal => OwaValue::int(0),
|
||||||
|
std::cmp::Ordering::Greater => OwaValue::int(1),
|
||||||
|
};
|
||||||
|
Ok(result.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a tuple of names and values
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if `names_ast` or `values_ast` are not the expected types
|
||||||
|
pub fn get_names_and_values(
|
||||||
|
interpreter: &mut Interpreter,
|
||||||
|
scope_id: usize,
|
||||||
|
names_ast: &OwaAst,
|
||||||
|
values_ast: &OwaAst,
|
||||||
|
) -> Result<(Vec<String>, Vec<OwaValue>), OwaError> {
|
||||||
|
let names = match names_ast {
|
||||||
|
OwaAst::Symbol(v) | OwaAst::Str(v) | OwaAst::Keyword(v) => vec![v.to_string()],
|
||||||
|
OwaAst::Vec(v) => v
|
||||||
|
.iter()
|
||||||
|
.map(super::parser::ast::OwaAst::to_sym)
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
_ => {
|
||||||
|
let result = interpreter.eval_value(names_ast, scope_id)?;
|
||||||
|
let result = result.to_sym()?;
|
||||||
|
vec![result.to_string()]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let values = if names.len() == 1 {
|
||||||
|
vec![interpreter.eval_value(values_ast, scope_id)?]
|
||||||
|
} else {
|
||||||
|
let result = interpreter.eval_value(values_ast, scope_id)?;
|
||||||
|
let result = result.as_vec()?;
|
||||||
|
result.iter().cloned().collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((names, values))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_def() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let [key, value_ast] = except_arity(args)?;
|
||||||
|
|
||||||
|
let (names, values) = get_names_and_values(interpreter, scope_id, key, value_ast)?;
|
||||||
|
for (name, value) in names.iter().zip(values.iter()) {
|
||||||
|
interpreter.define_runtime(scope_id, name, value.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if names.len() == 1 && values.len() == 1 {
|
||||||
|
Ok(values[0].clone().into())
|
||||||
|
} else {
|
||||||
|
Ok(OwaValue::vec(values).into())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_exec() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let [variables_ast, bootstrap_ast, target] = except_arity(args)?;
|
||||||
|
let variables = interpreter.eval_value(variables_ast, scope_id)?;
|
||||||
|
let variables = variables.as_map()?;
|
||||||
|
let target = interpreter.eval_value(target, scope_id)?.as_str()?;
|
||||||
|
|
||||||
|
// create temp scope
|
||||||
|
let temp_scope_id = interpreter.inherit_scope(scope_id)?;
|
||||||
|
|
||||||
|
// load shared
|
||||||
|
for (key, value) in variables {
|
||||||
|
let key = &key.to_sym()?;
|
||||||
|
let value = value.clone();
|
||||||
|
interpreter.define_runtime(temp_scope_id, key, value)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eval bootstrap
|
||||||
|
interpreter.eval_value(bootstrap_ast, temp_scope_id)?;
|
||||||
|
|
||||||
|
// isolate scope
|
||||||
|
let mut temp_scope = interpreter.get_scope_cloned(temp_scope_id)?;
|
||||||
|
temp_scope.parent = None;
|
||||||
|
temp_scope.token = std::sync::Arc::new(());
|
||||||
|
temp_scope.child_count = 0;
|
||||||
|
let isolated_scope_id = interpreter.allocate_scope(temp_scope)?;
|
||||||
|
|
||||||
|
// release temp scope (no longer needed after cloning)
|
||||||
|
interpreter.try_release_scope(temp_scope_id);
|
||||||
|
|
||||||
|
// eval
|
||||||
|
interpreter.run(target.as_ref(), isolated_scope_id)?;
|
||||||
|
let result = interpreter.get_scope_as_map(isolated_scope_id)?;
|
||||||
|
Ok(result.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_include() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let [raw_filename] = except_arity(args)?;
|
||||||
|
let raw_filename = interpreter.eval_value(raw_filename, scope_id)?;
|
||||||
|
let raw_filename = raw_filename.as_str()?;
|
||||||
|
interpreter
|
||||||
|
.run(raw_filename.as_ref(), scope_id)
|
||||||
|
.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_lambda() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let [params, body] = except_arity(args)?;
|
||||||
|
let params = params.as_vec()?;
|
||||||
|
let params = params
|
||||||
|
.iter()
|
||||||
|
.map(OwaAst::as_sym)
|
||||||
|
.collect::<Result<VectorSync<_>, _>>()?;
|
||||||
|
|
||||||
|
let token = interpreter.get_scope_token(scope_id)?;
|
||||||
|
let value = OwaValue::new_lambda(body.clone(), params, scope_id, token);
|
||||||
|
Ok(value.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_lookup() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let ([key], rest) = except_arity_min(args)?;
|
||||||
|
let key = key.as_sym().or_else(|_| {
|
||||||
|
let result = interpreter
|
||||||
|
.eval_value(&args[0], scope_id)
|
||||||
|
.map(|v| v.unparse())?;
|
||||||
|
Ok(Arc::from(result.as_str()))
|
||||||
|
})?;
|
||||||
|
let default = rest.first();
|
||||||
|
|
||||||
|
let value = interpreter.lookup_runtime(scope_id, &key);
|
||||||
|
match value {
|
||||||
|
Ok(v) => Ok(v.clone().into()),
|
||||||
|
Err(e) => default.map_or_else(
|
||||||
|
|| Err(e),
|
||||||
|
|default| interpreter.eval_value(default, scope_id).map(Into::into),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_macro() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let [params, body] = except_arity(args)?;
|
||||||
|
let params = params.as_vec()?;
|
||||||
|
|
||||||
|
let params = params
|
||||||
|
.iter()
|
||||||
|
.map(OwaAst::as_sym)
|
||||||
|
.collect::<Result<VectorSync<_>, _>>()?;
|
||||||
|
|
||||||
|
let token = interpreter.get_scope_token(scope_id)?;
|
||||||
|
let value = OwaValue::new_macro(body.clone(), params, scope_id, token);
|
||||||
|
Ok(value.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_trace() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
for arg in args {
|
||||||
|
match arg {
|
||||||
|
OwaValue::Str(v) => print!("{v}"),
|
||||||
|
_ => print!("{}", arg.unparse()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
Ok(OwaValue::str("").into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_typeof() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let [value] = except_arity(args)?;
|
||||||
|
Ok(OwaValue::kw(value.type_name()).into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_scope() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let inner_scope_id = interpreter.inherit_scope(scope_id)?;
|
||||||
|
let _ = interpreter.eval_collection::<_, Vec<_>>(args, inner_scope_id)?;
|
||||||
|
let inner_map = interpreter.get_scope_as_map(inner_scope_id)?;
|
||||||
|
interpreter.try_release_scope(inner_scope_id);
|
||||||
|
Ok(inner_map.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_set() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let [key, value_ast] = except_arity(args)?;
|
||||||
|
|
||||||
|
let (names, values) = get_names_and_values(interpreter, scope_id, key, value_ast)?;
|
||||||
|
for (name, value) in names.iter().zip(values.iter()) {
|
||||||
|
interpreter.set_runtime(scope_id, name, value.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if names.len() == 1 && values.len() == 1 {
|
||||||
|
Ok(values[0].clone().into())
|
||||||
|
} else {
|
||||||
|
Ok(OwaValue::vec(values).into())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_unset() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let mut result = Vec::with_capacity(args.len());
|
||||||
|
for arg in args {
|
||||||
|
let value = interpreter.lookup_runtime(scope_id, &arg.to_str())?.clone();
|
||||||
|
interpreter.unset_runtime(scope_id, &arg.to_str())?;
|
||||||
|
result.push(value);
|
||||||
|
}
|
||||||
|
Ok(OwaValue::vec(result).into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_seq() -> OwaValue {
|
||||||
|
OwaValue::native_macro(|interpreter, scope_id, args| {
|
||||||
|
let mut last = None;
|
||||||
|
for arg in args {
|
||||||
|
// Normalise both signal channels: Ok(Completion::*) from direct calls
|
||||||
|
// and Err(Signal(...)) from nested argument evaluation (e.g. inside
|
||||||
|
// a `namespace` / `scope` that uses eval_collection).
|
||||||
|
match Completion::from_result(interpreter.eval(arg, scope_id))? {
|
||||||
|
Completion::Value(v) => last = Some(v),
|
||||||
|
// Return is caught here — it exits the seq with the returned value
|
||||||
|
Completion::Return(v) => return Ok(Completion::Value(v)),
|
||||||
|
// Break/Continue propagate outward (they belong to an enclosing loop)
|
||||||
|
c => return Ok(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last.ok_or_else(|| OwaError::ArityMinError(1, args.len()))
|
||||||
|
.map(Completion::Value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("apply", owa_apply()),
|
||||||
|
("cmp", owa_cmp()),
|
||||||
|
("def", owa_def()),
|
||||||
|
("exec", owa_exec()),
|
||||||
|
("lambda", owa_lambda()),
|
||||||
|
("lookup", owa_lookup()),
|
||||||
|
("macro", owa_macro()),
|
||||||
|
("trace", owa_trace()),
|
||||||
|
("typeof", owa_typeof()),
|
||||||
|
("include", owa_include()),
|
||||||
|
("scope", owa_scope()),
|
||||||
|
("set!", owa_set()),
|
||||||
|
("unset!", owa_unset()),
|
||||||
|
("seq", owa_seq()),
|
||||||
|
("ast", ast::scope().local_runtime_to_map()),
|
||||||
|
("cond", cond::scope().local_runtime_to_map()),
|
||||||
|
("errors", errors::scope().local_runtime_to_map()),
|
||||||
|
("ffi", ffi::scope().local_runtime_to_map()),
|
||||||
|
("flow", flow::scope().local_runtime_to_map()),
|
||||||
|
("map", map::scope().local_runtime_to_map()),
|
||||||
|
("math", math::scope().local_runtime_to_map()),
|
||||||
|
("platform", platform::scope().local_runtime_to_map()),
|
||||||
|
("set", set::scope().local_runtime_to_map()),
|
||||||
|
("str", str::scope().local_runtime_to_map()),
|
||||||
|
("vec", vec::scope().local_runtime_to_map()),
|
||||||
|
])
|
||||||
|
}
|
||||||
19
src/builtins/platform.rs
Normal file
19
src/builtins/platform.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::core::{OwaValue, Scope};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_argv() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, _| {
|
||||||
|
let args = std::env::args().map(OwaValue::str);
|
||||||
|
Ok(OwaValue::vec(args).into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("os-name", OwaValue::kw(std::env::consts::OS)),
|
||||||
|
("os-family", OwaValue::kw(std::env::consts::FAMILY)),
|
||||||
|
("arch", OwaValue::str(std::env::consts::ARCH)),
|
||||||
|
("argv", owa_argv()),
|
||||||
|
])
|
||||||
|
}
|
||||||
47
src/builtins/set.rs
Normal file
47
src/builtins/set.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use crate::core::{Completion, OwaValue, Scope, except_arity};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_set_from() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args: &[OwaValue]| {
|
||||||
|
let mut elements = Vec::new();
|
||||||
|
for arg in args {
|
||||||
|
match arg {
|
||||||
|
OwaValue::Vec(values) => elements.extend(values.iter().cloned()),
|
||||||
|
OwaValue::Set(values) => elements.extend(values.iter().cloned()),
|
||||||
|
OwaValue::Map(values) => elements.extend(values.keys().cloned()),
|
||||||
|
OwaValue::Str(v) => {
|
||||||
|
elements.extend(v.chars().map(|v| OwaValue::str(v.to_string())));
|
||||||
|
}
|
||||||
|
_ => elements.push(arg.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OwaValue::set(elements).into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_fold() -> OwaValue {
|
||||||
|
OwaValue::native_function(|interpreter, _, args| {
|
||||||
|
let [lambda, initial, set] = except_arity(args)?;
|
||||||
|
|
||||||
|
let callable = lambda.as_callable()?;
|
||||||
|
let set = set.as_set()?;
|
||||||
|
let mut initial = initial.clone();
|
||||||
|
|
||||||
|
for value in set {
|
||||||
|
let call_args = [initial.clone(), value.clone()];
|
||||||
|
match Completion::from_result(callable.call(interpreter, &call_args))? {
|
||||||
|
Completion::Value(v) => initial = v,
|
||||||
|
Completion::Break => break,
|
||||||
|
Completion::Continue => continue,
|
||||||
|
Completion::Return(v) => return Ok(Completion::Value(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(initial.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([("from", owa_set_from()), ("fold", owa_fold())])
|
||||||
|
}
|
||||||
48
src/builtins/str.rs
Normal file
48
src/builtins/str.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
use crate::core::{OwaValue, Scope};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_to_str() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args: &[OwaValue]| {
|
||||||
|
let value = OwaValue::str(
|
||||||
|
args.iter()
|
||||||
|
.map(|v| match v {
|
||||||
|
OwaValue::Str(v) => v.to_string(),
|
||||||
|
_ => v.unparse(),
|
||||||
|
})
|
||||||
|
.collect::<String>(),
|
||||||
|
);
|
||||||
|
Ok(value.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_to_kw() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args: &[OwaValue]| {
|
||||||
|
let value = OwaValue::kw(
|
||||||
|
args.iter()
|
||||||
|
.map(|v| match v {
|
||||||
|
OwaValue::Str(v) | OwaValue::Keyword(v) => v.to_string(),
|
||||||
|
_ => v.unparse(),
|
||||||
|
})
|
||||||
|
.collect::<String>(),
|
||||||
|
);
|
||||||
|
Ok(value.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_serialize() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args| {
|
||||||
|
let value = OwaValue::str(args.iter().map(OwaValue::unparse).collect::<String>());
|
||||||
|
Ok(value.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("new", owa_to_str()),
|
||||||
|
("kw", owa_to_kw()),
|
||||||
|
("serialize", owa_serialize()),
|
||||||
|
])
|
||||||
|
}
|
||||||
66
src/builtins/vec.rs
Normal file
66
src/builtins/vec.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::core::{Completion, OwaValue, Scope, except_arity};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_vec_new() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args: &[OwaValue]| {
|
||||||
|
let value = OwaValue::vec(args.iter().cloned());
|
||||||
|
Ok(value.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_vec_from() -> OwaValue {
|
||||||
|
OwaValue::native_function(|_, _, args: &[OwaValue]| {
|
||||||
|
let mut result = Vec::with_capacity(args.len());
|
||||||
|
for arg in args {
|
||||||
|
match arg {
|
||||||
|
OwaValue::Vec(values) => result.extend(values.iter().cloned()),
|
||||||
|
OwaValue::Set(values) => result.extend(values.iter().cloned()),
|
||||||
|
OwaValue::Map(values) => {
|
||||||
|
result.extend(
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| OwaValue::vec([k.clone(), v.clone()])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OwaValue::Str(v) => result.extend(v.chars().map(|v| OwaValue::str(v.to_string()))),
|
||||||
|
_ => result.push(arg.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OwaValue::vec(result).into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn owa_fold() -> OwaValue {
|
||||||
|
OwaValue::native_function(|interpreter, _, args| {
|
||||||
|
let [lambda, initial, vector] = except_arity(args)?;
|
||||||
|
|
||||||
|
let callable = lambda.as_callable()?;
|
||||||
|
let vector = vector.as_vec()?;
|
||||||
|
let mut acc = initial.clone();
|
||||||
|
let mut call_args = [OwaValue::Int(0), OwaValue::Int(0), OwaValue::Int(0)];
|
||||||
|
|
||||||
|
for (idx, value) in vector.iter().enumerate() {
|
||||||
|
call_args[0] = std::mem::replace(&mut acc, OwaValue::Int(0));
|
||||||
|
call_args[1] = value.clone();
|
||||||
|
call_args[2] = OwaValue::int(idx);
|
||||||
|
match Completion::from_result(callable.call(interpreter, &call_args))? {
|
||||||
|
Completion::Value(v) => acc = v,
|
||||||
|
Completion::Break => break,
|
||||||
|
Completion::Continue => continue,
|
||||||
|
Completion::Return(v) => return Ok(Completion::Value(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(acc.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope() -> Scope {
|
||||||
|
Scope::new_with_runtime([
|
||||||
|
("new", owa_vec_new()),
|
||||||
|
("from", owa_vec_from()),
|
||||||
|
("fold", owa_fold()),
|
||||||
|
])
|
||||||
|
}
|
||||||
234
src/core/callable.rs
Normal file
234
src/core/callable.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
#![allow(unpredictable_function_pointer_comparisons)]
|
||||||
|
use std::{hash::Hash, sync::Arc};
|
||||||
|
|
||||||
|
use educe::Educe;
|
||||||
|
use rpds::VectorSync;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
core::{Completion, Interpreter, OwaError, OwaValue, ScopeId, special_names},
|
||||||
|
parser::ast::{Expander, OwaAst},
|
||||||
|
vec_unparse,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type OwaNativeFunction =
|
||||||
|
fn(&mut Interpreter, ScopeId, &[OwaValue]) -> Result<Completion, OwaError>;
|
||||||
|
pub type OwaNativeMacro = fn(&mut Interpreter, ScopeId, &[OwaAst]) -> Result<Completion, OwaError>;
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum OwaCallableCell {
|
||||||
|
NativeLambda(OwaNativeFunction),
|
||||||
|
NativeMacro(OwaNativeMacro),
|
||||||
|
Lambda(Arc<OwaAst>),
|
||||||
|
Macro(Arc<OwaAst>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Educe)]
|
||||||
|
#[educe(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct OwaCallable {
|
||||||
|
#[educe(Debug(ignore), Hash(ignore), PartialEq(ignore), PartialOrd(ignore))]
|
||||||
|
pub scope_id: ScopeId,
|
||||||
|
#[educe(Debug(ignore), Hash(ignore), PartialEq(ignore), PartialOrd(ignore))]
|
||||||
|
pub scope_token: Arc<()>,
|
||||||
|
pub params: VectorSync<Arc<str>>,
|
||||||
|
pub body: OwaCallableCell,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwaCallable {
|
||||||
|
fn call_macro<'a, I>(
|
||||||
|
&self,
|
||||||
|
interpreter: &mut Interpreter,
|
||||||
|
args: I,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
) -> Result<Completion, OwaError>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'a OwaAst>,
|
||||||
|
{
|
||||||
|
// remove annotations
|
||||||
|
let args = args.into_iter().map(|a| a.unannotate()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// check arity
|
||||||
|
if args.len() < self.params.len() {
|
||||||
|
return Err(OwaError::ArityError(self.params.len(), args.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// create call scope
|
||||||
|
self.load_expand_args(interpreter, scope_id, &args)?;
|
||||||
|
|
||||||
|
match &self.body {
|
||||||
|
OwaCallableCell::NativeMacro(native) => native(interpreter, scope_id, &args),
|
||||||
|
OwaCallableCell::Macro(body) => {
|
||||||
|
let expanded = Expander::new(interpreter, scope_id).expand(body, false, false);
|
||||||
|
debug!("expanded: {expanded:?}");
|
||||||
|
interpreter.eval(&expanded, scope_id)
|
||||||
|
}
|
||||||
|
_ => Err(OwaError::TypeError("macro".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls this callable with the given pre-evaluated arguments.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::ArityError`] if too few arguments are provided.
|
||||||
|
/// Returns [`OwaError::TypeError`] if the body is not a lambda variant.
|
||||||
|
/// Propagates any error returned by the lambda body or native function.
|
||||||
|
pub fn call(
|
||||||
|
&self,
|
||||||
|
interpreter: &mut Interpreter,
|
||||||
|
args: &[OwaValue],
|
||||||
|
) -> Result<Completion, OwaError> {
|
||||||
|
// check arity
|
||||||
|
if args.len() < self.params.len() {
|
||||||
|
return Err(OwaError::ArityError(self.params.len(), args.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// create scope
|
||||||
|
let call_scope_id = interpreter.inherit_scope(self.scope_id)?;
|
||||||
|
self.load_args(interpreter, call_scope_id, args)?;
|
||||||
|
|
||||||
|
// eval
|
||||||
|
let result = match &self.body {
|
||||||
|
OwaCallableCell::Lambda(body) => interpreter.eval(body, call_scope_id),
|
||||||
|
OwaCallableCell::NativeLambda(native) => native(interpreter, call_scope_id, args),
|
||||||
|
_ => Err(OwaError::TypeError(
|
||||||
|
"lambda".into(),
|
||||||
|
self.type_name().into(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
interpreter.try_release_scope(call_scope_id);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates the callable with the given raw AST arguments.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError`] if argument evaluation or the underlying call fails.
|
||||||
|
pub fn eval<'a, I>(
|
||||||
|
&self,
|
||||||
|
interpreter: &mut Interpreter,
|
||||||
|
raw_args: I,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
) -> Result<Completion, OwaError>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'a OwaAst>,
|
||||||
|
{
|
||||||
|
match &self.body {
|
||||||
|
OwaCallableCell::NativeMacro(_) | OwaCallableCell::Macro(_) => {
|
||||||
|
self.call_macro(interpreter, raw_args, scope_id)
|
||||||
|
}
|
||||||
|
OwaCallableCell::NativeLambda(_) | OwaCallableCell::Lambda(_) => {
|
||||||
|
let args: Vec<OwaValue> = interpreter.eval_collection(raw_args, scope_id)?;
|
||||||
|
self.call(interpreter, &args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines all call arguments (`this`, `%%`, `%&`, named params, indexed params)
|
||||||
|
/// in the given scope.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if any variable is already defined in the scope.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn load_args(
|
||||||
|
&self,
|
||||||
|
interpreter: &mut Interpreter,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
args: &[OwaValue],
|
||||||
|
) -> Result<(), OwaError> {
|
||||||
|
// define self
|
||||||
|
interpreter.define_runtime(
|
||||||
|
scope_id,
|
||||||
|
special_names::THIS_CALLABLE,
|
||||||
|
OwaValue::Callable(self.clone()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// define special arguments
|
||||||
|
interpreter.define_runtime(
|
||||||
|
scope_id,
|
||||||
|
special_names::ALL_ARGS,
|
||||||
|
OwaValue::vec(args.iter().cloned()),
|
||||||
|
)?;
|
||||||
|
interpreter.define_runtime(
|
||||||
|
scope_id,
|
||||||
|
special_names::REST_ARGS,
|
||||||
|
OwaValue::vec(args.iter().skip(self.params.len()).cloned()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// define named arguments
|
||||||
|
for (idx, param) in self.params.iter().enumerate() {
|
||||||
|
interpreter.define_runtime(scope_id, param, args[idx].clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// define indexed arguments
|
||||||
|
for (idx, arg) in args.iter().enumerate() {
|
||||||
|
interpreter.define_runtime(scope_id, &format!("%{}", idx + 1), arg.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn load_expand_args(
|
||||||
|
&self,
|
||||||
|
interpreter: &mut Interpreter,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
args: &[OwaAst],
|
||||||
|
) -> Result<(), OwaError> {
|
||||||
|
// define special arguments
|
||||||
|
interpreter.set_expand(
|
||||||
|
scope_id,
|
||||||
|
special_names::ALL_ARGS,
|
||||||
|
OwaAst::vec(args.iter().cloned()),
|
||||||
|
)?;
|
||||||
|
interpreter.set_expand(
|
||||||
|
scope_id,
|
||||||
|
special_names::REST_ARGS,
|
||||||
|
OwaAst::vec(args.iter().skip(self.params.len()).cloned()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// define named arguments
|
||||||
|
for (idx, param) in self.params.iter().enumerate() {
|
||||||
|
interpreter.set_expand(scope_id, param, args[idx].clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// define indexed arguments
|
||||||
|
for (idx, arg) in args.iter().enumerate() {
|
||||||
|
interpreter.set_expand(scope_id, &format!("%{}", idx + 1), arg.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_param_index(&self, name: &str) -> Option<usize> {
|
||||||
|
self.params.iter().position(|p| p.clone().as_ref() == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn type_name(&self) -> &'static str {
|
||||||
|
match self.body {
|
||||||
|
OwaCallableCell::NativeLambda(_) | OwaCallableCell::Lambda(_) => "lambda",
|
||||||
|
OwaCallableCell::NativeMacro(_) | OwaCallableCell::Macro(_) => "macro",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn unparse(&self) -> String {
|
||||||
|
match &self.body {
|
||||||
|
OwaCallableCell::Lambda(v) => match v.as_ref() {
|
||||||
|
OwaAst::Call(items) => format!("#{}", vec_unparse!(items.iter())),
|
||||||
|
_ => format!("#({})", v.unparse()),
|
||||||
|
},
|
||||||
|
OwaCallableCell::NativeLambda(_) => "#(!native.lambda!)".to_string(),
|
||||||
|
OwaCallableCell::Macro(v) => match v.as_ref() {
|
||||||
|
OwaAst::Call(items) => format!("#{}", vec_unparse!(items.iter())),
|
||||||
|
_ => format!("@({})", v.unparse()),
|
||||||
|
},
|
||||||
|
OwaCallableCell::NativeMacro(_) => "#(!native.macro!)".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
src/core/error.rs
Normal file
207
src/core/error.rs
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
use crate::core::OwaValue;
|
||||||
|
|
||||||
|
/// Payload carried by `break`, `continue`, and `return` signals.
|
||||||
|
///
|
||||||
|
/// This is kept separate from [`OwaError`] so that control flow can travel
|
||||||
|
/// through two independent channels:
|
||||||
|
///
|
||||||
|
/// * **`Ok(Completion::*)`** — produced directly by the `break`/`continue`/
|
||||||
|
/// `return` native functions and propagated through `eval`/`callable::call`.
|
||||||
|
/// * **`Err(OwaError::Signal(...))`** — produced when a signal passes through
|
||||||
|
/// a value-evaluation context (e.g. argument lists via `eval_collection`).
|
||||||
|
/// This mirrors the old viral-error propagation so that constructs like
|
||||||
|
/// `(for x v (continue))` still work correctly even though the `continue`
|
||||||
|
/// originates inside a nested lambda.
|
||||||
|
///
|
||||||
|
/// Both channels are unified by [`Completion::from_result`].
|
||||||
|
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum FlowOperation {
|
||||||
|
Break,
|
||||||
|
Continue,
|
||||||
|
Return(OwaValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the outcome of evaluating an expression.
|
||||||
|
///
|
||||||
|
/// Normal expressions produce `Value`. Control-flow constructs (`break`,
|
||||||
|
/// `continue`, `return`) produce the corresponding variant. These are NOT
|
||||||
|
/// errors — they are intentional signals that loops and sequences catch.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Completion {
|
||||||
|
/// The expression evaluated to a value normally.
|
||||||
|
Value(OwaValue),
|
||||||
|
/// A `break` signal was emitted.
|
||||||
|
Break,
|
||||||
|
/// A `continue` signal was emitted.
|
||||||
|
Continue,
|
||||||
|
/// A `return` signal was emitted with its return value.
|
||||||
|
Return(OwaValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completion {
|
||||||
|
/// Normalises a result by converting `OwaError::Signal` errors back into
|
||||||
|
/// `Completion` variants.
|
||||||
|
///
|
||||||
|
/// Control-flow signals may travel via two channels:
|
||||||
|
/// * `Ok(Completion::Break/Continue/Return)` — from a direct lambda call.
|
||||||
|
/// * `Err(OwaError::Signal(...))` — from nested argument evaluation.
|
||||||
|
///
|
||||||
|
/// Call this function wherever both channels must be handled (loops, seq).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// * `Ok(Completion::Value(v))` — the expression evaluated to a value normally.
|
||||||
|
/// * `Ok(Completion::Break/Continue/Return)` — a control-flow signal was emitted.
|
||||||
|
/// * `Err(OwaError::Panic(_))` — a `break` signal was emitted outside a loop.
|
||||||
|
pub fn from_result(result: Result<Self, OwaError>) -> Result<Self, OwaError> {
|
||||||
|
match result {
|
||||||
|
Err(OwaError::Signal(f)) => Ok(f.into()),
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unwraps the inner value, converting control-flow signals into
|
||||||
|
/// user-visible panic errors.
|
||||||
|
///
|
||||||
|
/// Use this only when control flow is genuinely unexpected (e.g. at the
|
||||||
|
/// top level of a file). Inside loops / sequences use [`from_result`] +
|
||||||
|
/// a pattern match instead.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if this is `Break`, `Continue`, or `Return`.
|
||||||
|
pub fn into_value(self) -> Result<OwaValue, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Value(v) => Ok(v),
|
||||||
|
Self::Break => Err(OwaError::Panic("Unexpected break outside loop".into())),
|
||||||
|
Self::Continue => Err(OwaError::Panic("Unexpected continue outside loop".into())),
|
||||||
|
Self::Return(v) => Err(OwaError::Panic(format!(
|
||||||
|
"Unexpected return: {}",
|
||||||
|
v.unparse()
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OwaValue> for Completion {
|
||||||
|
fn from(v: OwaValue) -> Self {
|
||||||
|
Self::Value(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FlowOperation> for Completion {
|
||||||
|
fn from(f: FlowOperation) -> Self {
|
||||||
|
match f {
|
||||||
|
FlowOperation::Break => Self::Break,
|
||||||
|
FlowOperation::Continue => Self::Continue,
|
||||||
|
FlowOperation::Return(v) => Self::Return(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum OwaError {
|
||||||
|
TypeError(String, String), // expected type A, got type B
|
||||||
|
ArityError(usize, usize), // expected N, got M
|
||||||
|
ArityMaxError(usize, usize), // expected max N args, got M
|
||||||
|
ArityMinError(usize, usize), // expected min N args, got M
|
||||||
|
InvalidMemberAccess(String, &'static str), // name A; type B
|
||||||
|
Panic(String),
|
||||||
|
EmptyCall,
|
||||||
|
RuntimeError(OwaValue),
|
||||||
|
UnboundVariable(String),
|
||||||
|
/// Internal: a control-flow signal propagating through value-evaluation
|
||||||
|
/// contexts (e.g. argument lists). Never shown to the user as a top-level
|
||||||
|
/// error; always caught by the nearest loop / sequence construct.
|
||||||
|
Signal(FlowOperation),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwaError {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn type_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::TypeError(_, _) => "TypeError",
|
||||||
|
Self::ArityError(_, _) => "ArityError",
|
||||||
|
Self::ArityMaxError(_, _) => "ArityMaxError",
|
||||||
|
Self::ArityMinError(_, _) => "ArityMinError",
|
||||||
|
Self::InvalidMemberAccess(_, _) => "InvalidMemberAccess",
|
||||||
|
Self::Panic(_) => "Panic",
|
||||||
|
Self::EmptyCall => "EmptyCall",
|
||||||
|
Self::RuntimeError(_) => "RuntimeError",
|
||||||
|
Self::UnboundVariable(_) => "UnboundVariable",
|
||||||
|
Self::Signal(_) => "Signal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn message(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::TypeError(expected, got) => {
|
||||||
|
format!("expected type {expected}, got type {got}")
|
||||||
|
}
|
||||||
|
Self::ArityError(expected, got) => {
|
||||||
|
format!("expected {expected} args, got {got}")
|
||||||
|
}
|
||||||
|
Self::ArityMaxError(max, got) => {
|
||||||
|
format!("expected max {max} args, got {got}")
|
||||||
|
}
|
||||||
|
Self::ArityMinError(min, got) => {
|
||||||
|
format!("expected min {min} args, got {got}")
|
||||||
|
}
|
||||||
|
Self::InvalidMemberAccess(name, type_name) => {
|
||||||
|
format!("cannot access member '{name}' on type {type_name}")
|
||||||
|
}
|
||||||
|
Self::Panic(message) => message.to_owned(),
|
||||||
|
Self::EmptyCall => "empty call".to_string(),
|
||||||
|
Self::RuntimeError(value) => format!("runtime error: {}", value.unparse()),
|
||||||
|
Self::UnboundVariable(name) => format!("unbound variable {name}"),
|
||||||
|
Self::Signal(FlowOperation::Break) => "break outside loop".to_string(),
|
||||||
|
Self::Signal(FlowOperation::Continue) => "continue outside loop".to_string(),
|
||||||
|
Self::Signal(FlowOperation::Return(v)) => {
|
||||||
|
format!("unexpected return: {}", v.unparse())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for OwaError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}: {}", self.type_name(), self.message())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects `args` into a fixed-size array of length `N`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::ArityError`] if the number of items in `args` is not exactly `N`.
|
||||||
|
pub fn except_arity<T: std::fmt::Debug, C, const N: usize>(args: C) -> Result<[T; N], OwaError>
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = T>,
|
||||||
|
{
|
||||||
|
let args = args.into_iter().collect::<Vec<_>>();
|
||||||
|
let len = args.len();
|
||||||
|
args.try_into().map_err(|_| OwaError::ArityError(N, len))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects `args` into a fixed-size array of length `N` or more,
|
||||||
|
/// returning the fixed-size array and any remaining items as a [`Vec`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::ArityMinError`] if the number of items in `args` is less than `N`.
|
||||||
|
pub fn except_arity_min<T: std::fmt::Debug, C, const N: usize>(
|
||||||
|
args: C,
|
||||||
|
) -> Result<([T; N], Vec<T>), OwaError>
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = T>,
|
||||||
|
{
|
||||||
|
let mut args = args.into_iter().collect::<Vec<_>>();
|
||||||
|
let len = args.len();
|
||||||
|
if len < N {
|
||||||
|
return Err(OwaError::ArityMinError(N, len));
|
||||||
|
}
|
||||||
|
let rest = args.split_off(N);
|
||||||
|
let fixed: [T; N] = args.try_into().map_err(|_| OwaError::ArityError(N, len))?;
|
||||||
|
Ok((fixed, rest))
|
||||||
|
}
|
||||||
492
src/core/interpreter.rs
Normal file
492
src/core/interpreter.rs
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rpds::{RedBlackTreeMapSync, RedBlackTreeSetSync, VectorSync};
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
core::{Completion, FlowOperation, OwaError, OwaValue, Scope, ScopeId, special_names},
|
||||||
|
parser::{ast::OwaAst, parse_file},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Interpreter {
|
||||||
|
scopes: Vec<Option<Scope>>,
|
||||||
|
free_list: Vec<ScopeId>,
|
||||||
|
pub max_scopes_count: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interpreter {
|
||||||
|
pub const fn new(max_scopes_count: Option<usize>) -> Self {
|
||||||
|
Self {
|
||||||
|
scopes: Vec::new(),
|
||||||
|
free_list: Vec::new(),
|
||||||
|
max_scopes_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves, parses and evaluates an OWA source file relative to the current
|
||||||
|
/// `__dir__` runtime variable.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::UnboundVariable`] if `__dir__` is not set in the scope.
|
||||||
|
/// Returns [`OwaError::Panic`] if the file cannot be found, read, or parsed.
|
||||||
|
/// Propagates any [`OwaError`] produced while evaluating the file.
|
||||||
|
pub fn run<S>(&mut self, filename: S, scope_id: ScopeId) -> Result<OwaValue, OwaError>
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
// resolve
|
||||||
|
let olddir = self.lookup_runtime(scope_id, special_names::DIR)?.clone();
|
||||||
|
let filename: String = filename.into();
|
||||||
|
let filename_path = std::path::Path::new(olddir.as_str()?.as_ref()).join(&filename);
|
||||||
|
trace!("run {}", filename_path.display());
|
||||||
|
let filename_resolved = resolve_owa_path(&filename_path)
|
||||||
|
.ok_or_else(|| OwaError::Panic(format!("Could not find file {filename}")))?;
|
||||||
|
|
||||||
|
// parse
|
||||||
|
let ast = parse_file(&filename_resolved).map_err(OwaError::Panic)?;
|
||||||
|
|
||||||
|
// update __dir__
|
||||||
|
let dir = std::path::Path::new(&filename_resolved)
|
||||||
|
.parent()
|
||||||
|
.and_then(|p| p.to_str());
|
||||||
|
|
||||||
|
if let Some(dir) = dir {
|
||||||
|
self.set_runtime(scope_id, special_names::DIR, OwaValue::str(dir))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eval — normalise both signal channels, then catch Return at the top level
|
||||||
|
let result = match Completion::from_result(self.eval(&ast, scope_id))? {
|
||||||
|
Completion::Value(v) | Completion::Return(v) => v,
|
||||||
|
Completion::Break => {
|
||||||
|
return Err(OwaError::Panic("Unexpected break at top level".into()));
|
||||||
|
}
|
||||||
|
Completion::Continue => {
|
||||||
|
return Err(OwaError::Panic("Unexpected continue at top level".into()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// restore __dir__
|
||||||
|
self.set_runtime(scope_id, special_names::DIR, olddir)?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates a single AST node and returns a [`Completion`].
|
||||||
|
///
|
||||||
|
/// Returns `Completion::Value` for normal expressions and
|
||||||
|
/// `Completion::Break`/`Continue`/`Return` for control-flow signals.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Propagates any [`OwaError`] produced during evaluation.
|
||||||
|
pub fn eval(&mut self, value: &OwaAst, scope_id: ScopeId) -> Result<Completion, OwaError> {
|
||||||
|
trace!("scopes count: {}", self.scopes.len());
|
||||||
|
match value {
|
||||||
|
OwaAst::Symbol(v) => self.eval_symbol(v, scope_id).map(Completion::Value),
|
||||||
|
OwaAst::Keyword(v) => Ok(OwaValue::kw(v).into()),
|
||||||
|
OwaAst::Str(v) => Ok(OwaValue::str(v).into()),
|
||||||
|
OwaAst::Float(v) => Ok(OwaValue::Float(*v).into()),
|
||||||
|
OwaAst::Int(v) => Ok(OwaValue::Int(*v).into()),
|
||||||
|
OwaAst::Vec(v) => self.eval_vector(v, scope_id).map(Completion::Value),
|
||||||
|
OwaAst::Call(v) => self.eval_call(v, scope_id),
|
||||||
|
OwaAst::Set(v) => self.eval_set(v, scope_id).map(Completion::Value),
|
||||||
|
OwaAst::Map(v) => self.eval_map(v, scope_id).map(Completion::Value),
|
||||||
|
OwaAst::Quote(v) => Ok(OwaValue::quote(v.clone()).into()),
|
||||||
|
OwaAst::Unquote(v) => self.eval(v.as_ref(), scope_id),
|
||||||
|
OwaAst::Lambda(v) => {
|
||||||
|
let call = OwaAst::call(v.iter().cloned());
|
||||||
|
let token = self.get_scope_token(scope_id)?;
|
||||||
|
let lambda = OwaValue::new_lambda(call, VectorSync::default(), scope_id, token);
|
||||||
|
Ok(lambda.into())
|
||||||
|
}
|
||||||
|
OwaAst::Annotation(v) => self.eval(&v.value, scope_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates a single AST node and unwraps it to an [`OwaValue`].
|
||||||
|
///
|
||||||
|
/// Control-flow signals (`Break`, `Continue`, `Return`) are converted to
|
||||||
|
/// [`OwaError::Signal`] so they propagate virally through argument
|
||||||
|
/// evaluation until the nearest loop / sequence catches them via
|
||||||
|
/// [`Completion::from_result`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Signal`] if the result is a control-flow signal.
|
||||||
|
/// Propagates any other [`OwaError`] produced during evaluation.
|
||||||
|
pub fn eval_value(&mut self, value: &OwaAst, scope_id: ScopeId) -> Result<OwaValue, OwaError> {
|
||||||
|
match self.eval(value, scope_id)? {
|
||||||
|
Completion::Value(v) => Ok(v),
|
||||||
|
Completion::Break => Err(OwaError::Signal(FlowOperation::Break)),
|
||||||
|
Completion::Continue => Err(OwaError::Signal(FlowOperation::Continue)),
|
||||||
|
Completion::Return(v) => Err(OwaError::Signal(FlowOperation::Return(v))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_symbol(&self, symbol: &str, scope_id: ScopeId) -> Result<OwaValue, OwaError> {
|
||||||
|
// specials
|
||||||
|
if is_special_symbol(symbol) {
|
||||||
|
match symbol {
|
||||||
|
special_names::SCOPE => return self.get_scope_as_map(scope_id),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal
|
||||||
|
self.lookup_runtime(scope_id, symbol).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_vector(
|
||||||
|
&mut self,
|
||||||
|
value: &VectorSync<OwaAst>,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
) -> Result<OwaValue, OwaError> {
|
||||||
|
self.eval_collection(value, scope_id).map(OwaValue::Vec)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_call(
|
||||||
|
&mut self,
|
||||||
|
value: &VectorSync<OwaAst>,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
) -> Result<Completion, OwaError> {
|
||||||
|
let raw_calle = value.first().ok_or(OwaError::EmptyCall)?;
|
||||||
|
let raw_calle = self.eval_value(raw_calle, scope_id)?;
|
||||||
|
let callable = raw_calle.as_callable()?;
|
||||||
|
|
||||||
|
callable.eval(self, value.iter().skip(1), scope_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_set(
|
||||||
|
&mut self,
|
||||||
|
value: &RedBlackTreeSetSync<OwaAst>,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
) -> Result<OwaValue, OwaError> {
|
||||||
|
self.eval_collection(value.iter(), scope_id)
|
||||||
|
.map(OwaValue::Set)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_map(
|
||||||
|
&mut self,
|
||||||
|
value: &RedBlackTreeMapSync<OwaAst, OwaAst>,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
) -> Result<OwaValue, OwaError> {
|
||||||
|
let iter = value.iter().flat_map(|(k, v)| [k, v]);
|
||||||
|
self.eval_collection::<_, Vec<_>>(iter, scope_id)
|
||||||
|
.map(OwaValue::map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates every item in the iterator as a value and collects the results into `B`.
|
||||||
|
///
|
||||||
|
/// Control-flow signals are forwarded as [`OwaError::Signal`] so they
|
||||||
|
/// propagate virally through argument-evaluation contexts and are later
|
||||||
|
/// caught by the nearest loop / sequence via [`Completion::from_result`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the first [`OwaError`] (including `Signal`) encountered.
|
||||||
|
pub fn eval_collection<'a, C, B>(&mut self, items: C, scope_id: ScopeId) -> Result<B, OwaError>
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = &'a OwaAst>,
|
||||||
|
B: FromIterator<OwaValue>,
|
||||||
|
{
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for item in items {
|
||||||
|
match self.eval(item, scope_id)? {
|
||||||
|
Completion::Value(v) => result.push(v),
|
||||||
|
Completion::Break => return Err(OwaError::Signal(FlowOperation::Break)),
|
||||||
|
Completion::Continue => return Err(OwaError::Signal(FlowOperation::Continue)),
|
||||||
|
Completion::Return(v) => return Err(OwaError::Signal(FlowOperation::Return(v))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up `name` by walking the scope parent chain without dot-path splitting.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if any scope ID in the chain is invalid.
|
||||||
|
/// Returns [`OwaError::UnboundVariable`] if `name` is not found in any scope.
|
||||||
|
pub fn lookup_expand(&self, scope: ScopeId, name: &str) -> Result<&OwaAst, OwaError> {
|
||||||
|
let scope = self.get_scope(scope)?;
|
||||||
|
if let Some(v) = scope.expand.get(name) {
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(OwaError::UnboundVariable(name.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines a new macro argument `name` in the given scope.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if scope ID is invalid.
|
||||||
|
pub fn set_expand(
|
||||||
|
&mut self,
|
||||||
|
scope: ScopeId,
|
||||||
|
name: &str,
|
||||||
|
value: OwaAst,
|
||||||
|
) -> Result<(), OwaError> {
|
||||||
|
let scope = self.get_scope_mut(scope)?;
|
||||||
|
scope.expand.insert(name.to_string(), value);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up `name` by walking the scope parent chain without dot-path splitting.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if any scope ID in the chain is invalid.
|
||||||
|
/// Returns [`OwaError::UnboundVariable`] if `name` is not found in any scope.
|
||||||
|
pub fn lookup_runtime_direct(&self, scope: ScopeId, name: &str) -> Result<&OwaValue, OwaError> {
|
||||||
|
let mut scope_id = Some(scope);
|
||||||
|
|
||||||
|
while let Some(s) = scope_id {
|
||||||
|
let scope = self.get_scope(s)?;
|
||||||
|
if let Some(v) = scope.runtime.get(name) {
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
scope_id = scope.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(OwaError::UnboundVariable(name.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up a (potentially dot-separated) `name` in the scope chain.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::UnboundVariable`] if the name (or any path segment) is not found.
|
||||||
|
/// Returns [`OwaError::InvalidMemberAccess`] if a non-map value is accessed as a map.
|
||||||
|
/// Returns [`OwaError::Panic`] if a scope ID is invalid.
|
||||||
|
pub fn lookup_runtime(&self, scope_id: ScopeId, name: &str) -> Result<&OwaValue, OwaError> {
|
||||||
|
// fast path
|
||||||
|
if !name.contains('.') {
|
||||||
|
return self.lookup_runtime_direct(scope_id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parts = name.splitn(2, '.');
|
||||||
|
let first_part = parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| OwaError::UnboundVariable(name.to_string()))?;
|
||||||
|
let rest = parts.next();
|
||||||
|
|
||||||
|
let mut current = self.lookup_runtime_direct(scope_id, first_part)?;
|
||||||
|
|
||||||
|
if let Some(rest) = rest {
|
||||||
|
for part in rest.split('.') {
|
||||||
|
let OwaValue::Map(map) = current else {
|
||||||
|
return Err(OwaError::InvalidMemberAccess(
|
||||||
|
name.to_string(),
|
||||||
|
current.type_name(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let key = OwaValue::kw(part);
|
||||||
|
current = map
|
||||||
|
.get(&key)
|
||||||
|
.ok_or_else(|| OwaError::UnboundVariable(name.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines a new variable `name` in the given scope.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if `name` is already defined in that scope
|
||||||
|
/// or if the scope ID is invalid.
|
||||||
|
pub fn define_runtime(
|
||||||
|
&mut self,
|
||||||
|
scope: ScopeId,
|
||||||
|
name: &str,
|
||||||
|
value: OwaValue,
|
||||||
|
) -> Result<(), OwaError> {
|
||||||
|
let scope = self.get_scope_mut(scope)?;
|
||||||
|
if scope.runtime.contains_key(name) {
|
||||||
|
let msg = format!("Variable {name} is already defined in this scope");
|
||||||
|
Err(OwaError::Panic(msg))
|
||||||
|
} else {
|
||||||
|
scope.runtime.insert(name.to_string(), value);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the value of an existing variable `name`, searching from `scope`
|
||||||
|
/// upward through the parent chain.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::UnboundVariable`] if `name` is not found in any scope.
|
||||||
|
/// Returns [`OwaError::Panic`] if a scope ID is invalid.
|
||||||
|
pub fn set_runtime(
|
||||||
|
&mut self,
|
||||||
|
scope: ScopeId,
|
||||||
|
name: &str,
|
||||||
|
value: OwaValue,
|
||||||
|
) -> Result<(), OwaError> {
|
||||||
|
let target = self.search_runtime(scope, name)?;
|
||||||
|
let scope_mut = self.get_scope_mut(target)?;
|
||||||
|
if let Some(slot) = scope_mut.runtime.get_mut(name) {
|
||||||
|
*slot = value;
|
||||||
|
} else {
|
||||||
|
scope_mut.runtime.insert(name.to_string(), value);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the variable `name` from the scope where it is first found.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::UnboundVariable`] if `name` is not found in any scope.
|
||||||
|
/// Returns [`OwaError::Panic`] if a scope ID is invalid.
|
||||||
|
pub fn unset_runtime(&mut self, scope: ScopeId, name: &str) -> Result<(), OwaError> {
|
||||||
|
let target = self.search_runtime(scope, name)?;
|
||||||
|
let scope_mut = self.get_scope_mut(target)?;
|
||||||
|
scope_mut.runtime.remove(name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_runtime(&self, scope: ScopeId, name: &str) -> Result<usize, OwaError> {
|
||||||
|
let mut cur = Some(scope);
|
||||||
|
let mut target = None;
|
||||||
|
|
||||||
|
while let Some(s) = cur {
|
||||||
|
let scope_ref = self.get_scope(s)?;
|
||||||
|
if scope_ref.runtime.contains_key(name) {
|
||||||
|
target = Some(s);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cur = scope_ref.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.ok_or_else(|| OwaError::UnboundVariable(name.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_scope(&mut self, scope: Scope) -> Result<ScopeId, OwaError> {
|
||||||
|
if let Some(free_id) = self.free_list.pop() {
|
||||||
|
self.scopes[free_id] = Some(scope);
|
||||||
|
return Ok(free_id);
|
||||||
|
}
|
||||||
|
if let Some(max) = self.max_scopes_count
|
||||||
|
&& self.scopes.len() >= max
|
||||||
|
{
|
||||||
|
return Err(OwaError::Panic("scope limit exceeded".to_string()));
|
||||||
|
}
|
||||||
|
let scope_id = self.scopes.len();
|
||||||
|
self.scopes.push(Some(scope));
|
||||||
|
Ok(scope_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inherit_scope(&mut self, parent: ScopeId) -> Result<ScopeId, OwaError> {
|
||||||
|
self.get_scope_mut(parent)?.child_count += 1;
|
||||||
|
self.allocate_scope(Scope::new(Some(parent)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scope_token(&self, scope_id: ScopeId) -> Result<Arc<()>, OwaError> {
|
||||||
|
Ok(self.get_scope(scope_id)?.token.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_release_scope(&mut self, scope_id: ScopeId) {
|
||||||
|
let Some(scope) = self.scopes.get(scope_id).and_then(|s| s.as_ref()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if scope.child_count > 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if Arc::strong_count(&scope.token) > 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let parent = match scope.parent {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
self.scopes[scope_id] = None;
|
||||||
|
self.free_list.push(scope_id);
|
||||||
|
|
||||||
|
if let Some(parent_scope) = self.scopes[parent].as_mut() {
|
||||||
|
parent_scope.child_count = parent_scope.child_count.saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the parent scope ID of the given scope, if any.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if `scope_id` is invalid.
|
||||||
|
pub fn get_scope_parent(&self, scope_id: ScopeId) -> Result<Option<ScopeId>, OwaError> {
|
||||||
|
let scope = self.get_scope(scope_id)?;
|
||||||
|
Ok(scope.parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects all variables visible from `scope_id` (including parent scopes)
|
||||||
|
/// into a map value.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if any scope ID in the chain is invalid.
|
||||||
|
/// Returns [`OwaError::TypeError`] if a parent scope map cannot be read as a map.
|
||||||
|
pub fn get_scope_as_map(&self, scope_id: ScopeId) -> Result<OwaValue, OwaError> {
|
||||||
|
let mut chain = Vec::new();
|
||||||
|
let mut cur = Some(scope_id);
|
||||||
|
while let Some(id) = cur {
|
||||||
|
let scope = self.get_scope(id)?;
|
||||||
|
chain.push(id);
|
||||||
|
cur = scope.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for &id in chain.iter().rev() {
|
||||||
|
let scope = self.get_scope(id)?;
|
||||||
|
for (k, v) in &scope.runtime {
|
||||||
|
map.insert(OwaValue::kw(k), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.insert(OwaValue::kw(special_names::IS_SCOPE), OwaValue::int(1));
|
||||||
|
|
||||||
|
Ok(OwaValue::Map(RedBlackTreeMapSync::from_iter(map)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a clone of the scope with the given ID.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::Panic`] if `scope_id` does not correspond to a live scope.
|
||||||
|
pub fn get_scope_cloned(&self, scope_id: ScopeId) -> Result<Scope, OwaError> {
|
||||||
|
let scope = self.scopes.get(scope_id).and_then(|inner| inner.as_ref());
|
||||||
|
scope
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| OwaError::Panic(format!("Attempt to access invalid scope {scope_id}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_scope(&self, scope_id: ScopeId) -> Result<&Scope, OwaError> {
|
||||||
|
let scope = self.scopes.get(scope_id).and_then(|inner| inner.as_ref());
|
||||||
|
scope.ok_or_else(|| OwaError::Panic(format!("Attempt to access invalid scope {scope_id}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_scope_mut(&mut self, scope_id: ScopeId) -> Result<&mut Scope, OwaError> {
|
||||||
|
let scope = self.scopes.get_mut(scope_id).and_then(|x| x.as_mut());
|
||||||
|
scope.ok_or_else(|| OwaError::Panic(format!("Attempt to access invalid scope {scope_id}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_owa_path(base: &std::path::Path) -> Option<String> {
|
||||||
|
[
|
||||||
|
base.to_path_buf(),
|
||||||
|
base.with_extension("owa"),
|
||||||
|
base.join("main.owa"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| p.is_file())
|
||||||
|
.and_then(|v| v.to_str().map(std::string::ToString::to_string))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_special_symbol(s: &str) -> bool {
|
||||||
|
s.starts_with("__") && s.ends_with("__")
|
||||||
|
}
|
||||||
12
src/core/mod.rs
Normal file
12
src/core/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
mod callable;
|
||||||
|
mod error;
|
||||||
|
mod interpreter;
|
||||||
|
mod scope;
|
||||||
|
pub mod special_names;
|
||||||
|
mod value;
|
||||||
|
|
||||||
|
pub use callable::*;
|
||||||
|
pub use error::*;
|
||||||
|
pub use interpreter::*;
|
||||||
|
pub use scope::*;
|
||||||
|
pub use value::*;
|
||||||
77
src/core/scope.rs
Normal file
77
src/core/scope.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{core::OwaValue, parser::ast::OwaAst};
|
||||||
|
|
||||||
|
pub type ScopeId = usize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Scope {
|
||||||
|
pub parent: Option<ScopeId>,
|
||||||
|
pub runtime: HashMap<String, OwaValue>,
|
||||||
|
pub expand: HashMap<String, OwaAst>,
|
||||||
|
pub child_count: usize,
|
||||||
|
pub token: Arc<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Scope {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
parent: None,
|
||||||
|
runtime: HashMap::new(),
|
||||||
|
expand: HashMap::new(),
|
||||||
|
child_count: 0,
|
||||||
|
token: Arc::new(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Scope {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.parent == other.parent && self.runtime == other.runtime && self.expand == other.expand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Scope {}
|
||||||
|
|
||||||
|
impl Scope {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(parent: Option<ScopeId>) -> Self {
|
||||||
|
Self {
|
||||||
|
parent,
|
||||||
|
runtime: HashMap::new(),
|
||||||
|
expand: HashMap::new(),
|
||||||
|
child_count: 0,
|
||||||
|
token: Arc::new(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_with_runtime<K, V, I>(runtime: I) -> Self
|
||||||
|
where
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<OwaValue>,
|
||||||
|
I: IntoIterator<Item = (K, V)>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
parent: None,
|
||||||
|
runtime: runtime
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k.into(), v.into()))
|
||||||
|
.collect(),
|
||||||
|
expand: HashMap::new(),
|
||||||
|
child_count: 0,
|
||||||
|
token: Arc::new(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn local_runtime_to_map(&self) -> OwaValue {
|
||||||
|
OwaValue::Map(
|
||||||
|
self.runtime
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (OwaValue::kw(k), v.clone()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/core/special_names.rs
Normal file
23
src/core/special_names.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/// Self-reference binding available inside every lambda/macro call body.
|
||||||
|
pub const THIS_CALLABLE: &str = "this";
|
||||||
|
|
||||||
|
/// Binding containing all arguments passed to the current callable.
|
||||||
|
pub const ALL_ARGS: &str = "%%";
|
||||||
|
|
||||||
|
/// Binding containing arguments beyond the declared named parameters.
|
||||||
|
pub const REST_ARGS: &str = "%&";
|
||||||
|
|
||||||
|
/// Runtime variable holding the resolved directory of the currently executing file.
|
||||||
|
pub const DIR: &str = "__dir__";
|
||||||
|
|
||||||
|
/// Special symbol that evaluates to the current scope as a map value.
|
||||||
|
pub const SCOPE: &str = "__scope__";
|
||||||
|
|
||||||
|
/// Map key inserted into every scope-map to identify it as a scope object.
|
||||||
|
pub const IS_SCOPE: &str = "__is_scope__";
|
||||||
|
|
||||||
|
/// Map key that makes a map value callable (callable-object protocol).
|
||||||
|
pub const CALL: &str = "__call__";
|
||||||
|
|
||||||
|
/// Keyword representing the null / absent value.
|
||||||
|
pub const KW_NULL: &str = "null";
|
||||||
347
src/core/value.rs
Normal file
347
src/core/value.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
use num_traits::ToPrimitive;
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use rpds::{RedBlackTreeMapSync, RedBlackTreeSetSync, VectorSync};
|
||||||
|
use std::{hash::Hash, sync::Arc};
|
||||||
|
|
||||||
|
use crate::core::{
|
||||||
|
OwaCallable, OwaCallableCell, OwaError, OwaNativeFunction, OwaNativeMacro, ScopeId,
|
||||||
|
special_names,
|
||||||
|
};
|
||||||
|
use crate::parser::ast::OwaAst;
|
||||||
|
|
||||||
|
/// Formats an iterator of items-with-`unparse()` into a space-separated string.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! vec_unparse {
|
||||||
|
($iter:expr) => {{
|
||||||
|
$iter
|
||||||
|
.map(|v| v.unparse())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ")
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates constructor and accessor methods shared by `OwaValue` and `OwaAst`.
|
||||||
|
///
|
||||||
|
/// The `kw()` constructor strips all characters listed in the parser's
|
||||||
|
/// `INDENT_IGNORE` set so that keyword identifiers are always clean.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! owa_base_impl {
|
||||||
|
() => {
|
||||||
|
pub fn kw<S: AsRef<str>>(s: S) -> Self {
|
||||||
|
Self::Keyword(Arc::from(s.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner string if this value is a `Keyword`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this is not a `Keyword` variant.
|
||||||
|
pub fn as_kw(&self) -> Result<Arc<str>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Keyword(s) => Ok(s.clone()),
|
||||||
|
_ => Err(OwaError::TypeError(
|
||||||
|
"keyword".into(),
|
||||||
|
self.type_name().into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn str<S: AsRef<str>>(s: S) -> Self {
|
||||||
|
Self::Str(Arc::from(s.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner string if this value is a `Str`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this is not a `Str` variant.
|
||||||
|
pub fn as_str(&self) -> Result<Arc<str>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Str(s) => Ok(s.clone()),
|
||||||
|
_ => Err(OwaError::TypeError("str".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_sym(&self) -> Result<String, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Str(s) => Ok(s.as_ref().into()),
|
||||||
|
Self::Keyword(s) => Ok(s.as_ref().into()),
|
||||||
|
_ => Err(OwaError::TypeError(
|
||||||
|
"str/keyword".into(),
|
||||||
|
self.type_name().into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn int<T: ToPrimitive>(i: T) -> Self {
|
||||||
|
Self::Int(i.to_i64().unwrap_or(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner integer if this value is an `Int`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this is not an `Int` variant.
|
||||||
|
pub fn as_int(&self) -> Result<i64, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Int(i) => Ok(*i),
|
||||||
|
_ => Err(OwaError::TypeError("int".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn float(f: f64) -> Self {
|
||||||
|
Self::Float(OrderedFloat(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner float if this value is a `Float`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this is not a `Float` variant.
|
||||||
|
pub fn as_float(&self) -> Result<OrderedFloat<f64>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Float(f) => Ok(*f),
|
||||||
|
_ => Err(OwaError::TypeError("float".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map<C>(coll: C) -> Self
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = Self>,
|
||||||
|
{
|
||||||
|
let mut map = RedBlackTreeMapSync::default();
|
||||||
|
let mut iter = coll.into_iter();
|
||||||
|
|
||||||
|
while let (Some(k), Some(v)) = (iter.next(), iter.next()) {
|
||||||
|
map = map.insert(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Map(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the inner map if this value is a `Map`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this is not a `Map` variant.
|
||||||
|
pub fn as_map(&self) -> Result<&RedBlackTreeMapSync<Self, Self>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Map(m) => Ok(m),
|
||||||
|
_ => Err(OwaError::TypeError("map".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn vec<C>(coll: C) -> Self
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = Self>,
|
||||||
|
{
|
||||||
|
Self::Vec(VectorSync::from_iter(coll.into_iter()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_vec(self) -> Self {
|
||||||
|
Self::Vec(VectorSync::from_iter(vec![self]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the inner vector if this value is a `Vec`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this is not a `Vec` variant.
|
||||||
|
pub fn as_vec(&self) -> Result<&VectorSync<Self>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Vec(v) => Ok(v),
|
||||||
|
_ => Err(OwaError::TypeError("vec".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the inner set if this value is a `Set`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this is not a `Set` variant.
|
||||||
|
pub fn as_set(&self) -> Result<&RedBlackTreeSetSync<Self>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Set(s) => Ok(s),
|
||||||
|
_ => Err(OwaError::TypeError("set".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn set<C>(coll: C) -> Self
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = Self>,
|
||||||
|
{
|
||||||
|
Self::Set(RedBlackTreeSetSync::from_iter(coll.into_iter()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_set(self) -> Self {
|
||||||
|
Self::Set(RedBlackTreeSetSync::from_iter(vec![self]))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum OwaValue {
|
||||||
|
Keyword(Arc<str>), // :ident
|
||||||
|
Str(Arc<str>), // "..."
|
||||||
|
Float(OrderedFloat<f64>), // 1.0
|
||||||
|
Int(i64), // 1
|
||||||
|
Vec(VectorSync<Self>), // [...]
|
||||||
|
Set(RedBlackTreeSetSync<Self>), // {...}
|
||||||
|
Map(RedBlackTreeMapSync<Self, Self>), // #{...}
|
||||||
|
Quote(Arc<OwaAst>), // '...
|
||||||
|
Callable(OwaCallable),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwaValue {
|
||||||
|
owa_base_impl!();
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_macro(
|
||||||
|
body: OwaAst,
|
||||||
|
params: VectorSync<Arc<str>>,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
scope_token: Arc<()>,
|
||||||
|
) -> Self {
|
||||||
|
Self::Callable(OwaCallable {
|
||||||
|
scope_id,
|
||||||
|
scope_token,
|
||||||
|
params,
|
||||||
|
body: OwaCallableCell::Macro(Arc::new(body)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_lambda(
|
||||||
|
body: OwaAst,
|
||||||
|
params: VectorSync<Arc<str>>,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
scope_token: Arc<()>,
|
||||||
|
) -> Self {
|
||||||
|
Self::Callable(OwaCallable {
|
||||||
|
scope_id,
|
||||||
|
scope_token,
|
||||||
|
params,
|
||||||
|
body: OwaCallableCell::Lambda(Arc::new(body)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the callable wrapped by this value, following `__call__` map keys.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if the value (or the resolved `__call__`
|
||||||
|
/// chain) is not a `Callable` variant.
|
||||||
|
pub fn as_callable(&self) -> Result<&OwaCallable, OwaError> {
|
||||||
|
let mut calle = self;
|
||||||
|
|
||||||
|
// magic __call__
|
||||||
|
while let Self::Map(items) = calle {
|
||||||
|
let item = items.get(&Self::kw(special_names::CALL));
|
||||||
|
match item {
|
||||||
|
Some(v) => calle = v,
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match calle {
|
||||||
|
Self::Callable(lambda) => Ok(lambda),
|
||||||
|
_ => Err(OwaError::TypeError(
|
||||||
|
"lambda or macro".into(),
|
||||||
|
calle.type_name().into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn native_function(function: OwaNativeFunction) -> Self {
|
||||||
|
Self::native(OwaCallableCell::NativeLambda(function))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn native_macro(r#macro: OwaNativeMacro) -> Self {
|
||||||
|
Self::native(OwaCallableCell::NativeMacro(r#macro))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn native(body: OwaCallableCell) -> Self {
|
||||||
|
Self::Callable(OwaCallable {
|
||||||
|
scope_id: 0,
|
||||||
|
scope_token: Arc::new(()),
|
||||||
|
params: VectorSync::from_iter(vec![]),
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn quote(value: Arc<OwaAst>) -> Self {
|
||||||
|
Self::Quote(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner AST node if this value is a `Quote`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this is not a `Quote` variant.
|
||||||
|
pub fn as_quote(&self) -> Result<Arc<OwaAst>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Quote(q) => Ok(q.clone()),
|
||||||
|
_ => Err(OwaError::TypeError("quote".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[allow(clippy::missing_const_for_fn)]
|
||||||
|
pub fn type_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Keyword(_) => "keyword",
|
||||||
|
Self::Str(_) => "str",
|
||||||
|
Self::Float(_) => "float",
|
||||||
|
Self::Int(_) => "int",
|
||||||
|
Self::Vec(_) => "vec",
|
||||||
|
Self::Set(_) => "set",
|
||||||
|
Self::Map(_) => "map",
|
||||||
|
Self::Quote(_) => "quote",
|
||||||
|
Self::Callable(lambda) => lambda.type_name(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn unparse(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Keyword(s) => format!(":{s}"),
|
||||||
|
Self::Str(s) => format!("\"{s}\""),
|
||||||
|
Self::Float(f) => {
|
||||||
|
let s = format!("{f}");
|
||||||
|
if s.contains('.') { s } else { format!("{s}.0") }
|
||||||
|
}
|
||||||
|
Self::Int(i) => i.to_string(),
|
||||||
|
Self::Vec(v) => {
|
||||||
|
format!("[{}]", vec_unparse!(v.iter()))
|
||||||
|
}
|
||||||
|
Self::Set(v) => {
|
||||||
|
format!("#{{{}}}", vec_unparse!(v.iter()))
|
||||||
|
}
|
||||||
|
Self::Map(v) => format!(
|
||||||
|
"{{{}}}",
|
||||||
|
vec_unparse!(v.iter().flat_map(|(k, v)| vec![k, v]))
|
||||||
|
),
|
||||||
|
Self::Quote(v) => format!("'{}", v.unparse()),
|
||||||
|
Self::Callable(v) => v.unparse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for OwaValue {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.unparse())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for OwaValue {
|
||||||
|
fn from(val: &str) -> Self {
|
||||||
|
Self::Str(Arc::from(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/lib.rs
Normal file
18
src/lib.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#![warn(
|
||||||
|
clippy::all,
|
||||||
|
clippy::pedantic,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::cargo,
|
||||||
|
clippy::perf
|
||||||
|
)]
|
||||||
|
#![allow(
|
||||||
|
clippy::multiple_crate_versions, // idk how to fix this
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_sign_loss,
|
||||||
|
clippy::needless_continue
|
||||||
|
)]
|
||||||
|
#![cfg_attr(test, allow(clippy::needless_pass_by_value))]
|
||||||
|
|
||||||
|
pub mod builtins;
|
||||||
|
pub mod core;
|
||||||
|
pub mod parser;
|
||||||
51
src/main.rs
Normal file
51
src/main.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use owa_rs::{
|
||||||
|
builtins,
|
||||||
|
core::{Interpreter, OwaValue, Scope, special_names},
|
||||||
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// flags
|
||||||
|
let argv = std::env::args().collect::<Vec<String>>();
|
||||||
|
let argv = argv.iter().map(|s| s.as_str()).collect::<Vec<_>>();
|
||||||
|
let no_owu = argv.contains(&"--no-owu");
|
||||||
|
|
||||||
|
// builtins
|
||||||
|
let builtins = [("builtins", builtins::scope().local_runtime_to_map())];
|
||||||
|
|
||||||
|
// init root scope
|
||||||
|
let mut interpreter = Interpreter::new(None);
|
||||||
|
let scope = Scope::new_with_runtime(builtins);
|
||||||
|
let scope_id = interpreter.allocate_scope(scope).unwrap();
|
||||||
|
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||||
|
interpreter
|
||||||
|
.define_runtime(
|
||||||
|
scope_id,
|
||||||
|
special_names::DIR,
|
||||||
|
OwaValue::str(cwd.to_str().unwrap_or(".")),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// eval target
|
||||||
|
let target_path = if no_owu {
|
||||||
|
let target = argv.iter().find(|s| s.ends_with(".owa"));
|
||||||
|
if let Some(target) = target {
|
||||||
|
target
|
||||||
|
} else {
|
||||||
|
error!("No owa file specified");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"modules/owu/main.owa"
|
||||||
|
};
|
||||||
|
interpreter
|
||||||
|
.run(target_path, scope_id)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
error!("Failed to load owu: {}", err.message());
|
||||||
|
std::process::exit(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
37
src/parser/annotation.rs
Normal file
37
src/parser/annotation.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use nom::{
|
||||||
|
IResult, Parser as _, bytes::complete::tag, character::multispace0, combinator::cut,
|
||||||
|
sequence::delimited,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, parse_value, ws};
|
||||||
|
|
||||||
|
pub(super) fn parse_annotation(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
delimited(
|
||||||
|
ws(tag("<")),
|
||||||
|
cut(parse_value),
|
||||||
|
cut((multispace0(), tag(">"))),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_annotation() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_annotation("<(:a :b :c)>"),
|
||||||
|
Ok((
|
||||||
|
"",
|
||||||
|
OwaAst::call([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c")])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_annotation("<(:a [:b :c] [(:d)])>garbage"),
|
||||||
|
Ok((
|
||||||
|
"garbage",
|
||||||
|
OwaAst::call([
|
||||||
|
OwaAst::kw("a"),
|
||||||
|
OwaAst::vec([OwaAst::kw("b"), OwaAst::kw("c"),]),
|
||||||
|
OwaAst::vec([OwaAst::call([OwaAst::kw("d")]),]),
|
||||||
|
])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
353
src/parser/ast.rs
Normal file
353
src/parser/ast.rs
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
use super::to_indent;
|
||||||
|
use crate::{
|
||||||
|
core::{Interpreter, OwaError, OwaValue, special_names},
|
||||||
|
owa_base_impl, vec_unparse,
|
||||||
|
};
|
||||||
|
use num_traits::ToPrimitive;
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use rpds::{RedBlackTreeMapSync, RedBlackTreeSetSync, VectorSync};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[warn(unpredictable_function_pointer_comparisons)]
|
||||||
|
pub struct OwaAnnotation {
|
||||||
|
pub annotation: Arc<OwaAst>,
|
||||||
|
pub value: Arc<OwaAst>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[warn(unpredictable_function_pointer_comparisons)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum OwaAst {
|
||||||
|
Annotation(OwaAnnotation), // <...>...
|
||||||
|
Call(VectorSync<Self>), // (...)
|
||||||
|
Float(OrderedFloat<f64>), // 1.0
|
||||||
|
Int(i64), // 1
|
||||||
|
Keyword(Arc<str>), // :ident
|
||||||
|
Lambda(VectorSync<Self>), // #(...)
|
||||||
|
Map(RedBlackTreeMapSync<Self, Self>), // #{...}
|
||||||
|
Quote(Arc<Self>), // '...
|
||||||
|
Set(RedBlackTreeSetSync<Self>), // {...}
|
||||||
|
Str(Arc<str>), // "..."
|
||||||
|
Symbol(Arc<str>), // ident
|
||||||
|
Unquote(Arc<Self>), // $...
|
||||||
|
Vec(VectorSync<Self>), // [...]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwaAst {
|
||||||
|
owa_base_impl!();
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn call<C>(coll: C) -> Self
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = Self>,
|
||||||
|
{
|
||||||
|
Self::Call(VectorSync::from_iter(coll))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_call(self) -> Self {
|
||||||
|
Self::Call(VectorSync::from_iter([self]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the inner vector if this node is a [`OwaAst::Call`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this node is not the `Call` variant.
|
||||||
|
pub fn as_call(&self) -> Result<&VectorSync<Self>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Call(v) => Ok(v),
|
||||||
|
_ => Err(OwaError::TypeError("call".into(), self.type_name().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_lambda<B>(body: B) -> Self
|
||||||
|
where
|
||||||
|
B: IntoIterator<Item = Self>,
|
||||||
|
{
|
||||||
|
Self::Lambda(VectorSync::from_iter(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sym<S: AsRef<str>>(s: S) -> Self {
|
||||||
|
let s = to_indent(s);
|
||||||
|
Self::Symbol(Arc::from(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner symbol string if this node is a [`OwaAst::Symbol`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`OwaError::TypeError`] if this node is not the `Symbol` variant.
|
||||||
|
pub fn as_sym(&self) -> Result<Arc<str>, OwaError> {
|
||||||
|
match self {
|
||||||
|
Self::Symbol(s) => Ok(s.clone()),
|
||||||
|
_ => Err(OwaError::TypeError(
|
||||||
|
"symbol".into(),
|
||||||
|
self.type_name().into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_quote(v: Self) -> Self {
|
||||||
|
Self::Quote(Arc::from(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_quote(self) -> Self {
|
||||||
|
Self::Quote(Arc::from(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_unquote(v: Self) -> Self {
|
||||||
|
Self::Unquote(Arc::from(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_unquote(self) -> Self {
|
||||||
|
Self::Unquote(Arc::from(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_annotation(self, ann: Self) -> Self {
|
||||||
|
Self::Annotation(OwaAnnotation {
|
||||||
|
annotation: Arc::from(ann),
|
||||||
|
value: Arc::from(self),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn type_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Symbol(_) => "symbol",
|
||||||
|
Self::Keyword(_) => "keyword",
|
||||||
|
Self::Str(_) => "str",
|
||||||
|
Self::Float(_) => "float",
|
||||||
|
Self::Int(_) => "int",
|
||||||
|
Self::Vec(_) => "vec",
|
||||||
|
Self::Call(_) => "call",
|
||||||
|
Self::Set(_) => "set",
|
||||||
|
Self::Map(_) => "map",
|
||||||
|
Self::Quote(_) => "quote",
|
||||||
|
Self::Unquote(_) => "unquote",
|
||||||
|
Self::Lambda(_) => "lambda",
|
||||||
|
Self::Annotation(v) => v.value.type_name(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn unparse(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Symbol(v) => v.to_string(),
|
||||||
|
Self::Keyword(v) => format!(":{v}"),
|
||||||
|
Self::Str(v) => format!("\"{v}\""),
|
||||||
|
Self::Float(v) => {
|
||||||
|
let s = format!("{v}");
|
||||||
|
if s.contains('.') { s } else { format!("{s}.0") }
|
||||||
|
}
|
||||||
|
Self::Int(v) => v.to_string(),
|
||||||
|
Self::Vec(v) => {
|
||||||
|
format!("[{}]", vec_unparse!(v.iter()))
|
||||||
|
}
|
||||||
|
Self::Call(v) => format!("({})", vec_unparse!(v.iter())),
|
||||||
|
Self::Set(v) => {
|
||||||
|
format!("#{{{}}}", vec_unparse!(v.iter()))
|
||||||
|
}
|
||||||
|
Self::Map(v) => format!("{{{}}}", vec_unparse!(v.iter().flat_map(|(k, v)| [k, v]))),
|
||||||
|
Self::Quote(v) => format!("'{}", v.unparse()),
|
||||||
|
Self::Unquote(v) => format!("${}", v.unparse()),
|
||||||
|
Self::Lambda(v) => format!("#({})", vec_unparse!(v.iter())),
|
||||||
|
Self::Annotation(ann) => {
|
||||||
|
format!("<{}>{}", ann.annotation.unparse(), ann.value.unparse())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_str(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Symbol(s) => s.to_string(),
|
||||||
|
Self::Str(s) => s.as_ref().to_string(),
|
||||||
|
_ => self.unparse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn unannotate(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Annotation(ann) => ann.value.unannotate(),
|
||||||
|
Self::Vec(v) => Self::Vec(v.iter().map(Self::unannotate).collect()),
|
||||||
|
Self::Call(v) => Self::Call(v.iter().map(Self::unannotate).collect()),
|
||||||
|
Self::Set(v) => Self::Set(v.iter().map(Self::unannotate).collect()),
|
||||||
|
Self::Map(v) => Self::map(
|
||||||
|
v.iter()
|
||||||
|
.flat_map(|(k, v)| [Self::unannotate(k), Self::unannotate(v)]),
|
||||||
|
),
|
||||||
|
Self::Lambda(v) => Self::Lambda(v.iter().map(Self::unannotate).collect()),
|
||||||
|
Self::Quote(v) => Self::Quote(Arc::from(v.unannotate())),
|
||||||
|
Self::Unquote(v) => Self::Unquote(Arc::from(v.unannotate())),
|
||||||
|
_ => self.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn pack(&self) -> OwaValue {
|
||||||
|
let type_name = OwaValue::kw(self.type_name());
|
||||||
|
let data = match self {
|
||||||
|
Self::Symbol(v) => OwaValue::str(v),
|
||||||
|
Self::Keyword(v) => OwaValue::kw(v),
|
||||||
|
Self::Str(v) => OwaValue::str(v),
|
||||||
|
Self::Float(v) => OwaValue::float(v.into_inner()),
|
||||||
|
Self::Int(v) => OwaValue::Int(*v),
|
||||||
|
Self::Vec(v) => OwaValue::vec(v.iter().map(Self::pack)),
|
||||||
|
Self::Call(v) => OwaValue::vec(v.iter().map(Self::pack)),
|
||||||
|
Self::Set(v) => OwaValue::set(v.iter().map(Self::pack)),
|
||||||
|
Self::Map(v) => OwaValue::map(v.iter().flat_map(|(k, v)| [k.pack(), v.pack()])),
|
||||||
|
Self::Quote(v) => OwaValue::quote(v.clone()),
|
||||||
|
Self::Unquote(v) => v.pack(),
|
||||||
|
Self::Lambda(v) => OwaValue::vec(v.iter().map(Self::pack)),
|
||||||
|
Self::Annotation(v) => OwaValue::map(vec![
|
||||||
|
OwaValue::kw("value"),
|
||||||
|
v.value.as_ref().pack(),
|
||||||
|
OwaValue::kw("annotation"),
|
||||||
|
v.annotation.as_ref().pack(),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
OwaValue::map(vec![
|
||||||
|
OwaValue::kw("type"),
|
||||||
|
type_name,
|
||||||
|
OwaValue::kw("data"),
|
||||||
|
data,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Expander<'a> {
|
||||||
|
interpreter: &'a mut Interpreter,
|
||||||
|
scope_id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Expander<'a> {
|
||||||
|
pub const fn new(interpreter: &'a mut Interpreter, scope_id: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
interpreter,
|
||||||
|
scope_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn expand(&self, ast: &OwaAst, in_unquote: bool, in_quote: bool) -> OwaAst {
|
||||||
|
self.expand_priv(ast, in_unquote, in_quote)
|
||||||
|
.pop()
|
||||||
|
.unwrap_or_else(|| ast.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn resolve(&self, sym: &str) -> Vec<OwaAst> {
|
||||||
|
let result = match sym {
|
||||||
|
special_names::ALL_ARGS | special_names::REST_ARGS => {
|
||||||
|
let val = self.interpreter.lookup_expand(self.scope_id, sym);
|
||||||
|
val.and_then(|v| v.as_vec())
|
||||||
|
.map(|v| v.iter().cloned().collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let val = self.interpreter.lookup_expand(self.scope_id, sym);
|
||||||
|
val.map(|v| vec![v.clone()])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result.unwrap_or_else(|_| vec![OwaAst::sym(sym)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn expand_priv(&self, ast: &OwaAst, in_unquote: bool, in_quote: bool) -> Vec<OwaAst> {
|
||||||
|
match ast {
|
||||||
|
OwaAst::Symbol(s) => {
|
||||||
|
if !in_unquote {
|
||||||
|
return vec![ast.clone()];
|
||||||
|
}
|
||||||
|
self.resolve(s.as_ref())
|
||||||
|
}
|
||||||
|
OwaAst::Vec(v) => {
|
||||||
|
let transformed = v.iter().map(|v| self.expand_priv(v, in_unquote, in_quote));
|
||||||
|
vec![OwaAst::vec(transformed.flatten())]
|
||||||
|
}
|
||||||
|
OwaAst::Call(v) => {
|
||||||
|
let transformed = v.iter().map(|v| self.expand_priv(v, in_unquote, in_quote));
|
||||||
|
vec![OwaAst::call(transformed.flatten())]
|
||||||
|
}
|
||||||
|
OwaAst::Set(v) => {
|
||||||
|
let transformed = v.iter().map(|v| self.expand_priv(v, in_unquote, in_quote));
|
||||||
|
vec![OwaAst::set(transformed.flatten())]
|
||||||
|
}
|
||||||
|
OwaAst::Map(v) => {
|
||||||
|
let transformed = v.iter().flat_map(|(k, v)| {
|
||||||
|
[
|
||||||
|
self.expand_priv(k, in_unquote, in_quote),
|
||||||
|
self.expand_priv(v, in_unquote, in_quote),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
vec![OwaAst::map(transformed.flatten())]
|
||||||
|
}
|
||||||
|
OwaAst::Lambda(v) => {
|
||||||
|
let transformed = v.iter().map(|v| self.expand_priv(v, in_unquote, in_quote));
|
||||||
|
vec![OwaAst::new_lambda(transformed.flatten())]
|
||||||
|
}
|
||||||
|
OwaAst::Quote(v) => vec![if in_quote {
|
||||||
|
ast.clone()
|
||||||
|
} else {
|
||||||
|
v.as_ref().clone()
|
||||||
|
}],
|
||||||
|
OwaAst::Unquote(v) => {
|
||||||
|
if in_unquote {
|
||||||
|
vec![ast.clone()]
|
||||||
|
} else {
|
||||||
|
self.expand_priv(v, true, in_quote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => vec![ast.clone()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for OwaAst {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.unparse())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_case::test_case;
|
||||||
|
|
||||||
|
#[test_case(OwaAst::sym("foo"), "foo")]
|
||||||
|
#[test_case(OwaAst::kw("foo"), ":foo")]
|
||||||
|
#[test_case(OwaAst::str("foo"), "\"foo\"")]
|
||||||
|
#[test_case(OwaAst::float(1.0), "1.0")]
|
||||||
|
#[test_case(OwaAst::float(1.2), "1.2")]
|
||||||
|
#[test_case(OwaAst::float(1.234), "1.234")]
|
||||||
|
#[test_case(
|
||||||
|
OwaAst::vec([OwaAst::int(1), OwaAst::str("foo"), OwaAst::kw("a")]),
|
||||||
|
"[1 \"foo\" :a]"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
OwaAst::call([OwaAst::int(1), OwaAst::float(1.0), OwaAst::kw("b"), OwaAst::str("foo")]),
|
||||||
|
"(1 1.0 :b \"foo\")"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
OwaAst::set([OwaAst::int(1), OwaAst::float(1.0), OwaAst::kw("b"), OwaAst::str("foo")]),
|
||||||
|
"#{1.0 1 :b \"foo\"}"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
OwaAst::map([
|
||||||
|
OwaAst::int(1), OwaAst::float(1.0),
|
||||||
|
OwaAst::kw("b"), OwaAst::str("foo"),
|
||||||
|
OwaAst::str("foo"), OwaAst::kw("b")
|
||||||
|
]),
|
||||||
|
"{1 1.0 :b \"foo\" \"foo\" :b}"
|
||||||
|
)]
|
||||||
|
fn unparse_tests(value: OwaAst, expected: &str) {
|
||||||
|
assert_eq!(value.unparse(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/parser/call.rs
Normal file
29
src/parser/call.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
use nom::{IResult, Parser as _, combinator::map};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, collection};
|
||||||
|
|
||||||
|
pub(super) fn parse_call(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(collection("(", ")"), OwaAst::call).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_call() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_call("(:a :b :c)"),
|
||||||
|
Ok((
|
||||||
|
"",
|
||||||
|
OwaAst::call([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c")])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_call("(:a [:b :c] [(:d)])garbage"),
|
||||||
|
Ok((
|
||||||
|
"garbage",
|
||||||
|
OwaAst::call([
|
||||||
|
OwaAst::kw("a"),
|
||||||
|
OwaAst::vec([OwaAst::kw("b"), OwaAst::kw("c"),]),
|
||||||
|
OwaAst::vec([OwaAst::call([OwaAst::kw("d")]),]),
|
||||||
|
])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/parser/keyword.rs
Normal file
14
src/parser/keyword.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use nom::{IResult, Parser as _, bytes::complete::tag, combinator::map, sequence::preceded};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, identifier};
|
||||||
|
|
||||||
|
pub(super) fn parse_keyword(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(preceded(tag(":"), identifier), OwaAst::kw).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keyword() {
|
||||||
|
assert_eq!(parse_keyword(":foo"), Ok(("", OwaAst::kw("foo"))));
|
||||||
|
assert_eq!(parse_keyword(":foo1232"), Ok(("", OwaAst::kw("foo1232"))));
|
||||||
|
assert_eq!(parse_keyword(":foo:s"), Ok((":s", OwaAst::kw("foo"))));
|
||||||
|
}
|
||||||
30
src/parser/lambda.rs
Normal file
30
src/parser/lambda.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
use nom::{IResult, Parser as _, combinator::map};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, collection};
|
||||||
|
|
||||||
|
pub(super) fn parse_lambda(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(collection("#(", ")"), OwaAst::new_lambda).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_call() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_lambda("#(:a :b :c)"),
|
||||||
|
Ok((
|
||||||
|
"",
|
||||||
|
OwaAst::new_lambda([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c")])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_lambda("#(:a [:b :c] [(:d)] #(:e))garbage"),
|
||||||
|
Ok((
|
||||||
|
"garbage",
|
||||||
|
OwaAst::new_lambda([
|
||||||
|
OwaAst::kw("a"),
|
||||||
|
OwaAst::vec([OwaAst::kw("b"), OwaAst::kw("c")]),
|
||||||
|
OwaAst::vec([OwaAst::call([OwaAst::kw("d")])]),
|
||||||
|
OwaAst::new_lambda([OwaAst::kw("e")])
|
||||||
|
])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/parser/map.rs
Normal file
39
src/parser/map.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
use nom::{IResult, Parser as _, combinator::map};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, collection};
|
||||||
|
|
||||||
|
pub(super) fn parse_map(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(collection("{", "}"), OwaAst::map).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_call() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_map("{:a :b :c}"),
|
||||||
|
Ok(("", OwaAst::map([OwaAst::kw("a"), OwaAst::kw("b")])))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_map("{:a :b :c :d}"),
|
||||||
|
Ok((
|
||||||
|
"",
|
||||||
|
OwaAst::map([
|
||||||
|
OwaAst::kw("a"),
|
||||||
|
OwaAst::kw("b"),
|
||||||
|
OwaAst::kw("c"),
|
||||||
|
OwaAst::kw("d")
|
||||||
|
])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_map("{:a #{:b :c} [(:d)] :e}garbage"),
|
||||||
|
Ok((
|
||||||
|
"garbage",
|
||||||
|
OwaAst::map([
|
||||||
|
OwaAst::kw("a"),
|
||||||
|
OwaAst::set([OwaAst::kw("b"), OwaAst::kw("c")]),
|
||||||
|
OwaAst::vec([OwaAst::call([OwaAst::kw("d"),]),]),
|
||||||
|
OwaAst::kw("e")
|
||||||
|
])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
163
src/parser/mod.rs
Normal file
163
src/parser/mod.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
use nom::{
|
||||||
|
IResult, Parser,
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::{is_not, tag},
|
||||||
|
character::complete::multispace1,
|
||||||
|
combinator::{cut, map, recognize},
|
||||||
|
error::Error,
|
||||||
|
multi::{many0, many1},
|
||||||
|
sequence::{delimited, pair, preceded},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod annotation;
|
||||||
|
pub mod ast;
|
||||||
|
mod call;
|
||||||
|
mod keyword;
|
||||||
|
mod lambda;
|
||||||
|
mod map;
|
||||||
|
mod number;
|
||||||
|
mod quote;
|
||||||
|
mod set;
|
||||||
|
mod string;
|
||||||
|
mod symbol;
|
||||||
|
mod unquote;
|
||||||
|
mod vector;
|
||||||
|
|
||||||
|
use annotation::parse_annotation;
|
||||||
|
use ast::OwaAst;
|
||||||
|
use call::parse_call;
|
||||||
|
use keyword::parse_keyword;
|
||||||
|
use map::parse_map;
|
||||||
|
use number::{parse_float, parse_integer};
|
||||||
|
use quote::parse_quote;
|
||||||
|
use set::parse_set;
|
||||||
|
use string::parse_string;
|
||||||
|
use symbol::parse_symbol;
|
||||||
|
use unquote::parse_unquote;
|
||||||
|
use vector::parse_vector;
|
||||||
|
|
||||||
|
use crate::parser::lambda::parse_lambda;
|
||||||
|
|
||||||
|
pub(crate) const INDENT_IGNORE: &str = " \t\n\r,;:{}[]()<>$#\"'";
|
||||||
|
|
||||||
|
pub(crate) fn to_indent<S: AsRef<str>>(s: S) -> String {
|
||||||
|
s.as_ref()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| !INDENT_IGNORE.contains(*c))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn identifier(input: &str) -> IResult<&str, &str> {
|
||||||
|
recognize(many1(is_not(INDENT_IGNORE))).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_comment(input: &str) -> IResult<&str, ()> {
|
||||||
|
map(preceded(tag(";;"), is_not("\n")), |_| ()).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_ws(input: &str) -> IResult<&str, ()> {
|
||||||
|
map(
|
||||||
|
many0(alt((map(multispace1, |_| ()), parse_comment))),
|
||||||
|
|_| (),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collection<'a>(
|
||||||
|
start: &str,
|
||||||
|
end: &str,
|
||||||
|
) -> impl Parser<&'a str, Output = Vec<OwaAst>, Error = Error<&'a str>> {
|
||||||
|
delimited(
|
||||||
|
ws(tag(start)),
|
||||||
|
cut(many0(parse_value)),
|
||||||
|
cut((skip_ws, tag(end))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ws<'a, O, F>(inner: F) -> impl Parser<&'a str, Output = O, Error = Error<&'a str>>
|
||||||
|
where
|
||||||
|
F: Parser<&'a str, Output = O, Error = Error<&'a str>>,
|
||||||
|
{
|
||||||
|
delimited(skip_ws, inner, skip_ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_atom(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
preceded(
|
||||||
|
skip_ws,
|
||||||
|
alt((
|
||||||
|
parse_float, // must be before parse_integer
|
||||||
|
parse_integer,
|
||||||
|
parse_string,
|
||||||
|
parse_keyword, // must be before parse_symbol
|
||||||
|
parse_symbol,
|
||||||
|
parse_lambda, // must be before parse_call
|
||||||
|
parse_call,
|
||||||
|
parse_set, // must be before parse_map
|
||||||
|
parse_map,
|
||||||
|
parse_vector,
|
||||||
|
parse_unquote,
|
||||||
|
parse_quote,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a single OWA value (with optional leading annotations) from `input`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns a [`nom::Err`] if the input does not contain a valid OWA expression.
|
||||||
|
pub fn parse_value(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(
|
||||||
|
pair(many0(parse_annotation), parse_atom),
|
||||||
|
|(annotations, value)| {
|
||||||
|
let mut result = value;
|
||||||
|
for annotation in annotations {
|
||||||
|
result = result.to_annotation(annotation);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a file from disk and parses its contents as a single OWA value.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error string if the file cannot be read or if its contents
|
||||||
|
/// cannot be parsed as a valid OWA expression.
|
||||||
|
pub fn parse_file(raw_filename: &str) -> Result<OwaAst, String> {
|
||||||
|
let content = std::fs::read_to_string(raw_filename)
|
||||||
|
.map_err(|err| format!("Failed to read file {raw_filename}: {err}"))?;
|
||||||
|
|
||||||
|
match parse_value(&content) {
|
||||||
|
Ok((_, value)) => Ok(value),
|
||||||
|
Err(err) => Err(format!("Failed to parse file {raw_filename}: {err:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_case::test_case;
|
||||||
|
|
||||||
|
#[test_case("(1 2 3)", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
|
||||||
|
#[test_case(r#"("hi" 1 "bye")"#, OwaAst::call([OwaAst::str("hi"), OwaAst::int(1), OwaAst::str("bye")]))]
|
||||||
|
#[test_case("[1 2 3]", OwaAst::vec([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
|
||||||
|
#[test_case(r#"["hello world!" 1 "bye"]"#, OwaAst::vec([OwaAst::str("hello world!"), OwaAst::int(1), OwaAst::str("bye")]))]
|
||||||
|
#[test_case(";; comment\n(1 2 3)", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
|
||||||
|
#[test_case("(1 ;; comment\n 2 3)", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
|
||||||
|
#[test_case("(1 2 ;; comment\n 3)", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
|
||||||
|
#[test_case("(1 2 3) ;; trailing comment", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
|
||||||
|
fn parse_tests(input: &str, expected: OwaAst) {
|
||||||
|
assert_eq!(parse_value(input).unwrap().1, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_comment_test() {
|
||||||
|
let input = ";; this is a comment\n(1 2 3)";
|
||||||
|
let expected = OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]);
|
||||||
|
assert_eq!(parse_value(input).unwrap().1, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
170
src/parser/number.rs
Normal file
170
src/parser/number.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
use nom::{
|
||||||
|
IResult, Parser as _,
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::tag,
|
||||||
|
character::complete::{char, one_of},
|
||||||
|
combinator::{map_res, opt, recognize},
|
||||||
|
multi::{many0, many1},
|
||||||
|
sequence::{preceded, terminated},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::parser::OwaAst;
|
||||||
|
|
||||||
|
fn hexadecimal(input: &str) -> IResult<&str, isize> {
|
||||||
|
map_res(
|
||||||
|
preceded(
|
||||||
|
alt((tag("0x"), tag("0X"))),
|
||||||
|
recognize(many1(terminated(
|
||||||
|
one_of("0123456789abcdefABCDEF"),
|
||||||
|
many0(char('_')),
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
|out: &str| isize::from_str_radix(&str::replace(out, "_", ""), 16),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn octal(input: &str) -> IResult<&str, isize> {
|
||||||
|
map_res(
|
||||||
|
preceded(
|
||||||
|
alt((tag("0o"), tag("0O"))),
|
||||||
|
recognize(many1(terminated(one_of("01234567"), many0(char('_'))))),
|
||||||
|
),
|
||||||
|
|out: &str| isize::from_str_radix(&str::replace(out, "_", ""), 8),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binary(input: &str) -> IResult<&str, isize> {
|
||||||
|
map_res(
|
||||||
|
preceded(
|
||||||
|
alt((tag("0b"), tag("0B"))),
|
||||||
|
recognize(many1(terminated(one_of("01"), many0(char('_'))))),
|
||||||
|
),
|
||||||
|
|out: &str| isize::from_str_radix(&str::replace(out, "_", ""), 2),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decimal(input: &str) -> IResult<&str, isize> {
|
||||||
|
map_res(
|
||||||
|
recognize((
|
||||||
|
opt(one_of("+-")),
|
||||||
|
many1(terminated(one_of("0123456789"), many0(char('_')))),
|
||||||
|
)),
|
||||||
|
|out: &str| str::replace(out, "_", "").parse::<isize>(),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decimal_str(input: &str) -> IResult<&str, &str> {
|
||||||
|
recognize(many1(terminated(one_of("0123456789"), many0(char('_'))))).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_integer(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
let (input, value) = (alt((hexadecimal, octal, binary, decimal))).parse(input)?;
|
||||||
|
Ok((input, OwaAst::int(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_float(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
let result = map_res(
|
||||||
|
alt((
|
||||||
|
// Case one: .42
|
||||||
|
recognize((
|
||||||
|
opt(one_of("+-")),
|
||||||
|
char('.'),
|
||||||
|
decimal_str,
|
||||||
|
opt((one_of("eE"), opt(one_of("+-")), decimal_str)),
|
||||||
|
)), // Case two: 42e42 and 42.42e42
|
||||||
|
recognize((
|
||||||
|
opt(one_of("+-")),
|
||||||
|
decimal_str,
|
||||||
|
opt(preceded(char('.'), decimal_str)),
|
||||||
|
one_of("eE"),
|
||||||
|
opt(one_of("+-")),
|
||||||
|
decimal_str,
|
||||||
|
)), // Case three: 42. and 42.42
|
||||||
|
recognize((opt(one_of("+-")), decimal_str, char('.'), opt(decimal_str))),
|
||||||
|
)),
|
||||||
|
|out: &str| -> Result<OwaAst, std::num::ParseFloatError> {
|
||||||
|
let result = out.parse::<f64>()?;
|
||||||
|
Ok(OwaAst::float(result))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.parse(input)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_integer() {
|
||||||
|
// decimal
|
||||||
|
assert_eq!(parse_integer("12garbage"), Ok(("garbage", OwaAst::int(12))));
|
||||||
|
assert_eq!(parse_integer("12"), Ok(("", OwaAst::int(12))));
|
||||||
|
assert_eq!(parse_integer("-12garbage"), Ok(("garbage", OwaAst::int(-12))));
|
||||||
|
// assert_eq!(integer("-12"), Ok(("", OwaAst::int(-12))));
|
||||||
|
|
||||||
|
// hexadecimal
|
||||||
|
assert_eq!(parse_integer("0x12"), Ok(("", OwaAst::int(0x12))));
|
||||||
|
assert_eq!(
|
||||||
|
parse_integer("0x12garbage"),
|
||||||
|
Ok(("garbage", OwaAst::int(0x12)))
|
||||||
|
);
|
||||||
|
// assert_eq!(integer("-0x12"), Ok(("", OwaAst::int(-0x12))));
|
||||||
|
// assert_eq!(
|
||||||
|
// integer("-0x12garbage"),
|
||||||
|
// Ok(("garbage", OwaAst::int(-0x12)))
|
||||||
|
// );
|
||||||
|
|
||||||
|
// octal
|
||||||
|
assert_eq!(parse_integer("0o12"), Ok(("", OwaAst::int(0o12))));
|
||||||
|
assert_eq!(
|
||||||
|
parse_integer("0o12garbage"),
|
||||||
|
Ok(("garbage", OwaAst::int(0o12)))
|
||||||
|
);
|
||||||
|
// assert_eq!(integer("-0o12"), Ok(("", OwaAst::int(-0o12))));
|
||||||
|
// assert_eq!(
|
||||||
|
// integer("-0o12garbage"),
|
||||||
|
// Ok(("garbage", OwaAst::int(-0o12)))
|
||||||
|
// );
|
||||||
|
|
||||||
|
// binary
|
||||||
|
assert_eq!(parse_integer("0b10"), Ok(("", OwaAst::int(0b10))));
|
||||||
|
assert_eq!(
|
||||||
|
parse_integer("0b10garbage"),
|
||||||
|
Ok(("garbage", OwaAst::int(0b10)))
|
||||||
|
);
|
||||||
|
// assert_eq!(integer("-0b10"), Ok(("", OwaAst::int(-0b10))));
|
||||||
|
// assert_eq!(
|
||||||
|
// integer("-0b10garbage"),
|
||||||
|
// Ok(("garbage", OwaAst::int(-0b10)))
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_float() {
|
||||||
|
// normal
|
||||||
|
assert_eq!(parse_float("12."), Ok(("", OwaAst::float(12.0))));
|
||||||
|
assert_eq!(parse_float("12.0"), Ok(("", OwaAst::float(12.0))));
|
||||||
|
assert_eq!(parse_float("-12.0"), Ok(("", OwaAst::float(-12.0))));
|
||||||
|
assert_eq!(parse_float("12.garbage"), Ok(("garbage", OwaAst::float(12.0))));
|
||||||
|
assert_eq!(
|
||||||
|
parse_float("12.0garbage"),
|
||||||
|
Ok(("garbage", OwaAst::float(12.0)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_float("-12.0garbage"),
|
||||||
|
Ok(("garbage", OwaAst::float(-12.0)))
|
||||||
|
);
|
||||||
|
|
||||||
|
// with exponent
|
||||||
|
assert_eq!(parse_float("12.0e12"), Ok(("", OwaAst::float(12.0e12))));
|
||||||
|
assert_eq!(
|
||||||
|
parse_float("12.0e12garbage"),
|
||||||
|
Ok(("garbage", OwaAst::float(12.0e12)))
|
||||||
|
);
|
||||||
|
assert_eq!(parse_float("-12.0e12"), Ok(("", OwaAst::float(-12.0e12))));
|
||||||
|
assert_eq!(
|
||||||
|
parse_float("-12.0e12garbage"),
|
||||||
|
Ok(("garbage", OwaAst::float(-12.0e12)))
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/parser/quote.rs
Normal file
24
src/parser/quote.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
use nom::{IResult, Parser as _, bytes::complete::tag, combinator::map, sequence::preceded};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, parse_value};
|
||||||
|
|
||||||
|
pub fn parse_quote(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(preceded(tag("'"), parse_value), OwaAst::new_quote).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keyword() {
|
||||||
|
assert_eq!(parse_quote("'foo"), Ok(("", OwaAst::sym("foo").to_quote())));
|
||||||
|
assert_eq!(
|
||||||
|
parse_quote("':foo1232"),
|
||||||
|
Ok(("", OwaAst::kw("foo1232").to_quote()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_quote("'\"foo\":s"),
|
||||||
|
Ok((":s", OwaAst::str("foo").to_quote()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_quote("''[\"foo\"]:s"),
|
||||||
|
Ok((":s", OwaAst::str("foo").to_vec().to_quote().to_quote()))
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/parser/set.rs
Normal file
29
src/parser/set.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
use nom::{IResult, Parser as _, combinator::map};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, collection};
|
||||||
|
|
||||||
|
pub fn parse_set(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(collection("#{", "}"), OwaAst::set).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_set("#{:a :b :c}"),
|
||||||
|
Ok((
|
||||||
|
"",
|
||||||
|
OwaAst::set([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c")])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_set("#{:a [:b #{\"c\"}] [(:d)]}garbage"),
|
||||||
|
Ok((
|
||||||
|
"garbage",
|
||||||
|
OwaAst::set([
|
||||||
|
OwaAst::kw("a"),
|
||||||
|
OwaAst::vec([OwaAst::kw("b"), OwaAst::set([OwaAst::str("c")]),]),
|
||||||
|
OwaAst::vec([OwaAst::call([OwaAst::kw("d"),]),])
|
||||||
|
])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
96
src/parser/string.rs
Normal file
96
src/parser/string.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
use nom::combinator::opt;
|
||||||
|
use nom::{IResult, Parser};
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
bytes::complete::{escaped, tag},
|
||||||
|
character::complete::{none_of, one_of},
|
||||||
|
combinator::map_res,
|
||||||
|
sequence::delimited,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::parser::OwaAst;
|
||||||
|
|
||||||
|
fn unescape_string(s: &str) -> Result<String, String> {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut chars = s.chars();
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch == '\\' {
|
||||||
|
match chars.next() {
|
||||||
|
Some('n') => result.push('\n'),
|
||||||
|
Some('r') => result.push('\r'),
|
||||||
|
Some('t') => result.push('\t'),
|
||||||
|
Some('b') => result.push('\x08'),
|
||||||
|
Some('f') => result.push('\x0C'),
|
||||||
|
Some('\\') => result.push('\\'),
|
||||||
|
Some('"') => result.push('"'),
|
||||||
|
Some(c) => {
|
||||||
|
return Err(format!("Unknown escape sequence: \\{c}"));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err("Unexpected end of string after backslash".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_string(i: &str) -> IResult<&str, OwaAst> {
|
||||||
|
let (input, value) = delimited(
|
||||||
|
tag("\""),
|
||||||
|
opt(map_res(
|
||||||
|
escaped(none_of("\"\\"), '\\', one_of("\"\\nrtbf")),
|
||||||
|
unescape_string,
|
||||||
|
)),
|
||||||
|
tag("\""),
|
||||||
|
)
|
||||||
|
.parse(i)?;
|
||||||
|
Ok((input, OwaAst::str(value.unwrap_or_else(String::new))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symbol() {
|
||||||
|
assert_eq!(parse_string("\"foo\""), Ok(("", OwaAst::str("foo"))));
|
||||||
|
assert_eq!(
|
||||||
|
parse_string("\"foo1232\""),
|
||||||
|
Ok(("", OwaAst::str("foo1232")))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_string("\"foo\"garbage"),
|
||||||
|
Ok(("garbage", OwaAst::str("foo")))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_string("\"foo\\\"bar\"garbage"),
|
||||||
|
Ok(("garbage", OwaAst::str("foo\"bar")))
|
||||||
|
);
|
||||||
|
// Test newline escape
|
||||||
|
assert_eq!(
|
||||||
|
parse_string("\"hello\\nworld\""),
|
||||||
|
Ok(("", OwaAst::str("hello\nworld")))
|
||||||
|
);
|
||||||
|
// Test tab escape
|
||||||
|
assert_eq!(
|
||||||
|
parse_string("\"tab\\there\""),
|
||||||
|
Ok(("", OwaAst::str("tab\there")))
|
||||||
|
);
|
||||||
|
// Test backslash escape
|
||||||
|
assert_eq!(
|
||||||
|
parse_string("\"path\\\\file\""),
|
||||||
|
Ok(("", OwaAst::str("path\\file")))
|
||||||
|
);
|
||||||
|
// Test carriage return
|
||||||
|
assert_eq!(
|
||||||
|
parse_string("\"line\\rend\""),
|
||||||
|
Ok(("", OwaAst::str("line\rend")))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test space
|
||||||
|
assert_eq!(
|
||||||
|
parse_string("\"hello world\""),
|
||||||
|
Ok(("", OwaAst::str("hello world")))
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/parser/symbol.rs
Normal file
22
src/parser/symbol.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use nom::{IResult, Parser as _, combinator::map};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, identifier};
|
||||||
|
|
||||||
|
pub(super) fn parse_symbol(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(identifier, OwaAst::sym).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn symbol() {
|
||||||
|
assert_eq!(parse_symbol("foo"), Ok(("", OwaAst::sym("foo"))));
|
||||||
|
assert_eq!(parse_symbol("foo1232"), Ok(("", OwaAst::sym("foo1232"))));
|
||||||
|
assert_eq!(
|
||||||
|
parse_symbol("foo:garbage"),
|
||||||
|
Ok((":garbage", OwaAst::sym("foo")))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/parser/unquote.rs
Normal file
27
src/parser/unquote.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
use nom::{IResult, Parser as _, bytes::complete::tag, combinator::map, sequence::preceded};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, parse_value};
|
||||||
|
|
||||||
|
pub fn parse_unquote(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(preceded(tag("$"), parse_value), OwaAst::new_unquote).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keyword() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_unquote("$foo"),
|
||||||
|
Ok(("", OwaAst::sym("foo").to_unquote()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_unquote("$:foo1232"),
|
||||||
|
Ok(("", OwaAst::kw("foo1232").to_unquote()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_unquote("$\"foo\":s"),
|
||||||
|
Ok((":s", OwaAst::str("foo").to_unquote()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_unquote("$$[\"foo\"]:s"),
|
||||||
|
Ok((":s", OwaAst::str("foo").to_vec().to_unquote().to_unquote()))
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/parser/vector.rs
Normal file
29
src/parser/vector.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
use nom::{IResult, Parser as _, combinator::map};
|
||||||
|
|
||||||
|
use crate::parser::{OwaAst, collection};
|
||||||
|
|
||||||
|
pub fn parse_vector(input: &str) -> IResult<&str, OwaAst> {
|
||||||
|
map(collection("[", "]"), OwaAst::vec).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vector() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_vector("[:a :b :c]"),
|
||||||
|
Ok((
|
||||||
|
"",
|
||||||
|
OwaAst::vec([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c"),])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_vector("[:a [:b :c] [[:d]]]garbage"),
|
||||||
|
Ok((
|
||||||
|
"garbage",
|
||||||
|
OwaAst::vec([
|
||||||
|
OwaAst::kw("a"),
|
||||||
|
OwaAst::vec([OwaAst::kw("b"), OwaAst::kw("c"),]),
|
||||||
|
OwaAst::vec([OwaAst::vec([OwaAst::kw("d"),]),]),
|
||||||
|
])
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue