feat: IDE支持 — workspace拆分、LSP服务器、VS Code扩展
**Workspace 拆分** - aster-core: 纯库,zero-dependency,包含 lexer/parser/ast/interpreter/error/analysis - aster: REPL 二进制,薄封装 aster-core - aster-lsp: LSP 语言服务器 **VS Code 扩展 (vscode-ext/)** - TextMate 语法高亮 (.ast 文件) - 语言配置 (注释切换、括号配对、自动缩进) - LSP 客户端 (extension.js) **LSP 服务端功能** - Diagnostics: 实时显示 lex/parse 错误红色波浪线 - Completion: 关键字 + 内置函数 + 用户定义符号补全 - Hover: 悬停显示变量/函数信息 - Signature Help: 函数参数提示 - Goto Definition: Ctrl+Click 跳转到声明处 - Find References: 查找所有引用位置 - Rename: F2 重命名符号 **新增 analysis 模块** - collect_symbols: AST 遍历收集符号 - find_declaration/find_all_references: Token 扫描定位声明和引用
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "aster-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,39 @@
|
||||
let arr = [10, 20, 30, 40, 50]
|
||||
print(arr)
|
||||
|
||||
print(arr[0] + arr[4])
|
||||
|
||||
let i = 1
|
||||
while(i < 4) {
|
||||
print(arr[i])
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
arr[0] = 99
|
||||
print(arr)
|
||||
|
||||
for (let j = 0; j < 3; j = j + 1) {
|
||||
arr[j] = arr[j] * 2
|
||||
}
|
||||
print(arr)
|
||||
|
||||
let nested = [[1, 2], [3, 4], [5, 6]]
|
||||
print(nested[1][0])
|
||||
|
||||
let empty = []
|
||||
print(empty)
|
||||
|
||||
let mixed = [1, "hello", true, nil]
|
||||
print(mixed)
|
||||
print(mixed[1][0])
|
||||
|
||||
let same = [1, 2, 3]
|
||||
let other = [1, 2, 3]
|
||||
print(same == other)
|
||||
print(same != [3, 2, 1])
|
||||
|
||||
let item = { name: "sword", price: 100 }
|
||||
let items = [item, { name: "shield", price: 50 }]
|
||||
print(items[0].name)
|
||||
items[1].price = 75
|
||||
print(items[1].price)
|
||||
@@ -0,0 +1,23 @@
|
||||
let a = 1
|
||||
a += 2
|
||||
print("a=", a)
|
||||
|
||||
let b = 2
|
||||
b -= 1
|
||||
print("b=", b)
|
||||
|
||||
let c = 5
|
||||
c *= 5
|
||||
print("c=", c)
|
||||
|
||||
let d = 10
|
||||
c /= 5
|
||||
print("d=", d)
|
||||
|
||||
let arr = [1, 2, 3]
|
||||
arr[0] += 2
|
||||
print(arr)
|
||||
|
||||
let obj = { name: "elmma", age: 26 }
|
||||
obj.age -= 2
|
||||
print(obj)
|
||||
@@ -0,0 +1,27 @@
|
||||
// test break
|
||||
let i = 0
|
||||
while (i < 10) {
|
||||
if (i == 5) { break }
|
||||
i = i + 1
|
||||
}
|
||||
print("break (expect 5):", i)
|
||||
|
||||
// test continue
|
||||
let sum = 0
|
||||
let j = 0
|
||||
while (j < 10) {
|
||||
j = j + 1
|
||||
if (j == 5) { continue }
|
||||
sum = sum + j
|
||||
}
|
||||
print("continue (expect 50):", sum)
|
||||
|
||||
// test for + break
|
||||
let found = -1
|
||||
for (let k = 0; k < 100; k = k + 1) {
|
||||
if (k * k > 50) {
|
||||
found = k
|
||||
break
|
||||
}
|
||||
}
|
||||
print("for break (expect 8):", found)
|
||||
@@ -0,0 +1,21 @@
|
||||
// Quick smoke test for new builtins
|
||||
print("--- len ---")
|
||||
print(len("hello"))
|
||||
print(len([1, 2, 3]))
|
||||
print(len({a: 1, b: 2, c: 3}))
|
||||
|
||||
print("--- typeof ---")
|
||||
print(typeof(42))
|
||||
print(typeof("hi"))
|
||||
print(typeof(true))
|
||||
print(typeof(nil))
|
||||
print(typeof([1, 2]))
|
||||
print(typeof({a: 1}))
|
||||
print(typeof(print))
|
||||
|
||||
print("--- push/pop ---")
|
||||
let arr = [1, 2]
|
||||
print("push:", push(arr, 3))
|
||||
print("arr:", arr)
|
||||
print("pop:", pop(arr))
|
||||
print("arr:", arr)
|
||||
@@ -0,0 +1,23 @@
|
||||
// for-in loop demo
|
||||
print("=== array ===")
|
||||
let arr = [10, 20, 30]
|
||||
for (x in arr) {
|
||||
print(x)
|
||||
}
|
||||
|
||||
print("=== object keys + values ===")
|
||||
let obj = {name: "Aster", year: 2025}
|
||||
for (k in obj) {
|
||||
print(k + ": " + obj[k])
|
||||
}
|
||||
|
||||
print("=== string chars ===")
|
||||
for (c in "Hi") {
|
||||
print(c)
|
||||
}
|
||||
|
||||
print("=== break ===")
|
||||
for (x in [1, 2, 3, 4, 5]) {
|
||||
if (x == 3) { break }
|
||||
print(x)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
let name = io.input("Your name: ");
|
||||
io.print("Hello, " + name)
|
||||
@@ -0,0 +1,9 @@
|
||||
let i = 0;
|
||||
while(i < 10) {
|
||||
print(i);
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 10; i = i + 1) {
|
||||
print(i)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fn add(a, b) { return a + b; }
|
||||
fn mul(a, b) { return a * b; }
|
||||
@@ -0,0 +1,4 @@
|
||||
let obj = { name: "aster", version: 1 }
|
||||
print(obj.name)
|
||||
obj.version = obj.version + 1
|
||||
print(obj.version)
|
||||
@@ -0,0 +1,5 @@
|
||||
let a = 7
|
||||
let b = a % 3
|
||||
print("b=", b)
|
||||
a %= 4
|
||||
print("a=", a)
|
||||
@@ -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,40 @@
|
||||
// 压测:循环、递归、闭包
|
||||
|
||||
fn fib(n) {
|
||||
if (n <= 1) {
|
||||
return n
|
||||
}
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
|
||||
fn make_adder(x) {
|
||||
return fn(y) { return x + y }
|
||||
}
|
||||
|
||||
let t0 = clock()
|
||||
|
||||
// 1. 循环压测:100万次算术
|
||||
let sum = 0
|
||||
let i = 0
|
||||
while (i < 1000000) {
|
||||
sum = sum + i
|
||||
i = i + 1
|
||||
}
|
||||
print("loop 1M:", sum)
|
||||
|
||||
// 2. 递归压测:fib(25)
|
||||
let f = fib(25)
|
||||
print("fib(25):", f)
|
||||
|
||||
// 3. 闭包压测:100万次调用
|
||||
let add10 = make_adder(10)
|
||||
let j = 0
|
||||
let total = 0
|
||||
while (j < 1000000) {
|
||||
total = add10(j)
|
||||
j = j + 1
|
||||
}
|
||||
print("closure 1M calls:", total)
|
||||
|
||||
let t1 = clock()
|
||||
print("total time (s):", t1 - t0)
|
||||
@@ -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,244 @@
|
||||
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 => {}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use crate::ast::stmt::Stmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Expr {
|
||||
/// 字面量:number / string / bool / nil
|
||||
Literal(Literal),
|
||||
|
||||
/// 变量引用
|
||||
Variable(String),
|
||||
|
||||
/// 赋值表达式:a = b
|
||||
Assign {
|
||||
name: String,
|
||||
op: AssignOp,
|
||||
value: Box<Expr>,
|
||||
},
|
||||
|
||||
/// 属性访问表达式:object.name
|
||||
Get {
|
||||
object: Box<Expr>,
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// 属性赋值表达式:object.name = value
|
||||
Set {
|
||||
object: Box<Expr>,
|
||||
name: String,
|
||||
op: AssignOp,
|
||||
value: Box<Expr>,
|
||||
},
|
||||
|
||||
/// 对象字面量:{ key: value, ... }
|
||||
ObjectLiteral {
|
||||
properties: Vec<(String, Expr)>,
|
||||
},
|
||||
|
||||
/// 数组字面量:[ expr, expr, ... ]
|
||||
ArrayLiteral {
|
||||
elements: Vec<Expr>,
|
||||
},
|
||||
|
||||
/// 索引访问:expr[index]
|
||||
IndexGet {
|
||||
array: Box<Expr>,
|
||||
index: Box<Expr>,
|
||||
},
|
||||
|
||||
/// 索引赋值:expr[index] = value
|
||||
IndexSet {
|
||||
array: Box<Expr>,
|
||||
index: Box<Expr>,
|
||||
op: AssignOp,
|
||||
value: Box<Expr>,
|
||||
},
|
||||
|
||||
/// 一元运算:!expr / -expr
|
||||
Unary {
|
||||
op: UnaryOp,
|
||||
right: Box<Expr>,
|
||||
},
|
||||
|
||||
/// 二元运算:a + b
|
||||
Binary {
|
||||
left: Box<Expr>,
|
||||
op: BinaryOp,
|
||||
right: Box<Expr>,
|
||||
},
|
||||
|
||||
/// 逻辑运算:&& ||
|
||||
Logical {
|
||||
left: Box<Expr>,
|
||||
op: LogicalOp,
|
||||
right: Box<Expr>,
|
||||
},
|
||||
|
||||
/// 三元运算:condition ? then_branch : else_branch
|
||||
Ternary {
|
||||
condition: Box<Expr>,
|
||||
then_branch: Box<Expr>,
|
||||
else_branch: Box<Expr>,
|
||||
},
|
||||
|
||||
/// 函数调用
|
||||
Call {
|
||||
callee: Box<Expr>,
|
||||
arguments: Vec<Expr>,
|
||||
},
|
||||
|
||||
/// 匿名函数(为闭包预留)
|
||||
Lambda {
|
||||
params: Vec<String>,
|
||||
body: Vec<Stmt>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Literal {
|
||||
Number(f64),
|
||||
String(String),
|
||||
Bool(bool),
|
||||
Nil,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum UnaryOp {
|
||||
Negate, // -
|
||||
Not, // !
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum BinaryOp {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
Mod,
|
||||
|
||||
Greater,
|
||||
GreaterEqual,
|
||||
Less,
|
||||
LessEqual,
|
||||
|
||||
Equal,
|
||||
NotEqual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum LogicalOp {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AssignOp {
|
||||
Equal, // =
|
||||
PlusEqual, // +=
|
||||
MinusEqual, // -=
|
||||
StarEqual, // *=
|
||||
SlashEqual, // /=
|
||||
PercentEqual, // %=
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod expr;
|
||||
pub mod stmt;
|
||||
|
||||
pub use expr::*;
|
||||
pub use stmt::*;
|
||||
@@ -0,0 +1,61 @@
|
||||
use crate::ast::expr::Expr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Stmt {
|
||||
/// let x = expr; or const x = expr;
|
||||
Let {
|
||||
name: String,
|
||||
initializer: Expr,
|
||||
mutable: bool, // true for let, false for const
|
||||
},
|
||||
|
||||
/// 表达式语句:expr;
|
||||
ExprStmt(Expr),
|
||||
|
||||
/// 代码块:{ ... }
|
||||
Block(Vec<Stmt>),
|
||||
|
||||
/// if 语句
|
||||
If {
|
||||
condition: Expr,
|
||||
then_branch: Box<Stmt>,
|
||||
else_branch: Option<Box<Stmt>>,
|
||||
},
|
||||
|
||||
/// while 循环
|
||||
While {
|
||||
condition: Expr,
|
||||
body: Box<Stmt>,
|
||||
},
|
||||
|
||||
/// for 循环 (C-style)
|
||||
For {
|
||||
initializer: Option<Box<Stmt>>,
|
||||
condition: Option<Expr>,
|
||||
step: Option<Expr>,
|
||||
body: Box<Stmt>,
|
||||
},
|
||||
|
||||
/// for-in 循环: for (var in iterable) body
|
||||
ForIn {
|
||||
var_name: String,
|
||||
iterable: Expr,
|
||||
body: Box<Stmt>,
|
||||
},
|
||||
|
||||
/// 函数声明
|
||||
Function {
|
||||
name: String,
|
||||
params: Vec<String>,
|
||||
body: Vec<Stmt>,
|
||||
},
|
||||
|
||||
/// return expr?;
|
||||
Return(Option<Expr>),
|
||||
|
||||
/// break
|
||||
Break,
|
||||
|
||||
/// continue
|
||||
Continue,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::lexer::Token;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RuntimeError {
|
||||
LexError { message: String, line: usize, column: usize },
|
||||
ParseError { message: String, token: Token },
|
||||
RuntimeError { message: String, token: Option<Token> },
|
||||
}
|
||||
|
||||
impl RuntimeError {
|
||||
pub fn lex(message: impl Into<String>, line: usize, column: usize) -> Self {
|
||||
RuntimeError::LexError {
|
||||
message: message.into(),
|
||||
line,
|
||||
column,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(message: impl Into<String>, token: Token) -> Self {
|
||||
RuntimeError::ParseError {
|
||||
message: message.into(),
|
||||
token,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RuntimeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RuntimeError::LexError { message, line, column } => {
|
||||
write!(f, "[Lex Error] {} at line {}, column {}", message, line, column)
|
||||
}
|
||||
RuntimeError::ParseError { message, token } => {
|
||||
write!(f, "[Parse Error] {} at line {}, column {}", message, token.line, token.column)
|
||||
}
|
||||
RuntimeError::RuntimeError { message, token } => {
|
||||
if let Some(tok) = token {
|
||||
write!(f, "[Runtime Error] {} at line {}, column {}", message, tok.line, tok.column)
|
||||
} else {
|
||||
write!(f, "[Runtime Error] {}", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RuntimeError {}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod error;
|
||||
|
||||
pub use error::*;
|
||||
@@ -0,0 +1,84 @@
|
||||
//! 标准库:core(len, typeof, push, pop)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::{Interpreter, Value};
|
||||
|
||||
pub fn len(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "len() expects 1 argument".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(s) => Ok(Value::Number(s.chars().count() as f64)),
|
||||
Value::Array(arr) => Ok(Value::Number(arr.borrow().len() as f64)),
|
||||
Value::Object(obj) => Ok(Value::Number(obj.borrow().len() as f64)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "len() expects a string, array, or object".into(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typeof_fn(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "typeof() expects 1 argument".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = match &args[0] {
|
||||
Value::Number(_) => "number",
|
||||
Value::String(_) => "string",
|
||||
Value::Bool(_) => "bool",
|
||||
Value::Nil => "nil",
|
||||
Value::Object(_) => "object",
|
||||
Value::Array(_) => "array",
|
||||
Value::Function(_) => "function",
|
||||
Value::NativeFunction(_) => "native_function",
|
||||
};
|
||||
Ok(Value::String(s.into()))
|
||||
}
|
||||
|
||||
pub fn push(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "push() expects 2 arguments (array, value)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let val = args[1].clone();
|
||||
match &args[0] {
|
||||
Value::Array(arr) => {
|
||||
arr.borrow_mut().push(val.clone());
|
||||
Ok(val)
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "push() expects an array as first argument".into(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "pop() expects 1 argument (array)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Array(arr) => arr
|
||||
.borrow_mut()
|
||||
.pop()
|
||||
.ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: "pop() on empty array".into(),
|
||||
token: None,
|
||||
}),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "pop() expects an array as first argument".into(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//! 标准库:io(print, input)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::{Interpreter, Value};
|
||||
|
||||
pub fn print(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
for arg in args.iter() {
|
||||
print!("{}", arg);
|
||||
}
|
||||
println!();
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
|
||||
pub fn input(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if let Some(prompt) = args.get(0) {
|
||||
print!("{}", prompt);
|
||||
std::io::Write::flush(&mut std::io::stdout()).unwrap();
|
||||
}
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin().read_line(&mut buffer).unwrap();
|
||||
Ok(Value::String(buffer.trim_end().to_string()))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//! 内置函数模块:按功能分组注册,便于扩展和维护。
|
||||
|
||||
mod core;
|
||||
mod io;
|
||||
mod os;
|
||||
pub mod string;
|
||||
|
||||
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(Rc::new(io::print)));
|
||||
io.insert("input".into(), Value::NativeFunction(Rc::new(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(Rc::new(io::print)), true);
|
||||
e.define("input".into(), Value::NativeFunction(Rc::new(io::input)), true);
|
||||
|
||||
// os
|
||||
let mut os = HashMap::new();
|
||||
os.insert("clock".into(), Value::NativeFunction(Rc::new(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(Rc::new(os::clock)), true);
|
||||
|
||||
// core — len, typeof, push, pop
|
||||
e.define("len".into(), Value::NativeFunction(Rc::new(core::len)), true);
|
||||
e.define("typeof".into(), Value::NativeFunction(Rc::new(core::typeof_fn)), true);
|
||||
e.define("push".into(), Value::NativeFunction(Rc::new(core::push)), true);
|
||||
e.define("pop".into(), Value::NativeFunction(Rc::new(core::pop)), true);
|
||||
|
||||
// require — module loader
|
||||
e.define("require".into(), Value::NativeFunction(Rc::new(super::module::require_fn)), true);
|
||||
|
||||
// string — split, trim, substring, replace, contains, upper, lower, starts_with, ends_with
|
||||
e.define("split".into(), Value::NativeFunction(Rc::new(string::split)), true);
|
||||
e.define("trim".into(), Value::NativeFunction(Rc::new(string::trim)), true);
|
||||
e.define("substring".into(), Value::NativeFunction(Rc::new(string::substring)), true);
|
||||
e.define("replace".into(), Value::NativeFunction(Rc::new(string::replace)), true);
|
||||
e.define("contains".into(), Value::NativeFunction(Rc::new(string::contains)), true);
|
||||
e.define("upper".into(), Value::NativeFunction(Rc::new(string::upper)), true);
|
||||
e.define("lower".into(), Value::NativeFunction(Rc::new(string::lower)), true);
|
||||
e.define("starts_with".into(), Value::NativeFunction(Rc::new(string::starts_with)), true);
|
||||
e.define("ends_with".into(), Value::NativeFunction(Rc::new(string::ends_with)), true);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! 标准库:os(clock)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::{Interpreter, Value};
|
||||
|
||||
pub fn clock(_interp: &mut Interpreter, _args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
Ok(Value::Number(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs_f64(),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//! 标准库:string(split, trim, substring, replace, contains, upper, lower, starts_with, ends_with)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::{Interpreter, 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 Interpreter, 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 Interpreter, 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 Interpreter, 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 Interpreter, 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 Interpreter, 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 Interpreter, 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 Interpreter, 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 Interpreter, 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 Interpreter, 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,50 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
use crate::ast::*;
|
||||
use crate::ast::expr::{Literal, UnaryOp, BinaryOp, LogicalOp, AssignOp};
|
||||
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;
|
||||
|
||||
impl super::Interpreter {
|
||||
// ========================================================================
|
||||
// evaluate — dispatcher
|
||||
// ========================================================================
|
||||
|
||||
pub fn evaluate(&mut self, expr: Expr) -> Result<Value, RuntimeError> {
|
||||
match expr {
|
||||
Expr::Literal(lit) => self.eval_literal(lit),
|
||||
Expr::Variable(name) => self.eval_variable(name),
|
||||
Expr::Assign { name, op, value } => self.eval_assign(name, op, *value),
|
||||
Expr::Get { object, name } => self.eval_get(*object, name),
|
||||
Expr::Set { object, name, op, value } => self.eval_set(*object, name, op, *value),
|
||||
Expr::IndexGet { array, index } => self.eval_index_get(*array, *index),
|
||||
Expr::IndexSet { array, index, op, value } => self.eval_index_set(*array, *index, op, *value),
|
||||
Expr::ObjectLiteral { properties } => self.eval_object_literal(properties),
|
||||
Expr::ArrayLiteral { elements } => self.eval_array_literal(elements),
|
||||
Expr::Unary { op, right } => self.eval_unary(op, *right),
|
||||
Expr::Binary { left, op, right } => self.eval_binary(*left, op, *right),
|
||||
Expr::Logical { left, op, right } => self.eval_logical(*left, op, *right),
|
||||
Expr::Ternary { condition, then_branch, else_branch } => {
|
||||
self.eval_ternary(*condition, *then_branch, *else_branch)
|
||||
}
|
||||
Expr::Call { callee, arguments } => self.eval_call(*callee, arguments),
|
||||
Expr::Lambda { params, body } => self.eval_lambda(params, body),
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// eval_* methods
|
||||
// ========================================================================
|
||||
|
||||
fn eval_literal(&mut self, lit: Literal) -> Result<Value, RuntimeError> {
|
||||
Ok(match lit {
|
||||
Literal::Number(n) => Value::Number(n),
|
||||
Literal::String(s) => Value::String(s),
|
||||
Literal::Bool(b) => Value::Bool(b),
|
||||
Literal::Nil => Value::Nil,
|
||||
})
|
||||
}
|
||||
|
||||
fn eval_variable(&mut self, name: String) -> Result<Value, RuntimeError> {
|
||||
match self.env.borrow().get(&name) {
|
||||
Some(val) => Ok(val),
|
||||
None => Err(RuntimeError::RuntimeError {
|
||||
message: format!("Undefined variable '{}'", name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_assign(&mut self, name: String, op: AssignOp, value: Expr) -> Result<Value, RuntimeError> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn eval_get(&mut self, object: Expr, name: String) -> Result<Value, RuntimeError> {
|
||||
let obj = self.evaluate(object)?;
|
||||
match obj {
|
||||
Value::Object(_) => {
|
||||
match obj.get(&name) {
|
||||
Some(val) => Ok(val),
|
||||
None => Err(RuntimeError::RuntimeError {
|
||||
message: format!("Undefined property '{}'", name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => match name.as_str() {
|
||||
"length" => Ok(Value::Number(arr.borrow().len() as f64)),
|
||||
"push" => {
|
||||
let arr = Rc::clone(&arr);
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, mut args: Vec<Value>| {
|
||||
let val = args.pop().unwrap_or(Value::Nil);
|
||||
arr.borrow_mut().push(val.clone());
|
||||
Ok(val)
|
||||
})))
|
||||
}
|
||||
"pop" => {
|
||||
let arr = Rc::clone(&arr);
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, _args: Vec<Value>| {
|
||||
arr.borrow_mut()
|
||||
.pop()
|
||||
.ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: "pop() on empty array".into(),
|
||||
token: None,
|
||||
})
|
||||
})))
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: format!("Array has no property '{}'", name),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
Value::String(s) => match name.as_str() {
|
||||
"length" => Ok(Value::Number(s.chars().count() as f64)),
|
||||
"upper" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::upper(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
"lower" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::lower(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
"trim" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::trim(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
"substring" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::substring(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
"replace" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::replace(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
"contains" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::contains(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
"starts_with" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::starts_with(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
"ends_with" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::ends_with(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
"split" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
super::builtins::string::split(_interp, all_args)
|
||||
})))
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: format!("String has no property '{}'", name),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Only objects have properties".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_set(&mut self, object: Expr, name: String, op: AssignOp, value: Expr) -> Result<Value, RuntimeError> {
|
||||
let obj = self.evaluate(object)?;
|
||||
let rhs = self.evaluate(value)?;
|
||||
match obj {
|
||||
Value::Object(_) => {
|
||||
let val = match op {
|
||||
AssignOp::Equal => rhs,
|
||||
_ => {
|
||||
let current = obj.get(&name).ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: format!("Property '{}' does not exist", name),
|
||||
token: None,
|
||||
})?;
|
||||
self.apply_assign_op(current, rhs, op)?
|
||||
}
|
||||
};
|
||||
obj.set(&name, val.clone())?;
|
||||
Ok(val)
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Only objects have properties".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_index_get(&mut self, array: Expr, index: Expr) -> Result<Value, RuntimeError> {
|
||||
let target = self.evaluate(array)?;
|
||||
let idx_val = self.evaluate(index)?;
|
||||
// Object index access: obj["key"]
|
||||
if let Value::Object(_) = &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 target.get(&key).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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_index_set(&mut self, array: Expr, index: Expr, op: AssignOp, value: Expr) -> Result<Value, RuntimeError> {
|
||||
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(_) = &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 current = target.get(&key).ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: format!("Property '{}' does not exist", key),
|
||||
token: None,
|
||||
})?;
|
||||
self.apply_assign_op(current, rhs, op)?
|
||||
}
|
||||
};
|
||||
target.set(&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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_object_literal(&mut self, properties: Vec<(String, Expr)>) -> Result<Value, RuntimeError> {
|
||||
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))))
|
||||
}
|
||||
|
||||
fn eval_array_literal(&mut self, elements: Vec<Expr>) -> Result<Value, RuntimeError> {
|
||||
let mut arr = Vec::new();
|
||||
for e in elements {
|
||||
arr.push(self.evaluate(e)?);
|
||||
}
|
||||
Ok(Value::Array(Rc::new(RefCell::new(arr))))
|
||||
}
|
||||
|
||||
fn eval_unary(&mut self, op: UnaryOp, right: Expr) -> Result<Value, RuntimeError> {
|
||||
let val = self.evaluate(right)?;
|
||||
match op {
|
||||
UnaryOp::Negate => match val {
|
||||
Value::Number(n) => Ok(Value::Number(-n)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Unary '-' on non-number".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
UnaryOp::Not => Ok(Value::Bool(!self.is_truthy(&val))),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_binary(&mut self, left: Expr, op: BinaryOp, right: Expr) -> Result<Value, RuntimeError> {
|
||||
let l = self.evaluate(left)?;
|
||||
let r = self.evaluate(right)?;
|
||||
match op {
|
||||
BinaryOp::Add => self.eval_binary_add(l, r),
|
||||
BinaryOp::Sub => self.eval_binary_arith(l, r, |a, b| a - b, "-"),
|
||||
BinaryOp::Mul => self.eval_binary_arith(l, r, |a, b| a * b, "*"),
|
||||
BinaryOp::Div => self.eval_binary_div(l, r),
|
||||
BinaryOp::Mod => self.eval_binary_mod(l, r),
|
||||
BinaryOp::Greater => Ok(Value::Bool(self.as_number(&l)? > self.as_number(&r)?)),
|
||||
BinaryOp::GreaterEqual => Ok(Value::Bool(self.as_number(&l)? >= self.as_number(&r)?)),
|
||||
BinaryOp::Less => Ok(Value::Bool(self.as_number(&l)? < self.as_number(&r)?)),
|
||||
BinaryOp::LessEqual => Ok(Value::Bool(self.as_number(&l)? <= self.as_number(&r)?)),
|
||||
BinaryOp::Equal => Ok(Value::Bool(self.is_equal(&l, &r))),
|
||||
BinaryOp::NotEqual => Ok(Value::Bool(!self.is_equal(&l, &r))),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_binary_add(&mut self, l: Value, r: Value) -> Result<Value, RuntimeError> {
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_binary_arith(
|
||||
&mut self,
|
||||
l: Value,
|
||||
r: Value,
|
||||
op_fn: fn(f64, f64) -> f64,
|
||||
name: &str,
|
||||
) -> Result<Value, RuntimeError> {
|
||||
match (l, r) {
|
||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(op_fn(a, b))),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: format!("Invalid '{}' operands", name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_binary_div(&mut self, l: Value, r: Value) -> Result<Value, RuntimeError> {
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_binary_mod(&mut self, l: Value, r: Value) -> Result<Value, RuntimeError> {
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_logical(&mut self, left: Expr, op: LogicalOp, right: Expr) -> Result<Value, RuntimeError> {
|
||||
let l = self.evaluate(left)?;
|
||||
match op {
|
||||
LogicalOp::And => {
|
||||
Ok(if !self.is_truthy(&l) { l } else { self.evaluate(right)? })
|
||||
}
|
||||
LogicalOp::Or => {
|
||||
Ok(if self.is_truthy(&l) { l } else { self.evaluate(right)? })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_ternary(&mut self, condition: Expr, then_branch: Expr, else_branch: Expr) -> Result<Value, RuntimeError> {
|
||||
let cond = self.evaluate(condition)?;
|
||||
if self.is_truthy(&cond) {
|
||||
self.evaluate(then_branch)
|
||||
} else {
|
||||
self.evaluate(else_branch)
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_call(&mut self, callee: Expr, arguments: Vec<Expr>) -> Result<Value, RuntimeError> {
|
||||
let func = self.evaluate(callee)?;
|
||||
let mut args = Vec::new();
|
||||
for e in arguments {
|
||||
args.push(self.evaluate(e)?);
|
||||
}
|
||||
self.call_function(func, args)
|
||||
}
|
||||
|
||||
fn eval_lambda(&mut self, params: Vec<String>, body: Vec<Stmt>) -> Result<Value, RuntimeError> {
|
||||
Ok(Value::Function(Rc::new(Function {
|
||||
params,
|
||||
body,
|
||||
env: Rc::clone(&self.env),
|
||||
name: None,
|
||||
})))
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// call_function
|
||||
// ========================================================================
|
||||
|
||||
pub fn call_function(&mut self, func_val: Value, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
match func_val {
|
||||
Value::NativeFunction(native_fn) => native_fn(self, args),
|
||||
Value::Function(f) => self.call_user_function(f, args),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Attempt to call non-function".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn call_user_function(&mut self, f: Rc<Function>, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
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)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Helpers
|
||||
// ========================================================================
|
||||
|
||||
pub fn is_truthy(&self, val: &Value) -> bool {
|
||||
match val {
|
||||
Value::Nil => false,
|
||||
Value::Bool(b) => *b,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub 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))
|
||||
})
|
||||
}
|
||||
(Value::Function(x), Value::Function(y)) => Rc::ptr_eq(x, y),
|
||||
(Value::NativeFunction(x), Value::NativeFunction(y)) => Rc::ptr_eq(x, y),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
use crate::ast::*;
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Signal;
|
||||
use super::{Value, Env, Function};
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
impl super::Interpreter {
|
||||
// ========================================================================
|
||||
// execute — dispatcher
|
||||
// ========================================================================
|
||||
|
||||
pub fn execute(&mut self, stmt: Stmt) -> Result<Signal, RuntimeError> {
|
||||
match stmt {
|
||||
Stmt::Let { name, initializer, mutable } => self.exec_let(name, initializer, mutable),
|
||||
Stmt::ExprStmt(expr) => self.exec_expr_stmt(expr),
|
||||
Stmt::Block(stmts) => self.exec_block(stmts),
|
||||
Stmt::If { condition, then_branch, else_branch } => {
|
||||
self.exec_if(condition, *then_branch, else_branch.map(|b| *b))
|
||||
}
|
||||
Stmt::While { condition, body } => self.exec_while(condition, *body),
|
||||
Stmt::For { initializer, condition, step, body } => {
|
||||
self.exec_for(initializer, condition, step, *body)
|
||||
}
|
||||
Stmt::ForIn { var_name, iterable, body } => self.exec_for_in(var_name, iterable, *body),
|
||||
Stmt::Function { name, params, body } => self.exec_function(name, params, body),
|
||||
Stmt::Return(expr_opt) => self.exec_return(expr_opt),
|
||||
Stmt::Break => Ok(Signal::Break),
|
||||
Stmt::Continue => Ok(Signal::Continue),
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// exec_* methods
|
||||
// ========================================================================
|
||||
|
||||
fn exec_let(&mut self, name: String, initializer: Expr, mutable: bool) -> Result<Signal, RuntimeError> {
|
||||
let val = self.evaluate(initializer)?;
|
||||
self.env.borrow_mut().define(name, val, mutable);
|
||||
Ok(Signal::None)
|
||||
}
|
||||
|
||||
fn exec_expr_stmt(&mut self, expr: Expr) -> Result<Signal, RuntimeError> {
|
||||
self.evaluate(expr)?;
|
||||
Ok(Signal::None)
|
||||
}
|
||||
|
||||
fn exec_block(&mut self, stmts: Vec<Stmt>) -> Result<Signal, RuntimeError> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn exec_if(
|
||||
&mut self,
|
||||
condition: Expr,
|
||||
then_branch: Stmt,
|
||||
else_branch: Option<Stmt>,
|
||||
) -> Result<Signal, RuntimeError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_while(&mut self, condition: Expr, body: Stmt) -> Result<Signal, RuntimeError> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn exec_for(
|
||||
&mut self,
|
||||
initializer: Option<Box<Stmt>>,
|
||||
condition: Option<Expr>,
|
||||
step: Option<Expr>,
|
||||
body: Stmt,
|
||||
) -> Result<Signal, RuntimeError> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn exec_for_in(
|
||||
&mut self,
|
||||
var_name: String,
|
||||
iterable: Expr,
|
||||
body: Stmt,
|
||||
) -> Result<Signal, RuntimeError> {
|
||||
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 {
|
||||
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())?;
|
||||
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)
|
||||
}
|
||||
|
||||
fn exec_function(
|
||||
&mut self,
|
||||
name: String,
|
||||
params: Vec<String>,
|
||||
body: Vec<Stmt>,
|
||||
) -> Result<Signal, RuntimeError> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn exec_return(&mut self, expr_opt: Option<Expr>) -> Result<Signal, RuntimeError> {
|
||||
if let Some(expr) = expr_opt {
|
||||
Ok(Signal::Return(self.evaluate(expr)?))
|
||||
} else {
|
||||
Ok(Signal::Return(Value::Nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use crate::ast::*;
|
||||
use crate::error::RuntimeError;
|
||||
use super::{Env, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct Interpreter {
|
||||
/// 当前作用域(用户代码所在 env,parent 指向 builtins_env)
|
||||
pub env: Rc<RefCell<Env>>,
|
||||
/// 内置函数根作用域(parent = None,所有内置函数注册在此)
|
||||
pub builtins_env: Rc<RefCell<Env>>,
|
||||
/// 模块缓存:规范路径 → exports 对象
|
||||
pub module_cache: RefCell<HashMap<String, Value>>,
|
||||
/// 当前执行文件的目录,用于 require() 解析相对路径
|
||||
pub current_dir: String,
|
||||
}
|
||||
|
||||
impl Interpreter {
|
||||
pub fn new() -> Self {
|
||||
// 根层:仅包含内置函数
|
||||
let builtins_env = Rc::new(RefCell::new(Env::new(None)));
|
||||
super::builtins::register_all(&builtins_env);
|
||||
|
||||
// 用户层:parent 指向 builtins_env,用户定义的变量都在这层
|
||||
let script_env = Rc::new(RefCell::new(Env::new(Some(Rc::clone(&builtins_env)))));
|
||||
|
||||
Self {
|
||||
env: script_env,
|
||||
builtins_env,
|
||||
module_cache: RefCell::new(HashMap::new()),
|
||||
current_dir: std::env::current_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| ".".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 指定当前目录的构造器,用于 `run_file` 时设置脚本所在目录
|
||||
pub fn with_current_dir(dir: String) -> Self {
|
||||
let mut interp = Self::new();
|
||||
interp.current_dir = dir;
|
||||
interp
|
||||
}
|
||||
|
||||
pub fn interpret(&mut self, statements: Vec<Stmt>) -> Result<(), RuntimeError> {
|
||||
for stmt in statements {
|
||||
self.execute(stmt)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,124 @@
|
||||
pub mod builtins;
|
||||
pub mod env;
|
||||
pub mod eval;
|
||||
pub mod exec;
|
||||
pub mod interpreter;
|
||||
pub mod module;
|
||||
|
||||
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 = Rc<dyn Fn(&mut super::Interpreter, 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 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 {
|
||||
/// 容器内值的格式化:字符串加引号以区分类型,其余类型用 Display
|
||||
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>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,140 @@
|
||||
//! 模块系统:require() 加载器
|
||||
//!
|
||||
//! `require("path/to/module.ast")` 加载并执行指定的 Aster 文件,
|
||||
//! 返回一个包含模块所有顶层定义的 `Value::Object`。
|
||||
//! 模块在自己的作用域中执行,只能访问内置函数,无法访问调用者的变量。
|
||||
//! 第二次 require 同一文件会返回缓存的对象。
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::lexer::Lexer;
|
||||
use crate::parser::Parser;
|
||||
use super::{Interpreter, Env, Value, Signal};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// require() 的内置函数实现
|
||||
pub fn require_fn(interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
// 1. 参数验证
|
||||
let path_str = match args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(other) => return Err(runtime_error(format!(
|
||||
"require() expects a string argument, got {}", other))),
|
||||
None => return Err(runtime_error(
|
||||
"require() expects 1 argument (string path)")),
|
||||
};
|
||||
|
||||
// 2. 路径解析
|
||||
let resolved = resolve_path(&interp.current_dir, &path_str)?;
|
||||
|
||||
// 3. 缓存查找
|
||||
if let Some(cached) = interp.module_cache.borrow().get(&resolved) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
|
||||
// 4. 读取文件
|
||||
let src = std::fs::read_to_string(&resolved)
|
||||
.map_err(|e| runtime_error(format!(
|
||||
"Module '{}' not found: {}", path_str, e)))?;
|
||||
|
||||
// 5. 词法分析
|
||||
let (tokens, lex_errors) = Lexer::new(&src).tokenize();
|
||||
if !lex_errors.is_empty() {
|
||||
return Err(runtime_error(format!(
|
||||
"Lex error in module '{}': {}", path_str, lex_errors[0])));
|
||||
}
|
||||
|
||||
// 6. 语法分析
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, parse_errors) = parser.parse();
|
||||
if !parse_errors.is_empty() {
|
||||
return Err(runtime_error(format!(
|
||||
"Parse error in module '{}': {}", path_str, parse_errors[0])));
|
||||
}
|
||||
|
||||
// 7. 创建隔离的模块 env(父级 = builtins_env,看不到调用者的变量)
|
||||
let module_env = Rc::new(RefCell::new(Env::new(Some(Rc::clone(&interp.builtins_env)))));
|
||||
|
||||
// 8. 在缓存中插入占位符(支持循环 require)
|
||||
let exports_map = Rc::new(RefCell::new(HashMap::new()));
|
||||
let exports = Value::Object(Rc::clone(&exports_map));
|
||||
interp.module_cache.borrow_mut().insert(resolved.clone(), exports.clone());
|
||||
|
||||
// 9. 保存调用者状态
|
||||
let previous_env = Rc::clone(&interp.env);
|
||||
let previous_dir = interp.current_dir.clone();
|
||||
|
||||
// 10. 切换到模块上下文
|
||||
interp.env = module_env.clone();
|
||||
interp.current_dir = module_dir(&resolved);
|
||||
|
||||
// 11. 执行模块
|
||||
for stmt in &stmts {
|
||||
match interp.execute(stmt.clone()) {
|
||||
Ok(Signal::Break) | Ok(Signal::Continue) => {
|
||||
// 恢复状态后返回错误
|
||||
interp.env = previous_env;
|
||||
interp.current_dir = previous_dir;
|
||||
interp.module_cache.borrow_mut().remove(&resolved);
|
||||
return Err(runtime_error(
|
||||
"break/continue outside of loop in module"));
|
||||
}
|
||||
Err(e) => {
|
||||
// 恢复状态后传播错误
|
||||
interp.env = previous_env;
|
||||
interp.current_dir = previous_dir;
|
||||
interp.module_cache.borrow_mut().remove(&resolved);
|
||||
return Err(e);
|
||||
}
|
||||
Ok(Signal::None) | Ok(Signal::Return(_)) => {
|
||||
// 正常执行或顶层 return,继续
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 12. 恢复调用者状态
|
||||
interp.env = previous_env;
|
||||
interp.current_dir = previous_dir;
|
||||
|
||||
// 13. 收集 exports(模块 env 中的所有直接绑定)
|
||||
for (name, (val, _mutable)) in module_env.borrow().values.clone() {
|
||||
exports_map.borrow_mut().insert(name, val);
|
||||
}
|
||||
|
||||
Ok(exports)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
/// 解析模块路径:相对路径基于 current_dir,用 canonicalize 规范化
|
||||
fn resolve_path(current_dir: &str, path_str: &str) -> Result<String, RuntimeError> {
|
||||
let path = Path::new(path_str);
|
||||
let resolved = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
Path::new(current_dir).join(path)
|
||||
};
|
||||
std::fs::canonicalize(&resolved)
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.map_err(|_| runtime_error(format!(
|
||||
"Module '{}' not found (resolved to '{}')",
|
||||
path_str, resolved.display())))
|
||||
}
|
||||
|
||||
/// 提取模块文件所在目录
|
||||
fn module_dir(resolved: &str) -> String {
|
||||
Path::new(resolved)
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| ".".to_string())
|
||||
}
|
||||
|
||||
fn runtime_error(msg: impl Into<String>) -> RuntimeError {
|
||||
RuntimeError::RuntimeError {
|
||||
message: msg.into(),
|
||||
token: None,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,380 @@
|
||||
// src/lexer/lexer.rs
|
||||
|
||||
use super::{Token, TokenKind};
|
||||
use crate::error::RuntimeError;
|
||||
|
||||
pub struct Lexer {
|
||||
src: Vec<char>,
|
||||
current: usize,
|
||||
line: usize,
|
||||
column: usize,
|
||||
}
|
||||
|
||||
impl Lexer {
|
||||
pub fn new(input: &str) -> Self {
|
||||
Self {
|
||||
src: input.chars().collect(),
|
||||
current: 0,
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tokenize(mut self) -> (Vec<Token>, Vec<RuntimeError>) {
|
||||
let mut tokens = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
while !self.is_at_end() {
|
||||
if let Some(token) = self.next_token(&mut errors) {
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::EOF,
|
||||
line: self.line,
|
||||
column: self.column,
|
||||
});
|
||||
|
||||
(tokens, errors)
|
||||
}
|
||||
|
||||
fn next_token(&mut self, errors: &mut Vec<RuntimeError>) -> Option<Token> {
|
||||
self.skip_whitespace();
|
||||
|
||||
if self.is_at_end() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let line = self.line;
|
||||
let column = self.column;
|
||||
let c = self.advance();
|
||||
|
||||
let kind = match c {
|
||||
'(' => TokenKind::LeftParen,
|
||||
')' => TokenKind::RightParen,
|
||||
'{' => TokenKind::LeftBrace,
|
||||
'}' => TokenKind::RightBrace,
|
||||
',' => TokenKind::Comma,
|
||||
';' => TokenKind::Semicolon,
|
||||
|
||||
'+' => {
|
||||
if self.match_char('=') {
|
||||
TokenKind::PlusEqual
|
||||
} else {
|
||||
TokenKind::Plus
|
||||
}
|
||||
}
|
||||
'-' => {
|
||||
if self.match_char('=') {
|
||||
TokenKind::MinusEqual
|
||||
} else {
|
||||
TokenKind::Minus
|
||||
}
|
||||
}
|
||||
'*' => {
|
||||
if self.match_char('=') {
|
||||
TokenKind::StarEqual
|
||||
} else {
|
||||
TokenKind::Star
|
||||
}
|
||||
}
|
||||
'.' => TokenKind::Dot,
|
||||
':' => TokenKind::Colon,
|
||||
'?' => TokenKind::Question,
|
||||
'[' => TokenKind::LeftBracket,
|
||||
']' => TokenKind::RightBracket,
|
||||
|
||||
'/' => {
|
||||
if self.match_char('/') {
|
||||
// 单行注释
|
||||
while !self.is_at_end() && self.peek() != '\n' {
|
||||
self.advance();
|
||||
}
|
||||
return None;
|
||||
} else if self.match_char('*') {
|
||||
// 多行注释 /* ... */
|
||||
return self.block_comment(line, column, errors);
|
||||
} else if self.match_char('=') {
|
||||
TokenKind::SlashEqual
|
||||
} else {
|
||||
TokenKind::Slash
|
||||
}
|
||||
}
|
||||
|
||||
'%' => {
|
||||
if self.match_char('=') {
|
||||
TokenKind::PercentEqual
|
||||
} else {
|
||||
TokenKind::Percent
|
||||
}
|
||||
}
|
||||
|
||||
'!' => {
|
||||
if self.match_char('=') {
|
||||
TokenKind::BangEqual
|
||||
} else {
|
||||
TokenKind::Bang
|
||||
}
|
||||
}
|
||||
|
||||
'=' => {
|
||||
if self.match_char('=') {
|
||||
TokenKind::EqualEqual
|
||||
} else {
|
||||
TokenKind::Equal
|
||||
}
|
||||
}
|
||||
|
||||
'>' => {
|
||||
if self.match_char('=') {
|
||||
TokenKind::GreaterEqual
|
||||
} else {
|
||||
TokenKind::Greater
|
||||
}
|
||||
}
|
||||
|
||||
'<' => {
|
||||
if self.match_char('=') {
|
||||
TokenKind::LessEqual
|
||||
} else {
|
||||
TokenKind::Less
|
||||
}
|
||||
}
|
||||
|
||||
'&' => {
|
||||
if self.match_char('&') {
|
||||
TokenKind::AndAnd
|
||||
} else {
|
||||
errors.push(RuntimeError::lex("Unexpected '&'. Did you mean '&&'?", line, column));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
'|' => {
|
||||
if self.match_char('|') {
|
||||
TokenKind::OrOr
|
||||
} else {
|
||||
errors.push(RuntimeError::lex("Unexpected '|'. Did you mean '||'?", line, column));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
'"' | '\'' => return self.string_literal(line, column, errors, c),
|
||||
|
||||
c if c.is_ascii_digit() => {
|
||||
return Some(self.number_literal(c, line, column));
|
||||
}
|
||||
|
||||
c if is_ident_start(c) => {
|
||||
return Some(self.identifier(c, line, column));
|
||||
}
|
||||
|
||||
_ => {
|
||||
errors.push(RuntimeError::lex(
|
||||
format!("Unexpected character '{}'", c),
|
||||
line,
|
||||
column,
|
||||
));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(Token { kind, line, column })
|
||||
}
|
||||
|
||||
// ---------------- helpers ----------------
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
loop {
|
||||
if self.is_at_end() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.peek() {
|
||||
' ' | '\t' | '\r' => {
|
||||
self.advance();
|
||||
}
|
||||
'\n' => {
|
||||
self.advance();
|
||||
self.line += 1;
|
||||
self.column = 1;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> char {
|
||||
let c = self.src[self.current];
|
||||
self.current += 1;
|
||||
self.column += 1;
|
||||
c
|
||||
}
|
||||
|
||||
fn match_char(&mut self, expected: char) -> bool {
|
||||
if self.is_at_end() || self.peek() != expected {
|
||||
return false;
|
||||
}
|
||||
self.advance();
|
||||
true
|
||||
}
|
||||
|
||||
fn peek(&self) -> char {
|
||||
self.src[self.current]
|
||||
}
|
||||
|
||||
fn is_at_end(&self) -> bool {
|
||||
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,
|
||||
column: usize,
|
||||
errors: &mut Vec<RuntimeError>,
|
||||
quote: char,
|
||||
) -> Option<Token> {
|
||||
let mut value = String::new();
|
||||
|
||||
while !self.is_at_end() && self.peek() != quote {
|
||||
let c = self.advance();
|
||||
if c == '\\' {
|
||||
// Handle escape sequences
|
||||
if self.is_at_end() {
|
||||
errors.push(RuntimeError::lex("Unterminated string literal", line, column));
|
||||
return None;
|
||||
}
|
||||
match self.advance() {
|
||||
'n' => value.push('\n'),
|
||||
't' => value.push('\t'),
|
||||
'r' => value.push('\r'),
|
||||
'"' => value.push('"'),
|
||||
'\'' => value.push('\''),
|
||||
'\\' => value.push('\\'),
|
||||
other => {
|
||||
errors.push(RuntimeError::lex(
|
||||
format!("Unknown escape sequence '\\{}'", other),
|
||||
line,
|
||||
column,
|
||||
));
|
||||
// Consume the rest of the string to avoid cascading errors
|
||||
while !self.is_at_end() && self.peek() != quote {
|
||||
self.advance();
|
||||
}
|
||||
if !self.is_at_end() {
|
||||
self.advance(); // consume closing quote
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_at_end() {
|
||||
errors.push(RuntimeError::lex("Unterminated string literal", line, column));
|
||||
return None;
|
||||
}
|
||||
|
||||
self.advance(); // consume closing quote
|
||||
|
||||
Some(Token {
|
||||
kind: TokenKind::String(value),
|
||||
line,
|
||||
column,
|
||||
})
|
||||
}
|
||||
|
||||
fn number_literal(&mut self, first: char, line: usize, column: usize) -> Token {
|
||||
let mut s = String::new();
|
||||
s.push(first);
|
||||
|
||||
while !self.is_at_end() && self.peek().is_ascii_digit() {
|
||||
s.push(self.advance());
|
||||
}
|
||||
|
||||
if !self.is_at_end() && self.peek() == '.' {
|
||||
s.push(self.advance());
|
||||
while !self.is_at_end() && self.peek().is_ascii_digit() {
|
||||
s.push(self.advance());
|
||||
}
|
||||
}
|
||||
|
||||
let value = s.parse::<f64>().unwrap();
|
||||
|
||||
Token {
|
||||
kind: TokenKind::Number(value),
|
||||
line,
|
||||
column,
|
||||
}
|
||||
}
|
||||
|
||||
fn identifier(&mut self, first: char, line: usize, column: usize) -> Token {
|
||||
let mut s = String::new();
|
||||
s.push(first);
|
||||
|
||||
while !self.is_at_end() && is_ident_part(self.peek()) {
|
||||
s.push(self.advance());
|
||||
}
|
||||
|
||||
let kind = match s.as_str() {
|
||||
"let" => TokenKind::Let,
|
||||
"const" => TokenKind::Const,
|
||||
"fn" => TokenKind::Fn,
|
||||
"if" => TokenKind::If,
|
||||
"else" => TokenKind::Else,
|
||||
"while" => TokenKind::While,
|
||||
"for" => TokenKind::For,
|
||||
"in" => TokenKind::In,
|
||||
"break" => TokenKind::Break,
|
||||
"continue" => TokenKind::Continue,
|
||||
"return" => TokenKind::Return,
|
||||
"true" => TokenKind::True,
|
||||
"false" => TokenKind::False,
|
||||
"nil" => TokenKind::Nil,
|
||||
_ => TokenKind::Identifier(s),
|
||||
};
|
||||
|
||||
Token { kind, line, column }
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ident_start(c: char) -> bool {
|
||||
c.is_ascii_alphabetic() || c == '_'
|
||||
}
|
||||
|
||||
fn is_ident_part(c: char) -> bool {
|
||||
is_ident_start(c) || c.is_ascii_digit()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod token;
|
||||
pub mod lexer;
|
||||
|
||||
pub use token::{Token, TokenKind};
|
||||
pub use lexer::Lexer;
|
||||
@@ -0,0 +1,459 @@
|
||||
use super::*;
|
||||
|
||||
/// Helper: tokenize input and return only TokenKinds (drop EOF and positions).
|
||||
fn tokenize(input: &str) -> Vec<TokenKind> {
|
||||
let (tokens, _errors) = Lexer::new(input).tokenize();
|
||||
tokens.into_iter().filter_map(|t| {
|
||||
if matches!(t.kind, TokenKind::EOF) { None } else { Some(t.kind) }
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Helper: tokenize and return full tokens (with positions).
|
||||
fn tokenize_full(input: &str) -> (Vec<Token>, Vec<RuntimeError>) {
|
||||
Lexer::new(input).tokenize()
|
||||
}
|
||||
|
||||
// ----- single-character tokens -----
|
||||
|
||||
#[test]
|
||||
fn single_char_tokens() {
|
||||
let kinds = tokenize("(){},;.:");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::LeftParen, TokenKind::RightParen,
|
||||
TokenKind::LeftBrace, TokenKind::RightBrace,
|
||||
TokenKind::Comma, TokenKind::Semicolon,
|
||||
TokenKind::Dot, TokenKind::Colon,
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brackets() {
|
||||
let kinds = tokenize("[]");
|
||||
assert_eq!(kinds, vec![TokenKind::LeftBracket, TokenKind::RightBracket]);
|
||||
}
|
||||
|
||||
// ----- operators -----
|
||||
|
||||
#[test]
|
||||
fn arithmetic_operators() {
|
||||
let kinds = tokenize("+ - * / %");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::Plus, TokenKind::Minus, TokenKind::Star,
|
||||
TokenKind::Slash, TokenKind::Percent,
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparison_operators() {
|
||||
let kinds = tokenize("< <= > >=");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::Less, TokenKind::LessEqual,
|
||||
TokenKind::Greater, TokenKind::GreaterEqual,
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equality_operators() {
|
||||
let kinds = tokenize("== !=");
|
||||
assert_eq!(kinds, vec![TokenKind::EqualEqual, TokenKind::BangEqual]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_operators() {
|
||||
let kinds = tokenize("&& || !");
|
||||
assert_eq!(kinds, vec![TokenKind::AndAnd, TokenKind::OrOr, TokenKind::Bang]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assignment_operators() {
|
||||
let kinds = tokenize("= += -= *= /= %=");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::Equal,
|
||||
TokenKind::PlusEqual, TokenKind::MinusEqual,
|
||||
TokenKind::StarEqual, TokenKind::SlashEqual,
|
||||
TokenKind::PercentEqual,
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operator_proximity_no_spaces() {
|
||||
let kinds = tokenize("a+b-c*d/e%f");
|
||||
// identifiers a, b, c, d, e, f interleaved with operators
|
||||
let ids: Vec<_> = kinds.iter().filter(|k| matches!(k, TokenKind::Identifier(_))).collect();
|
||||
assert_eq!(ids.len(), 6);
|
||||
}
|
||||
|
||||
// ----- numbers -----
|
||||
|
||||
#[test]
|
||||
fn integer_literal() {
|
||||
let kinds = tokenize("42");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_literal() {
|
||||
let kinds = tokenize("3.14");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(3.14)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_and_negative() {
|
||||
// Note: `-` is a separate unary operator, but `0` should lex correctly
|
||||
let kinds = tokenize("0");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(0.0)]);
|
||||
}
|
||||
|
||||
// ----- strings -----
|
||||
|
||||
#[test]
|
||||
fn string_literal() {
|
||||
let kinds = tokenize("\"hello\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("hello".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string() {
|
||||
let kinds = tokenize("\"\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_spaces() {
|
||||
let kinds = tokenize("\"hello world\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("hello world".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_strings() {
|
||||
let kinds = tokenize("\"a\" \"b\"");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::String("a".into()),
|
||||
TokenKind::String("b".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_escape_newline() {
|
||||
let kinds = tokenize("\"a\\nb\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_escape_tab() {
|
||||
let kinds = tokenize("\"a\\tb\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("a\tb".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_escape_quote() {
|
||||
let kinds = tokenize("\"he\\\"llo\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_escape_backslash() {
|
||||
let kinds = tokenize("\"a\\\\b\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("a\\b".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_escape_carriage_return() {
|
||||
let kinds = tokenize("\"a\\rb\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("a\rb".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_unknown_escape_is_error() {
|
||||
let (_, errors) = tokenize_full("\"\\x\"");
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert!(errors[0].to_string().contains("Unknown escape"));
|
||||
}
|
||||
|
||||
// ----- single-quoted strings -----
|
||||
|
||||
#[test]
|
||||
fn single_quoted_string() {
|
||||
let kinds = tokenize("'hello'");
|
||||
assert_eq!(kinds, vec![TokenKind::String("hello".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_quoted_empty_string() {
|
||||
let kinds = tokenize("''");
|
||||
assert_eq!(kinds, vec![TokenKind::String("".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_quoted_with_escape() {
|
||||
let kinds = tokenize("'a\\'b'");
|
||||
assert_eq!(kinds, vec![TokenKind::String("a'b".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_quoted_with_newline_escape() {
|
||||
let kinds = tokenize("'a\\nb'");
|
||||
assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_quote_strings() {
|
||||
let kinds = tokenize("\"double\" 'single'");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::String("double".into()),
|
||||
TokenKind::String("single".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_quote_can_contain_double_quote() {
|
||||
let kinds = tokenize("'he\"llo'");
|
||||
assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_quote_can_contain_single_quote() {
|
||||
let kinds = tokenize("\"it's\"");
|
||||
assert_eq!(kinds, vec![TokenKind::String("it's".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unterminated_single_quoted_string() {
|
||||
let (_, errors) = tokenize_full("'unterminated");
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert!(errors[0].to_string().contains("Unterminated"));
|
||||
}
|
||||
|
||||
// ----- identifiers and keywords -----
|
||||
|
||||
#[test]
|
||||
fn identifiers() {
|
||||
let kinds = tokenize("foo bar _private x1");
|
||||
let ids: Vec<_> = kinds.iter().filter_map(|k| {
|
||||
if let TokenKind::Identifier(s) = k { Some(s.clone()) } else { None }
|
||||
}).collect();
|
||||
assert_eq!(ids, vec!["foo", "bar", "_private", "x1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keywords() {
|
||||
let kinds = tokenize("let const fn if else while for in break continue return true false nil");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::Let, TokenKind::Const, TokenKind::Fn,
|
||||
TokenKind::If, TokenKind::Else,
|
||||
TokenKind::While, TokenKind::For, TokenKind::In,
|
||||
TokenKind::Break, TokenKind::Continue,
|
||||
TokenKind::Return,
|
||||
TokenKind::True, TokenKind::False,
|
||||
TokenKind::Nil,
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifier_looks_like_keyword_prefix() {
|
||||
// "lettuce" should be identifier, not "let" + error
|
||||
let kinds = tokenize("lettuce");
|
||||
assert_eq!(kinds, vec![TokenKind::Identifier("lettuce".into())]);
|
||||
}
|
||||
|
||||
// ----- comments -----
|
||||
|
||||
#[test]
|
||||
fn line_comment_ignored() {
|
||||
let kinds = tokenize("// this is a comment\n42");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_at_end_of_file() {
|
||||
let kinds = tokenize("42 // trailing comment");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_only_line() {
|
||||
let kinds = tokenize("// just a comment");
|
||||
assert!(kinds.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_between_tokens() {
|
||||
let kinds = tokenize("1 // comment\n2");
|
||||
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]
|
||||
fn mixed_whitespace() {
|
||||
let kinds = tokenize(" \t 42\r\n 84");
|
||||
assert_eq!(kinds, vec![TokenKind::Number(42.0), TokenKind::Number(84.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_whitespace() {
|
||||
let kinds = tokenize(" \n\t \n ");
|
||||
assert!(kinds.is_empty());
|
||||
}
|
||||
|
||||
// ----- errors -----
|
||||
|
||||
#[test]
|
||||
fn unterminated_string() {
|
||||
let (_, errors) = tokenize_full("\"no closing quote");
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert!(errors[0].to_string().contains("Unterminated"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_ampersand() {
|
||||
let (_, errors) = tokenize_full("&");
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert!(errors[0].to_string().contains("Unexpected '&'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_pipe() {
|
||||
let (_, errors) = tokenize_full("|");
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert!(errors[0].to_string().contains("Unexpected '|'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unexpected_character() {
|
||||
let (_, errors) = tokenize_full("@");
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert!(errors[0].to_string().contains("Unexpected character"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_does_not_block_rest() {
|
||||
// Bare `&` produces an error, but tokenization continues
|
||||
let (tokens, errors) = tokenize_full("& 42");
|
||||
assert_eq!(errors.len(), 1);
|
||||
let kinds: Vec<_> = tokens.iter().filter(|t| !matches!(t.kind, TokenKind::EOF)).map(|t| t.kind.clone()).collect();
|
||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||
}
|
||||
|
||||
// ----- position tracking -----
|
||||
|
||||
#[test]
|
||||
fn positions_single_line() {
|
||||
let (tokens, _) = tokenize_full("let x = 5");
|
||||
// let @ 1:1
|
||||
// x @ 1:5
|
||||
// = @ 1:7
|
||||
// 5 @ 1:9
|
||||
// EOF
|
||||
let token_kinds: Vec<_> = tokens.iter().map(|t| (&t.kind, t.line, t.column)).collect();
|
||||
// Check positions for the first token (let)
|
||||
assert_eq!(token_kinds[0], (&TokenKind::Let, 1, 1));
|
||||
// Check positions for fourth token (5)
|
||||
assert_eq!(token_kinds[3], (&TokenKind::Number(5.0), 1, 9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions_multi_line() {
|
||||
let (tokens, _) = tokenize_full("let\nx\n=");
|
||||
// let @ 1:1
|
||||
// x @ 2:1
|
||||
// = @ 3:1
|
||||
let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect();
|
||||
assert_eq!(token_kinds[0], (1, 1)); // let
|
||||
assert_eq!(token_kinds[1], (2, 1)); // x
|
||||
assert_eq!(token_kinds[2], (3, 1)); // =
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn position_after_string() {
|
||||
let (tokens, _) = tokenize_full("\"hi\" 42");
|
||||
// "hi" @ 1:1
|
||||
// 42 @ 1:6 (after "hi" which is 4 chars + space)
|
||||
let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect();
|
||||
assert_eq!(token_kinds[0], (1, 1)); // "hi" (the opening quote position)
|
||||
assert_eq!(token_kinds[1], (1, 6)); // 42
|
||||
}
|
||||
|
||||
// ----- edge cases -----
|
||||
|
||||
#[test]
|
||||
fn empty_input() {
|
||||
let kinds = tokenize("");
|
||||
assert!(kinds.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_expression() {
|
||||
let kinds = tokenize("if (x >= 0 && y < 10) { return x + y; }");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::If,
|
||||
TokenKind::LeftParen,
|
||||
TokenKind::Identifier("x".into()),
|
||||
TokenKind::GreaterEqual,
|
||||
TokenKind::Number(0.0),
|
||||
TokenKind::AndAnd,
|
||||
TokenKind::Identifier("y".into()),
|
||||
TokenKind::Less,
|
||||
TokenKind::Number(10.0),
|
||||
TokenKind::RightParen,
|
||||
TokenKind::LeftBrace,
|
||||
TokenKind::Return,
|
||||
TokenKind::Identifier("x".into()),
|
||||
TokenKind::Plus,
|
||||
TokenKind::Identifier("y".into()),
|
||||
TokenKind::Semicolon,
|
||||
TokenKind::RightBrace,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// lexer/token.rs
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TokenKind {
|
||||
// 单字符
|
||||
LeftParen, RightParen,
|
||||
LeftBrace, RightBrace,
|
||||
LeftBracket, RightBracket,
|
||||
Comma, Semicolon, Dot,
|
||||
Colon,
|
||||
Question,
|
||||
|
||||
// 运算符
|
||||
Plus, Minus, Star, Slash, Percent,
|
||||
Bang,
|
||||
Equal,
|
||||
Greater, GreaterEqual,
|
||||
Less, LessEqual,
|
||||
EqualEqual, BangEqual,
|
||||
AndAnd, OrOr,
|
||||
PlusEqual, MinusEqual, StarEqual, SlashEqual, PercentEqual,
|
||||
|
||||
// 字面量
|
||||
Identifier(String),
|
||||
Number(f64),
|
||||
String(String),
|
||||
|
||||
// 关键字
|
||||
Let,
|
||||
Const,
|
||||
Fn,
|
||||
If,
|
||||
Else,
|
||||
While,
|
||||
For,
|
||||
In,
|
||||
Break,
|
||||
Continue,
|
||||
Return,
|
||||
True,
|
||||
False,
|
||||
Nil,
|
||||
|
||||
EOF,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Token {
|
||||
pub kind: TokenKind,
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
pub mod lexer;
|
||||
pub mod ast;
|
||||
pub mod parser;
|
||||
pub mod interpreter;
|
||||
pub mod error;
|
||||
pub mod analysis;
|
||||
|
||||
use lexer::Lexer;
|
||||
use parser::Parser;
|
||||
use interpreter::Interpreter;
|
||||
use error::RuntimeError;
|
||||
|
||||
use std::io::Write;
|
||||
use std::io::stdin;
|
||||
use std::io::stdout;
|
||||
|
||||
pub fn print_errors(errors: &[RuntimeError]) {
|
||||
for err in errors {
|
||||
eprintln!("{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
std::process::exit(65);
|
||||
}
|
||||
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, parse_errors) = parser.parse();
|
||||
if !parse_errors.is_empty() {
|
||||
print_errors(&parse_errors);
|
||||
std::process::exit(65);
|
||||
}
|
||||
|
||||
let script_dir = std::path::Path::new(filename)
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
let mut interpreter = Interpreter::with_current_dir(script_dir);
|
||||
if let Err(e) = interpreter.interpret(stmts) {
|
||||
eprintln!("Error: {}", e);
|
||||
std::process::exit(70);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_repl() {
|
||||
println!("Welcome to Aster REPL!");
|
||||
println!("Type ':exit' to quit.");
|
||||
println!("Type ':reset' to reset the interpreter state.");
|
||||
|
||||
let mut interpreter = Interpreter::new();
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
print!(">>> ");
|
||||
stdout().flush().unwrap();
|
||||
|
||||
line.clear();
|
||||
if let Err(e) = stdin().read_line(&mut line) {
|
||||
eprintln!("Error reading input: {}", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match line {
|
||||
":exit" => {
|
||||
println!("Goodbye!");
|
||||
break;
|
||||
}
|
||||
":reset" => {
|
||||
interpreter = Interpreter::new();
|
||||
println!("Interpreter reset.");
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let (tokens, lex_errors) = Lexer::new(line).tokenize();
|
||||
if !lex_errors.is_empty() {
|
||||
print_errors(&lex_errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, parse_errors) = parser.parse();
|
||||
if !parse_errors.is_empty() {
|
||||
print_errors(&parse_errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = interpreter.interpret(stmts) {
|
||||
eprintln!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod parser;
|
||||
|
||||
pub use parser::Parser;
|
||||
@@ -0,0 +1,630 @@
|
||||
use crate::ast::*;
|
||||
use crate::lexer::{Token, TokenKind};
|
||||
use crate::error::RuntimeError;
|
||||
|
||||
pub struct Parser {
|
||||
tokens: Vec<Token>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(tokens: Vec<Token>) -> Self {
|
||||
Self { tokens, current: 0 }
|
||||
}
|
||||
|
||||
pub fn parse(&mut self) -> (Vec<Stmt>, Vec<RuntimeError>) {
|
||||
let mut statements = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
while !self.is_at_end() {
|
||||
match self.declaration() {
|
||||
Ok(stmt) => statements.push(stmt),
|
||||
Err(err) => {
|
||||
errors.push(err);
|
||||
self.synchronize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(statements, errors)
|
||||
}
|
||||
|
||||
fn declaration(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
if self.match_kind(&[TokenKind::Let]) {
|
||||
self.let_declaration(true)
|
||||
} else if self.match_kind(&[TokenKind::Const]) {
|
||||
self.let_declaration(false)
|
||||
} else if self.match_kind(&[TokenKind::Fn]) {
|
||||
self.fn_declaration()
|
||||
} else {
|
||||
self.statement()
|
||||
}
|
||||
}
|
||||
|
||||
fn statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
if self.match_kind(&[TokenKind::If]) {
|
||||
self.if_statement()
|
||||
} else if self.match_kind(&[TokenKind::While]) {
|
||||
self.while_statement()
|
||||
} else if self.match_kind(&[TokenKind::For]) {
|
||||
self.for_statement()
|
||||
} else if self.match_kind(&[TokenKind::Break]) {
|
||||
self.break_statement()
|
||||
} else if self.match_kind(&[TokenKind::Continue]) {
|
||||
self.continue_statement()
|
||||
} else if self.match_kind(&[TokenKind::Return]) {
|
||||
self.return_statement()
|
||||
} else if self.match_kind(&[TokenKind::LeftBrace]) {
|
||||
Ok(Stmt::Block(self.block()?))
|
||||
} else {
|
||||
self.expression_statement()
|
||||
}
|
||||
}
|
||||
|
||||
fn let_declaration(&mut self, mutable: bool) -> Result<Stmt, RuntimeError> {
|
||||
let name = self.consume_ident("Expected variable name.")?;
|
||||
self.consume(TokenKind::Equal, "Expected '=' after variable name.")?;
|
||||
let initializer = self.expression()?;
|
||||
self.match_kind(&[TokenKind::Semicolon]); // 分号可选
|
||||
|
||||
Ok(Stmt::Let { name, initializer, mutable })
|
||||
}
|
||||
|
||||
fn fn_declaration(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
let name = self.consume_ident("Expected function name.")?;
|
||||
self.consume(TokenKind::LeftParen, "Expected '(' after function name.")?;
|
||||
|
||||
let mut params = Vec::new();
|
||||
if !self.check(&TokenKind::RightParen) {
|
||||
loop {
|
||||
params.push(self.consume_ident("Expected parameter name.")?);
|
||||
if !self.match_kind(&[TokenKind::Comma]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after parameters.")?;
|
||||
self.consume(TokenKind::LeftBrace, "Expected '{' before function body.")?;
|
||||
let body = self.block()?;
|
||||
|
||||
Ok(Stmt::Function { name, params, body })
|
||||
}
|
||||
|
||||
fn lambda_expr(&mut self) -> Result<Expr, RuntimeError> {
|
||||
self.consume(TokenKind::LeftParen, "Expected '(' after 'fn'.")?;
|
||||
let mut params = Vec::new();
|
||||
if !self.check(&TokenKind::RightParen) {
|
||||
loop {
|
||||
params.push(self.consume_ident("Expected parameter name.")?);
|
||||
if !self.match_kind(&[TokenKind::Comma]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after parameters.")?;
|
||||
self.consume(TokenKind::LeftBrace, "Expected '{' before function body.")?;
|
||||
let body = self.block()?;
|
||||
Ok(Expr::Lambda { params, body })
|
||||
}
|
||||
|
||||
fn if_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
self.consume(TokenKind::LeftParen, "Expected '(' after 'if'.")?;
|
||||
let condition = self.expression()?;
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after condition.")?;
|
||||
|
||||
let then_branch = Box::new(self.statement()?);
|
||||
let else_branch = if self.match_kind(&[TokenKind::Else]) {
|
||||
Some(Box::new(self.statement()?))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Stmt::If { condition, then_branch, else_branch })
|
||||
}
|
||||
|
||||
fn while_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
self.consume(TokenKind::LeftParen, "Expected '(' after 'while'.")?;
|
||||
let condition = self.expression()?;
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after condition.")?;
|
||||
|
||||
let body = Box::new(self.statement()?);
|
||||
Ok(Stmt::While { condition, body })
|
||||
}
|
||||
|
||||
fn for_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
self.consume(TokenKind::LeftParen, "Expected '(' after 'for'.")?;
|
||||
|
||||
// Detect for-in: for (Identifier in expr) body
|
||||
if let TokenKind::Identifier(var_name) = &self.peek().kind {
|
||||
let var_name = var_name.clone();
|
||||
if matches!(self.peek_next().kind, TokenKind::In) {
|
||||
self.advance(); // consume identifier
|
||||
self.advance(); // consume 'in'
|
||||
let iterable = self.expression()?;
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after for-in expression.")?;
|
||||
let body = Box::new(self.statement()?);
|
||||
return Ok(Stmt::ForIn { var_name, iterable, body });
|
||||
}
|
||||
}
|
||||
|
||||
let initializer = if self.match_kind(&[TokenKind::Semicolon]) {
|
||||
None
|
||||
} else if self.match_kind(&[TokenKind::Let]) {
|
||||
Some(self.let_declaration(true)?)
|
||||
} else {
|
||||
Some(self.expression_statement()?)
|
||||
};
|
||||
|
||||
let condition = if !self.check(&TokenKind::Semicolon) {
|
||||
Some(self.expression()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.consume(TokenKind::Semicolon, "Expected ';' after loop condition.")?;
|
||||
let step = if !self.check(&TokenKind::RightParen) {
|
||||
Some(self.expression()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after for clauses.")?;
|
||||
let body = Box::new(self.statement()?);
|
||||
|
||||
Ok(Stmt::For {
|
||||
initializer: initializer.map(Box::new),
|
||||
condition,
|
||||
step,
|
||||
body
|
||||
})
|
||||
}
|
||||
|
||||
fn break_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
self.match_kind(&[TokenKind::Semicolon]); // 分号可选
|
||||
Ok(Stmt::Break)
|
||||
}
|
||||
|
||||
fn continue_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
self.match_kind(&[TokenKind::Semicolon]); // 分号可选
|
||||
Ok(Stmt::Continue)
|
||||
}
|
||||
|
||||
fn return_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
let value = if !self.check(&TokenKind::Semicolon) && !self.check(&TokenKind::RightBrace) {
|
||||
Some(self.expression()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.match_kind(&[TokenKind::Semicolon]); // 分号可选
|
||||
Ok(Stmt::Return(value))
|
||||
}
|
||||
|
||||
fn block(&mut self) -> Result<Vec<Stmt>, RuntimeError> {
|
||||
let mut statements = Vec::new();
|
||||
|
||||
while !self.check(&TokenKind::RightBrace) && !self.is_at_end() {
|
||||
statements.push(self.declaration()?);
|
||||
}
|
||||
|
||||
self.consume(TokenKind::RightBrace, "Expected '}' after block.")?;
|
||||
Ok(statements)
|
||||
}
|
||||
|
||||
fn expression_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
let expr = self.expression()?;
|
||||
self.match_kind(&[TokenKind::Semicolon]); // 分号可选
|
||||
Ok(Stmt::ExprStmt(expr))
|
||||
}
|
||||
|
||||
fn expression(&mut self) -> Result<Expr, RuntimeError> {
|
||||
self.assignment()
|
||||
}
|
||||
|
||||
fn ternary(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let expr = self.logical_or()?;
|
||||
if self.match_kind(&[TokenKind::Question]) {
|
||||
let then_branch = self.assignment()?;
|
||||
self.consume(TokenKind::Colon, "Expected ':' after then branch of ternary.")?;
|
||||
let else_branch = self.assignment()?;
|
||||
Ok(Expr::Ternary {
|
||||
condition: Box::new(expr),
|
||||
then_branch: Box::new(then_branch),
|
||||
else_branch: Box::new(else_branch),
|
||||
})
|
||||
} else {
|
||||
Ok(expr)
|
||||
}
|
||||
}
|
||||
|
||||
fn assignment(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let expr = self.ternary()?;
|
||||
|
||||
let op = if self.match_kind(&[TokenKind::Equal]) {
|
||||
Some(AssignOp::Equal)
|
||||
} else if self.match_kind(&[TokenKind::PlusEqual]) {
|
||||
Some(AssignOp::PlusEqual)
|
||||
} else if self.match_kind(&[TokenKind::MinusEqual]) {
|
||||
Some(AssignOp::MinusEqual)
|
||||
} else if self.match_kind(&[TokenKind::StarEqual]) {
|
||||
Some(AssignOp::StarEqual)
|
||||
} else if self.match_kind(&[TokenKind::SlashEqual]) {
|
||||
Some(AssignOp::SlashEqual)
|
||||
} else if self.match_kind(&[TokenKind::PercentEqual]) {
|
||||
Some(AssignOp::PercentEqual)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(op) = op {
|
||||
let value = self.assignment()?;
|
||||
return match expr {
|
||||
Expr::Variable(name) => Ok(Expr::Assign { name, op, value: Box::new(value) }),
|
||||
Expr::Get { object, name } => Ok(Expr::Set { object, name, op, value: Box::new(value) }),
|
||||
Expr::IndexGet { array, index } => Ok(Expr::IndexSet { array, index, op, value: Box::new(value) }),
|
||||
_ => Err(RuntimeError::parse("Invalid assignment target.", self.previous().clone())),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn logical_or(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut expr = self.logical_and()?;
|
||||
|
||||
while self.match_kind(&[TokenKind::OrOr]) {
|
||||
let right = self.logical_and()?;
|
||||
expr = Expr::Logical {
|
||||
left: Box::new(expr),
|
||||
op: LogicalOp::Or,
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn logical_and(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut expr = self.equality()?;
|
||||
|
||||
while self.match_kind(&[TokenKind::AndAnd]) {
|
||||
let right = self.equality()?;
|
||||
expr = Expr::Logical {
|
||||
left: Box::new(expr),
|
||||
op: LogicalOp::And,
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn equality(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut expr = self.comparison()?;
|
||||
|
||||
while self.match_kind(&[TokenKind::EqualEqual, TokenKind::BangEqual]) {
|
||||
let op = match self.previous().kind {
|
||||
TokenKind::EqualEqual => BinaryOp::Equal,
|
||||
TokenKind::BangEqual => BinaryOp::NotEqual,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let right = self.comparison()?;
|
||||
expr = Expr::Binary {
|
||||
left: Box::new(expr),
|
||||
op,
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn comparison(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut expr = self.term()?;
|
||||
|
||||
while self.match_kind(&[TokenKind::Greater, TokenKind::GreaterEqual, TokenKind::Less, TokenKind::LessEqual]) {
|
||||
let op = match self.previous().kind {
|
||||
TokenKind::Greater => BinaryOp::Greater,
|
||||
TokenKind::GreaterEqual => BinaryOp::GreaterEqual,
|
||||
TokenKind::Less => BinaryOp::Less,
|
||||
TokenKind::LessEqual => BinaryOp::LessEqual,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let right = self.term()?;
|
||||
expr = Expr::Binary {
|
||||
left: Box::new(expr),
|
||||
op,
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn term(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut expr = self.factor()?;
|
||||
|
||||
while self.match_kind(&[TokenKind::Plus, TokenKind::Minus]) {
|
||||
let op = match self.previous().kind {
|
||||
TokenKind::Plus => BinaryOp::Add,
|
||||
TokenKind::Minus => BinaryOp::Sub,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let right = self.factor()?;
|
||||
expr = Expr::Binary {
|
||||
left: Box::new(expr),
|
||||
op,
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn factor(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut expr = self.unary()?;
|
||||
|
||||
while self.match_kind(&[TokenKind::Star, TokenKind::Slash, TokenKind::Percent]) {
|
||||
let op = match self.previous().kind {
|
||||
TokenKind::Star => BinaryOp::Mul,
|
||||
TokenKind::Slash => BinaryOp::Div,
|
||||
TokenKind::Percent => BinaryOp::Mod,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let right = self.unary()?;
|
||||
expr = Expr::Binary {
|
||||
left: Box::new(expr),
|
||||
op,
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn unary(&mut self) -> Result<Expr, RuntimeError> {
|
||||
if self.match_kind(&[TokenKind::Bang, TokenKind::Minus]) {
|
||||
let op = match self.previous().kind {
|
||||
TokenKind::Bang => UnaryOp::Not,
|
||||
TokenKind::Minus => UnaryOp::Negate,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let right = self.unary()?;
|
||||
return Ok(Expr::Unary {
|
||||
op,
|
||||
right: Box::new(right),
|
||||
});
|
||||
}
|
||||
|
||||
self.call()
|
||||
}
|
||||
|
||||
fn call(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut expr = self.primary()?;
|
||||
|
||||
loop {
|
||||
if self.match_kind(&[TokenKind::LeftParen]) {
|
||||
let mut arguments = Vec::new();
|
||||
if !self.check(&TokenKind::RightParen) {
|
||||
loop {
|
||||
arguments.push(self.expression()?);
|
||||
if !self.match_kind(&[TokenKind::Comma]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after arguments.")?;
|
||||
expr = Expr::Call {
|
||||
callee: Box::new(expr),
|
||||
arguments,
|
||||
};
|
||||
} else if self.match_kind(&[TokenKind::Dot]) {
|
||||
let name = self.consume_ident("Expected property name after '.'.")?;
|
||||
expr = Expr::Get {
|
||||
object: Box::new(expr),
|
||||
name,
|
||||
};
|
||||
} else if self.match_kind(&[TokenKind::LeftBracket]) {
|
||||
let index = self.expression()?;
|
||||
self.consume(TokenKind::RightBracket, "Expected ']' after index.")?;
|
||||
expr = Expr::IndexGet {
|
||||
array: Box::new(expr),
|
||||
index: Box::new(index),
|
||||
};
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn primary(&mut self) -> Result<Expr, RuntimeError> {
|
||||
// 布尔值
|
||||
if self.match_kind(&[TokenKind::True]) {
|
||||
return Ok(Expr::Literal(Literal::Bool(true)));
|
||||
}
|
||||
if self.match_kind(&[TokenKind::False]) {
|
||||
return Ok(Expr::Literal(Literal::Bool(false)));
|
||||
}
|
||||
if self.match_kind(&[TokenKind::Nil]) {
|
||||
return Ok(Expr::Literal(Literal::Nil));
|
||||
}
|
||||
|
||||
// 数字
|
||||
if let TokenKind::Number(n) = self.peek().kind {
|
||||
self.advance();
|
||||
return Ok(Expr::Literal(Literal::Number(n)));
|
||||
}
|
||||
|
||||
// 字符串
|
||||
if let TokenKind::String(s) = &self.peek().kind {
|
||||
let s = s.clone();
|
||||
self.advance();
|
||||
return Ok(Expr::Literal(Literal::String(s)));
|
||||
}
|
||||
|
||||
// 标识符(变量)
|
||||
if let TokenKind::Identifier(name) = &self.peek().kind {
|
||||
let name = name.clone();
|
||||
self.advance();
|
||||
return Ok(Expr::Variable(name));
|
||||
}
|
||||
|
||||
// 匿名函数(闭包): fn(params) { body }
|
||||
if self.match_kind(&[TokenKind::Fn]) {
|
||||
return self.lambda_expr();
|
||||
}
|
||||
|
||||
// 括号表达式
|
||||
if self.match_kind(&[TokenKind::LeftParen]) {
|
||||
let expr = self.expression()?;
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after expression.")?;
|
||||
return Ok(expr);
|
||||
}
|
||||
|
||||
if self.match_kind(&[TokenKind::LeftBrace]) {
|
||||
return self.object_literal();
|
||||
}
|
||||
|
||||
if self.match_kind(&[TokenKind::LeftBracket]) {
|
||||
return self.array_literal();
|
||||
}
|
||||
|
||||
Err(RuntimeError::parse("Expected expression.", self.peek().clone()))
|
||||
}
|
||||
|
||||
fn match_kind(&mut self, kinds: &[TokenKind]) -> bool {
|
||||
for kind in kinds {
|
||||
if self.check(kind) {
|
||||
self.advance();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn consume(&mut self, kind: TokenKind, msg: &str) -> Result<(), RuntimeError> {
|
||||
if self.check(&kind) {
|
||||
self.advance();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(RuntimeError::parse(msg, self.peek().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_ident(&mut self, msg: &str) -> Result<String, RuntimeError> {
|
||||
match &self.peek().kind {
|
||||
TokenKind::Identifier(name) => {
|
||||
let name = name.clone();
|
||||
self.advance();
|
||||
Ok(name)
|
||||
}
|
||||
_ => Err(RuntimeError::parse(msg, self.peek().clone())),
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&self, kind: &TokenKind) -> bool {
|
||||
if self.is_at_end() {
|
||||
return false;
|
||||
}
|
||||
std::mem::discriminant(&self.peek().kind)
|
||||
== std::mem::discriminant(kind)
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> &Token {
|
||||
if !self.is_at_end() {
|
||||
self.current += 1;
|
||||
}
|
||||
self.previous()
|
||||
}
|
||||
|
||||
fn is_at_end(&self) -> bool {
|
||||
matches!(self.peek().kind, TokenKind::EOF)
|
||||
}
|
||||
|
||||
fn peek(&self) -> &Token {
|
||||
&self.tokens[self.current]
|
||||
}
|
||||
|
||||
fn peek_next(&self) -> &Token {
|
||||
&self.tokens[self.current + 1]
|
||||
}
|
||||
|
||||
fn previous(&self) -> &Token {
|
||||
&self.tokens[self.current - 1]
|
||||
}
|
||||
|
||||
fn object_literal(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut properties = Vec::new();
|
||||
if !self.check(&TokenKind::RightBrace) {
|
||||
loop {
|
||||
let name = self.consume_ident("Expected property name in object literal")?;
|
||||
self.consume(TokenKind::Colon, "Expected ':' after property name in object literal")?;
|
||||
let value = self.expression()?;
|
||||
properties.push((name, value));
|
||||
if !self.match_kind(&[TokenKind::Comma]) {
|
||||
break;
|
||||
}
|
||||
// Allow trailing comma
|
||||
if self.check(&TokenKind::RightBrace) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.consume(TokenKind::RightBrace, "Expected '}' after object literal")?;
|
||||
Ok(Expr::ObjectLiteral { properties })
|
||||
}
|
||||
|
||||
fn array_literal(&mut self) -> Result<Expr, RuntimeError> {
|
||||
let mut elements = Vec::new();
|
||||
if !self.check(&TokenKind::RightBracket) {
|
||||
loop {
|
||||
elements.push(self.expression()?);
|
||||
if !self.match_kind(&[TokenKind::Comma]) {
|
||||
break;
|
||||
}
|
||||
// Allow trailing comma
|
||||
if self.check(&TokenKind::RightBracket) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.consume(TokenKind::RightBracket, "Expected ']' after array literal")?;
|
||||
Ok(Expr::ArrayLiteral { elements })
|
||||
}
|
||||
|
||||
fn synchronize(&mut self) {
|
||||
// Always consume at least one token to guarantee forward progress,
|
||||
// preventing infinite loops when previous() is already a semicolon.
|
||||
if !self.is_at_end() {
|
||||
self.advance();
|
||||
}
|
||||
|
||||
while !self.is_at_end() {
|
||||
if self.current > 0 && matches!(self.previous().kind, TokenKind::Semicolon) {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.peek().kind {
|
||||
TokenKind::Let
|
||||
| TokenKind::Const
|
||||
| TokenKind::Fn
|
||||
| TokenKind::If
|
||||
| TokenKind::While
|
||||
| TokenKind::For
|
||||
| TokenKind::Return
|
||||
| TokenKind::Break
|
||||
| TokenKind::Continue => return,
|
||||
_ => {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,836 @@
|
||||
use super::*;
|
||||
use crate::lexer::Lexer;
|
||||
|
||||
/// Helper: lex + parse, return (stmts, errors).
|
||||
fn parse(input: &str) -> (Vec<Stmt>, Vec<RuntimeError>) {
|
||||
let (tokens, _) = Lexer::new(input).tokenize();
|
||||
Parser::new(tokens).parse()
|
||||
}
|
||||
|
||||
/// Helper: lex + parse single expression (wraps in `let _ = <expr>;`).
|
||||
fn parse_expr(input: &str) -> Expr {
|
||||
let (stmts, errors) = parse(&format!("let __x = {};", input));
|
||||
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
||||
match &stmts[0] {
|
||||
Stmt::Let { initializer, .. } => initializer.clone(),
|
||||
_ => panic!("Expected Let statement"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: lex + parse single statement.
|
||||
fn parse_stmt(input: &str) -> Stmt {
|
||||
let (stmts, errors) = parse(input);
|
||||
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
||||
assert_eq!(stmts.len(), 1);
|
||||
stmts[0].clone()
|
||||
}
|
||||
|
||||
// ----- literals -----
|
||||
|
||||
#[test]
|
||||
fn parse_number() {
|
||||
match parse_expr("42") {
|
||||
Expr::Literal(Literal::Number(42.0)) => {}
|
||||
e => panic!("Expected Number(42.0), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_float() {
|
||||
match parse_expr("3.14") {
|
||||
Expr::Literal(Literal::Number(n)) => assert!((n - 3.14).abs() < 0.001),
|
||||
e => panic!("Expected Number, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_string() {
|
||||
match parse_expr("\"hello\"") {
|
||||
Expr::Literal(Literal::String(s)) => assert_eq!(s, "hello"),
|
||||
e => panic!("Expected String, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bool_true() {
|
||||
match parse_expr("true") {
|
||||
Expr::Literal(Literal::Bool(true)) => {}
|
||||
e => panic!("Expected Bool(true), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bool_false() {
|
||||
match parse_expr("false") {
|
||||
Expr::Literal(Literal::Bool(false)) => {}
|
||||
e => panic!("Expected Bool(false), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nil() {
|
||||
match parse_expr("nil") {
|
||||
Expr::Literal(Literal::Nil) => {}
|
||||
e => panic!("Expected Nil, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- variables -----
|
||||
|
||||
#[test]
|
||||
fn parse_variable() {
|
||||
match parse_expr("x") {
|
||||
Expr::Variable(name) => assert_eq!(name, "x"),
|
||||
e => panic!("Expected Variable, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- unary -----
|
||||
|
||||
#[test]
|
||||
fn parse_negation() {
|
||||
match parse_expr("-5") {
|
||||
Expr::Unary { op: UnaryOp::Negate, right } => {
|
||||
match *right {
|
||||
Expr::Literal(Literal::Number(5.0)) => {}
|
||||
e => panic!("Expected Number(5), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Unary(Negate, Number), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_not() {
|
||||
match parse_expr("!true") {
|
||||
Expr::Unary { op: UnaryOp::Not, right } => {
|
||||
match *right {
|
||||
Expr::Literal(Literal::Bool(true)) => {}
|
||||
e => panic!("Expected Bool(true), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Unary(Not, Bool), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_double_negation() {
|
||||
match parse_expr("!!true") {
|
||||
Expr::Unary { op: UnaryOp::Not, right } => {
|
||||
match *right {
|
||||
Expr::Unary { op: UnaryOp::Not, .. } => {}
|
||||
e => panic!("Expected nested Unary, got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Unary(Not, ...), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- binary precedence -----
|
||||
|
||||
#[test]
|
||||
fn parse_addition() {
|
||||
match parse_expr("1 + 2") {
|
||||
Expr::Binary { op: BinaryOp::Add, left, right } => {
|
||||
match (*left, *right) {
|
||||
(Expr::Literal(Literal::Number(1.0)), Expr::Literal(Literal::Number(2.0))) => {}
|
||||
e => panic!("Expected (1, 2), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Binary(Add), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multiplication_precedence() {
|
||||
// 1 + 2 * 3 => 1 + (2 * 3)
|
||||
match parse_expr("1 + 2 * 3") {
|
||||
Expr::Binary { op: BinaryOp::Add, left, right } => {
|
||||
match *left {
|
||||
Expr::Literal(Literal::Number(1.0)) => {}
|
||||
e => panic!("Expected left=1, got {:?}", e),
|
||||
}
|
||||
match *right {
|
||||
Expr::Binary { op: BinaryOp::Mul, .. } => {}
|
||||
e => panic!("Expected right=Binary(Mul), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Binary(Add), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_modulo() {
|
||||
match parse_expr("7 % 3") {
|
||||
Expr::Binary { op: BinaryOp::Mod, .. } => {}
|
||||
e => panic!("Expected Binary(Mod), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- comparison -----
|
||||
|
||||
#[test]
|
||||
fn parse_comparison_chain() {
|
||||
// a < b == c
|
||||
match parse_expr("a < b == c") {
|
||||
Expr::Binary { op: BinaryOp::Equal, left, right } => {
|
||||
match *left {
|
||||
Expr::Binary { op: BinaryOp::Less, .. } => {}
|
||||
e => panic!("Expected left=Binary(Less), got {:?}", e),
|
||||
}
|
||||
match *right {
|
||||
Expr::Variable(name) => assert_eq!(name, "c"),
|
||||
e => panic!("Expected right=Variable(c), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Binary(Equal), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- logical -----
|
||||
|
||||
#[test]
|
||||
fn parse_logical_and() {
|
||||
match parse_expr("true && false") {
|
||||
Expr::Logical { op: LogicalOp::And, .. } => {}
|
||||
e => panic!("Expected Logical(And), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_logical_or() {
|
||||
match parse_expr("true || false") {
|
||||
Expr::Logical { op: LogicalOp::Or, .. } => {}
|
||||
e => panic!("Expected Logical(Or), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_logical_precedence() {
|
||||
// a || b && c => a || (b && c)
|
||||
match parse_expr("a || b && c") {
|
||||
Expr::Logical { op: LogicalOp::Or, left, right } => {
|
||||
match *left {
|
||||
Expr::Variable(name) => assert_eq!(name, "a"),
|
||||
e => panic!("Expected left=a, got {:?}", e),
|
||||
}
|
||||
match *right {
|
||||
Expr::Logical { op: LogicalOp::And, .. } => {}
|
||||
e => panic!("Expected right=Logical(And), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Logical(Or), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- ternary -----
|
||||
|
||||
#[test]
|
||||
fn parse_ternary_simple() {
|
||||
match parse_expr("a ? 1 : 2") {
|
||||
Expr::Ternary { condition, then_branch, else_branch } => {
|
||||
assert!(matches!(*condition, Expr::Variable(_)));
|
||||
assert!(matches!(*then_branch, Expr::Literal(Literal::Number(1.0))));
|
||||
assert!(matches!(*else_branch, Expr::Literal(Literal::Number(2.0))));
|
||||
}
|
||||
e => panic!("Expected Ternary, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ternary_nested_right_assoc() {
|
||||
// a ? b : c ? d : e → a ? b : (c ? d : e)
|
||||
match parse_expr("a ? b : c ? d : e") {
|
||||
Expr::Ternary { condition, then_branch, else_branch } => {
|
||||
assert!(matches!(*condition, Expr::Variable(_)));
|
||||
assert!(matches!(*then_branch, Expr::Variable(_)));
|
||||
assert!(matches!(*else_branch, Expr::Ternary { .. }));
|
||||
}
|
||||
e => panic!("Expected Ternary with nested Ternary else, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ternary_with_assignment_in_else() {
|
||||
// a ? b : c = d → a ? b : (c = d)
|
||||
match parse_expr("a ? b : c = 5") {
|
||||
Expr::Ternary { else_branch, .. } => {
|
||||
assert!(matches!(*else_branch, Expr::Assign { .. }));
|
||||
}
|
||||
e => panic!("Expected Ternary with Assign else, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- assignment -----
|
||||
|
||||
#[test]
|
||||
fn parse_simple_assignment() {
|
||||
match parse_expr("x = 5") {
|
||||
Expr::Assign { name, op: AssignOp::Equal, value } => {
|
||||
assert_eq!(name, "x");
|
||||
match *value {
|
||||
Expr::Literal(Literal::Number(5.0)) => {}
|
||||
e => panic!("Expected Number(5), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Assign, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_compound_assignment() {
|
||||
match parse_expr("x += 2") {
|
||||
Expr::Assign { name, op: AssignOp::PlusEqual, value } => {
|
||||
assert_eq!(name, "x");
|
||||
match *value {
|
||||
Expr::Literal(Literal::Number(2.0)) => {}
|
||||
e => panic!("Expected Number(2), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Assign(PlusEqual), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_all_compound_ops() {
|
||||
let ops = [
|
||||
("+=", AssignOp::PlusEqual),
|
||||
("-=", AssignOp::MinusEqual),
|
||||
("*=", AssignOp::StarEqual),
|
||||
("/=", AssignOp::SlashEqual),
|
||||
("%=", AssignOp::PercentEqual),
|
||||
];
|
||||
for (op_str, expected_op) in &ops {
|
||||
let expr = parse_expr(&format!("x {} 1", op_str));
|
||||
match expr {
|
||||
Expr::Assign { op, .. } => {
|
||||
assert_eq!(std::mem::discriminant(&op), std::mem::discriminant(expected_op));
|
||||
}
|
||||
e => panic!("Expected Assign for '{}', got {:?}", op_str, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- property access -----
|
||||
|
||||
#[test]
|
||||
fn parse_property_get() {
|
||||
match parse_expr("obj.name") {
|
||||
Expr::Get { object, name } => {
|
||||
match *object {
|
||||
Expr::Variable(n) => assert_eq!(n, "obj"),
|
||||
e => panic!("Expected Variable(obj), got {:?}", e),
|
||||
}
|
||||
assert_eq!(name, "name");
|
||||
}
|
||||
e => panic!("Expected Get, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_property_set() {
|
||||
match parse_expr("obj.name = 5") {
|
||||
Expr::Set { object, name, op: AssignOp::Equal, value } => {
|
||||
match *object {
|
||||
Expr::Variable(n) => assert_eq!(n, "obj"),
|
||||
e => panic!("Expected Variable(obj), got {:?}", e),
|
||||
}
|
||||
assert_eq!(name, "name");
|
||||
match *value {
|
||||
Expr::Literal(Literal::Number(5.0)) => {}
|
||||
e => panic!("Expected Number(5), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Set, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_chained_property_access() {
|
||||
match parse_expr("a.b.c") {
|
||||
Expr::Get { object, name } => {
|
||||
assert_eq!(name, "c");
|
||||
match *object {
|
||||
Expr::Get { name: ref inner, .. } => assert_eq!(inner, "b"),
|
||||
e => panic!("Expected chained Get, got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Get, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- index access -----
|
||||
|
||||
#[test]
|
||||
fn parse_index_get() {
|
||||
match parse_expr("arr[0]") {
|
||||
Expr::IndexGet { array, index } => {
|
||||
match *array {
|
||||
Expr::Variable(n) => assert_eq!(n, "arr"),
|
||||
e => panic!("Expected Variable(arr), got {:?}", e),
|
||||
}
|
||||
match *index {
|
||||
Expr::Literal(Literal::Number(0.0)) => {}
|
||||
e => panic!("Expected Number(0), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected IndexGet, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_index_set() {
|
||||
match parse_expr("arr[i] = 42") {
|
||||
Expr::IndexSet { array, index, op: AssignOp::Equal, value } => {
|
||||
match *array {
|
||||
Expr::Variable(n) => assert_eq!(n, "arr"),
|
||||
e => panic!("Expected Variable(arr), got {:?}", e),
|
||||
}
|
||||
match *index {
|
||||
Expr::Variable(n) => assert_eq!(n, "i"),
|
||||
e => panic!("Expected Variable(i), got {:?}", e),
|
||||
}
|
||||
match *value {
|
||||
Expr::Literal(Literal::Number(42.0)) => {}
|
||||
e => panic!("Expected Number(42), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected IndexSet, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nested_index() {
|
||||
match parse_expr("a[1][2]") {
|
||||
Expr::IndexGet { array, .. } => {
|
||||
match *array {
|
||||
Expr::IndexGet { .. } => {}
|
||||
e => panic!("Expected nested IndexGet, got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected IndexGet, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- function calls -----
|
||||
|
||||
#[test]
|
||||
fn parse_call_no_args() {
|
||||
match parse_expr("f()") {
|
||||
Expr::Call { callee, arguments } => {
|
||||
match *callee {
|
||||
Expr::Variable(n) => assert_eq!(n, "f"),
|
||||
e => panic!("Expected Variable(f), got {:?}", e),
|
||||
}
|
||||
assert!(arguments.is_empty());
|
||||
}
|
||||
e => panic!("Expected Call, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_call_with_args() {
|
||||
match parse_expr("add(1, 2, 3)") {
|
||||
Expr::Call { callee, arguments } => {
|
||||
match *callee {
|
||||
Expr::Variable(n) => assert_eq!(n, "add"),
|
||||
e => panic!("Expected Variable(add), got {:?}", e),
|
||||
}
|
||||
assert_eq!(arguments.len(), 3);
|
||||
}
|
||||
e => panic!("Expected Call, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_chained_calls() {
|
||||
match parse_expr("f()()") {
|
||||
Expr::Call { callee, .. } => {
|
||||
match *callee {
|
||||
Expr::Call { callee, .. } => {
|
||||
match *callee {
|
||||
Expr::Variable(n) => assert_eq!(n, "f"),
|
||||
e => panic!("Expected Variable(f), got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected inner Call, got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Call, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_method_style_call() {
|
||||
// obj.method()
|
||||
match parse_expr("obj.method()") {
|
||||
Expr::Call { callee, .. } => {
|
||||
match *callee {
|
||||
Expr::Get { .. } => {}
|
||||
e => panic!("Expected Get, got {:?}", e),
|
||||
}
|
||||
}
|
||||
e => panic!("Expected Call, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- lambda -----
|
||||
|
||||
#[test]
|
||||
fn parse_simple_lambda() {
|
||||
match parse_expr("fn(x, y) { return x + y; }") {
|
||||
Expr::Lambda { params, body } => {
|
||||
assert_eq!(params, vec!["x", "y"]);
|
||||
assert_eq!(body.len(), 1);
|
||||
}
|
||||
e => panic!("Expected Lambda, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_lambda_no_params() {
|
||||
match parse_expr("fn() { return 42; }") {
|
||||
Expr::Lambda { params, body } => {
|
||||
assert!(params.is_empty());
|
||||
assert_eq!(body.len(), 1);
|
||||
}
|
||||
e => panic!("Expected Lambda, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- object literal -----
|
||||
|
||||
#[test]
|
||||
fn parse_empty_object() {
|
||||
match parse_expr("{}") {
|
||||
Expr::ObjectLiteral { properties } => assert!(properties.is_empty()),
|
||||
e => panic!("Expected ObjectLiteral, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_object_with_properties() {
|
||||
match parse_expr("{ name: \"aster\", version: 1 }") {
|
||||
Expr::ObjectLiteral { properties } => {
|
||||
assert_eq!(properties.len(), 2);
|
||||
assert_eq!(properties[0].0, "name");
|
||||
assert_eq!(properties[1].0, "version");
|
||||
}
|
||||
e => panic!("Expected ObjectLiteral, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- array literal -----
|
||||
|
||||
#[test]
|
||||
fn parse_empty_array() {
|
||||
match parse_expr("[]") {
|
||||
Expr::ArrayLiteral { elements } => assert!(elements.is_empty()),
|
||||
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_array_with_elements() {
|
||||
match parse_expr("[1, 2, 3]") {
|
||||
Expr::ArrayLiteral { elements } => assert_eq!(elements.len(), 3),
|
||||
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nested_arrays() {
|
||||
match parse_expr("[[1, 2], [3, 4]]") {
|
||||
Expr::ArrayLiteral { elements } => {
|
||||
assert_eq!(elements.len(), 2);
|
||||
for e in &elements {
|
||||
assert!(matches!(e, Expr::ArrayLiteral { .. }));
|
||||
}
|
||||
}
|
||||
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn parse_let_statement() {
|
||||
match parse_stmt("let x = 5;") {
|
||||
Stmt::Let { name, initializer, .. } => {
|
||||
assert_eq!(name, "x");
|
||||
assert!(matches!(initializer, Expr::Literal(Literal::Number(5.0))));
|
||||
}
|
||||
e => panic!("Expected Let, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_let_without_semicolon() {
|
||||
// Semicolons are optional
|
||||
match parse_stmt("let x = 5") {
|
||||
Stmt::Let { name, .. } => assert_eq!(name, "x"),
|
||||
e => panic!("Expected Let, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_const_statement() {
|
||||
match parse_stmt("const x = 5;") {
|
||||
Stmt::Let { name, mutable, .. } => {
|
||||
assert_eq!(name, "x");
|
||||
assert!(!mutable, "const should set mutable=false");
|
||||
}
|
||||
e => panic!("Expected Let (const), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_expression_statement() {
|
||||
match parse_stmt("x;") {
|
||||
Stmt::ExprStmt(Expr::Variable(name)) => assert_eq!(name, "x"),
|
||||
e => panic!("Expected ExprStmt(Variable), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_if_statement() {
|
||||
match parse_stmt("if (x) { 1; }") {
|
||||
Stmt::If { condition, then_branch, else_branch } => {
|
||||
assert!(matches!(condition, Expr::Variable(_)));
|
||||
assert!(matches!(*then_branch, Stmt::Block(_)));
|
||||
assert!(else_branch.is_none());
|
||||
}
|
||||
e => panic!("Expected If, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_if_else_statement() {
|
||||
match parse_stmt("if (x) { 1; } else { 2; }") {
|
||||
Stmt::If { else_branch, .. } => {
|
||||
assert!(else_branch.is_some());
|
||||
}
|
||||
e => panic!("Expected If with else, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_while_statement() {
|
||||
match parse_stmt("while (x < 10) { x = x + 1; }") {
|
||||
Stmt::While { condition, body } => {
|
||||
assert!(matches!(condition, Expr::Binary { .. }));
|
||||
assert!(matches!(*body, Stmt::Block(_)));
|
||||
}
|
||||
e => panic!("Expected While, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_statement() {
|
||||
match parse_stmt("for (let i = 0; i < 10; i = i + 1) { }") {
|
||||
Stmt::For { initializer, condition, step, body } => {
|
||||
assert!(initializer.is_some());
|
||||
assert!(condition.is_some());
|
||||
assert!(step.is_some());
|
||||
assert!(matches!(*body, Stmt::Block(_)));
|
||||
}
|
||||
e => panic!("Expected For, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_empty_clauses() {
|
||||
match parse_stmt("for (;;) { }") {
|
||||
Stmt::For { initializer, condition, step, .. } => {
|
||||
assert!(initializer.is_none());
|
||||
assert!(condition.is_none());
|
||||
assert!(step.is_none());
|
||||
}
|
||||
e => panic!("Expected For, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_in_array() {
|
||||
match parse_stmt("for (x in arr) { print(x); }") {
|
||||
Stmt::ForIn { var_name, iterable, body } => {
|
||||
assert_eq!(var_name, "x");
|
||||
assert!(matches!(iterable, Expr::Variable(_)));
|
||||
assert!(matches!(*body, Stmt::Block(_)));
|
||||
}
|
||||
e => panic!("Expected ForIn, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_in_with_object_literal() {
|
||||
match parse_stmt("for (k in {a:1,b:2}) { print(k); }") {
|
||||
Stmt::ForIn { var_name, iterable, .. } => {
|
||||
assert_eq!(var_name, "k");
|
||||
assert!(matches!(iterable, Expr::ObjectLiteral { .. }));
|
||||
}
|
||||
e => panic!("Expected ForIn, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_in_with_expression() {
|
||||
match parse_stmt("for (v in get_list()) { }") {
|
||||
Stmt::ForIn { var_name, .. } => {
|
||||
assert_eq!(var_name, "v");
|
||||
}
|
||||
e => panic!("Expected ForIn, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_in_distinct_from_c_style() {
|
||||
// "for (x = 0; ...)" is C-style for, not for-in
|
||||
match parse_stmt("for (x = 0; x < 10; x = x + 1) { }") {
|
||||
Stmt::For { .. } => {}
|
||||
e => panic!("Expected C-style For, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_block() {
|
||||
match parse_stmt("{ let x = 1; x; }") {
|
||||
Stmt::Block(stmts) => assert_eq!(stmts.len(), 2),
|
||||
e => panic!("Expected Block, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_return_statement() {
|
||||
match parse_stmt("return 42;") {
|
||||
Stmt::Return(Some(expr)) => {
|
||||
assert!(matches!(expr, Expr::Literal(Literal::Number(42.0))));
|
||||
}
|
||||
e => panic!("Expected Return(42), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_return_nil() {
|
||||
match parse_stmt("return;") {
|
||||
Stmt::Return(None) => {}
|
||||
e => panic!("Expected Return(None), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_break_statement() {
|
||||
match parse_stmt("break;") {
|
||||
Stmt::Break => {}
|
||||
e => panic!("Expected Break, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_continue_statement() {
|
||||
match parse_stmt("continue;") {
|
||||
Stmt::Continue => {}
|
||||
e => panic!("Expected Continue, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_function_declaration() {
|
||||
match parse_stmt("fn add(a, b) { return a + b; }") {
|
||||
Stmt::Function { name, params, body } => {
|
||||
assert_eq!(name, "add");
|
||||
assert_eq!(params, vec!["a", "b"]);
|
||||
assert_eq!(body.len(), 1);
|
||||
}
|
||||
e => panic!("Expected Function, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_function_no_params() {
|
||||
match parse_stmt("fn greet() { print(\"hi\"); }") {
|
||||
Stmt::Function { name, params, .. } => {
|
||||
assert_eq!(name, "greet");
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
e => panic!("Expected Function, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- error recovery -----
|
||||
|
||||
#[test]
|
||||
fn error_recovery_skips_past_semicolon() {
|
||||
// First statement is invalid, second should parse fine
|
||||
let (stmts, errors) = parse("let = 5; let y = 10;");
|
||||
assert!(!errors.is_empty(), "Should have parse errors");
|
||||
// Should have at least the second statement
|
||||
assert!(stmts.len() >= 1);
|
||||
// The second statement should be a valid Let for y
|
||||
match &stmts.last().unwrap() {
|
||||
Stmt::Let { name, .. } => assert_eq!(name, "y"),
|
||||
e => panic!("Expected Let(y), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_recovery_at_statement_boundary() {
|
||||
let (stmts, errors) = parse("let x = @@@; let y = 10;");
|
||||
assert!(!errors.is_empty());
|
||||
assert!(stmts.len() >= 1);
|
||||
}
|
||||
|
||||
// ----- parse errors -----
|
||||
|
||||
#[test]
|
||||
fn error_invalid_assignment_target() {
|
||||
let (_stmts, errors) = parse("5 = 10;");
|
||||
assert!(!errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_missing_closing_paren() {
|
||||
let (_stmts, errors) = parse("if (x { 1; }");
|
||||
assert!(!errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_missing_semicolon_after_for_condition() {
|
||||
// for(;;){} is valid — test something actually invalid
|
||||
let (_stmts, _errors) = parse("for (let i = 0 i < 10) { }");
|
||||
assert!(!_errors.is_empty());
|
||||
}
|
||||
|
||||
// ----- parenthesized expression -----
|
||||
|
||||
#[test]
|
||||
fn parse_parenthesized_expression() {
|
||||
match parse_expr("(1 + 2)") {
|
||||
Expr::Binary { op: BinaryOp::Add, .. } => {}
|
||||
e => panic!("Expected Binary(Add), got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nested_parentheses() {
|
||||
match parse_expr("((42))") {
|
||||
Expr::Literal(Literal::Number(42.0)) => {}
|
||||
e => panic!("Expected Number(42), got {:?}", e),
|
||||
}
|
||||
}
|
||||
+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; }
|
||||
Reference in New Issue
Block a user