Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af63957b11 | |||
| a70e5fbc30 | |||
| deab894864 | |||
| 9f75ec68ee | |||
| fa6ea512b3 | |||
| abe0844816 | |||
| 7b9bded5ee | |||
| 9a193fd7ca | |||
| ddee395f52 | |||
| 9c9a17b149 | |||
| 299003a718 | |||
| d854b22006 | |||
| 89952139c5 | |||
| bdffe9e3c5 | |||
| 99c9e9c9bd | |||
| df9ecb4de1 | |||
| e356c208a6 | |||
| 9428e270af | |||
| bc74b5ee91 | |||
| f0bba2f2aa | |||
| 8e49df8062 | |||
| bf705069a5 | |||
| a7bedf6fa1 |
@@ -1 +1,2 @@
|
||||
/target
|
||||
**/node_modules
|
||||
|
||||
@@ -5,20 +5,57 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
# Build
|
||||
# Build entire workspace
|
||||
cargo build
|
||||
|
||||
# Run REPL (interactive)
|
||||
cargo run
|
||||
cargo run --bin aster
|
||||
|
||||
# Execute a script file
|
||||
cargo run -- examples/script.ast
|
||||
cargo run --bin aster -- aster-core/examples/script.ast
|
||||
|
||||
# Run tests (none yet; the project has no test/ directory)
|
||||
# Run tests
|
||||
cargo test
|
||||
```
|
||||
|
||||
This is a pure-Rust project with no external dependencies (`edition = "2024"`).
|
||||
This is a Rust workspace. `aster-core` is a pure-Rust library with no external dependencies (`edition = "2024"`).
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
aster/
|
||||
├── Cargo.toml # workspace root (aster-core, aster, aster-lsp)
|
||||
├── aster-core/ # core library — lexer, parser, AST, interpreter
|
||||
├── aster/ # REPL binary (thin wrapper around aster-core)
|
||||
├── aster-lsp/ # LSP server (diagnostics, etc.)
|
||||
└── vscode-ext/ # VS Code extension (syntax highlighting + LSP client)
|
||||
```
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
The `vscode-ext/` directory contains a VS Code extension that provides syntax highlighting and language server support for `.ast` files.
|
||||
|
||||
### Install (development)
|
||||
|
||||
```bash
|
||||
# 1. Build the LSP server
|
||||
cargo build --bin aster-lsp
|
||||
|
||||
# 2. Install extension npm dependencies
|
||||
npm --prefix vscode-ext install
|
||||
|
||||
# 3. Register the extension in VS Code
|
||||
|
||||
# Windows: create junction
|
||||
powershell -Command "New-Item -ItemType Junction -Path '$env:USERPROFILE\.vscode\extensions\aster-lang' -Target '$PWD\vscode-ext' -Force"
|
||||
|
||||
# macOS / Linux: symlink
|
||||
ln -s "$PWD/vscode-ext" "$HOME/.vscode/extensions/aster-lang"
|
||||
```
|
||||
|
||||
After installation, reload VS Code (`Ctrl+Shift+P` → "Developer: Reload Window"). Open any `.ast` file — syntax errors will appear as red squiggly underlines.
|
||||
|
||||
**Note:** The LSP server binary must be built (`cargo build --bin aster-lsp`) before the extension will work. After making changes to `aster-lsp`, rebuild and reload the VS Code window.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -28,20 +65,20 @@ This is a pure-Rust project with no external dependencies (`edition = "2024"`).
|
||||
Source text → Lexer → Tokens → Parser → AST → Interpreter → Output
|
||||
```
|
||||
|
||||
### Module map
|
||||
### Module map (`aster-core/src/`)
|
||||
|
||||
| Module | Responsibility |
|
||||
|---|---|
|
||||
| `lexer/` | Tokenizer. `Lexer::tokenize()` takes `&str` and returns `(Vec<Token>, Vec<RuntimeError>)`. Tokens carry `line`/`column` for error reporting. Supports `//` single-line comments. |
|
||||
| `lexer/` | Tokenizer. `Lexer::tokenize()` takes `&str` and returns `(Vec<Token>, Vec<RuntimeError>)`. Tokens carry `line`/`column` for error reporting. Supports `//` single-line and `/* */` block comments. |
|
||||
| `parser/` | Recursive-descent parser (Pratt-style for expressions). `Parser::parse()` returns `(Vec<Stmt>, Vec<RuntimeError>)`. Uses `synchronize()` for error recovery — skips to the next statement boundary on parse failure. |
|
||||
| `ast/` | AST node definitions. `Expr` (in `expr.rs`) covers literals, variables, assignment, property access/indexing, unary/binary/logical ops, calls, lambdas, and object/array literals. `Stmt` (in `stmt.rs`) covers let, expression stmts, blocks, if/while/for, functions, return/break/continue. |
|
||||
| `interpreter/` | Tree-walking evaluator. `Interpreter` holds an `env: Rc<RefCell<Env>>`. `Env` is a linked-list of scopes for lexical nesting. The `Signal` enum propagates `Return`/`Break`/`Continue` through the call stack. `builtins/` registers native functions (`print`, `input`, `clock`) organized as `io` and `os` modules (also available as global functions). |
|
||||
| `error/` | Three error variants: `LexError` (line+column), `ParseError` (token), `RuntimeError` (optional token). All implement `Display` and `std::error::Error`. |
|
||||
| `main.rs` | Two modes: `run_file` (parse-then-execute, exits with code 65 on lex/parse errors, 70 on runtime errors) and `run_repl` (per-line evaluation with `:exit`/`:reset` commands). |
|
||||
| `lib.rs` | Crate root — declares public modules, re-exports key types, provides `run_file()` and `run_repl()` convenience functions. |
|
||||
|
||||
### Key design decisions
|
||||
|
||||
- **No external crate dependencies.** Everything is built on Rust's stdlib.
|
||||
- **`aster-core` has no external crate dependencies.** Everything is built on Rust's stdlib. `aster-lsp` depends on `lsp-server` + `lsp-types`.
|
||||
- **Rc<RefCell<>> for shared ownership.** `Env` chains, `Value::Object`, `Value::Array`, and `Value::Function` all use `Rc<RefCell<>>` for interior mutability — matching the dynamic, garbage-collected feel without a GC.
|
||||
- **Error-tolerant parsing.** Both lexer and parser collect errors into a `Vec` rather than failing on the first error, so multiple diagnostics can be reported per run.
|
||||
- **Closures capture by environment.** `Function` structs store the `Env` at definition time; lambda expressions (`fn(params) { body }`) work the same way.
|
||||
|
||||
Generated
+187
@@ -5,3 +5,190 @@ version = 4
|
||||
[[package]]
|
||||
name = "aster"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aster-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aster-core"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "aster-lsp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aster-core",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "lsp-server"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6ada348dbc2703cbe7637b2dda05cff84d3da2819c24abcb305dd613e0ba2e"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.97.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fluent-uri",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
+3
-6
@@ -1,6 +1,3 @@
|
||||
[package]
|
||||
name = "aster"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
[workspace]
|
||||
members = ["aster-core", "aster", "aster-lsp"]
|
||||
resolver = "2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Aster
|
||||
|
||||
Aster 是一门使用 Rust 编写的动态类型脚本语言解释器,基于树遍历(tree-walker)实现。无外部依赖,纯标准库。
|
||||
Aster 是一门使用 Rust 编写的动态类型脚本语言,采用**基于栈的字节码 VM** 执行。无外部依赖,纯标准库。
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -84,6 +84,21 @@ while (true) {
|
||||
if (condition) { break; }
|
||||
if (skip) { continue; }
|
||||
}
|
||||
|
||||
// for-in 遍历(数组 / 字符串)
|
||||
for (let item in [1, 2, 3]) {
|
||||
print(item);
|
||||
}
|
||||
```
|
||||
|
||||
### 模块系统
|
||||
|
||||
```js
|
||||
// 加载并执行外部脚本,返回模块导出的对象
|
||||
let math = require("math.ast");
|
||||
print(math.add(3, 4));
|
||||
|
||||
// 第二次 require 同一文件会返回缓存的对象(支持循环引用)
|
||||
```
|
||||
|
||||
### 函数与闭包
|
||||
@@ -144,23 +159,25 @@ let t = os.clock(); // 模块化调用
|
||||
## 架构
|
||||
|
||||
```
|
||||
源码文本 → Lexer → Tokens → Parser → AST → Interpreter → 输出
|
||||
源码文本 → Lexer → Tokens → Parser → AST → Compiler → Bytecode → VM → 输出
|
||||
```
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| `lexer/` | 词法分析。`Lexer::tokenize()` 将源码转为 `(Vec<Token>, Vec<Error>)`,Token 记录行列号用于错误报告,支持 `//` 单行注释 |
|
||||
| `lexer/` | 词法分析。`Lexer::tokenize()` 将源码转为 `(Vec<Token>, Vec<Error>)`,Token 记录行列号用于错误报告,支持 `//` 单行注释和 `/* */` 块注释 |
|
||||
| `parser/` | 递归下降 + Pratt 解析器。`Parser::parse()` 返回 `(Vec<Stmt>, Vec<Error>)`,遇到语法错误通过 `synchronize()` 跳过至下一条语句边界继续解析 |
|
||||
| `ast/` | AST 节点定义。`Expr`(表达式)涵盖字面量、变量、赋值、属性/索引访问、一元/二元/逻辑运算、函数调用、lambda 和对象/数组字面量。`Stmt`(语句)涵盖 let、表达式语句、块、if/while/for、函数、return/break/continue |
|
||||
| `interpreter/` | 树遍历求值器。`Env` 是基于 `Rc<RefCell<>>` 的链式作用域。`Signal` 枚举通过调用栈传播 `Return`/`Break`/`Continue`。`builtins/` 按 `io` 和 `os` 模块组织原生函数,同时注册为全局函数以方便使用 |
|
||||
| `runtime/` | 运行时类型:`Value` 枚举、`FunctionProto`(编译后的函数蓝图)、`Closure`(函数原型 + 捕获的 upvalue)、`Runtime` trait、`NativeFn` 类型。`builtins/` 按功能分组注册原生函数 |
|
||||
| `vm/` | 字节码 VM。`Compiler` 将 AST 编译为基于栈的字节码(44 条指令),`Vm` 执行字节码。支持闭包 upvalue 捕获、3 层嵌套闭包、`require()` 模块加载 |
|
||||
| `error/` | 三种错误:`LexError`(行列号)、`ParseError`(Token)、`RuntimeError`(可选 Token) |
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
- **零外部依赖** — 全部基于 Rust 标准库构建
|
||||
- **基于栈的字节码 VM** — AST 先编译为字节码再执行,单一执行路径,无语义漂移
|
||||
- **错误容忍解析** — 词法分析器和解析器均将错误收集到 `Vec` 中,单次运行可报告多个诊断信息,而非在第一个错误处就中止
|
||||
- **`Rc<RefCell<>>` 共享所有权** — 用于 `Env` 链、`Value::Object`、`Value::Array` 和 `Value::Function`,提供动态可变语义
|
||||
- **词法作用域闭包** — `Function` 在定义时捕获 `Env`,lambda 表达式同理
|
||||
- **`Rc<RefCell<>>` 共享所有权** — 用于 `Value::Object`、`Value::Array` 和 `UpvalueObj`,提供动态可变语义。闭包通过 upvalue 机制捕获外层局部变量
|
||||
- **词法作用域闭包** — 编译时解析 upvalue 捕获,通过增量链路支持任意深度嵌套;`resolve_upvalue` 中显式处理 3 层穿透(本地→父级→祖级),更深层级通过逐层编译自然建立
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "aster-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,524 @@
|
||||
// ============================================================================
|
||||
// Aster 性能基准测试
|
||||
// 运行: cargo run -- examples/benchmark.ast
|
||||
// 目的: 用于树遍历解释器 vs VM 的性能对比
|
||||
//
|
||||
// 注: debug 模式下总耗时约 30-60s,release 模式会快 10-50x
|
||||
// ============================================================================
|
||||
|
||||
// --- 高阶工具函数 (被后续 benchmark 使用) ---
|
||||
|
||||
fn map(arr, func) {
|
||||
let result = []
|
||||
for (x in arr) {
|
||||
push(result, func(x))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn filter(arr, pred) {
|
||||
let result = []
|
||||
for (x in arr) {
|
||||
if (pred(x)) {
|
||||
push(result, x)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn reduce(arr, func, init) {
|
||||
let acc = init
|
||||
for (x in arr) {
|
||||
acc = func(acc, x)
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
fn sort(arr) {
|
||||
let n = len(arr)
|
||||
let i = 0
|
||||
while (i < n - 1) {
|
||||
let j = 0
|
||||
while (j < n - i - 1) {
|
||||
if (arr[j] > arr[j + 1]) {
|
||||
let tmp = arr[j]
|
||||
arr[j] = arr[j + 1]
|
||||
arr[j + 1] = tmp
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// --- 基准测试框架 ---
|
||||
|
||||
fn bench(name, thunk) {
|
||||
let t0 = clock()
|
||||
let result = thunk()
|
||||
let t1 = clock()
|
||||
let elapsed = t1 - t0
|
||||
print(name + ": " + result + " \ttime=" + elapsed + "s")
|
||||
return result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 1. 算术运算压测 — 测试基本运算 + 循环调度开销
|
||||
// ============================================================================
|
||||
|
||||
print("=== 1. Arithmetic ===")
|
||||
|
||||
bench("1a. int loop 1M", fn() {
|
||||
let sum = 0
|
||||
let i = 0
|
||||
while (i < 1000000) {
|
||||
sum = sum + i
|
||||
i = i + 1
|
||||
}
|
||||
return sum
|
||||
})
|
||||
|
||||
bench("1b. FP mul 500K", fn() {
|
||||
let x = 1.0
|
||||
let i = 0
|
||||
while (i < 500000) {
|
||||
x = x * 1.00001
|
||||
i = i + 1
|
||||
}
|
||||
return x
|
||||
})
|
||||
|
||||
bench("1c. compound assign 1M", fn() {
|
||||
let a = 0
|
||||
let i = 0
|
||||
while (i < 1000000) {
|
||||
a += i
|
||||
i += 1
|
||||
}
|
||||
return a
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 2. 函数调用开销 — 测试各种调用模式
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 2. Function Calls ===")
|
||||
|
||||
fn empty() { return 42 }
|
||||
|
||||
bench("2a. call empty fn 200K", fn() {
|
||||
let i = 0
|
||||
while (i < 200000) {
|
||||
empty()
|
||||
i = i + 1
|
||||
}
|
||||
return empty()
|
||||
})
|
||||
|
||||
fn add(a, b) { return a + b }
|
||||
|
||||
bench("2b. call add(a,b) 200K", fn() {
|
||||
let s = 0
|
||||
let i = 0
|
||||
while (i < 200000) {
|
||||
s = add(s, i)
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
fn fact(n) {
|
||||
if (n <= 1) { return 1 }
|
||||
return n * fact(n - 1)
|
||||
}
|
||||
|
||||
bench("2c. recursion fact(10) 10K", fn() {
|
||||
let i = 0
|
||||
while (i < 10000) {
|
||||
fact(10)
|
||||
i = i + 1
|
||||
}
|
||||
return fact(10)
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 3. 闭包压测 — 闭包创建与调用
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 3. Closures ===")
|
||||
|
||||
fn make_mult(k) {
|
||||
return fn(x) { return x * k }
|
||||
}
|
||||
|
||||
bench("3a. create + call closure 100K", fn() {
|
||||
let m = make_mult(7)
|
||||
let s = 0
|
||||
let i = 0
|
||||
while (i < 100000) {
|
||||
s = s + m(i)
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
bench("3b. inline lambda map 10K", fn() {
|
||||
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
let i = 0
|
||||
let s = 0
|
||||
while (i < 10000) {
|
||||
let doubled = map(arr, fn(x) { return x * 2 })
|
||||
s = s + doubled[0]
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 4. 数组操作
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 4. Array ===")
|
||||
|
||||
bench("4a. array index get 1M", fn() {
|
||||
let arr = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
|
||||
let s = 0
|
||||
let i = 0
|
||||
while (i < 1000000) {
|
||||
s = s + arr[i % 10]
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
bench("4b. array index set 500K", fn() {
|
||||
let arr = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
let i = 0
|
||||
while (i < 500000) {
|
||||
arr[i % 10] = i
|
||||
i = i + 1
|
||||
}
|
||||
return arr[0] + arr[9]
|
||||
})
|
||||
|
||||
bench("4c. push + pop 100K", fn() {
|
||||
let arr = []
|
||||
let i = 0
|
||||
while (i < 100000) {
|
||||
push(arr, i)
|
||||
i = i + 1
|
||||
}
|
||||
while (len(arr) > 0) {
|
||||
pop(arr)
|
||||
}
|
||||
return len(arr)
|
||||
})
|
||||
|
||||
bench("4d. for-in array 10K items", fn() {
|
||||
let arr = []
|
||||
let i = 0
|
||||
while (i < 10000) {
|
||||
push(arr, i)
|
||||
i = i + 1
|
||||
}
|
||||
let s = 0
|
||||
for (x in arr) {
|
||||
s = s + x
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 5. 对象操作
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 5. Object ===")
|
||||
|
||||
bench("5a. object field read 200K", fn() {
|
||||
let obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }
|
||||
let s = 0
|
||||
let i = 0
|
||||
while (i < 200000) {
|
||||
s = s + obj.a + obj.b + obj.c
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
bench("5b. object field write 200K", fn() {
|
||||
let obj = { x: 0, y: 0 }
|
||||
let i = 0
|
||||
while (i < 200000) {
|
||||
obj.x = i
|
||||
obj.y = obj.x + 1
|
||||
i = i + 1
|
||||
}
|
||||
return obj.x + obj.y
|
||||
})
|
||||
|
||||
bench("5c. object bracket get 200K", fn() {
|
||||
let obj = { name: "test", age: 25 }
|
||||
let s = ""
|
||||
let i = 0
|
||||
while (i < 200000) {
|
||||
s = obj["name"]
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 6. 字符串操作
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 6. String ===")
|
||||
|
||||
bench("6a. string concat 50K", fn() {
|
||||
let s = ""
|
||||
let i = 0
|
||||
while (i < 50000) {
|
||||
s = s + "a"
|
||||
i = i + 1
|
||||
}
|
||||
return len(s)
|
||||
})
|
||||
|
||||
bench("6b. split + join pattern 10K", fn() {
|
||||
let csv = "alice,bob,carol,dave,eve"
|
||||
let i = 0
|
||||
let c = 0
|
||||
while (i < 10000) {
|
||||
let parts = split(csv, ",")
|
||||
c = c + len(parts)
|
||||
i = i + 1
|
||||
}
|
||||
return c
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 7. 高阶函数链 — map/filter/reduce
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 7. Higher-order ===")
|
||||
|
||||
bench("7a. map+filter+reduce chain 1000", fn() {
|
||||
let data = []
|
||||
let i = 0
|
||||
while (i < 100) {
|
||||
push(data, i)
|
||||
i = i + 1
|
||||
}
|
||||
let n = 0
|
||||
while (n < 1000) {
|
||||
let r = reduce(
|
||||
map(
|
||||
filter(data, fn(x) { return x % 2 == 0 }),
|
||||
fn(x) { return x * x }
|
||||
),
|
||||
fn(a, b) { return a + b },
|
||||
0
|
||||
)
|
||||
n = n + 1
|
||||
}
|
||||
return n
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 8. 复杂算法 — 冒泡排序
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 8. Algorithms ===")
|
||||
|
||||
bench("8a. bubble sort 100 items x 50", fn() {
|
||||
let template = [42, 17, 8, 99, 3, 56, 23, 91, 5, 67,
|
||||
34, 12, 78, 1, 55, 88, 19, 73, 44, 28,
|
||||
66, 37, 81, 14, 50, 95, 7, 63, 29, 41,
|
||||
85, 21, 52, 9, 76, 33, 68, 15, 47, 92,
|
||||
25, 58, 4, 71, 39, 83, 11, 54, 97, 30,
|
||||
64, 18, 49, 2, 87, 36, 74, 22, 59, 45,
|
||||
90, 6, 53, 27, 80, 13, 48, 94, 31, 69,
|
||||
16, 57, 43, 89, 24, 61, 8, 75, 38, 46,
|
||||
100, 20, 65, 10, 79, 35, 72, 26, 82, 51,
|
||||
98, 32, 77, 40, 84, 62, 93, 3, 70, 70]
|
||||
let i = 0
|
||||
while (i < 50) {
|
||||
let arr = []
|
||||
let j = 0
|
||||
while (j < 100) {
|
||||
push(arr, template[j])
|
||||
j = j + 1
|
||||
}
|
||||
sort(arr)
|
||||
i = i + 1
|
||||
}
|
||||
return i
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 9. 斐波那契 — 递归 vs 迭代对比
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 9. Fibonacci ===")
|
||||
|
||||
fn fib_rec(n) {
|
||||
if (n <= 1) { return n }
|
||||
return fib_rec(n - 1) + fib_rec(n - 2)
|
||||
}
|
||||
|
||||
bench("9a. fib_rec(25) x 10", fn() {
|
||||
let i = 0
|
||||
let r = 0
|
||||
while (i < 10) {
|
||||
r = fib_rec(25)
|
||||
i = i + 1
|
||||
}
|
||||
return r
|
||||
})
|
||||
|
||||
fn fib_iter(n) {
|
||||
if (n <= 1) { return n }
|
||||
let a = 0
|
||||
let b = 1
|
||||
let i = 2
|
||||
while (i <= n) {
|
||||
let tmp = a + b
|
||||
a = b
|
||||
b = tmp
|
||||
i = i + 1
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
bench("9b. fib_iter(100) x 10K", fn() {
|
||||
let i = 0
|
||||
while (i < 10000) {
|
||||
fib_iter(100)
|
||||
i = i + 1
|
||||
}
|
||||
return fib_iter(100)
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 10. 变量访问 — 全局 vs 局部 vs 深层闭包
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 10. Variable Access ===")
|
||||
|
||||
let g = 100
|
||||
|
||||
fn use_global() {
|
||||
return g + 1
|
||||
}
|
||||
|
||||
bench("10a. global var read 200K", fn() {
|
||||
let i = 0
|
||||
let s = 0
|
||||
while (i < 200000) {
|
||||
s = s + g
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
bench("10b. local var read 200K", fn() {
|
||||
let local = 100
|
||||
let i = 0
|
||||
let s = 0
|
||||
while (i < 200000) {
|
||||
s = s + local
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
fn deep_scope() {
|
||||
let a = 1
|
||||
let b = 2
|
||||
let c = 3
|
||||
let d = 4
|
||||
let e = 5
|
||||
return fn() {
|
||||
return fn() {
|
||||
return fn() {
|
||||
return a + b + c + d + e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bench("10c. nested closure call 100K", fn() {
|
||||
let get = deep_scope()()()
|
||||
let i = 0
|
||||
let s = 0
|
||||
while (i < 100000) {
|
||||
s = s + get()
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 11. 分支判断
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 11. Conditionals ===")
|
||||
|
||||
bench("11a. if-else chain 500K", fn() {
|
||||
let s = 0
|
||||
let i = 0
|
||||
while (i < 500000) {
|
||||
if (i % 3 == 0) {
|
||||
s = s + 1
|
||||
} else if (i % 3 == 1) {
|
||||
s = s + 2
|
||||
} else {
|
||||
s = s + 3
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 12. 综合混合负载
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== 12. Mixed Workload ===")
|
||||
|
||||
bench("12a. mixed (arith+obj+array+call)", fn() {
|
||||
let s = 0
|
||||
let i = 0
|
||||
while (i < 50000) {
|
||||
s = s + i * 2 - i / 2 + i % 7
|
||||
|
||||
let pt = { x: i, y: i + 1, z: i + 2 }
|
||||
s = s + pt.x + pt.y - pt.z
|
||||
|
||||
let arr = [i, i + 1, i + 2, i + 3, i + 4]
|
||||
s = s + arr[0] + arr[i % 5]
|
||||
|
||||
s = s + add(pt.x, arr[1])
|
||||
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 13. 总结
|
||||
// ============================================================================
|
||||
|
||||
print("")
|
||||
print("=== Benchmark Complete ===")
|
||||
@@ -0,0 +1,2 @@
|
||||
fn add(a, b) { return a + b; }
|
||||
fn mul(a, b) { return a * b; }
|
||||
@@ -0,0 +1,209 @@
|
||||
// ============================================================================
|
||||
// 实用脚本集合:文本统计、数组工具、排序、数据转换
|
||||
// 运行: cargo run -- examples/practical.ast
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
// 1. 文本统计工具
|
||||
// ============================================================================
|
||||
fn count_chars(s) {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
fn count_words(s) {
|
||||
let count = 0
|
||||
let in_word = false
|
||||
for (c in s) {
|
||||
if (c == " " || c == "\n" || c == "\t") {
|
||||
in_word = false
|
||||
} else if (!in_word) {
|
||||
count = count + 1
|
||||
in_word = true
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fn text_stats(text) {
|
||||
print("=== Text Stats ===")
|
||||
print("chars:", count_chars(text))
|
||||
print("words:", count_words(text))
|
||||
}
|
||||
|
||||
let sample = "hello world from Aster scripting language"
|
||||
text_stats(sample)
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 2. 数组工具函数 (map / filter / reduce)
|
||||
// ============================================================================
|
||||
|
||||
fn map(arr, func) {
|
||||
let result = []
|
||||
for (x in arr) {
|
||||
push(result, func(x))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn filter(arr, pred) {
|
||||
let result = []
|
||||
for (x in arr) {
|
||||
if (pred(x)) {
|
||||
push(result, x)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn reduce(arr, func, init) {
|
||||
let acc = init
|
||||
for (x in arr) {
|
||||
acc = func(acc, x)
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
let nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
|
||||
let doubled = map(nums, fn(x) { return x * 2 })
|
||||
print("map (*2):", doubled)
|
||||
|
||||
let evens = filter(nums, fn(x) { return x % 2 == 0 })
|
||||
print("filter (even):", evens)
|
||||
|
||||
let sum = reduce(nums, fn(a, b) { return a + b }, 0)
|
||||
print("reduce (sum):", sum)
|
||||
|
||||
// 链式: 偶数平方和
|
||||
let even_squares = map(
|
||||
filter(nums, fn(x) { return x % 2 == 0 }),
|
||||
fn(x) { return x * x }
|
||||
)
|
||||
let sum_squares = reduce(even_squares, fn(a, b) { return a + b }, 0)
|
||||
print("sum of even squares:", sum_squares)
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 3. 冒泡排序
|
||||
// ============================================================================
|
||||
|
||||
fn sort(arr) {
|
||||
let n = len(arr)
|
||||
let i = 0
|
||||
while (i < n - 1) {
|
||||
let j = 0
|
||||
while (j < n - i - 1) {
|
||||
if (arr[j] > arr[j + 1]) {
|
||||
let tmp = arr[j]
|
||||
arr[j] = arr[j + 1]
|
||||
arr[j + 1] = tmp
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
let unsorted = [42, 17, 8, 99, 3, 56]
|
||||
print("unsorted:", unsorted)
|
||||
print("sorted:", sort(unsorted))
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 4. 数据转换 (类 JSON 处理)
|
||||
// ============================================================================
|
||||
|
||||
let users = [
|
||||
{ name: "Alice", age: 28, active: true },
|
||||
{ name: "Bob", age: 35, active: false },
|
||||
{ name: "Carol", age: 22, active: true },
|
||||
{ name: "Dave", age: 40, active: true },
|
||||
]
|
||||
|
||||
// 找出活跃用户的名字,按年龄排序
|
||||
let active = filter(users, fn(u) { return u["active"] })
|
||||
print("active users:", len(active))
|
||||
|
||||
// 计算平均年龄
|
||||
let total_age = reduce(users, fn(acc, u) { return acc + u["age"] }, 0)
|
||||
print("avg age:", total_age / len(users))
|
||||
|
||||
// 生成名字列表
|
||||
let names = map(users, fn(u) { return u["name"] })
|
||||
print("names:", names)
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 5. 斐波那契 (递归 + 迭代 + 缓存对比)
|
||||
// ============================================================================
|
||||
|
||||
fn fib_rec(n) {
|
||||
if (n <= 1) { return n }
|
||||
return fib_rec(n - 1) + fib_rec(n - 2)
|
||||
}
|
||||
|
||||
fn fib_iter(n) {
|
||||
if (n <= 1) { return n }
|
||||
let a = 0
|
||||
let b = 1
|
||||
let i = 2
|
||||
while (i <= n) {
|
||||
let tmp = a + b
|
||||
a = b
|
||||
b = tmp
|
||||
i = i + 1
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
print("fib_rec(20):", fib_rec(20))
|
||||
print("fib_iter(20):", fib_iter(20))
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 6. 回文检测
|
||||
// ============================================================================
|
||||
|
||||
fn is_palindrome(s) {
|
||||
let n = len(s)
|
||||
let i = 0
|
||||
while (i < n / 2) {
|
||||
if (s[i] != s[n - 1 - i]) {
|
||||
return false
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
print("is_palindrome('radar'):", is_palindrome("radar"))
|
||||
print("is_palindrome('hello'):", is_palindrome("hello"))
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 7. 字符串工具(split / trim / substring / replace)
|
||||
// ============================================================================
|
||||
|
||||
print("=== string utils ===")
|
||||
|
||||
let csv = "Alice,Bob,Carol,Dave"
|
||||
let parts = split(csv, ",")
|
||||
print("split CSV:", parts)
|
||||
print("names count:", len(parts))
|
||||
|
||||
let padded = " hello "
|
||||
print("trim:", "'" + trim(padded) + "'")
|
||||
|
||||
print("substring('hello', 1, 3):", substring("hello", 1, 3))
|
||||
|
||||
let template = "Hello, NAME!"
|
||||
let greeting = replace(template, "NAME", "Aster")
|
||||
print("replace:", greeting)
|
||||
|
||||
print("contains('hello', 'ell'):", contains("hello", "ell"))
|
||||
print("upper:", upper("hello"))
|
||||
print("lower:", lower("HELLO"))
|
||||
print("starts_with('hello', 'he'):", starts_with("hello", "he"))
|
||||
print("ends_with('hello', 'lo'):", ends_with("hello", "lo"))
|
||||
@@ -0,0 +1,4 @@
|
||||
let math = require("math_mod.ast");
|
||||
print(math.add(10, 32));
|
||||
print(math.mul(6, 7));
|
||||
print(math.add(math.mul(2, 3), 1));
|
||||
@@ -0,0 +1,257 @@
|
||||
use crate::ast::{Expr, Stmt};
|
||||
use crate::lexer::{Token, TokenKind};
|
||||
|
||||
/// A symbol collected from walking the AST.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Symbol {
|
||||
pub name: String,
|
||||
pub kind: SymbolKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SymbolKind {
|
||||
Variable,
|
||||
Function { params: Vec<String> },
|
||||
}
|
||||
|
||||
/// Collect all declared symbols from the given statements.
|
||||
/// Walks into nested scopes but does NOT perform scope-aware filtering —
|
||||
/// that's the caller's job (e.g. the LSP completion handler).
|
||||
pub fn collect_symbols(stmts: &[Stmt]) -> Vec<Symbol> {
|
||||
let mut symbols = Vec::new();
|
||||
collect_stmts(stmts, &mut symbols);
|
||||
symbols
|
||||
}
|
||||
|
||||
fn collect_stmts(stmts: &[Stmt], symbols: &mut Vec<Symbol>) {
|
||||
for stmt in stmts {
|
||||
collect_stmt(stmt, symbols);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_stmt(stmt: &Stmt, symbols: &mut Vec<Symbol>) {
|
||||
match stmt {
|
||||
Stmt::Let { name, initializer, .. } => {
|
||||
symbols.push(Symbol {
|
||||
name: name.clone(),
|
||||
kind: SymbolKind::Variable,
|
||||
});
|
||||
collect_expr(initializer, symbols);
|
||||
}
|
||||
Stmt::Function { name, params, body } => {
|
||||
symbols.push(Symbol {
|
||||
name: name.clone(),
|
||||
kind: SymbolKind::Function {
|
||||
params: params.clone(),
|
||||
},
|
||||
});
|
||||
for p in params {
|
||||
symbols.push(Symbol {
|
||||
name: p.clone(),
|
||||
kind: SymbolKind::Variable,
|
||||
});
|
||||
}
|
||||
collect_stmts(body, symbols);
|
||||
}
|
||||
Stmt::Block(body) => collect_stmts(body, symbols),
|
||||
Stmt::If {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} => {
|
||||
collect_expr(condition, symbols);
|
||||
collect_stmt(then_branch, symbols);
|
||||
if let Some(else_b) = else_branch {
|
||||
collect_stmt(else_b, symbols);
|
||||
}
|
||||
}
|
||||
Stmt::While { condition, body } => {
|
||||
collect_expr(condition, symbols);
|
||||
collect_stmt(body, symbols);
|
||||
}
|
||||
Stmt::For {
|
||||
initializer,
|
||||
condition,
|
||||
step,
|
||||
body,
|
||||
} => {
|
||||
if let Some(init) = initializer {
|
||||
collect_stmt(init, symbols);
|
||||
}
|
||||
if let Some(cond) = condition {
|
||||
collect_expr(cond, symbols);
|
||||
}
|
||||
if let Some(step_expr) = step {
|
||||
collect_expr(step_expr, symbols);
|
||||
}
|
||||
collect_stmt(body, symbols);
|
||||
}
|
||||
Stmt::ForIn {
|
||||
var_name,
|
||||
iterable,
|
||||
body,
|
||||
} => {
|
||||
symbols.push(Symbol {
|
||||
name: var_name.clone(),
|
||||
kind: SymbolKind::Variable,
|
||||
});
|
||||
collect_expr(iterable, symbols);
|
||||
collect_stmt(body, symbols);
|
||||
}
|
||||
Stmt::ExprStmt(expr) | Stmt::Return(Some(expr)) => {
|
||||
collect_expr(expr, symbols);
|
||||
}
|
||||
Stmt::Return(None) | Stmt::Break | Stmt::Continue => {}
|
||||
Stmt::Try { body, catch_var, catch_body, finally_body } => {
|
||||
collect_stmts(body, symbols);
|
||||
if let Some(var) = catch_var {
|
||||
symbols.push(Symbol { name: var.clone(), kind: SymbolKind::Variable });
|
||||
}
|
||||
if let Some(cb) = catch_body {
|
||||
collect_stmts(cb, symbols);
|
||||
}
|
||||
if let Some(fb) = finally_body {
|
||||
collect_stmts(fb, symbols);
|
||||
}
|
||||
}
|
||||
Stmt::Throw(expr) => collect_expr(expr, symbols),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_expr(expr: &Expr, symbols: &mut Vec<Symbol>) {
|
||||
match expr {
|
||||
Expr::Literal(_) | Expr::Variable(_) => {}
|
||||
Expr::Assign { value, .. } => collect_expr(value, symbols),
|
||||
Expr::Get { object, .. } => collect_expr(object, symbols),
|
||||
Expr::Set { object, value, .. } => {
|
||||
collect_expr(object, symbols);
|
||||
collect_expr(value, symbols);
|
||||
}
|
||||
Expr::ObjectLiteral { properties } => {
|
||||
for (_, val) in properties {
|
||||
collect_expr(val, symbols);
|
||||
}
|
||||
}
|
||||
Expr::ArrayLiteral { elements } => {
|
||||
for elem in elements {
|
||||
collect_expr(elem, symbols);
|
||||
}
|
||||
}
|
||||
Expr::IndexGet { array, index } => {
|
||||
collect_expr(array, symbols);
|
||||
collect_expr(index, symbols);
|
||||
}
|
||||
Expr::IndexSet {
|
||||
array,
|
||||
index,
|
||||
value,
|
||||
..
|
||||
} => {
|
||||
collect_expr(array, symbols);
|
||||
collect_expr(index, symbols);
|
||||
collect_expr(value, symbols);
|
||||
}
|
||||
Expr::Unary { right, .. } => collect_expr(right, symbols),
|
||||
Expr::Binary { left, right, .. } | Expr::Logical { left, right, .. } => {
|
||||
collect_expr(left, symbols);
|
||||
collect_expr(right, symbols);
|
||||
}
|
||||
Expr::Ternary {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} => {
|
||||
collect_expr(condition, symbols);
|
||||
collect_expr(then_branch, symbols);
|
||||
collect_expr(else_branch, symbols);
|
||||
}
|
||||
Expr::Call {
|
||||
callee,
|
||||
arguments,
|
||||
} => {
|
||||
collect_expr(callee, symbols);
|
||||
for arg in arguments {
|
||||
collect_expr(arg, symbols);
|
||||
}
|
||||
}
|
||||
Expr::Lambda { params, body } => {
|
||||
for p in params {
|
||||
symbols.push(Symbol {
|
||||
name: p.clone(),
|
||||
kind: SymbolKind::Variable,
|
||||
});
|
||||
}
|
||||
collect_stmts(body, symbols);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Token-based position lookups ──────────────────────────────────
|
||||
|
||||
/// Scan tokens for the declaration site of `name`.
|
||||
/// Recognises `let name`, `const name`, `fn name`, and `for (name in …)`.
|
||||
/// Returns 1-based (line, column).
|
||||
pub fn find_declaration(tokens: &[Token], name: &str) -> Option<(usize, usize)> {
|
||||
let len = tokens.len();
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
match &tokens[i].kind {
|
||||
TokenKind::Let | TokenKind::Const => {
|
||||
if let Some(tok) = tokens.get(i + 1) {
|
||||
if let TokenKind::Identifier(n) = &tok.kind {
|
||||
if n == name {
|
||||
return Some((tok.line, tok.column));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TokenKind::Fn => {
|
||||
// fn name( ... or fn name { ...
|
||||
if let Some(tok) = tokens.get(i + 1) {
|
||||
if let TokenKind::Identifier(n) = &tok.kind {
|
||||
if n == name {
|
||||
return Some((tok.line, tok.column));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TokenKind::For => {
|
||||
// for ( name in ... )
|
||||
// Skip past LeftParen
|
||||
let mut j = i + 1;
|
||||
if j < len && tokens[j].kind == TokenKind::LeftParen {
|
||||
j += 1;
|
||||
}
|
||||
if j < len {
|
||||
if let TokenKind::Identifier(n) = &tokens[j].kind {
|
||||
if n == name {
|
||||
return Some((tokens[j].line, tokens[j].column));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Scan tokens for every occurrence of `Identifier(name)`.
|
||||
/// Returns 1-based (line, column) for each match.
|
||||
pub fn find_all_references(tokens: &[Token], name: &str) -> Vec<(usize, usize)> {
|
||||
tokens
|
||||
.iter()
|
||||
.filter_map(|tok| {
|
||||
if let TokenKind::Identifier(n) = &tok.kind {
|
||||
if n == name {
|
||||
Some((tok.line, tok.column))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -130,7 +130,7 @@ pub enum LogicalOp {
|
||||
Or,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AssignOp {
|
||||
Equal, // =
|
||||
PlusEqual, // +=
|
||||
@@ -58,4 +58,15 @@ pub enum Stmt {
|
||||
|
||||
/// continue
|
||||
Continue,
|
||||
|
||||
/// try { body } catch (e) { catch_body } finally { finally_body }
|
||||
Try {
|
||||
body: Vec<Stmt>,
|
||||
catch_var: Option<String>,
|
||||
catch_body: Option<Vec<Stmt>>,
|
||||
finally_body: Option<Vec<Stmt>>,
|
||||
},
|
||||
|
||||
/// throw expr;
|
||||
Throw(Expr),
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Re-export shared types from runtime (backward compatibility)
|
||||
pub use crate::runtime::{Value, NativeFn, Runtime};
|
||||
pub use crate::runtime::builtins;
|
||||
@@ -92,6 +92,9 @@ impl Lexer {
|
||||
self.advance();
|
||||
}
|
||||
return None;
|
||||
} else if self.match_char('*') {
|
||||
// 多行注释 /* ... */
|
||||
return self.block_comment(line, column, errors);
|
||||
} else if self.match_char('=') {
|
||||
TokenKind::SlashEqual
|
||||
} else {
|
||||
@@ -225,6 +228,32 @@ impl Lexer {
|
||||
self.current >= self.src.len()
|
||||
}
|
||||
|
||||
/// consume multi-line comment, tracking line numbers for error reporting
|
||||
fn block_comment(
|
||||
&mut self,
|
||||
start_line: usize,
|
||||
start_column: usize,
|
||||
errors: &mut Vec<RuntimeError>,
|
||||
) -> Option<Token> {
|
||||
while !self.is_at_end() {
|
||||
let c = self.advance();
|
||||
if c == '\n' {
|
||||
self.line += 1;
|
||||
self.column = 1;
|
||||
} else if c == '*' && !self.is_at_end() && self.peek() == '/' {
|
||||
self.advance(); // consume closing '/'
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// EOF in block comment
|
||||
errors.push(RuntimeError::lex(
|
||||
"Unterminated block comment (missing */)",
|
||||
start_line,
|
||||
start_column,
|
||||
));
|
||||
None
|
||||
}
|
||||
|
||||
fn string_literal(
|
||||
&mut self,
|
||||
line: usize,
|
||||
@@ -331,6 +360,10 @@ impl Lexer {
|
||||
"true" => TokenKind::True,
|
||||
"false" => TokenKind::False,
|
||||
"nil" => TokenKind::Nil,
|
||||
"try" => TokenKind::Try,
|
||||
"catch" => TokenKind::Catch,
|
||||
"finally" => TokenKind::Finally,
|
||||
"throw" => TokenKind::Throw,
|
||||
_ => TokenKind::Identifier(s),
|
||||
};
|
||||
|
||||
@@ -282,6 +282,58 @@ fn comment_between_tokens() {
|
||||
assert_eq!(kinds, vec![TokenKind::Number(1.0), TokenKind::Number(2.0)]);
|
||||
}
|
||||
|
||||
// ----- block comments /* ... */ -----
|
||||
|
||||
#[test]
|
||||
fn block_comment_ignored() {
|
||||
let kinds = tokenize("42 /* comment */ 99");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(42.0), TokenKind::Number(99.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_block_comment() {
|
||||
let kinds = tokenize("42/**/99");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(42.0), TokenKind::Number(99.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_comment_multiline() {
|
||||
let kinds = tokenize("1 /* line 1\nline 2\nline 3 */ 2");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(1.0), TokenKind::Number(2.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_comment_only() {
|
||||
let kinds = tokenize("/* just a block comment */");
|
||||
assert!(kinds.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_comment_with_asterisks_inside() {
|
||||
let kinds = tokenize("1 /*** weird ***/ 2");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(1.0), TokenKind::Number(2.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_comment_line_numbers_tracked() {
|
||||
let (tokens, _errors) = tokenize_full("/* line1\nline2 */\n42");
|
||||
let num = tokens.iter().find(|t| matches!(t.kind, TokenKind::Number(_))).unwrap();
|
||||
assert_eq!(num.line, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unterminated_block_comment() {
|
||||
let (_, errors) = tokenize_full("/* no closing");
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert!(errors[0].to_string().contains("Unterminated block comment"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_then_line_comment() {
|
||||
let kinds = tokenize("/* block */ // line\n42");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||
}
|
||||
|
||||
// ----- whitespace -----
|
||||
|
||||
#[test]
|
||||
@@ -39,6 +39,10 @@ pub enum TokenKind {
|
||||
True,
|
||||
False,
|
||||
Nil,
|
||||
Try,
|
||||
Catch,
|
||||
Finally,
|
||||
Throw,
|
||||
|
||||
EOF,
|
||||
}
|
||||
@@ -1,26 +1,29 @@
|
||||
mod lexer;
|
||||
mod ast;
|
||||
mod parser;
|
||||
mod interpreter;
|
||||
mod error;
|
||||
pub mod lexer;
|
||||
pub mod ast;
|
||||
pub mod parser;
|
||||
pub mod runtime;
|
||||
pub mod interpreter;
|
||||
pub mod vm;
|
||||
pub mod error;
|
||||
pub mod analysis;
|
||||
|
||||
use lexer::Lexer;
|
||||
use parser::Parser;
|
||||
use interpreter::Interpreter;
|
||||
use vm::compiler::Compiler;
|
||||
use vm::vm::Vm;
|
||||
use error::RuntimeError;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
use std::io::Write;
|
||||
use std::io::stdin;
|
||||
use std::io::stdout;
|
||||
|
||||
fn print_errors(errors: &[RuntimeError]) {
|
||||
pub fn print_errors(errors: &[RuntimeError]) {
|
||||
for err in errors {
|
||||
eprintln!("{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_file(src: String) {
|
||||
pub fn run_file(filename: &str, src: String) {
|
||||
let (tokens, lex_errors) = Lexer::new(&src).tokenize();
|
||||
if !lex_errors.is_empty() {
|
||||
print_errors(&lex_errors);
|
||||
@@ -34,19 +37,32 @@ fn run_file(src: String) {
|
||||
std::process::exit(65);
|
||||
}
|
||||
|
||||
let mut interpreter = Interpreter::new();
|
||||
if let Err(e) = interpreter.interpret(stmts) {
|
||||
let script_dir = std::path::Path::new(filename)
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
let proto = match Compiler::compile(&stmts) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Compile Error: {}", e);
|
||||
std::process::exit(70);
|
||||
}
|
||||
};
|
||||
|
||||
let mut vm = Vm::with_current_dir(script_dir);
|
||||
if let Err(e) = vm.run(std::rc::Rc::new(proto)) {
|
||||
eprintln!("Error: {}", e);
|
||||
std::process::exit(70);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_repl() {
|
||||
pub fn run_repl() {
|
||||
println!("Welcome to Aster REPL!");
|
||||
println!("Type ':exit' to quit.");
|
||||
println!("Type ':reset' to reset the interpreter state.");
|
||||
println!("Type ':reset' to reset the VM state.");
|
||||
|
||||
let mut interpreter = Interpreter::new();
|
||||
let mut vm = Vm::new();
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
@@ -70,8 +86,8 @@ fn run_repl() {
|
||||
break;
|
||||
}
|
||||
":reset" => {
|
||||
interpreter = Interpreter::new();
|
||||
println!("Interpreter reset.");
|
||||
vm = Vm::new();
|
||||
println!("VM reset.");
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
@@ -90,30 +106,15 @@ fn run_repl() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = interpreter.interpret(stmts) {
|
||||
let proto = match Compiler::compile(&stmts) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Compile Error: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(e) = vm.run(std::rc::Rc::new(proto)) {
|
||||
eprintln!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
match args.len() {
|
||||
1 => run_repl(),
|
||||
2 => {
|
||||
let filename = &args[1];
|
||||
match fs::read_to_string(filename) {
|
||||
Ok(src) => run_file(src),
|
||||
Err(e) => {
|
||||
eprintln!("Error reading file '{}': {}", filename, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Usage: aster [script]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,10 @@ impl Parser {
|
||||
self.continue_statement()
|
||||
} else if self.match_kind(&[TokenKind::Return]) {
|
||||
self.return_statement()
|
||||
} else if self.match_kind(&[TokenKind::Try]) {
|
||||
self.try_statement()
|
||||
} else if self.match_kind(&[TokenKind::Throw]) {
|
||||
self.throw_statement()
|
||||
} else if self.match_kind(&[TokenKind::LeftBrace]) {
|
||||
Ok(Stmt::Block(self.block()?))
|
||||
} else {
|
||||
@@ -201,6 +205,39 @@ impl Parser {
|
||||
Ok(Stmt::Return(value))
|
||||
}
|
||||
|
||||
fn try_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
self.consume(TokenKind::LeftBrace, "Expected '{' after 'try'.")?;
|
||||
let body = self.block()?;
|
||||
|
||||
let mut catch_var = None;
|
||||
let mut catch_body = None;
|
||||
if self.match_kind(&[TokenKind::Catch]) {
|
||||
self.consume(TokenKind::LeftParen, "Expected '(' after 'catch'.")?;
|
||||
catch_var = Some(self.consume_ident("Expected exception variable.")?);
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after catch variable.")?;
|
||||
self.consume(TokenKind::LeftBrace, "Expected '{' before catch body.")?;
|
||||
catch_body = Some(self.block()?);
|
||||
}
|
||||
|
||||
let mut finally_body = None;
|
||||
if self.match_kind(&[TokenKind::Finally]) {
|
||||
self.consume(TokenKind::LeftBrace, "Expected '{' after 'finally'.")?;
|
||||
finally_body = Some(self.block()?);
|
||||
}
|
||||
|
||||
if catch_var.is_none() && finally_body.is_none() {
|
||||
return Err(RuntimeError::parse("Expected 'catch' or 'finally' after 'try'.", self.peek().clone()));
|
||||
}
|
||||
|
||||
Ok(Stmt::Try { body, catch_var, catch_body, finally_body })
|
||||
}
|
||||
|
||||
fn throw_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
let expr = self.expression()?;
|
||||
self.match_kind(&[TokenKind::Semicolon]); // 分号可选
|
||||
Ok(Stmt::Throw(expr))
|
||||
}
|
||||
|
||||
fn block(&mut self) -> Result<Vec<Stmt>, RuntimeError> {
|
||||
let mut statements = Vec::new();
|
||||
|
||||
@@ -567,6 +604,10 @@ impl Parser {
|
||||
if !self.match_kind(&[TokenKind::Comma]) {
|
||||
break;
|
||||
}
|
||||
// Allow trailing comma
|
||||
if self.check(&TokenKind::RightBrace) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.consume(TokenKind::RightBrace, "Expected '}' after object literal")?;
|
||||
@@ -581,6 +622,10 @@ impl Parser {
|
||||
if !self.match_kind(&[TokenKind::Comma]) {
|
||||
break;
|
||||
}
|
||||
// Allow trailing comma
|
||||
if self.check(&TokenKind::RightBracket) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.consume(TokenKind::RightBracket, "Expected ']' after array literal")?;
|
||||
@@ -608,7 +653,9 @@ impl Parser {
|
||||
| TokenKind::For
|
||||
| TokenKind::Return
|
||||
| TokenKind::Break
|
||||
| TokenKind::Continue => return,
|
||||
| TokenKind::Continue
|
||||
| TokenKind::Try
|
||||
| TokenKind::Throw => return,
|
||||
_ => {
|
||||
self.advance();
|
||||
}
|
||||
@@ -551,6 +551,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_array_trailing_comma() {
|
||||
match parse_expr("[1, 2, 3,]") {
|
||||
Expr::ArrayLiteral { elements } => assert_eq!(elements.len(), 3),
|
||||
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_object_trailing_comma() {
|
||||
match parse_expr("{a: 1, b: 2,}") {
|
||||
Expr::ObjectLiteral { properties } => assert_eq!(properties.len(), 2),
|
||||
e => panic!("Expected ObjectLiteral, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- statements -----
|
||||
|
||||
#[test]
|
||||
@@ -1,9 +1,9 @@
|
||||
//! 标准库:core(len, typeof, push, pop)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Value;
|
||||
use crate::runtime::{Runtime, Value};
|
||||
|
||||
pub fn len(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
pub fn len(_runtime: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "len() expects 1 argument".into(),
|
||||
@@ -21,7 +21,7 @@ pub fn len(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typeof_fn(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
pub fn typeof_fn(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "typeof() expects 1 argument".into(),
|
||||
@@ -41,7 +41,7 @@ pub fn typeof_fn(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
Ok(Value::String(s.into()))
|
||||
}
|
||||
|
||||
pub fn push(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
pub fn push(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "push() expects 2 arguments (array, value)".into(),
|
||||
@@ -61,7 +61,7 @@ pub fn push(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
pub fn pop(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "pop() expects 1 argument (array)".into(),
|
||||
@@ -1,9 +1,9 @@
|
||||
//! 标准库:io(print, input)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Value;
|
||||
use crate::runtime::{Runtime, Value};
|
||||
|
||||
pub fn print(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
pub fn print(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
for arg in args.iter() {
|
||||
print!("{}", arg);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ pub fn print(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
|
||||
pub fn input(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
pub fn input(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if let Some(prompt) = args.get(0) {
|
||||
print!("{}", prompt);
|
||||
std::io::Write::flush(&mut std::io::stdout()).unwrap();
|
||||
@@ -0,0 +1,45 @@
|
||||
//! Builtin function modules.
|
||||
|
||||
pub mod core;
|
||||
pub mod io;
|
||||
pub mod os;
|
||||
pub mod string;
|
||||
|
||||
use crate::runtime::Value;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Register all standard library functions into a HashMap.
|
||||
pub fn register_all(map: &mut HashMap<String, Value>) {
|
||||
// io
|
||||
let mut io_map = HashMap::new();
|
||||
io_map.insert("print".into(), Value::NativeFunction(Rc::new(io::print)));
|
||||
io_map.insert("input".into(), Value::NativeFunction(Rc::new(io::input)));
|
||||
map.insert("io".into(), Value::Object(Rc::new(RefCell::new(io_map))));
|
||||
map.insert("print".into(), Value::NativeFunction(Rc::new(io::print)));
|
||||
map.insert("input".into(), Value::NativeFunction(Rc::new(io::input)));
|
||||
|
||||
// os
|
||||
let mut os_map = HashMap::new();
|
||||
os_map.insert("clock".into(), Value::NativeFunction(Rc::new(os::clock)));
|
||||
map.insert("os".into(), Value::Object(Rc::new(RefCell::new(os_map))));
|
||||
map.insert("clock".into(), Value::NativeFunction(Rc::new(os::clock)));
|
||||
|
||||
// core
|
||||
map.insert("len".into(), Value::NativeFunction(Rc::new(core::len)));
|
||||
map.insert("typeof".into(), Value::NativeFunction(Rc::new(core::typeof_fn)));
|
||||
map.insert("push".into(), Value::NativeFunction(Rc::new(core::push)));
|
||||
map.insert("pop".into(), Value::NativeFunction(Rc::new(core::pop)));
|
||||
|
||||
// string
|
||||
map.insert("split".into(), Value::NativeFunction(Rc::new(string::split)));
|
||||
map.insert("trim".into(), Value::NativeFunction(Rc::new(string::trim)));
|
||||
map.insert("substring".into(), Value::NativeFunction(Rc::new(string::substring)));
|
||||
map.insert("replace".into(), Value::NativeFunction(Rc::new(string::replace)));
|
||||
map.insert("contains".into(), Value::NativeFunction(Rc::new(string::contains)));
|
||||
map.insert("upper".into(), Value::NativeFunction(Rc::new(string::upper)));
|
||||
map.insert("lower".into(), Value::NativeFunction(Rc::new(string::lower)));
|
||||
map.insert("starts_with".into(), Value::NativeFunction(Rc::new(string::starts_with)));
|
||||
map.insert("ends_with".into(), Value::NativeFunction(Rc::new(string::ends_with)));
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
//! 标准库:os(clock)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Value;
|
||||
use crate::runtime::{Runtime, Value};
|
||||
|
||||
pub fn clock(_args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
pub fn clock(_interp: &mut dyn Runtime, _args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
Ok(Value::Number(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -0,0 +1,203 @@
|
||||
//! 标准库:string(split, trim, substring, replace, contains, upper, lower, starts_with, ends_with)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::runtime::{Runtime, Value};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
fn require_string(val: &Value, arg_name: &str) -> Result<String, RuntimeError> {
|
||||
match val {
|
||||
Value::String(s) => Ok(s.clone()),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: format!("{} must be a string", arg_name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// split(str, delim) → array of substrings
|
||||
// ============================================================================
|
||||
|
||||
pub fn split(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "split() expects 2 arguments (string, delimiter)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let delim = require_string(&args[1], "Second argument (delimiter)")?;
|
||||
|
||||
let parts: Vec<Value> = if delim.is_empty() {
|
||||
// Split by empty string → characters
|
||||
s.chars().map(|c| Value::String(c.to_string())).collect()
|
||||
} else {
|
||||
s.split(&delim).map(|p| Value::String(p.to_string())).collect()
|
||||
};
|
||||
|
||||
Ok(Value::Array(Rc::new(RefCell::new(parts))))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// trim(str) → string with leading/trailing whitespace removed
|
||||
// ============================================================================
|
||||
|
||||
pub fn trim(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "trim() expects 1 argument (string)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "Argument")?;
|
||||
Ok(Value::String(s.trim().to_string()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// substring(str, start, len?) → substring
|
||||
// ============================================================================
|
||||
|
||||
pub fn substring(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "substring() expects at least 2 arguments (string, start, len?)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let start = as_usize(&args[1], "Second argument (start)")?;
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
let start = start.min(chars.len());
|
||||
let end = if args.len() >= 3 {
|
||||
(start + as_usize(&args[2], "Third argument (length)")?).min(chars.len())
|
||||
} else {
|
||||
chars.len()
|
||||
};
|
||||
Ok(Value::String(chars[start..end].iter().collect()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// replace(str, old, new) → string with all occurrences replaced
|
||||
// ============================================================================
|
||||
|
||||
pub fn replace(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 3 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "replace() expects 3 arguments (string, old, new)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let old = require_string(&args[1], "Second argument (old)")?;
|
||||
let new = require_string(&args[2], "Third argument (new)")?;
|
||||
|
||||
if old.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "replace(): 'old' must not be empty".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Value::String(s.replace(&old, &new)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// contains(str, needle) → bool
|
||||
// ============================================================================
|
||||
|
||||
pub fn contains(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "contains() expects 2 arguments (string, needle)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let needle = require_string(&args[1], "Second argument (needle)")?;
|
||||
Ok(Value::Bool(s.contains(&needle)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// upper(str) → uppercase (ASCII)
|
||||
// ============================================================================
|
||||
|
||||
pub fn upper(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "upper() expects 1 argument (string)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "Argument")?;
|
||||
Ok(Value::String(s.to_ascii_uppercase()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// lower(str) → lowercase (ASCII)
|
||||
// ============================================================================
|
||||
|
||||
pub fn lower(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "lower() expects 1 argument (string)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "Argument")?;
|
||||
Ok(Value::String(s.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// starts_with(str, prefix) → bool
|
||||
// ============================================================================
|
||||
|
||||
pub fn starts_with(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "starts_with() expects 2 arguments (string, prefix)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let prefix = require_string(&args[1], "Second argument (prefix)")?;
|
||||
Ok(Value::Bool(s.starts_with(&prefix)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ends_with(str, suffix) → bool
|
||||
// ============================================================================
|
||||
|
||||
pub fn ends_with(_interp: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "ends_with() expects 2 arguments (string, suffix)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let suffix = require_string(&args[1], "Second argument (suffix)")?;
|
||||
Ok(Value::Bool(s.ends_with(&suffix)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper
|
||||
// ============================================================================
|
||||
|
||||
fn as_usize(val: &Value, arg_name: &str) -> Result<usize, RuntimeError> {
|
||||
match val {
|
||||
Value::Number(n) => {
|
||||
if *n < 0.0 || n.fract() != 0.0 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: format!("{} must be a non-negative integer, got {}", arg_name, n),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
Ok(*n as usize)
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: format!("{} must be a number", arg_name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
pub mod builtins;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt;
|
||||
|
||||
/// Trait abstracting runtime services that native functions may need.
|
||||
pub trait Runtime {
|
||||
fn require(&mut self, path: &str) -> Result<Value, crate::error::RuntimeError>;
|
||||
}
|
||||
|
||||
pub type NativeFn = Rc<dyn Fn(&mut dyn Runtime, Vec<Value>) -> Result<Value, crate::error::RuntimeError>>;
|
||||
|
||||
// ============================================================================
|
||||
// Compiled function types
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FunctionProto {
|
||||
pub name: Option<String>,
|
||||
pub arity: u8,
|
||||
pub code: Vec<u8>,
|
||||
pub constants: Vec<Value>,
|
||||
pub protos: Vec<Rc<FunctionProto>>,
|
||||
pub upvalue_count: u8,
|
||||
pub upvalues: Vec<(bool, u8)>, // (is_local, index)
|
||||
pub lines: Vec<(usize, usize)>, // (bytecode_offset, source_line)
|
||||
pub exception_handlers: Vec<ExceptionHandler>,
|
||||
}
|
||||
|
||||
/// Exception handler entry: defines a try-catch-finally region.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExceptionHandler {
|
||||
pub try_start: usize, // inclusive
|
||||
pub try_end: usize, // exclusive
|
||||
pub catch_ip: usize, // catch handler IP (0 if no catch clause)
|
||||
pub catch_slot: u8, // local slot for catch variable (0 if no catch)
|
||||
pub finally_ip: usize, // finally rethrow entry (0 if no finally)
|
||||
}
|
||||
|
||||
impl FunctionProto {
|
||||
pub fn new(name: Option<String>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
arity: 0,
|
||||
code: Vec::new(),
|
||||
constants: Vec::new(),
|
||||
protos: Vec::new(),
|
||||
upvalue_count: 0,
|
||||
upvalues: Vec::new(),
|
||||
lines: Vec::new(),
|
||||
exception_handlers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_constant(&mut self, val: Value) -> u16 {
|
||||
for (i, c) in self.constants.iter().enumerate() {
|
||||
if values_eq(c, &val) {
|
||||
return i as u16;
|
||||
}
|
||||
}
|
||||
let idx = self.constants.len();
|
||||
self.constants.push(val);
|
||||
idx as u16
|
||||
}
|
||||
}
|
||||
|
||||
fn values_eq(a: &Value, b: &Value) -> bool {
|
||||
match (a, b) {
|
||||
(Value::Number(x), Value::Number(y)) => (x - y).abs() < f64::EPSILON,
|
||||
(Value::String(x), Value::String(y)) => x == y,
|
||||
(Value::Bool(x), Value::Bool(y)) => x == y,
|
||||
(Value::Nil, Value::Nil) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// An upvalue — a reference to a local variable in an enclosing function.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpvalueObj {
|
||||
pub location: usize,
|
||||
pub closed: Option<Value>,
|
||||
}
|
||||
|
||||
/// A runtime closure: compiled function proto + captured upvalues.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Closure {
|
||||
pub proto: Rc<FunctionProto>,
|
||||
pub upvalues: Vec<Rc<RefCell<UpvalueObj>>>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Value
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Value {
|
||||
Number(f64),
|
||||
String(String),
|
||||
Bool(bool),
|
||||
Nil,
|
||||
Object(Rc<RefCell<HashMap<String, Value>>>),
|
||||
Array(Rc<RefCell<Vec<Value>>>),
|
||||
Function(Rc<Closure>),
|
||||
NativeFunction(NativeFn),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn get(&self, key: &str) -> Option<Value> {
|
||||
match self {
|
||||
Value::Object(obj) => obj.borrow().get(key).cloned(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&self, key: &str, val: Value) -> Result<(), crate::error::RuntimeError> {
|
||||
match self {
|
||||
Value::Object(obj) => {
|
||||
obj.borrow_mut().insert(key.to_string(), val);
|
||||
Ok(())
|
||||
},
|
||||
_ => Err(crate::error::RuntimeError::RuntimeError {
|
||||
message: "Only objects have properties".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::Number(n) => write!(f, "Number({:?})", n),
|
||||
Value::String(s) => write!(f, "String({:?})", s),
|
||||
Value::Bool(b) => write!(f, "Bool({:?})", b),
|
||||
Value::Nil => write!(f, "Nil"),
|
||||
Value::Object(_) => write!(f, "Object(...)"),
|
||||
Value::Array(_) => write!(f, "Array(...)"),
|
||||
Value::Function(_) => write!(f, "Function(...)"),
|
||||
Value::NativeFunction(_) => write!(f, "NativeFunction(...)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
fn fmt_element(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Value::String(s) => write!(f, "\"{}\"", s),
|
||||
other => write!(f, "{}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Value::Number(n) => write!(f, "{}", n),
|
||||
Value::String(s) => write!(f, "{}", s),
|
||||
Value::Bool(b) => write!(f, "{}", b),
|
||||
Value::Nil => write!(f, "nil"),
|
||||
Value::Object(obj) => {
|
||||
let obj = obj.borrow();
|
||||
write!(f, "{{ ")?;
|
||||
for (key, value) in obj.iter() {
|
||||
write!(f, "{}: ", key)?;
|
||||
value.fmt_element(f)?;
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "}}")
|
||||
},
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
write!(f, "[")?;
|
||||
for (i, val) in arr.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
val.fmt_element(f)?;
|
||||
}
|
||||
write!(f, "]")
|
||||
},
|
||||
Value::Function(_) => write!(f, "<function>"),
|
||||
Value::NativeFunction(_) => write!(f, "<native function>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `require()` builtin — thin wrapper that delegates to `Runtime::require`.
|
||||
pub fn require_fn(runtime: &mut dyn Runtime, args: Vec<Value>) -> Result<Value, crate::error::RuntimeError> {
|
||||
let path_str = match args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(other) => return Err(crate::error::RuntimeError::RuntimeError {
|
||||
message: format!("require() expects a string argument, got {}", other),
|
||||
token: None,
|
||||
}),
|
||||
None => return Err(crate::error::RuntimeError::RuntimeError {
|
||||
message: "require() expects 1 argument (string path)".into(),
|
||||
token: None,
|
||||
}),
|
||||
};
|
||||
runtime.require(&path_str)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
pub mod opcode;
|
||||
pub mod compiler;
|
||||
pub mod vm;
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Bytecode opcode definitions for the Aster VM.
|
||||
//!
|
||||
//! Each instruction is encoded as a 1-byte opcode tag followed by
|
||||
//! variable-length operands (u8 for local slots, u16 for constant pool
|
||||
//! indices, i16 for relative jump offsets).
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum OpCode {
|
||||
// --- Stack manipulation ---
|
||||
Pop = 0,
|
||||
Dup = 1,
|
||||
|
||||
// --- Constants (operand: u16 index into constant pool) ---
|
||||
LoadConst = 2,
|
||||
LoadNil = 3,
|
||||
LoadTrue = 4,
|
||||
LoadFalse = 5,
|
||||
|
||||
// --- Local variables (operand: u8 slot index) ---
|
||||
LoadLocal = 6,
|
||||
StoreLocal = 7,
|
||||
LoadUpvalue = 42, // operand: u8 upvalue index
|
||||
StoreUpvalue = 43, // operand: u8 upvalue index
|
||||
|
||||
// --- Global variables (operand: u16 index into constant pool for name) ---
|
||||
LoadGlobal = 8,
|
||||
StoreGlobal = 9,
|
||||
DefineGlobal = 10,
|
||||
|
||||
// --- Property access (operand: u16 index into constant pool for name) ---
|
||||
GetProperty = 11,
|
||||
SetProperty = 12,
|
||||
|
||||
// --- Index access (operands on stack) ---
|
||||
GetIndex = 13,
|
||||
SetIndex = 14,
|
||||
|
||||
// --- Binary arithmetic ---
|
||||
Add = 15,
|
||||
Sub = 16,
|
||||
Mul = 17,
|
||||
Div = 18,
|
||||
Mod = 19,
|
||||
|
||||
// --- Unary ---
|
||||
Negate = 20,
|
||||
Not = 21,
|
||||
|
||||
// --- Comparison ---
|
||||
Equal = 22,
|
||||
NotEqual = 23,
|
||||
Greater = 24,
|
||||
GreaterEqual = 25,
|
||||
Less = 26,
|
||||
LessEqual = 27,
|
||||
|
||||
// --- Control flow (operand: i16 relative jump offset) ---
|
||||
Jump = 28,
|
||||
JumpIfFalse = 29,
|
||||
JumpIfTrue = 30,
|
||||
PopJumpIfFalse = 31,
|
||||
|
||||
// --- Functions ---
|
||||
Call = 32, // operand: u8 arg count
|
||||
Return = 33,
|
||||
Closure = 34, // operand: u16 proto idx, then N*(u8 is_local, u8 index)
|
||||
|
||||
// --- Object/Array construction ---
|
||||
NewObject = 35,
|
||||
NewArray = 36, // operand: u16 element count
|
||||
|
||||
// --- For-in support ---
|
||||
ForInInit = 37,
|
||||
ForInNext = 38, // operand: i16 done jump offset
|
||||
|
||||
// --- Compound assignment ---
|
||||
CompoundAssignLocal = 39, // operand: u8 slot + u8 compound-op tag
|
||||
CompoundAssignProp = 40, // operand: u16 name idx + u8 compound-op tag
|
||||
CompoundAssignIndex = 41, // operand: u8 compound-op tag
|
||||
CompoundAssignUpvalue = 44, // operand: u8 upvalue idx + u8 compound-op tag
|
||||
CompoundAssignGlobal = 45, // operand: u16 name idx + u8 compound-op tag
|
||||
Throw = 46, // pops exception value, begins stack unwind
|
||||
}
|
||||
|
||||
impl OpCode {
|
||||
pub fn from_u8(byte: u8) -> Option<Self> {
|
||||
match byte {
|
||||
0..=41 | 44..=46 => Some(unsafe { std::mem::transmute::<u8, OpCode>(byte) }),
|
||||
42 => Some(OpCode::LoadUpvalue),
|
||||
43 => Some(OpCode::StoreUpvalue),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Total number of distinct opcodes
|
||||
pub const COUNT: usize = 47;
|
||||
}
|
||||
|
||||
/// Compound assignment operator tags (used as operand byte after
|
||||
/// CompoundAssignLocal/Prop/Index).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum CompoundOp {
|
||||
PlusEqual = 0,
|
||||
MinusEqual = 1,
|
||||
StarEqual = 2,
|
||||
SlashEqual = 3,
|
||||
PercentEqual = 4,
|
||||
}
|
||||
|
||||
impl CompoundOp {
|
||||
pub fn from_u8(byte: u8) -> Option<Self> {
|
||||
if byte <= 4 {
|
||||
Some(unsafe { std::mem::transmute::<u8, CompoundOp>(byte) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Instruction encoding helpers (for compiler use)
|
||||
// ============================================================================
|
||||
|
||||
/// Write a u8 operand after the opcode byte.
|
||||
pub fn emit_u8(code: &mut Vec<u8>, op: OpCode, operand: u8) {
|
||||
code.push(op as u8);
|
||||
code.push(operand);
|
||||
}
|
||||
|
||||
/// Write a u16 operand after the opcode byte (little-endian).
|
||||
pub fn emit_u16(code: &mut Vec<u8>, op: OpCode, operand: u16) {
|
||||
code.push(op as u8);
|
||||
code.push((operand & 0xFF) as u8);
|
||||
code.push(((operand >> 8) & 0xFF) as u8);
|
||||
}
|
||||
|
||||
/// Write an i16 operand after the opcode byte (little-endian).
|
||||
pub fn emit_i16(code: &mut Vec<u8>, op: OpCode, offset: i16) {
|
||||
code.push(op as u8);
|
||||
code.push((offset & 0xFF) as u8);
|
||||
code.push(((offset >> 8) & 0xFF) as u8);
|
||||
}
|
||||
|
||||
/// Write a standalone opcode byte.
|
||||
pub fn emit_op(code: &mut Vec<u8>, op: OpCode) {
|
||||
code.push(op as u8);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Instruction decoding helpers (for VM use)
|
||||
// ============================================================================
|
||||
|
||||
/// Read a u8 at position `ip + 1` (right after the opcode byte).
|
||||
pub fn read_u8(code: &[u8], ip: usize) -> u8 {
|
||||
code[ip + 1]
|
||||
}
|
||||
|
||||
/// Read a u16 at position `ip + 1` (little-endian, right after the opcode byte).
|
||||
pub fn read_u16(code: &[u8], ip: usize) -> u16 {
|
||||
(code[ip + 1] as u16) | ((code[ip + 2] as u16) << 8)
|
||||
}
|
||||
|
||||
/// Read an i16 at position `ip + 1` (little-endian, right after the opcode byte).
|
||||
pub fn read_i16(code: &[u8], ip: usize) -> i16 {
|
||||
((code[ip + 1] as u16) | ((code[ip + 2] as u16) << 8)) as i16
|
||||
}
|
||||
|
||||
/// Size in bytes of an opcode with no operand.
|
||||
pub const SIZE_OP: usize = 1;
|
||||
/// Size in bytes of an opcode with a u8 operand.
|
||||
pub const SIZE_U8: usize = 2;
|
||||
/// Size in bytes of an opcode with a u16/i16 operand.
|
||||
pub const SIZE_U16: usize = 3;
|
||||
/// Size in bytes of an opcode with a u16 + u8 operand.
|
||||
pub const SIZE_U16_PLUS1: usize = 4;
|
||||
@@ -1,8 +1,10 @@
|
||||
use super::*;
|
||||
use crate::runtime::Value;
|
||||
use crate::lexer::Lexer;
|
||||
use crate::parser::Parser;
|
||||
use crate::vm::compiler::Compiler;
|
||||
use crate::vm::vm::Vm;
|
||||
|
||||
/// Full pipeline: source → tokens → ast → interpret → last expression value.
|
||||
/// Full pipeline: source → tokens → ast → compile → vm → last expression value.
|
||||
/// Wraps in `let __result = <expr>;` so we can read the value back.
|
||||
fn eval_expr(input: &str) -> Value {
|
||||
let wrapped = format!("let __result = {};", input);
|
||||
@@ -10,25 +12,27 @@ fn eval_expr(input: &str) -> Value {
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, errors) = parser.parse();
|
||||
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
||||
let mut interpreter = Interpreter::new();
|
||||
interpreter.interpret(stmts).expect("Runtime error");
|
||||
interpreter.env.borrow().get("__result").expect("No __result in env")
|
||||
let proto = Compiler::compile(&stmts).expect("Compile error");
|
||||
let mut vm = Vm::new();
|
||||
vm.run(std::rc::Rc::new(proto)).expect("Runtime error");
|
||||
vm.get_global("__result").expect("No __result in globals")
|
||||
}
|
||||
|
||||
/// Full pipeline for multiple statements. Returns the interpreter for env inspection.
|
||||
fn run(input: &str) -> Interpreter {
|
||||
/// Full pipeline for multiple statements. Returns the VM for state inspection.
|
||||
fn run(input: &str) -> Vm {
|
||||
let (tokens, _) = Lexer::new(input).tokenize();
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, errors) = parser.parse();
|
||||
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
||||
let mut interpreter = Interpreter::new();
|
||||
interpreter.interpret(stmts).expect("Runtime error");
|
||||
interpreter
|
||||
let proto = Compiler::compile(&stmts).expect("Compile error");
|
||||
let mut vm = Vm::new();
|
||||
vm.run(std::rc::Rc::new(proto)).expect("Runtime error");
|
||||
vm
|
||||
}
|
||||
|
||||
/// Helper: get a variable from the interpreter's environment.
|
||||
fn get_var(interp: &Interpreter, name: &str) -> Value {
|
||||
interp.env.borrow().get(name).unwrap_or(Value::Nil)
|
||||
/// Helper: get a variable from the VM's globals.
|
||||
fn get_var(vm: &Vm, name: &str) -> Value {
|
||||
vm.get_global(name).unwrap_or(Value::Nil)
|
||||
}
|
||||
|
||||
/// Helper: assert a number value.
|
||||
@@ -1259,3 +1263,779 @@ fn error_forin_non_iterable() {
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for for-in on non-iterable");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// String builtins
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_split_by_comma() {
|
||||
match eval_expr("split(\"a,b,c\", \",\")") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 3);
|
||||
match &arr[0] { Value::String(s) => assert_eq!(s, "a"), v => panic!("{:?}", v) }
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_split_by_empty_delim_returns_chars() {
|
||||
match eval_expr("split(\"ab\", \"\")") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 2);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_split_no_match_returns_whole_string() {
|
||||
match eval_expr("split(\"hello\", \",\")") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 1);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_trim_spaces() {
|
||||
match eval_expr("trim(\" hello \")") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_trim_no_whitespace() {
|
||||
match eval_expr("trim(\"hello\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_substring_start() {
|
||||
match eval_expr("substring(\"hello\", 1)") {
|
||||
Value::String(s) => assert_eq!(s, "ello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_substring_start_and_length() {
|
||||
match eval_expr("substring(\"hello\", 1, 3)") {
|
||||
Value::String(s) => assert_eq!(s, "ell"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_substring_start_beyond_length() {
|
||||
match eval_expr("substring(\"hi\", 10)") {
|
||||
Value::String(s) => assert_eq!(s, ""),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_replace_single() {
|
||||
match eval_expr("replace(\"hello world\", \"world\", \"aster\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello aster"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_replace_multiple() {
|
||||
match eval_expr("replace(\"a,a,a\", \"a\", \"b\")") {
|
||||
Value::String(s) => assert_eq!(s, "b,b,b"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_replace_no_match() {
|
||||
match eval_expr("replace(\"hello\", \"x\", \"y\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_contains_true() {
|
||||
assert_bool(&eval_expr("contains(\"hello\", \"ell\")"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_contains_false() {
|
||||
assert_bool(&eval_expr("contains(\"hello\", \"xyz\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_upper() {
|
||||
match eval_expr("upper(\"hello\")") {
|
||||
Value::String(s) => assert_eq!(s, "HELLO"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_lower() {
|
||||
match eval_expr("lower(\"HELLO\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_starts_with_true() {
|
||||
assert_bool(&eval_expr("starts_with(\"hello\", \"he\")"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_starts_with_false() {
|
||||
assert_bool(&eval_expr("starts_with(\"hello\", \"lo\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_ends_with_true() {
|
||||
assert_bool(&eval_expr("ends_with(\"hello\", \"lo\")"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_ends_with_false() {
|
||||
assert_bool(&eval_expr("ends_with(\"hello\", \"he\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_split_wrong_arg_count() {
|
||||
let result = std::panic::catch_unwind(|| { eval_expr("split(\"a\")"); });
|
||||
assert!(result.is_err(), "Expected error for split with 1 arg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_trim_non_string() {
|
||||
let result = std::panic::catch_unwind(|| { eval_expr("trim(42)"); });
|
||||
assert!(result.is_err(), "Expected error for trim(42)");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Array properties & methods (dot notation)
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_array_length_property() {
|
||||
assert_num(&eval_expr("[1, 2, 3].length"), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_empty_array_length() {
|
||||
assert_num(&eval_expr("[].length"), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_length_via_variable() {
|
||||
let interp = run("let arr = [10, 20, 30, 40]; let len = arr.length;");
|
||||
assert_num(&get_var(&interp, "len"), 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_push_method_returns_value() {
|
||||
let interp = run("let arr = [1, 2]; let result = arr.push(3);");
|
||||
assert_num(&get_var(&interp, "result"), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_push_method_modifies_array() {
|
||||
let interp = run("let arr = [1, 2]; arr.push(3);");
|
||||
match get_var(&interp, "arr") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 3);
|
||||
assert_num(&arr[2], 3.0);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_push_method_chained() {
|
||||
// push returns the pushed value, so chaining .push().push() won't work
|
||||
// but push returns the pushed value, verify that independently
|
||||
let interp = run("let arr = []; let a = arr.push(1); let b = arr.push(2); let c = arr.push(3);");
|
||||
assert_num(&get_var(&interp, "a"), 1.0);
|
||||
assert_num(&get_var(&interp, "b"), 2.0);
|
||||
assert_num(&get_var(&interp, "c"), 3.0);
|
||||
match get_var(&interp, "arr") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 3);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_pop_method_returns_last() {
|
||||
assert_num(&eval_expr("[1, 2, 3].pop()"), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_pop_method_modifies_array() {
|
||||
let interp = run("let arr = [1, 2, 3]; let popped = arr.pop();");
|
||||
assert_num(&get_var(&interp, "popped"), 3.0);
|
||||
match get_var(&interp, "arr") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 2);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_array_pop_empty() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("[].pop()");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for pop() on empty array");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_array_unknown_property() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("[1, 2].foo");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for unknown array property");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_non_array_push_method() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("42.push(1)");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for .push() on non-array");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// String properties & methods (dot notation)
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_string_length_property() {
|
||||
assert_num(&eval_expr("\"hello\".length"), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_empty_string_length() {
|
||||
assert_num(&eval_expr("\"\".length"), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_length_via_variable() {
|
||||
let interp = run("let s = \"hi there\"; let len = s.length;");
|
||||
assert_num(&get_var(&interp, "len"), 8.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_upper_method() {
|
||||
match eval_expr("\"hello\".upper()") {
|
||||
Value::String(s) => assert_eq!(s, "HELLO"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_lower_method() {
|
||||
match eval_expr("\"HELLO\".lower()") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_trim_method() {
|
||||
match eval_expr("\" hi \".trim()") {
|
||||
Value::String(s) => assert_eq!(s, "hi"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_contains_method_true() {
|
||||
assert_bool(&eval_expr("\"hello\".contains(\"ell\")"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_contains_method_false() {
|
||||
assert_bool(&eval_expr("\"hello\".contains(\"xyz\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_starts_with_method() {
|
||||
assert_bool(&eval_expr("\"hello\".starts_with(\"he\")"), true);
|
||||
assert_bool(&eval_expr("\"hello\".starts_with(\"lo\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_ends_with_method() {
|
||||
assert_bool(&eval_expr("\"hello\".ends_with(\"lo\")"), true);
|
||||
assert_bool(&eval_expr("\"hello\".ends_with(\"he\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_substring_method() {
|
||||
match eval_expr("\"hello\".substring(1, 3)") {
|
||||
Value::String(s) => assert_eq!(s, "ell"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_replace_method() {
|
||||
match eval_expr("\"hello world\".replace(\"world\", \"aster\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello aster"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_split_method() {
|
||||
match eval_expr("\"a,b,c\".split(\",\")") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 3);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_method_chaining() {
|
||||
match eval_expr("\" hello \".trim().upper()") {
|
||||
Value::String(s) => assert_eq!(s, "HELLO"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_string_unknown_property() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("\"hi\".foo");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for unknown string property");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Module system (require)
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_require_math_module() {
|
||||
let interp = run("let math = require(\"tests/fixtures/math.ast\");");
|
||||
let math = get_var(&interp, "math");
|
||||
match math {
|
||||
Value::Object(obj) => {
|
||||
let obj = obj.borrow();
|
||||
assert!(obj.contains_key("add"), "math should contain 'add'");
|
||||
assert!(obj.contains_key("mul"), "math should contain 'mul'");
|
||||
assert!(obj.contains_key("version"), "math should contain 'version'");
|
||||
}
|
||||
v => panic!("Expected Object, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_call_exported_function() {
|
||||
let interp = run(r#"
|
||||
let math = require("tests/fixtures/math.ast");
|
||||
let result = math.add(10, 32);
|
||||
"#);
|
||||
assert_num(&get_var(&interp, "result"), 42.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_cache_returns_same_object() {
|
||||
let interp = run(r#"
|
||||
let a = require("tests/fixtures/math.ast");
|
||||
let b = require("tests/fixtures/math.ast");
|
||||
let same = a == b;
|
||||
"#);
|
||||
assert_bool(&get_var(&interp, "same"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_module_isolation() {
|
||||
// 模块看不到调用者的变量,但模块内部定义可以正常工作
|
||||
let interp = run(r#"
|
||||
let secret = 99;
|
||||
let g = require("tests/fixtures/greet.ast");
|
||||
let msg = g.greet("Aster");
|
||||
"#);
|
||||
match get_var(&interp, "msg") {
|
||||
Value::String(s) => assert_eq!(s, "Hello!"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_module_with_builtins() {
|
||||
// 模块内部可以使用内置函数(因为 builtins_env 是模块 env 的祖先)
|
||||
let interp = run(r#"
|
||||
let m = require("tests/fixtures/math.ast");
|
||||
let r = m.add(5, 7);
|
||||
"#);
|
||||
assert_num(&get_var(&interp, "r"), 12.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_error_file_not_found() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
run("let x = require(\"nonexistent_file_xyz.ast\");");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for missing module file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_error_no_args() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
run("let x = require();");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for require() with no args");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_error_non_string_arg() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
run("let x = require(42);");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for require() with non-string arg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_error_broken_syntax() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
run("let x = require(\"tests/fixtures/broken_syntax.ast\");");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for module with syntax error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_require_empty_module() {
|
||||
// 空模块应该返回空对象
|
||||
let interp = run("let empty = require(\"tests/fixtures/sub/mod.ast\");");
|
||||
match get_var(&interp, "empty") {
|
||||
Value::Object(obj) => {
|
||||
let obj = obj.borrow();
|
||||
assert!(obj.contains_key("sub"), "empty module should contain 'sub'");
|
||||
}
|
||||
v => panic!("Expected Object, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Exception handling: throw
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_throw_caught() {
|
||||
let vm = run("
|
||||
let result = 'none';
|
||||
try {
|
||||
throw 'caught!';
|
||||
} catch (e) {
|
||||
result = e;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "result") {
|
||||
Value::String(s) => assert_eq!(s, "caught!"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_throw_preserves_value_type() {
|
||||
let vm = run("
|
||||
let msg = '';
|
||||
let code = 0;
|
||||
try { throw 'error'; } catch (e) { msg = e; }
|
||||
try { throw 42; } catch (e) { code = e; }
|
||||
");
|
||||
match get_var(&vm, "msg") {
|
||||
Value::String(s) => assert_eq!(s, "error"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
assert_num(&get_var(&vm, "code"), 42.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_throw_caught_correct_handler() {
|
||||
let vm = run("
|
||||
let outer = 'none';
|
||||
let inner = 'none';
|
||||
try {
|
||||
try {
|
||||
throw 'inner';
|
||||
} catch (e) {
|
||||
inner = e;
|
||||
}
|
||||
} catch (e) {
|
||||
outer = e;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "inner") {
|
||||
Value::String(s) => assert_eq!(s, "inner"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
match get_var(&vm, "outer") {
|
||||
Value::String(s) => assert_eq!(s, "none"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Exception handling: runtime errors caught
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_catch_division_by_zero() {
|
||||
let vm = run("
|
||||
let msg = 'none';
|
||||
try {
|
||||
let x = 1 / 0;
|
||||
} catch (e) {
|
||||
msg = e;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "msg") {
|
||||
Value::String(s) => assert!(s.contains("Division by zero") || s.contains("division"), "got: {}", s),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_catch_undefined_variable() {
|
||||
let vm = run("
|
||||
let msg = 'none';
|
||||
try {
|
||||
let x = no_such_var;
|
||||
} catch (e) {
|
||||
msg = e;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "msg") {
|
||||
Value::String(s) => assert!(s.contains("Undefined"), "got: {}", s),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_catch_index_out_of_bounds() {
|
||||
let vm = run("
|
||||
let msg = 'none';
|
||||
try {
|
||||
let x = [1, 2][99];
|
||||
} catch (e) {
|
||||
msg = e;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "msg") {
|
||||
Value::String(s) => assert!(s.contains("out of bounds"), "got: {}", s),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_catch_call_non_function() {
|
||||
let vm = run("
|
||||
let msg = 'none';
|
||||
try {
|
||||
42();
|
||||
} catch (e) {
|
||||
msg = e;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "msg") {
|
||||
Value::String(s) => assert!(s.contains("non-function"), "got: {}", s),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_no_error_runs_normally() {
|
||||
let vm = run("
|
||||
let result = 0;
|
||||
try {
|
||||
result = 42;
|
||||
} catch (e) {
|
||||
result = -1;
|
||||
}
|
||||
");
|
||||
assert_num(&get_var(&vm, "result"), 42.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Exception handling: finally
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_finally_runs_on_success() {
|
||||
let vm = run("
|
||||
let flag = 0;
|
||||
try {
|
||||
flag = 1;
|
||||
} finally {
|
||||
flag = flag + 10;
|
||||
}
|
||||
");
|
||||
assert_num(&get_var(&vm, "flag"), 11.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_finally_runs_on_throw() {
|
||||
let vm = run("
|
||||
let flag = 0;
|
||||
let msg = 'none';
|
||||
try {
|
||||
throw 'oops';
|
||||
} catch (e) {
|
||||
msg = e;
|
||||
} finally {
|
||||
flag = flag + 10;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "msg") {
|
||||
Value::String(s) => assert_eq!(s, "oops"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
assert_num(&get_var(&vm, "flag"), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_finally_runs_on_runtime_error() {
|
||||
let vm = run("
|
||||
let flag = 0;
|
||||
let msg = 'none';
|
||||
try {
|
||||
let x = 1 / 0;
|
||||
} catch (e) {
|
||||
msg = 'caught';
|
||||
} finally {
|
||||
flag = 1;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "msg") {
|
||||
Value::String(s) => assert_eq!(s, "caught"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
assert_num(&get_var(&vm, "flag"), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_finally_without_catch() {
|
||||
// try-finally without catch: finally runs, then rethrows
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
run("
|
||||
let flag = 0;
|
||||
try {
|
||||
throw 'boom';
|
||||
} finally {
|
||||
flag = 1;
|
||||
}
|
||||
");
|
||||
});
|
||||
assert!(result.is_err(), "Expected uncaught exception");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nested_try_catch() {
|
||||
let vm = run("
|
||||
let outer = 'none';
|
||||
let inner = 'none';
|
||||
try {
|
||||
try {
|
||||
throw 'inner_error';
|
||||
} catch (e) {
|
||||
inner = e;
|
||||
}
|
||||
} catch (e) {
|
||||
outer = e;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "inner") {
|
||||
Value::String(s) => assert_eq!(s, "inner_error"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
match get_var(&vm, "outer") {
|
||||
Value::String(s) => assert_eq!(s, "none"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_throw_in_catch_propagates() {
|
||||
let vm = run("
|
||||
let outer = 'none';
|
||||
let inner = 'none';
|
||||
try {
|
||||
try {
|
||||
throw 'first';
|
||||
} catch (e) {
|
||||
inner = e;
|
||||
throw 'second';
|
||||
}
|
||||
} catch (e) {
|
||||
outer = e;
|
||||
}
|
||||
");
|
||||
match get_var(&vm, "inner") {
|
||||
Value::String(s) => assert_eq!(s, "first"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
match get_var(&vm, "outer") {
|
||||
Value::String(s) => assert_eq!(s, "second"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_try_catch_in_function() {
|
||||
let vm = run("
|
||||
fn safe_div(a, b) {
|
||||
try {
|
||||
return a / b;
|
||||
} catch (e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
let r1 = safe_div(10, 2);
|
||||
let r2 = safe_div(10, 0);
|
||||
");
|
||||
assert_num(&get_var(&vm, "r1"), 5.0);
|
||||
assert_num(&get_var(&vm, "r2"), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_finally_known_limitation_return() {
|
||||
// Note: return/break/continue inside try-finally do NOT yet run finally
|
||||
// (this is a known limitation). This test documents current behavior.
|
||||
let vm = run("
|
||||
let flag = 0;
|
||||
fn test() {
|
||||
try {
|
||||
return 42;
|
||||
} finally {
|
||||
flag = 1;
|
||||
}
|
||||
}
|
||||
let r = test();
|
||||
");
|
||||
assert_num(&get_var(&vm, "r"), 42.0);
|
||||
// flag stays 0 because finally doesn't run before return (known limitation)
|
||||
assert_num(&get_var(&vm, "flag"), 0.0);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+3
@@ -0,0 +1,3 @@
|
||||
fn broken ( {
|
||||
return 1;
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
fn greet(name) { return "Hello!"; }
|
||||
let version = 1;
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
fn add(a, b) { return a + b; }
|
||||
fn mul(a, b) { return a * b; }
|
||||
let version = "1.0";
|
||||
+1
@@ -0,0 +1 @@
|
||||
fn sub(a, b) { return a - b; }
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "aster-lsp"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
aster-core = { path = "../aster-core" }
|
||||
lsp-server = "0.7"
|
||||
lsp-types = "0.97"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,165 @@
|
||||
use aster_core::analysis::{self, SymbolKind};
|
||||
use aster_core::lexer::Lexer;
|
||||
use aster_core::parser::Parser;
|
||||
use lsp_server::{Connection, Message, Request, Response};
|
||||
use lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionList, CompletionParams, Uri,
|
||||
};
|
||||
|
||||
const KEYWORDS: &[&str] = &[
|
||||
"let", "const", "fn", "if", "else", "while", "for", "in",
|
||||
"break", "continue", "return", "true", "false", "nil",
|
||||
"try", "catch", "finally", "throw",
|
||||
];
|
||||
|
||||
const BUILTINS: &[&str] = &[
|
||||
"print", "input", "clock", "len", "typeof", "push", "pop",
|
||||
"split", "trim", "substring", "replace", "contains",
|
||||
"upper", "lower", "starts_with", "ends_with", "require",
|
||||
];
|
||||
|
||||
pub fn handle_completion(
|
||||
documents: &std::collections::HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: CompletionParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid completion params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document_position.text_document.uri;
|
||||
let position = params.text_document_position.position;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
let prefix = get_word_prefix(text, position);
|
||||
|
||||
let mut items: Vec<CompletionItem> = Vec::new();
|
||||
|
||||
// Keywords
|
||||
for kw in KEYWORDS {
|
||||
if kw.starts_with(&prefix) {
|
||||
items.push(CompletionItem {
|
||||
label: kw.to_string(),
|
||||
kind: Some(CompletionItemKind::KEYWORD),
|
||||
detail: Some("keyword".into()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Builtins
|
||||
for b in BUILTINS {
|
||||
if b.starts_with(&prefix) {
|
||||
items.push(CompletionItem {
|
||||
label: b.to_string(),
|
||||
kind: Some(CompletionItemKind::FUNCTION),
|
||||
detail: Some("builtin".into()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// User-defined symbols (from AST)
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, _) = parser.parse();
|
||||
let symbols = analysis::collect_symbols(&stmts);
|
||||
|
||||
for sym in &symbols {
|
||||
if sym.name.starts_with(&prefix) {
|
||||
let (kind, detail) = match &sym.kind {
|
||||
SymbolKind::Variable => (CompletionItemKind::VARIABLE, None),
|
||||
SymbolKind::Function { params } => (
|
||||
CompletionItemKind::FUNCTION,
|
||||
Some(format!("fn {}({})", sym.name, params.join(", "))),
|
||||
),
|
||||
};
|
||||
// Deduplicate: skip if already in the list
|
||||
if items.iter().any(|i| i.label == sym.name) {
|
||||
continue;
|
||||
}
|
||||
items.push(CompletionItem {
|
||||
label: sym.name.clone(),
|
||||
kind: Some(kind),
|
||||
detail,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let result = CompletionList {
|
||||
is_incomplete: false,
|
||||
items,
|
||||
};
|
||||
|
||||
let resp = Response::new_ok(id, result);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
|
||||
/// Extract the identifier prefix immediately before the cursor position.
|
||||
fn get_word_prefix(text: &str, position: lsp_types::Position) -> String {
|
||||
let line = match get_line(text, position.line as usize) {
|
||||
Some(l) => l,
|
||||
None => return String::new(),
|
||||
};
|
||||
|
||||
let col = position.character as usize;
|
||||
let prefix_bytes = if col > line.len() { line } else { &line[..col] };
|
||||
|
||||
// Walk backwards through the prefix to the start of an identifier
|
||||
let mut end = prefix_bytes.len();
|
||||
while end > 0 {
|
||||
let ch = prefix_bytes.as_bytes()[end - 1] as char;
|
||||
if ch.is_alphanumeric() || ch == '_' {
|
||||
end -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prefix_bytes[end..].to_string()
|
||||
}
|
||||
|
||||
/// Get the full word at a given position (for hover).
|
||||
pub fn get_word_at_position(text: &str, position: lsp_types::Position) -> String {
|
||||
let line = match get_line(text, position.line as usize) {
|
||||
Some(l) => l,
|
||||
None => return String::new(),
|
||||
};
|
||||
|
||||
let col = position.character as usize;
|
||||
let bytes = line.as_bytes();
|
||||
|
||||
// Find start of word
|
||||
let mut start = col.min(bytes.len());
|
||||
while start > 0 {
|
||||
let ch = bytes[start - 1] as char;
|
||||
if ch.is_alphanumeric() || ch == '_' {
|
||||
start -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find end of word
|
||||
let mut end = col.min(bytes.len());
|
||||
while end < bytes.len() {
|
||||
let ch = bytes[end] as char;
|
||||
if ch.is_alphanumeric() || ch == '_' {
|
||||
end += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
line[start..end].to_string()
|
||||
}
|
||||
|
||||
fn get_line(text: &str, line_idx: usize) -> Option<&str> {
|
||||
text.lines().nth(line_idx)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis;
|
||||
use aster_core::lexer::Lexer;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse, Location, Position, Range, Uri};
|
||||
|
||||
use crate::completion;
|
||||
|
||||
pub fn handle_goto_definition(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: GotoDefinitionParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid goto def params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document_position_params.text_document.uri;
|
||||
let position = params.text_document_position_params.position;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
let word = completion::get_word_at_position(text, position);
|
||||
|
||||
if word.is_empty() {
|
||||
let resp = lsp_server::Response::new_ok::<Option<GotoDefinitionResponse>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
|
||||
if let Some((line, col)) = analysis::find_declaration(&tokens, &word) {
|
||||
let l = (line as u32).saturating_sub(1);
|
||||
let c = (col as u32).saturating_sub(1);
|
||||
let location = Location {
|
||||
uri: uri.clone(),
|
||||
range: Range {
|
||||
start: Position { line: l, character: c },
|
||||
end: Position { line: l, character: c + word.len() as u32 },
|
||||
},
|
||||
};
|
||||
let response: GotoDefinitionResponse = GotoDefinitionResponse::Scalar(location);
|
||||
let resp = lsp_server::Response::new_ok(id, response);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
} else {
|
||||
let resp = lsp_server::Response::new_ok::<Option<GotoDefinitionResponse>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis::{self, SymbolKind};
|
||||
use aster_core::lexer::Lexer;
|
||||
use aster_core::parser::Parser;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{HoverContents, MarkupContent, MarkupKind, TextDocumentPositionParams, Uri};
|
||||
|
||||
use crate::completion;
|
||||
|
||||
const BUILTINS: &[&str] = &[
|
||||
"print", "input", "clock", "len", "typeof", "push", "pop",
|
||||
"split", "trim", "substring", "replace", "contains",
|
||||
"upper", "lower", "starts_with", "ends_with", "require",
|
||||
];
|
||||
|
||||
pub fn handle_hover(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: TextDocumentPositionParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid hover params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document.uri;
|
||||
let position = params.position;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
let word = completion::get_word_at_position(text, position);
|
||||
|
||||
if word.is_empty() {
|
||||
let resp = lsp_server::Response::new_ok::<Option<()>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check builtins
|
||||
if BUILTINS.contains(&word.as_str()) {
|
||||
let contents = HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("**builtin** `{}`", word),
|
||||
});
|
||||
let hover = lsp_types::Hover {
|
||||
contents,
|
||||
range: None,
|
||||
};
|
||||
let resp = lsp_server::Response::new_ok(id, hover);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check keywords
|
||||
const KEYWORDS: &[&str] = &[
|
||||
"let", "const", "fn", "if", "else", "while", "for", "in",
|
||||
"break", "continue", "return", "true", "false", "nil",
|
||||
];
|
||||
if KEYWORDS.contains(&word.as_str()) {
|
||||
let contents = HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("**keyword** `{}`", word),
|
||||
});
|
||||
let hover = lsp_types::Hover { contents, range: None };
|
||||
let resp = lsp_server::Response::new_ok(id, hover);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check user-defined symbols
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, _) = parser.parse();
|
||||
let symbols = analysis::collect_symbols(&stmts);
|
||||
|
||||
for sym in &symbols {
|
||||
if sym.name == word {
|
||||
let markdown = match &sym.kind {
|
||||
SymbolKind::Variable => {
|
||||
format!("**variable** `{}`", sym.name)
|
||||
}
|
||||
SymbolKind::Function { params } => {
|
||||
format!("**fn** `{}({})`", sym.name, params.join(", "))
|
||||
}
|
||||
};
|
||||
let contents = HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: markdown,
|
||||
});
|
||||
let hover = lsp_types::Hover { contents, range: None };
|
||||
let resp = lsp_server::Response::new_ok(id, hover);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Not found — return null
|
||||
let resp = lsp_server::Response::new_ok::<Option<()>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod completion;
|
||||
mod goto_def;
|
||||
mod hover;
|
||||
mod references;
|
||||
mod rename;
|
||||
mod server;
|
||||
mod signature;
|
||||
|
||||
fn main() {
|
||||
server::run();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis;
|
||||
use aster_core::lexer::Lexer;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{Location, Position, Range, ReferenceParams, Uri};
|
||||
|
||||
use crate::completion;
|
||||
|
||||
pub fn handle_references(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: ReferenceParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid references params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document_position.text_document.uri;
|
||||
let position = params.text_document_position.position;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
let word = completion::get_word_at_position(text, position);
|
||||
|
||||
if word.is_empty() {
|
||||
let resp = lsp_server::Response::new_ok::<Option<Vec<Location>>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
let positions = analysis::find_all_references(&tokens, &word);
|
||||
|
||||
let locations: Vec<Location> = positions
|
||||
.into_iter()
|
||||
.map(|(line, col)| {
|
||||
let l = (line as u32).saturating_sub(1);
|
||||
let c = (col as u32).saturating_sub(1);
|
||||
Location {
|
||||
uri: uri.clone(),
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: l,
|
||||
character: c,
|
||||
},
|
||||
end: Position {
|
||||
line: l,
|
||||
character: c + word.len() as u32,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let resp = lsp_server::Response::new_ok(id, locations);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis;
|
||||
use aster_core::lexer::Lexer;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{Position, Range, RenameParams, TextEdit, Uri, WorkspaceEdit};
|
||||
|
||||
use crate::completion;
|
||||
|
||||
pub fn handle_rename(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: RenameParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid rename params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document_position.text_document.uri;
|
||||
let position = params.text_document_position.position;
|
||||
let new_name = params.new_name;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
let word = completion::get_word_at_position(text, position);
|
||||
|
||||
if word.is_empty() || word == new_name {
|
||||
let resp = lsp_server::Response::new_ok::<Option<WorkspaceEdit>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
let positions = analysis::find_all_references(&tokens, &word);
|
||||
|
||||
let edits: Vec<TextEdit> = positions
|
||||
.into_iter()
|
||||
.map(|(line, col)| {
|
||||
let l = (line as u32).saturating_sub(1);
|
||||
let c = (col as u32).saturating_sub(1);
|
||||
TextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: l,
|
||||
character: c,
|
||||
},
|
||||
end: Position {
|
||||
line: l,
|
||||
character: c + word.len() as u32,
|
||||
},
|
||||
},
|
||||
new_text: new_name.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut changes = HashMap::new();
|
||||
changes.insert(uri, edits);
|
||||
|
||||
let edit = WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let resp = lsp_server::Response::new_ok(id, edit);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::lexer::Lexer;
|
||||
use aster_core::parser::Parser;
|
||||
use aster_core::error::RuntimeError;
|
||||
|
||||
use lsp_server::{Connection, Message, Notification};
|
||||
use lsp_types::{
|
||||
Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams,
|
||||
DidCloseTextDocumentParams, DidOpenTextDocumentParams, InitializeParams,
|
||||
Position, PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability,
|
||||
TextDocumentSyncKind, Uri,
|
||||
};
|
||||
|
||||
use crate::completion;
|
||||
use crate::goto_def;
|
||||
use crate::hover;
|
||||
use crate::references;
|
||||
use crate::rename;
|
||||
use crate::signature;
|
||||
|
||||
pub fn run() {
|
||||
eprintln!("Aster LSP server starting...");
|
||||
|
||||
let (connection, io_threads) = Connection::stdio();
|
||||
|
||||
let capabilities = ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||
TextDocumentSyncKind::FULL,
|
||||
)),
|
||||
completion_provider: Some(lsp_types::CompletionOptions {
|
||||
trigger_characters: Some(
|
||||
(b'a'..=b'z').chain(b'A'..=b'Z')
|
||||
.map(|c| (c as char).to_string())
|
||||
.chain(std::iter::once(".".to_string()))
|
||||
.chain(std::iter::once("_".to_string()))
|
||||
.collect()
|
||||
),
|
||||
resolve_provider: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
||||
signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
|
||||
definition_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
references_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
rename_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
..ServerCapabilities::default()
|
||||
};
|
||||
|
||||
let init_result = connection
|
||||
.initialize(serde_json::to_value(&capabilities).unwrap())
|
||||
.unwrap();
|
||||
let _init_params: InitializeParams = serde_json::from_value(init_result).unwrap();
|
||||
|
||||
eprintln!("Aster LSP initialized successfully");
|
||||
|
||||
let mut documents: HashMap<Uri, String> = HashMap::new();
|
||||
|
||||
for msg in &connection.receiver {
|
||||
match msg {
|
||||
Message::Request(req) => {
|
||||
if connection.handle_shutdown(&req).unwrap() {
|
||||
break;
|
||||
}
|
||||
handle_request(&documents, &connection, req);
|
||||
}
|
||||
Message::Notification(notif) => {
|
||||
handle_notification(&mut documents, &connection, notif);
|
||||
}
|
||||
Message::Response(_resp) => {}
|
||||
}
|
||||
}
|
||||
|
||||
io_threads.join().unwrap();
|
||||
eprintln!("Aster LSP server stopped.");
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: lsp_server::Request,
|
||||
) {
|
||||
match req.method.as_str() {
|
||||
"textDocument/completion" => {
|
||||
completion::handle_completion(documents, connection, req);
|
||||
}
|
||||
"textDocument/hover" => {
|
||||
hover::handle_hover(documents, connection, req);
|
||||
}
|
||||
"textDocument/signatureHelp" => {
|
||||
signature::handle_signature_help(documents, connection, req);
|
||||
}
|
||||
"textDocument/definition" => {
|
||||
goto_def::handle_goto_definition(documents, connection, req);
|
||||
}
|
||||
"textDocument/references" => {
|
||||
references::handle_references(documents, connection, req);
|
||||
}
|
||||
"textDocument/rename" => {
|
||||
rename::handle_rename(documents, connection, req);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unhandled request: {}", req.method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_notification(
|
||||
documents: &mut HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
notif: Notification,
|
||||
) {
|
||||
match notif.method.as_str() {
|
||||
"textDocument/didOpen" => {
|
||||
let params: DidOpenTextDocumentParams =
|
||||
serde_json::from_value(notif.params).unwrap();
|
||||
let uri = params.text_document.uri.clone();
|
||||
let text = params.text_document.text.clone();
|
||||
documents.insert(uri.clone(), text.clone());
|
||||
publish_diagnostics(connection, &uri, &text);
|
||||
}
|
||||
"textDocument/didChange" => {
|
||||
let params: DidChangeTextDocumentParams =
|
||||
serde_json::from_value(notif.params).unwrap();
|
||||
let uri = params.text_document.uri.clone();
|
||||
if let Some(change) = params.content_changes.into_iter().last() {
|
||||
documents.insert(uri.clone(), change.text.clone());
|
||||
publish_diagnostics(connection, &uri, &change.text);
|
||||
}
|
||||
}
|
||||
"textDocument/didClose" => {
|
||||
let params: DidCloseTextDocumentParams =
|
||||
serde_json::from_value(notif.params).unwrap();
|
||||
documents.remove(¶ms.text_document.uri);
|
||||
let clear = PublishDiagnosticsParams {
|
||||
uri: params.text_document.uri.clone(),
|
||||
diagnostics: vec![],
|
||||
version: None,
|
||||
};
|
||||
send_notification(connection, "textDocument/publishDiagnostics", clear);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_diagnostics(connection: &Connection, uri: &Uri, text: &str) {
|
||||
let (tokens, lex_errors) = Lexer::new(text).tokenize();
|
||||
let mut errors = lex_errors;
|
||||
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (_stmts, parse_errors) = parser.parse();
|
||||
errors.extend(parse_errors);
|
||||
|
||||
let diagnostics: Vec<Diagnostic> = errors
|
||||
.iter()
|
||||
.filter_map(error_to_diagnostic)
|
||||
.collect();
|
||||
|
||||
let params = PublishDiagnosticsParams {
|
||||
uri: uri.clone(),
|
||||
diagnostics,
|
||||
version: None,
|
||||
};
|
||||
|
||||
send_notification(connection, "textDocument/publishDiagnostics", params);
|
||||
}
|
||||
|
||||
fn error_to_diagnostic(error: &RuntimeError) -> Option<Diagnostic> {
|
||||
match error {
|
||||
RuntimeError::LexError { message, line, column } => {
|
||||
let l = (*line as u32).saturating_sub(1);
|
||||
let c = (*column as u32).saturating_sub(1);
|
||||
Some(Diagnostic {
|
||||
range: Range {
|
||||
start: Position { line: l, character: c },
|
||||
end: Position { line: l, character: c.saturating_add(1) },
|
||||
},
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
message: format!("[Lex] {}", message),
|
||||
..Diagnostic::default()
|
||||
})
|
||||
}
|
||||
RuntimeError::ParseError { message, token } => {
|
||||
let l = (token.line as u32).saturating_sub(1);
|
||||
let c = (token.column as u32).saturating_sub(1);
|
||||
Some(Diagnostic {
|
||||
range: Range {
|
||||
start: Position { line: l, character: c },
|
||||
end: Position { line: l, character: c.saturating_add(1) },
|
||||
},
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
message: format!("[Parse] {}", message),
|
||||
..Diagnostic::default()
|
||||
})
|
||||
}
|
||||
RuntimeError::RuntimeError { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_notification(connection: &Connection, method: &str, params: impl serde::Serialize) {
|
||||
let notification = Notification::new(method.to_string(), serde_json::to_value(params).unwrap());
|
||||
let _ = connection.sender.send(Message::Notification(notification));
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis::{self, SymbolKind};
|
||||
use aster_core::lexer::Lexer;
|
||||
use aster_core::parser::Parser;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{
|
||||
ParameterInformation, SignatureHelp, SignatureHelpParams,
|
||||
SignatureInformation, Uri,
|
||||
};
|
||||
|
||||
pub fn handle_signature_help(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: SignatureHelpParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid signature help params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document_position_params.text_document.uri;
|
||||
let position = params.text_document_position_params.position;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
|
||||
// Find the function being called at cursor
|
||||
let (func_name, arg_index) = find_call_at_position(text, position);
|
||||
|
||||
if func_name.is_empty() {
|
||||
let resp = lsp_server::Response::new_ok::<Option<()>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Look up the function in user symbols
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, _) = parser.parse();
|
||||
let symbols = analysis::collect_symbols(&stmts);
|
||||
|
||||
for sym in &symbols {
|
||||
if sym.name == func_name {
|
||||
if let SymbolKind::Function { params } = &sym.kind {
|
||||
let param_labels: Vec<String> = params.iter().map(|p| p.clone()).collect();
|
||||
let sig = SignatureInformation {
|
||||
label: format!("{}({})", func_name, param_labels.join(", ")),
|
||||
documentation: None,
|
||||
parameters: Some(
|
||||
param_labels
|
||||
.iter()
|
||||
.map(|p| ParameterInformation {
|
||||
label: lsp_types::ParameterLabel::Simple(p.clone()),
|
||||
documentation: None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
active_parameter: None,
|
||||
};
|
||||
let help = SignatureHelp {
|
||||
signatures: vec![sig],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(arg_index.min(param_labels.len().saturating_sub(1)) as u32),
|
||||
};
|
||||
let resp = lsp_server::Response::new_ok(id, help);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let resp = lsp_server::Response::new_ok::<Option<()>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
|
||||
/// Naive text-based detection of a function call at cursor position.
|
||||
/// Returns (function_name, argument_index).
|
||||
fn find_call_at_position(text: &str, position: lsp_types::Position) -> (String, usize) {
|
||||
let line = match text.lines().nth(position.line as usize) {
|
||||
Some(l) => l,
|
||||
None => return (String::new(), 0),
|
||||
};
|
||||
|
||||
let col = position.character as usize;
|
||||
let before_cursor = if col > line.len() { line } else { &line[..col] };
|
||||
|
||||
// Walk backwards from cursor counting parentheses depth
|
||||
let mut depth = 0;
|
||||
|
||||
let bytes = before_cursor.as_bytes();
|
||||
let mut i = bytes.len();
|
||||
|
||||
while i > 0 {
|
||||
i -= 1;
|
||||
let ch = bytes[i] as char;
|
||||
match ch {
|
||||
')' => depth += 1,
|
||||
'(' => {
|
||||
if depth > 0 {
|
||||
depth -= 1;
|
||||
} else {
|
||||
// Found the opening paren of our call
|
||||
// Count commas between here and cursor to get arg index
|
||||
let args_slice = &before_cursor[i + 1..];
|
||||
let arg_index = args_slice.bytes().filter(|&b| b == b',').count();
|
||||
|
||||
// Find the function name before '('
|
||||
let before_paren = before_cursor[..i].trim_end();
|
||||
return (extract_last_identifier(before_paren), arg_index);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(String::new(), 0)
|
||||
}
|
||||
|
||||
fn extract_last_identifier(s: &str) -> String {
|
||||
let bytes = s.as_bytes();
|
||||
let mut end = bytes.len();
|
||||
while end > 0 {
|
||||
let ch = bytes[end - 1] as char;
|
||||
if ch.is_alphanumeric() || ch == '_' || ch == '.' {
|
||||
end -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If there's a dot, take what's after it (method call)
|
||||
let ident = &s[end..];
|
||||
if let Some(dot_pos) = ident.rfind('.') {
|
||||
ident[dot_pos + 1..].to_string()
|
||||
} else {
|
||||
ident.to_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "aster"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
aster-core = { path = "../aster-core" }
|
||||
@@ -0,0 +1,24 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
match args.len() {
|
||||
1 => aster_core::run_repl(),
|
||||
2 => {
|
||||
let filename = &args[1];
|
||||
match fs::read_to_string(filename) {
|
||||
Ok(src) => aster_core::run_file(filename, src),
|
||||
Err(e) => {
|
||||
eprintln!("Error reading file '{}': {}", filename, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Usage: aster [script]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
//! 内置函数模块:按功能分组注册,便于扩展和维护。
|
||||
|
||||
mod core;
|
||||
mod io;
|
||||
mod os;
|
||||
|
||||
use crate::interpreter::{Env, Value};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// 向全局环境注册所有标准库(io、os 等)
|
||||
pub fn register_all(env: &Rc<RefCell<Env>>) {
|
||||
let mut e = env.borrow_mut();
|
||||
|
||||
// io
|
||||
let mut io = HashMap::new();
|
||||
io.insert("print".into(), Value::NativeFunction(io::print));
|
||||
io.insert("input".into(), Value::NativeFunction(io::input));
|
||||
e.define("io".into(), Value::Object(Rc::new(RefCell::new(io))), true);
|
||||
// input and print can be used as global functions for convenience
|
||||
e.define("print".into(), Value::NativeFunction(io::print), true);
|
||||
e.define("input".into(), Value::NativeFunction(io::input), true);
|
||||
|
||||
// os
|
||||
let mut os = HashMap::new();
|
||||
os.insert("clock".into(), Value::NativeFunction(os::clock));
|
||||
e.define("os".into(), Value::Object(Rc::new(RefCell::new(os))), true);
|
||||
// clock can also be used as a global function for convenience
|
||||
e.define("clock".into(), Value::NativeFunction(os::clock), true);
|
||||
|
||||
// core — len, typeof, push, pop
|
||||
e.define("len".into(), Value::NativeFunction(core::len), true);
|
||||
e.define("typeof".into(), Value::NativeFunction(core::typeof_fn), true);
|
||||
e.define("push".into(), Value::NativeFunction(core::push), true);
|
||||
e.define("pop".into(), Value::NativeFunction(core::pop), true);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
use super::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Env {
|
||||
/// (value, is_mutable) — `is_mutable` is true for `let`, false for `const`.
|
||||
pub values: HashMap<String, (Value, bool)>,
|
||||
pub parent: Option<Rc<RefCell<Env>>>,
|
||||
}
|
||||
|
||||
impl Env {
|
||||
pub fn new(parent: Option<Rc<RefCell<Env>>>) -> Self {
|
||||
Self {
|
||||
values: HashMap::new(),
|
||||
parent,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn define(&mut self, name: String, val: Value, mutable: bool) {
|
||||
self.values.insert(name, (val, mutable));
|
||||
}
|
||||
|
||||
/// Assign a new value to an existing binding. Returns `Err(msg)` if the
|
||||
/// binding exists but was declared with `const` (immutable).
|
||||
pub fn assign(&mut self, name: &str, val: Value) -> Result<bool, String> {
|
||||
if let Some((_, false)) = self.values.get(name) {
|
||||
return Err(format!("Cannot reassign constant '{}'", name));
|
||||
}
|
||||
if self.values.contains_key(name) {
|
||||
self.values.insert(name.to_string(), (val, true));
|
||||
Ok(true)
|
||||
} else if let Some(parent) = &self.parent {
|
||||
parent.borrow_mut().assign(name, val)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<Value> {
|
||||
if let Some((val, _)) = self.values.get(name) {
|
||||
Some(val.clone())
|
||||
} else if let Some(parent) = &self.parent {
|
||||
parent.borrow().get(name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,612 +0,0 @@
|
||||
use crate::ast::*;
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Signal;
|
||||
use super::{Value, Env, Function};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct Interpreter {
|
||||
pub env: Rc<RefCell<Env>>,
|
||||
}
|
||||
|
||||
impl Interpreter {
|
||||
pub fn new() -> Self {
|
||||
let env = Rc::new(RefCell::new(Env::new(None)));
|
||||
super::builtins::register_all(&env);
|
||||
Self { env }
|
||||
}
|
||||
|
||||
pub fn interpret(&mut self, statements: Vec<Stmt>) -> Result<(), RuntimeError> {
|
||||
for stmt in statements {
|
||||
self.execute(stmt)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute(&mut self, stmt: Stmt) -> Result<Signal, RuntimeError> {
|
||||
match stmt {
|
||||
Stmt::Let { name, initializer, mutable } => {
|
||||
let val = self.evaluate(initializer)?;
|
||||
self.env.borrow_mut().define(name, val, mutable);
|
||||
Ok(Signal::None)
|
||||
}
|
||||
Stmt::ExprStmt(expr) => {
|
||||
self.evaluate(expr)?;
|
||||
Ok(Signal::None)
|
||||
}
|
||||
Stmt::Block(stmts) => {
|
||||
let previous = Rc::clone(&self.env);
|
||||
self.env = Rc::new(RefCell::new(Env::new(Some(previous))));
|
||||
let mut signal = Signal::None;
|
||||
for s in stmts {
|
||||
signal = self.execute(s)?;
|
||||
if !matches!(signal, Signal::None) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let parent = self.env.borrow().parent.as_ref().unwrap().clone();
|
||||
self.env = parent;
|
||||
Ok(signal)
|
||||
}
|
||||
Stmt::If { condition, then_branch, else_branch } => {
|
||||
let cond_val = self.evaluate(condition)?;
|
||||
if self.is_truthy(&cond_val) {
|
||||
self.execute(*then_branch)
|
||||
} else if let Some(else_branch) = else_branch {
|
||||
self.execute(*else_branch)
|
||||
} else {
|
||||
Ok(Signal::None)
|
||||
}
|
||||
}
|
||||
Stmt::While { condition, body } => {
|
||||
loop {
|
||||
let cond_val = self.evaluate(condition.clone())?;
|
||||
if !self.is_truthy(&cond_val) {
|
||||
break;
|
||||
}
|
||||
match self.execute(*body.clone())? {
|
||||
Signal::Break => break,
|
||||
Signal::Continue => continue,
|
||||
sig @ Signal::Return(_) => return Ok(sig),
|
||||
Signal::None => {}
|
||||
}
|
||||
}
|
||||
Ok(Signal::None)
|
||||
}
|
||||
Stmt::For { initializer, condition, step, body } => {
|
||||
if let Some(init) = initializer {
|
||||
self.execute(*init)?;
|
||||
}
|
||||
loop {
|
||||
if let Some(cond) = &condition {
|
||||
let cond_val = self.evaluate(cond.clone())?;
|
||||
if !self.is_truthy(&cond_val) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match self.execute(*body.clone())? {
|
||||
Signal::Break => break,
|
||||
Signal::Continue => {
|
||||
if let Some(step) = &step {
|
||||
self.evaluate(step.clone())?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
sig @ Signal::Return(_) => return Ok(sig),
|
||||
Signal::None => {}
|
||||
}
|
||||
if let Some(step) = &step {
|
||||
self.evaluate(step.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(Signal::None)
|
||||
}
|
||||
Stmt::ForIn { var_name, iterable, body } => {
|
||||
let iter_val = self.evaluate(iterable)?;
|
||||
let items: Vec<Value> = match &iter_val {
|
||||
Value::Array(arr) => arr.borrow().clone(),
|
||||
Value::Object(obj) => obj.borrow().keys().map(|k| Value::String(k.clone())).collect(),
|
||||
Value::String(s) => s.chars().map(|c| Value::String(c.to_string())).collect(),
|
||||
_ => {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "for-in requires an array, object, or string".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mut signal = Signal::None;
|
||||
for item in items {
|
||||
// Create a new scope for each iteration so the loop variable
|
||||
// is isolated and break/continue clean up correctly.
|
||||
let previous = Rc::clone(&self.env);
|
||||
self.env = Rc::new(RefCell::new(Env::new(Some(previous))));
|
||||
self.env.borrow_mut().define(var_name.clone(), item, true);
|
||||
|
||||
signal = self.execute(*body.clone())?;
|
||||
// Restore parent scope before checking signal
|
||||
let parent = self.env.borrow().parent.as_ref().unwrap().clone();
|
||||
self.env = parent;
|
||||
|
||||
match signal {
|
||||
Signal::Break => { signal = Signal::None; break; }
|
||||
Signal::Continue => { signal = Signal::None; continue; }
|
||||
sig @ Signal::Return(_) => return Ok(sig),
|
||||
Signal::None => {}
|
||||
}
|
||||
}
|
||||
Ok(signal)
|
||||
}
|
||||
Stmt::Function { name, params, body } => {
|
||||
let func = Value::Function(Rc::new(Function {
|
||||
params,
|
||||
body,
|
||||
env: Rc::clone(&self.env),
|
||||
name: Some(name.clone()),
|
||||
}));
|
||||
self.env.borrow_mut().define(name, func, true);
|
||||
Ok(Signal::None)
|
||||
}
|
||||
Stmt::Return(expr_opt) => {
|
||||
if let Some(expr) = expr_opt {
|
||||
Ok(Signal::Return(self.evaluate(expr)?))
|
||||
} else {
|
||||
Ok(Signal::Return(Value::Nil))
|
||||
}
|
||||
}
|
||||
Stmt::Break => Ok(Signal::Break),
|
||||
Stmt::Continue => Ok(Signal::Continue),
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate(&mut self, expr: Expr) -> Result<Value, RuntimeError> {
|
||||
match expr {
|
||||
Expr::Literal(lit) => Ok(match lit {
|
||||
crate::ast::expr::Literal::Number(n) => Value::Number(n),
|
||||
crate::ast::expr::Literal::String(s) => Value::String(s),
|
||||
crate::ast::expr::Literal::Bool(b) => Value::Bool(b),
|
||||
crate::ast::expr::Literal::Nil => Value::Nil,
|
||||
}),
|
||||
Expr::Variable(name) => {
|
||||
match self.env.borrow().get(&name) {
|
||||
Some(val) => Ok(val),
|
||||
None => Err(RuntimeError::RuntimeError {
|
||||
message: format!("Undefined variable '{}'", name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Expr::Assign { name, op, value } => {
|
||||
let rhs = self.evaluate(*value)?;
|
||||
let val = match op {
|
||||
AssignOp::Equal => rhs,
|
||||
_ => {
|
||||
let current = self.env.borrow().get(&name).ok_or_else(|| RuntimeError::RuntimeError { message: format!("Undefined variable '{}'", name), token: None })?;
|
||||
self.apply_assign_op(current.clone(), rhs, op)?
|
||||
}
|
||||
};
|
||||
match self.env.borrow_mut().assign(&name, val.clone()) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: format!("Undefined variable '{}'", name),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
Err(msg) => {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: msg,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(val)
|
||||
}
|
||||
Expr::Get { object, name } => {
|
||||
let obj = self.evaluate(*object)?;
|
||||
match obj {
|
||||
Value::Object(map) => {
|
||||
match map.borrow().get(&name) {
|
||||
Some(val) => Ok(val.clone()),
|
||||
None => Err(RuntimeError::RuntimeError {
|
||||
message: format!("Undefined property '{}'", name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Only objects have properties".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Expr::Set { object, name, op, value } => {
|
||||
let obj = self.evaluate(*object)?;
|
||||
let rhs = self.evaluate(*value)?;
|
||||
match obj {
|
||||
Value::Object(map) => {
|
||||
let val = match op {
|
||||
AssignOp::Equal => rhs,
|
||||
_ => {
|
||||
let map_borrow = map.borrow();
|
||||
let current = map_borrow.get(&name).ok_or_else(|| RuntimeError::RuntimeError { message: format!("Property '{}' does not exist", name), token: None })?;
|
||||
self.apply_assign_op(current.clone(), rhs, op)?
|
||||
}
|
||||
};
|
||||
map.borrow_mut().insert(name, val.clone());
|
||||
Ok(val)
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Only objects have properties".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Expr::IndexGet { array, index } => {
|
||||
let target = self.evaluate(*array)?;
|
||||
let idx_val = self.evaluate(*index)?;
|
||||
// Object index access: obj["key"]
|
||||
if let Value::Object(map) = &target {
|
||||
let key = match &idx_val {
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(RuntimeError::RuntimeError {
|
||||
message: "Object index must be a string".into(),
|
||||
token: None,
|
||||
}),
|
||||
};
|
||||
return map.borrow().get(&key).cloned().ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: format!("Undefined property '{}'", key),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let i = self.as_array_index(&idx_val)?;
|
||||
match target {
|
||||
Value::Array(vec) => {
|
||||
let vec = vec.borrow();
|
||||
vec.get(i).cloned().ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: format!("Index {} out of bounds (len {})", i, vec.len()),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
Value::String(s) => {
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
chars.get(i).map(|&c| Value::String(c.to_string())).ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: format!("Index {} out of bounds (len {})", i, chars.len()),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Index access on non-array, non-string, non-object value".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Expr::IndexSet { array, index, op, value } => {
|
||||
let target = self.evaluate(*array)?;
|
||||
let idx_val = self.evaluate(*index)?;
|
||||
let rhs = self.evaluate(*value)?;
|
||||
// Object index assignment: obj["key"] = value
|
||||
if let Value::Object(map) = &target {
|
||||
let key = match &idx_val {
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(RuntimeError::RuntimeError {
|
||||
message: "Object index must be a string".into(),
|
||||
token: None,
|
||||
}),
|
||||
};
|
||||
let val = match op {
|
||||
AssignOp::Equal => rhs,
|
||||
_ => {
|
||||
let map_borrow = map.borrow();
|
||||
let current = map_borrow.get(&key).ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: format!("Property '{}' does not exist", key),
|
||||
token: None,
|
||||
})?;
|
||||
self.apply_assign_op(current.clone(), rhs, op)?
|
||||
}
|
||||
};
|
||||
map.borrow_mut().insert(key, val.clone());
|
||||
return Ok(val);
|
||||
}
|
||||
let i = self.as_array_index(&idx_val)?;
|
||||
match target {
|
||||
Value::Array(vec) => {
|
||||
let mut vec = vec.borrow_mut();
|
||||
if i >= vec.len() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: format!("Index {} out of bounds (len {})", i, vec.len()),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let val = match op {
|
||||
AssignOp::Equal => rhs,
|
||||
_ => {
|
||||
let current = vec[i].clone();
|
||||
self.apply_assign_op(current, rhs, op)?
|
||||
}
|
||||
};
|
||||
vec[i] = val.clone();
|
||||
Ok(val)
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Index assignment on non-array, non-object value".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Expr::ObjectLiteral { properties } => {
|
||||
let mut map = HashMap::new();
|
||||
for (key, value_expr) in properties {
|
||||
let value = self.evaluate(value_expr)?;
|
||||
map.insert(key, value);
|
||||
}
|
||||
Ok(Value::Object(Rc::new(RefCell::new(map))))
|
||||
}
|
||||
Expr::ArrayLiteral { elements } => {
|
||||
let mut arr = Vec::new();
|
||||
for e in elements {
|
||||
arr.push(self.evaluate(e)?);
|
||||
}
|
||||
Ok(Value::Array(Rc::new(RefCell::new(arr))))
|
||||
}
|
||||
Expr::Unary { op, right } => {
|
||||
let val = self.evaluate(*right)?;
|
||||
match op {
|
||||
crate::ast::expr::UnaryOp::Negate => match val {
|
||||
Value::Number(n) => Ok(Value::Number(-n)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Unary '-' on non-number".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
crate::ast::expr::UnaryOp::Not => Ok(Value::Bool(!self.is_truthy(&val))),
|
||||
}
|
||||
}
|
||||
Expr::Binary { left, op, right } => {
|
||||
let l = self.evaluate(*left)?;
|
||||
let r = self.evaluate(*right)?;
|
||||
match op {
|
||||
crate::ast::expr::BinaryOp::Add => {
|
||||
// If either operand is a string, coerce both to string and concatenate.
|
||||
if matches!(l, Value::String(_)) || matches!(r, Value::String(_)) {
|
||||
Ok(Value::String(format!("{}{}", l, r)))
|
||||
} else {
|
||||
match (l, r) {
|
||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a+b)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Invalid '+' operands".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::ast::expr::BinaryOp::Sub => match (l,r) {
|
||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a-b)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Invalid '-' operands".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
crate::ast::expr::BinaryOp::Mul => match (l,r) {
|
||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a*b)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Invalid '*' operands".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
crate::ast::expr::BinaryOp::Div => match (l,r) {
|
||||
(Value::Number(_), Value::Number(b)) if b == 0.0 => {
|
||||
Err(RuntimeError::RuntimeError {
|
||||
message: "Division by zero".to_string(),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a/b)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Invalid '/' operands".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
crate::ast::expr::BinaryOp::Mod => match (l,r) {
|
||||
(Value::Number(_), Value::Number(b)) if b == 0.0 => {
|
||||
Err(RuntimeError::RuntimeError {
|
||||
message: "Modulo by zero".to_string(),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a%b)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Invalid '%' operands".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
crate::ast::expr::BinaryOp::Greater => Ok(Value::Bool(self.as_number(&l)? > self.as_number(&r)?)),
|
||||
crate::ast::expr::BinaryOp::GreaterEqual => Ok(Value::Bool(self.as_number(&l)? >= self.as_number(&r)?)),
|
||||
crate::ast::expr::BinaryOp::Less => Ok(Value::Bool(self.as_number(&l)? < self.as_number(&r)?)),
|
||||
crate::ast::expr::BinaryOp::LessEqual => Ok(Value::Bool(self.as_number(&l)? <= self.as_number(&r)?)),
|
||||
crate::ast::expr::BinaryOp::Equal => Ok(Value::Bool(self.is_equal(&l,&r))),
|
||||
crate::ast::expr::BinaryOp::NotEqual => Ok(Value::Bool(!self.is_equal(&l,&r))),
|
||||
}
|
||||
}
|
||||
Expr::Logical { left, op, right } => {
|
||||
let l = self.evaluate(*left)?;
|
||||
match op {
|
||||
crate::ast::expr::LogicalOp::And => {
|
||||
Ok(if !self.is_truthy(&l) { l } else { self.evaluate(*right)? })
|
||||
}
|
||||
crate::ast::expr::LogicalOp::Or => {
|
||||
Ok(if self.is_truthy(&l) { l } else { self.evaluate(*right)? })
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Ternary { condition, then_branch, else_branch } => {
|
||||
let cond = self.evaluate(*condition)?;
|
||||
if self.is_truthy(&cond) {
|
||||
self.evaluate(*then_branch)
|
||||
} else {
|
||||
self.evaluate(*else_branch)
|
||||
}
|
||||
}
|
||||
Expr::Call { callee, arguments } => {
|
||||
let func = self.evaluate(*callee)?;
|
||||
let mut args = Vec::new();
|
||||
for e in arguments {
|
||||
args.push(self.evaluate(e)?);
|
||||
}
|
||||
self.call_function(func, args)
|
||||
}
|
||||
Expr::Lambda { params, body } => {
|
||||
Ok(Value::Function(Rc::new(Function {
|
||||
params,
|
||||
body,
|
||||
env: Rc::clone(&self.env),
|
||||
name: None,
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn call_function(&mut self, func_val: Value, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
match func_val {
|
||||
Value::NativeFunction(native_fn) => {
|
||||
native_fn(args)
|
||||
}
|
||||
Value::Function(f) => {
|
||||
let env = Rc::new(RefCell::new(Env::new(Some(Rc::clone(&f.env)))));
|
||||
|
||||
if let Some(name) = &f.name {
|
||||
env.borrow_mut().define(name.clone(), Value::Function(Rc::clone(&f)), true);
|
||||
}
|
||||
|
||||
for (i,param) in f.params.iter().enumerate() {
|
||||
let val = args.get(i).cloned().unwrap_or(Value::Nil);
|
||||
env.borrow_mut().define(param.clone(), val, true);
|
||||
}
|
||||
|
||||
let previous = Rc::clone(&self.env);
|
||||
self.env = env;
|
||||
let mut ret = Value::Nil;
|
||||
for stmt in &f.body {
|
||||
match self.execute(stmt.clone())? {
|
||||
Signal::Return(val) => { ret = val; break; }
|
||||
Signal::None => {}
|
||||
Signal::Break | Signal::Continue => {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "break/continue outside of loop".to_string(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
self.env = previous;
|
||||
Ok(ret)
|
||||
}
|
||||
_ => {
|
||||
Err(RuntimeError::RuntimeError {
|
||||
message: "Attempt to call non-function".to_string(),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_truthy(&self, val: &Value) -> bool {
|
||||
match val {
|
||||
Value::Nil => false,
|
||||
Value::Bool(b) => *b,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_equal(&self, a: &Value, b: &Value) -> bool {
|
||||
match (a,b) {
|
||||
(Value::Nil, Value::Nil) => true,
|
||||
(Value::Bool(x), Value::Bool(y)) => x==y,
|
||||
(Value::Number(x), Value::Number(y)) => x==y,
|
||||
(Value::String(x), Value::String(y)) => x==y,
|
||||
(Value::Array(x), Value::Array(y)) => {
|
||||
let x = x.borrow();
|
||||
let y = y.borrow();
|
||||
if x.len() != y.len() { return false; }
|
||||
x.iter().zip(y.iter()).all(|(a,b)| self.is_equal(a,b))
|
||||
}
|
||||
(Value::Object(x), Value::Object(y)) => {
|
||||
let x = x.borrow();
|
||||
let y = y.borrow();
|
||||
if x.len() != y.len() { return false; }
|
||||
x.iter().all(|(k, v)| {
|
||||
y.get(k).map_or(false, |yv| self.is_equal(v, yv))
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_assign_op(&self, left: Value, right: Value, op: AssignOp) -> Result<Value, RuntimeError> {
|
||||
match op {
|
||||
AssignOp::Equal => Ok(right),
|
||||
AssignOp::PlusEqual => {
|
||||
if let Value::String(s) = &left {
|
||||
Ok(Value::String(format!("{}{}", s, right)))
|
||||
} else {
|
||||
let left_num = self.as_number(&left)?;
|
||||
let right_num = self.as_number(&right)?;
|
||||
Ok(Value::Number(left_num + right_num))
|
||||
}
|
||||
}
|
||||
AssignOp::MinusEqual => {
|
||||
let left_num = self.as_number(&left)?;
|
||||
let right_num = self.as_number(&right)?;
|
||||
Ok(Value::Number(left_num - right_num))
|
||||
}
|
||||
AssignOp::StarEqual => {
|
||||
let left_num = self.as_number(&left)?;
|
||||
let right_num = self.as_number(&right)?;
|
||||
Ok(Value::Number(left_num * right_num))
|
||||
}
|
||||
AssignOp::SlashEqual => {
|
||||
let left_num = self.as_number(&left)?;
|
||||
let right_num = self.as_number(&right)?;
|
||||
Ok(Value::Number(left_num / right_num))
|
||||
}
|
||||
AssignOp::PercentEqual => {
|
||||
let left_num = self.as_number(&left)?;
|
||||
let right_num = self.as_number(&right)?;
|
||||
Ok(Value::Number(left_num % right_num))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_number(&self, val: &Value) -> Result<f64, RuntimeError> {
|
||||
if let Value::Number(n) = val {
|
||||
Ok(*n)
|
||||
} else {
|
||||
Err(RuntimeError::RuntimeError {
|
||||
message: "Expected number".to_string(),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn as_array_index(&self, val: &Value) -> Result<usize, RuntimeError> {
|
||||
if let Value::Number(n) = val {
|
||||
if *n < 0.0 || n.fract() != 0.0 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: format!("Index must be a non-negative integer, got {}", n),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
Ok(*n as usize)
|
||||
} else {
|
||||
Err(RuntimeError::RuntimeError {
|
||||
message: "Index must be a number".to_string(),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,87 +0,0 @@
|
||||
pub mod builtins;
|
||||
pub mod env;
|
||||
pub mod interpreter;
|
||||
|
||||
pub use env::Env;
|
||||
pub use interpreter::Interpreter;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt;
|
||||
|
||||
pub type NativeFn = fn(Vec<Value>) -> Result<Value, crate::error::RuntimeError>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Value {
|
||||
Number(f64),
|
||||
String(String),
|
||||
Bool(bool),
|
||||
Nil,
|
||||
Object(Rc<RefCell<HashMap<String, Value>>>),
|
||||
Array(Rc<RefCell<Vec<Value>>>),
|
||||
Function(Rc<Function>),
|
||||
NativeFunction(NativeFn),
|
||||
}
|
||||
|
||||
pub enum Signal {
|
||||
None, // 正常执行
|
||||
Return(Value), // return 语句携带的返回值
|
||||
Break, // break 信号
|
||||
Continue, // continue 信号
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::Number(n) => write!(f, "Number({:?})", n),
|
||||
Value::String(s) => write!(f, "String({:?})", s),
|
||||
Value::Bool(b) => write!(f, "Bool({:?})", b),
|
||||
Value::Nil => write!(f, "Nil"),
|
||||
Value::Object(_) => write!(f, "Object(...)"),
|
||||
Value::Array(_) => write!(f, "Array(...)"),
|
||||
Value::Function(_) => write!(f, "Function(...)"),
|
||||
Value::NativeFunction(_) => write!(f, "NativeFunction(...)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Value::Number(n) => write!(f, "{}", n),
|
||||
Value::String(s) => write!(f, "{}", s),
|
||||
Value::Bool(b) => write!(f, "{}", b),
|
||||
Value::Nil => write!(f, "nil"),
|
||||
Value::Object(obj) => {
|
||||
let obj = obj.borrow();
|
||||
write!(f, "{{ ")?;
|
||||
for (key, value) in obj.iter() {
|
||||
write!(f, "{}: {}, ", key, value)?;
|
||||
}
|
||||
write!(f, "}}")
|
||||
},
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
write!(f, "[")?;
|
||||
for (i, val) in arr.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{}", val)?;
|
||||
}
|
||||
write!(f, "]")
|
||||
},
|
||||
Value::Function(_) => write!(f, "<function>"),
|
||||
Value::NativeFunction(_) => write!(f, "<native function>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Function {
|
||||
pub params: Vec<String>,
|
||||
pub body: Vec<crate::ast::Stmt>,
|
||||
pub env: Rc<RefCell<Env>>, // 闭包捕获环境
|
||||
pub name: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.vscode/**
|
||||
node_modules/**
|
||||
.gitignore
|
||||
@@ -0,0 +1,43 @@
|
||||
const path = require('path');
|
||||
const { LanguageClient, TransportKind } = require('vscode-languageclient/node');
|
||||
|
||||
/** @type {LanguageClient} */
|
||||
let client;
|
||||
|
||||
/**
|
||||
* @param {import('vscode').ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const serverExe = isWindows ? 'aster-lsp.exe' : 'aster-lsp';
|
||||
|
||||
// __dirname resolves to the real filesystem path (following the junction),
|
||||
// so .. goes to the workspace root where target/ lives
|
||||
const serverPath = path.join(__dirname, '..', 'target', 'debug', serverExe);
|
||||
|
||||
const serverOptions = {
|
||||
command: serverPath,
|
||||
args: [],
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
documentSelector: [{ scheme: 'file', language: 'aster' }],
|
||||
};
|
||||
|
||||
client = new LanguageClient(
|
||||
'aster-lsp',
|
||||
'Aster Language Server',
|
||||
serverOptions,
|
||||
clientOptions
|
||||
);
|
||||
|
||||
client.start();
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
if (client) {
|
||||
return client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "//",
|
||||
"blockComment": ["/*", "*/"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"" },
|
||||
{ "open": "'", "close": "'" }
|
||||
],
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"]
|
||||
],
|
||||
"wordPattern": "[a-zA-Z_][a-zA-Z0-9_]*"
|
||||
}
|
||||
Generated
+96
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "aster-lang",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aster-lang",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz",
|
||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageclient": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
|
||||
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.1.0",
|
||||
"semver": "^7.3.7",
|
||||
"vscode-languageserver-protocol": "3.17.5"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.82.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-protocol": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vscode-jsonrpc": "8.2.0",
|
||||
"vscode-languageserver-types": "3.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-types": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "aster-lang",
|
||||
"displayName": "Aster Language Support",
|
||||
"description": "Syntax highlighting and language support for the Aster scripting language",
|
||||
"version": "0.1.0",
|
||||
"publisher": "aster",
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"main": "./extension.js",
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "aster",
|
||||
"aliases": [
|
||||
"Aster",
|
||||
"aster"
|
||||
],
|
||||
"extensions": [
|
||||
".ast"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "aster",
|
||||
"scopeName": "source.aster",
|
||||
"path": "./syntaxes/aster.tmLanguage.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "Aster",
|
||||
"scopeName": "source.aster",
|
||||
"fileTypes": ["ast"],
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{ "include": "#strings" },
|
||||
{ "include": "#numbers" },
|
||||
{ "include": "#function-declaration" },
|
||||
{ "include": "#keywords" },
|
||||
{ "include": "#constants" },
|
||||
{ "include": "#builtins" },
|
||||
{ "include": "#operators" },
|
||||
{ "include": "#punctuation" },
|
||||
{ "include": "#identifiers" }
|
||||
],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.aster",
|
||||
"begin": "/\\*",
|
||||
"end": "\\*/",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.aster",
|
||||
"match": "."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "comment.line.double-slash.aster",
|
||||
"match": "//.*$"
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.quoted.double.aster",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.aster",
|
||||
"match": "\\\\[ntr\"\\\\]"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.aster",
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.aster",
|
||||
"match": "\\\\[ntr'\\\\]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"numbers": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.numeric.float.aster",
|
||||
"match": "\\b\\d+\\.\\d+\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.numeric.integer.aster",
|
||||
"match": "\\b\\d+\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"function-declaration": {
|
||||
"match": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.control.aster" },
|
||||
"2": { "name": "entity.name.function.aster" }
|
||||
}
|
||||
},
|
||||
"keywords": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.control.aster",
|
||||
"match": "\\b(let|const|fn|if|else|while|for|in|break|continue|return|try|catch|finally|throw)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"constants": {
|
||||
"match": "\\b(true|false|nil)\\b",
|
||||
"name": "constant.language.aster"
|
||||
},
|
||||
"builtins": {
|
||||
"match": "\\b(print|input|clock|len|typeof|push|pop|split|trim|substring|replace|contains|upper|lower|starts_with|ends_with|require)\\b",
|
||||
"name": "support.function.builtin.aster"
|
||||
},
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.assignment.compound.aster",
|
||||
"match": "\\+=|-=|\\*=|/=|%="
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.aster",
|
||||
"match": "&&|\\|\\||!"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.aster",
|
||||
"match": "==|!=|<=|>="
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.aster",
|
||||
"match": "="
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.aster",
|
||||
"match": "[+\\-*/%]"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.aster",
|
||||
"match": "<|>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"punctuation": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "punctuation.section.scope.aster",
|
||||
"match": "[{}]"
|
||||
},
|
||||
{
|
||||
"name": "punctuation.section.brackets.aster",
|
||||
"match": "[\\[\\]]"
|
||||
},
|
||||
{
|
||||
"name": "punctuation.section.parens.aster",
|
||||
"match": "[\\(\\)]"
|
||||
},
|
||||
{
|
||||
"name": "punctuation.separator.aster",
|
||||
"match": "[,;:\\.]"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.ternary.question.aster",
|
||||
"match": "\\?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"identifiers": {
|
||||
"match": "[a-zA-Z_][a-zA-Z0-9_]*",
|
||||
"name": "variable.other.aster"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user