Files
OxideJS-New/crates/oxide_cli/src/main.rs
T

906 lines
34 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![allow(clippy::arc_with_non_send_sync)]
#![allow(dead_code)]
use std::fs;
use std::net::{IpAddr, SocketAddr};
use std::process::ExitCode;
use std::time::{Duration, Instant};
use ansi_term::Colour::Red;
use clap::{Parser, Subcommand};
use oxide_cli::format_js_value;
use oxide_cli::server::cleanup::{well_known_cleanup, CleanupOutcome};
use oxide_cli::server::client::send_control_request;
use oxide_cli::server::log as server_log;
use oxide_cli::server::protocol::{ForgeTarget, LookupResult, ServerRequest, ServerResponse};
use oxide_cli::server::server::{run_server, run_server_rm, RmServerConfig, ServerConfig};
use oxide_cli::server::sidecar;
use oxide_cli::server::spawn;
use oxide_cli::server::watchdog;
use oxide_compiler::compiler::Compiler;
use oxide_compiler::compiler_error;
use oxide_engine::Engine;
use oxide_kernel::kernel::KernelConfig;
use oxide_kernel::shape_forge::ShapeForge;
use oxide_kernel::string_forge::PermInterner;
use oxide_kernel::{kernel_error, kernel_info};
use oxide_log::{Level, SUBSYSTEM_COUNT};
use oxide_net::Endpoint;
use oxide_vm::vm::Vm;
use oxide_vm::vm_error;
use oxide_vm::JsValue;
mod bench;
#[derive(Parser)]
#[command(
name = "oxide",
// 版本号后附构建期 git 短哈希,`--version` 自报即新鲜度自证锚点。
version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_COMMIT"), ")"),
about = "OxideJS - Rust JavaScript engine"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(short, long, global = true)]
quiet: bool,
/// 聚合并输出现有计数器(GC 统计、内联缓存、session 堆账目、指令数)。
/// 默认关闭,关闭时零开销;输出每指标一行,与 bench 输出对齐。
#[arg(long, global = true)]
profile: bool,
}
#[derive(Subcommand)]
enum Commands {
Eval {
code: String,
/// 逐指令 trace:每条指令向 stderr 写一行 pc + opcode + 操作数。
#[arg(long)]
trace: bool,
},
Run {
file: String,
#[arg(long, default_value = "1")]
repeat: u64,
/// 逐指令 trace:每条指令向 stderr 写一行 pc + opcode + 操作数。
#[arg(long)]
trace: bool,
},
Compile {
#[arg(short = 'e')]
expr: Option<String>,
file: Option<String>,
#[arg(long)]
no_dce: bool,
/// 关闭 liveness/精确 DCE/RegAlloc 链,vreg 原样当物理号(调试降级路径)。
#[arg(long)]
no_regalloc: bool,
},
Bench {
#[arg(default_value = "js")]
mode: Option<String>,
#[arg(short, long)]
filter: Option<String>,
#[arg(long, default_value = "2")]
warmup: u32,
#[arg(long, default_value = "10")]
iterations: u32,
#[arg(long)]
update_baseline: bool,
/// 指令周期采样周期(2 的幂,0 关闭,默认关闭)。开启时测量迭代每
/// period 条指令记一条样本,run 末按 flat_id 输出 top-K 直方图。
#[arg(long, default_value = "0")]
sample: u64,
/// 采样直方图 top-K 大小(默认 10)。
#[arg(long, default_value = "10")]
sample_top: usize,
},
Test {
suite: Option<String>,
},
/// 持久 server:常驻进程,复用预热池,近零 spawn 成本。
Server {
#[command(subcommand)]
command: ServerCommands,
},
}
/// server 子命令:持久 server 进程管理与 --rm 独立模式。
#[derive(Subcommand)]
enum ServerCommands {
/// 启动 server。持久形态为守护形态(spawn 脱离的后台进程,就绪探测通过后返回);
/// --rm 为独立形态(进程唯一 socket、不注册 sidecar、空闲超时或断开即退出)。
Start {
/// 独立模式:进程唯一 socket 路径,不注册 sidecar,空闲超时或断开即自动退出。
#[arg(long)]
rm: bool,
/// 空闲超时(秒),仅对 --rm 有效;窗口内无连接或连接上无数据即自动退出。
#[arg(long, default_value = "30")]
idle_timeout: u64,
/// 常驻 worker 线程数;缺省取宿主核数。
#[arg(long)]
workers: Option<u32>,
/// TCP 监听端口;0 表示动态端口(操作系统分配空闲端口),缺省取
/// well-known 端口。
#[arg(long)]
port: Option<u16>,
/// TCP 绑定地址;缺省 loopback(127.0.0.1),两臂(--rm 与持久)共享。
#[arg(long, default_value = "127.0.0.1")]
host: IpAddr,
/// 前台形态:当前进程直接运行持久 server(守护形态 spawn 的子进程用,
/// 不向用户暴露)。
#[arg(long, hide = true)]
foreground: bool,
},
/// 向持久 server 发送关闭请求。
Stop,
/// 查询 server 状态(池与运行时长)。
Status,
/// 健康检查。
Health,
/// 查询 server 信息(版本、socket 路径、进程号)。
Info,
/// 查询 server 版本。
Version,
/// 清理残留文件(无 sidecar、陈旧 socket、杀刻度匹配进程)。
Cleanup,
/// 向存活 server 发关闭请求,等其退出后拉起新 server 进程。
Restart,
/// 查看 server 日志(well-known 日志文件)。
Log {
/// 持续跟踪新行(tail 语义,500 毫秒轮询)。
#[arg(long)]
follow: bool,
/// 级别阈值过滤:只显示不低于该级别的行。
#[arg(long, value_enum)]
level: Option<LogLevel>,
/// 显示最后 N 行(缺省全部)。
#[arg(long)]
lines: Option<usize>,
},
/// 查询内部 forge 状态(code/object/string/property),加三个行动旗标。
Forge {
/// 目标 forge:code(字节码缓存)/ object(shape 表)/ string(字符串
/// intern)/ property(属性模板缓存)。
target: ForgeTargetCli,
/// 触发垃圾回收(对池内全体空闲 VM 执行完整 session GC)。
#[arg(long)]
gc: bool,
/// 清空字节码缓存(LRU)。
#[arg(long)]
clear_cache: bool,
/// 在字符串 intern 表纯查询一个键的 id(不插入)。
#[arg(long)]
lookup: Option<String>,
},
/// 监控 server 存活并在崩溃后自动重启(前台进程,60 秒窗口 5 次崩溃即停)。
Watchdog {
/// 常驻 worker 线程数;缺省取宿主核数。
#[arg(long)]
workers: Option<u32>,
},
}
/// forge 查询目标(CLI 侧,映射到协议侧 `ForgeTarget`)。
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
enum ForgeTargetCli {
/// 字节码 LRU 缓存。
Code,
/// 隐藏类 shape 表。
Object,
/// 字符串 intern 表。
String,
/// 属性模板缓存。
Property,
}
impl ForgeTargetCli {
/// 映射到协议侧 `ForgeTarget`。
fn to_protocol(self) -> ForgeTarget {
match self {
ForgeTargetCli::Code => ForgeTarget::Code,
ForgeTargetCli::Object => ForgeTarget::Object,
ForgeTargetCli::String => ForgeTarget::String,
ForgeTargetCli::Property => ForgeTarget::Property,
}
}
}
/// 日志级别阈值(读取侧过滤,映射到 `oxide_log::Level`)。
#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
}
impl LogLevel {
/// 映射到 `oxide_log::Level`。
fn as_level(self) -> Level {
match self {
LogLevel::Error => Level::Error,
LogLevel::Warn => Level::Warn,
LogLevel::Info => Level::Info,
LogLevel::Debug => Level::Debug,
}
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.command {
Some(Commands::Eval { code, trace }) => {
let engine = make_engine(cli.verbose, cli.quiet);
eval(&code, &engine, trace, cli.profile, true)
}
Some(Commands::Run { file, repeat, trace }) => {
let engine = make_engine(cli.verbose, cli.quiet);
for n in 0..repeat {
let code = run(&file, &engine, trace, cli.profile);
// 失败即终止并传播退出码,供脚本与 CI 区分成败。
if code != ExitCode::SUCCESS {
return code;
}
if n + 1 < repeat {
eprintln!("[oxide] iteration {}/{} done", n + 1, repeat);
}
}
ExitCode::SUCCESS
}
Some(Commands::Compile {
expr,
file,
no_dce,
no_regalloc,
}) => {
let engine = make_engine(false, false);
compile(&engine, expr, file, no_dce, no_regalloc)
}
Some(Commands::Bench {
mode,
filter,
warmup,
iterations,
update_baseline,
sample,
sample_top,
}) => {
let engine = make_engine(false, false);
// 采样周期边界检查:须为 0(关闭)或 2 的幂,否则指令边界的
// `steps & (period - 1) == 0` 判定退化(周期不整除步数序列)。
if sample != 0 && sample & (sample - 1) != 0 {
eprintln!("--sample period must be 0 (disabled) or a power of two, got {sample}");
return ExitCode::FAILURE;
}
let config = bench::BenchConfig {
mode: mode.unwrap_or_else(|| "js".to_string()),
filter,
warmup,
iterations,
update_baseline,
sample_period: sample,
sample_top,
};
bench::run_benchmarks(config, &engine)
}
Some(Commands::Test { .. }) => not_implemented("test"),
Some(Commands::Server { command }) => server_command(command),
None => repl(),
}
}
/// server 子命令分派:全部十一臂为真实实现(start 两形态、cleanup、
/// 五个控制臂、restart、log、forge、watchdog)。
///
/// # 步骤
/// 1. Start:rm 为真调独立模式入口(进程唯一路径);foreground 为真当前进程
/// 直接运行持久 server(守护形态 spawn 的子进程用);否则 spawn 脱离的
/// server 子进程并就绪探测通过后返回(守护形态,well-known 路径);
/// `--workers` 为 Some 时覆盖 worker 数。
/// 2. Cleanup:调 well-known 路径清理入口,四态结果映射退出码(无残留与
/// 已清理退 0,拒绝与杀进程失败退 1)并打印结果。
/// 3. 五个控制臂(version/status/health/info/stop):发对应控制请求,
/// 成功打印渲染文本退 0,失败打印错误消息退 1。
/// 4. Restart:发关闭请求等旧 server 退出,拉起新 server 进程并探测就绪
/// (详见 restart_command)。
/// 5. Log:读 well-known 日志文件,按级别阈值与最后 N 行过滤后打印,
/// `--follow` 时进入阻塞跟踪循环(详见 log_command)。
/// 6. Forge:发 forge 查询请求(目标加三行动旗标),成功打印渲染文本退 0,
/// 失败打印错误消息退 1。
/// 7. Watchdog:前台监控进程,拉起 server、三态裁决、崩溃后自动重启
/// (详见 run_watchdog)。
///
/// # 边界与前提
/// - `--idle-timeout` 不带 --rm 时静默忽略(语义门控归后续任务)。
/// - 全局旗标 -v / -q / --profile 在 server 臂为空操作。
///
/// # 副作用
/// - start 持久形态 spawn 脱离进程,就绪探测通过后返回(server 由 init
/// 收领继续运行)。
/// - cleanup 可能向 sidecar 记录的进程发 SIGTERM 并删除残留文件。
/// - 控制臂向 server 建立并关闭一条 Unix socket 连接。
/// - watchdog spawn 脱离的 server 子进程,崩溃后经同一入口重启。
fn server_command(command: ServerCommands) -> ExitCode {
match command {
ServerCommands::Start {
rm,
idle_timeout,
workers,
port,
host,
foreground,
} => {
if rm {
let mut config = RmServerConfig {
idle_timeout: Duration::from_secs(idle_timeout),
..RmServerConfig::default()
};
if let Some(n) = workers {
config.worker_count = n as usize;
}
// 显式端口映射 TCP 端点(绑定地址取 --host):0 为动态端口,
// 由操作系统在绑定后分配空闲端口;不带端口旗标时保持 Unix
// 缺省。
if let Some(port) = port {
config.endpoint = Endpoint::Tcp(SocketAddr::new(host, port));
}
start_result(run_server_rm(&config))
} else if foreground {
let port = port.unwrap_or(sidecar::WELL_KNOWN_PORT);
let mut config = ServerConfig {
endpoint: Endpoint::Tcp(SocketAddr::new(host, port)),
bind_addr: host,
..ServerConfig::well_known()
};
if let Some(n) = workers {
config.worker_count = n as usize;
}
start_result(run_server(&config))
} else {
start_daemon(workers, host, port.unwrap_or(sidecar::WELL_KNOWN_PORT))
}
}
ServerCommands::Cleanup => match well_known_cleanup() {
CleanupOutcome::Clean => {
println!("无残留:sidecar 与 socket 文件均不存在。");
ExitCode::SUCCESS
}
CleanupOutcome::Removed => {
println!("残留已清理:孤儿文件已删除。");
ExitCode::SUCCESS
}
CleanupOutcome::RefusedLiveServer => {
eprintln!("存在存活 server 且进程号无法可靠确定,未触碰文件,需人工检查。");
ExitCode::FAILURE
}
CleanupOutcome::KillFailed => {
eprintln!("已发终止信号但进程在等待窗口内未退出,未触碰文件。");
ExitCode::FAILURE
}
},
ServerCommands::Stop => control_arm(&ServerRequest::Shutdown, |response| match response {
ServerResponse::Shutdown => Some("已停止".to_string()),
_ => None,
}),
ServerCommands::Status => control_arm(&ServerRequest::Status, |response| match response {
ServerResponse::Status {
pool_available,
pool_total,
pool_peak,
uptime_ms,
} => Some(format!("池可用 {pool_available}/{pool_total},峰值 {pool_peak},运行 {uptime_ms} 毫秒")),
_ => None,
}),
ServerCommands::Health => control_arm(&ServerRequest::Health, |response| match response {
ServerResponse::Health { healthy } => Some(if *healthy { "healthy".into() } else { "unhealthy".into() }),
_ => None,
}),
ServerCommands::Info => control_arm(&ServerRequest::Info, |response| match response {
ServerResponse::Info {
version,
socket_path,
pid,
listen_addr,
} => Some(format!(
"版本:{version}\nendpoint:{socket_path}\n监听地址:{listen_addr}\n进程号:{pid}"
)),
_ => None,
}),
ServerCommands::Version => control_arm(&ServerRequest::Version, |response| match response {
ServerResponse::Version { version } => Some(version.clone()),
_ => None,
}),
ServerCommands::Restart => restart_command(),
ServerCommands::Log { follow, level, lines } => log_command(follow, level.map(LogLevel::as_level), lines),
ServerCommands::Forge {
target,
gc,
clear_cache,
lookup,
} => {
let request = ServerRequest::ForgeQuery {
target: target.to_protocol(),
gc,
clear_cache,
lookup,
};
control_arm(&request, |response| match response {
ServerResponse::ForgeStatus {
target,
entries,
capacity,
gc_collected,
cache_cleared,
lookup,
} => Some(render_forge_status(
*target,
*entries,
*capacity,
*gc_collected,
*cache_cleared,
lookup.clone(),
)),
_ => None,
})
}
ServerCommands::Watchdog { workers } => watchdog::run_watchdog(workers),
}
}
/// 渲染 `ForgeStatus` 为多行文本:首行条目数,容量 / gc / 清缓存 / lookup
/// 行按旗标结果附加。
fn render_forge_status(
target: ForgeTarget, entries: usize, capacity: usize, gc_collected: Option<usize>, cache_cleared: bool,
lookup: Option<LookupResult>,
) -> String {
let mut lines = vec![format!("{} forge: {entries} entries", target.as_str())];
if capacity != 0 {
lines.push(format!("capacity: {capacity}"));
}
if let Some(n) = gc_collected {
lines.push(format!("gc: collected {n} idle VMs"));
}
if cache_cleared {
lines.push("cache cleared".to_string());
}
if let Some(result) = lookup {
lines.push(match result {
LookupResult::Interned { id } => format!("lookup: interned id={id}"),
LookupResult::Absent => "lookup: not interned".to_string(),
});
}
lines.join("\n")
}
/// 控制臂公共入口:发请求,成功打印渲染文本退 0,失败打印消息退 1。
/// 渲染函数返回 None 表示响应变体与请求不匹配(防御性兜底)。
fn control_arm(request: &ServerRequest, render: fn(&ServerResponse) -> Option<String>) -> ExitCode {
match send_control_request(request) {
Ok(response) => match render(&response) {
Some(text) => {
println!("{text}");
ExitCode::SUCCESS
}
None => {
eprintln!("{}", Red.paint(format!("意外的响应帧:{response:?}")));
ExitCode::FAILURE
}
},
Err(err) => {
eprintln!("{}", Red.paint(err.to_string()));
ExitCode::FAILURE
}
}
}
/// start 持久形态:spawn 脱离的前台 server 子进程(`server start
/// --foreground --host H --port N`),就绪探测通过后打印返回。
///
/// # 步骤
/// 1. spawn 脱离的前台 server 子进程(标准流全空、不等待);spawn 失败以
/// 红色打印退 1。
/// 2. 就绪探测:轮询健康请求直至得 healthy(100 毫秒间隔、10 秒截止);
/// 超时以红色打印退 1。
/// 3. 打印「server 已启动」退 0。
///
/// # 边界与前提
/// - sidecar 文件由被拉起的 server 进程自己写(先认领再绑定 socket 的启动
/// 顺序契约在 server 进程内执行),本命令不写 sidecar。
/// - 已有存活 server 时再跑本命令:第二个 spawn 的子进程在认领阶段被拒
/// (同版本不触碰文件),探测看到第一个 server 即通过,命令退 0(天然幂等)。
///
/// # 副作用
/// - server 为脱离进程,CLI 退出后由 init 收领继续运行。
fn start_daemon(workers: Option<u32>, host: IpAddr, port: u16) -> ExitCode {
// spawn:脱离子进程,标准流全空,不等待。
if let Err(err) = spawn::spawn_detached_server(workers, host, port) {
eprintln!("{}", Red.paint(format!("spawn server 进程失败:{err}")));
return ExitCode::FAILURE;
}
// 就绪探测:单循环全覆盖「sidecar 出现 + 健康请求通过」。
if let Err(msg) = spawn::wait_server_ready(Duration::from_secs(10)) {
eprintln!("{}", Red.paint(format!("{msg}(可用 `oxide server cleanup` 人工检查)")));
return ExitCode::FAILURE;
}
println!("server 已启动");
ExitCode::SUCCESS
}
/// restart 子命令:向存活 server 发关闭请求,等其退出后拉起新 server 进程。
///
/// # 步骤
/// 1. 发关闭请求;三分支错误以红色打印退 1,无已注册 server 时消息即预设
/// 不满足提示。
/// 2. 轮询 well-known sidecar 文件消失(50 毫秒间隔、10 秒截止)判旧 server
/// 排水完成;超时提示人工检查退 1。
/// 3. 用当前可执行文件拉起脱离的 `oxide server start`(标准流全空、不等待);
/// spawn 失败退 1。
/// 4. 轮询健康请求直至得 healthy(100 毫秒间隔、10 秒截止)判新 server 就绪;
/// 超时退 1。
/// 5. 打印「已重启」退 0。
///
/// # 边界与前提
/// - 新 server 用缺省配置拉起(worker 数回落宿主核数),不继承旧 server 旗标。
///
/// # 副作用
/// - 旧 server 排空退出并删除 sidecar 与 socket 文件。
/// - 新 server 为脱离进程,CLI 退出后由 init 收领继续运行。
fn restart_command() -> ExitCode {
// 发关闭请求:三分支错误都以红色打印退 1,无 server 时消息即预设不满足提示。
if let Err(err) = send_control_request(&ServerRequest::Shutdown) {
eprintln!("{}", Red.paint(err.to_string()));
return ExitCode::FAILURE;
}
// 等旧 server 退出:sidecar 是退出序列的最后删除项,消失即排水完成。
let sidecar_path = sidecar::well_known_sidecar_path();
let deadline = Instant::now() + Duration::from_secs(10);
while sidecar_path.exists() {
if Instant::now() >= deadline {
eprintln!("{}", Red.paint("server 未在 10 秒内退出,可用 `oxide server cleanup` 人工检查"));
return ExitCode::FAILURE;
}
std::thread::sleep(Duration::from_millis(50));
}
// 拉起新 server:脱离进程,标准流全空,不等待。新 server 按缺省配置拉起
// (well-known 绑定地址与端口,不继承旧旗标)。
if let Err(err) = spawn::spawn_detached_server(None, sidecar::WELL_KNOWN_BIND_ADDR, sidecar::WELL_KNOWN_PORT) {
eprintln!("{}", Red.paint(format!("拉起新 server 失败:{err}")));
return ExitCode::FAILURE;
}
// 就绪探测:健康请求通过即新 server 的监听与 accept 循环在运行。
if let Err(msg) = spawn::wait_server_ready(Duration::from_secs(10)) {
eprintln!("{}", Red.paint(format!("新 {msg}")));
return ExitCode::FAILURE;
}
println!("已重启");
ExitCode::SUCCESS
}
/// log 子命令:读 well-known 日志文件,按级别阈值与最后 N 行过滤后打印;
/// `--follow` 时进入阻塞跟踪循环。
///
/// # 步骤
/// 1. 取 well-known 日志路径;文件不存在时以红色打印退 1(不等待文件出现,
/// `--follow` 同样行为,保持单一)。
/// 2. 读文件、过滤、逐行打印;io 错误以红色打印退 1。
/// 3. `--follow` 时进入阻塞跟踪循环(标准输入中断即进程默认行为退出)。
fn log_command(follow: bool, level: Option<Level>, lines: Option<usize>) -> ExitCode {
let path = sidecar::well_known_log_path();
if !path.exists() {
eprintln!("{}", Red.paint(format!("日志文件不存在:{}(server 可能未启动过)", path.display())));
return ExitCode::FAILURE;
}
match server_log::read_log(&path, level, lines) {
Ok(log_lines) => {
for line in log_lines {
println!("{line}");
}
}
Err(err) => {
eprintln!("{}", Red.paint(format!("读取日志文件失败:{err}")));
return ExitCode::FAILURE;
}
}
if follow {
server_log::follow(&path, level);
}
ExitCode::SUCCESS
}
/// 启动结果到退出码:成功退 0,错误以红色打印到 stderr 后退 1。
fn start_result(result: Result<(), oxide_cli::server::server::ServerError>) -> ExitCode {
match result {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("{}", Red.paint(format!("server 启动失败:{err}")));
ExitCode::FAILURE
}
}
}
/// 建引擎:`KernelConfig::standard()` 按 -v / -q 填 `log_levels` 后 `Engine::new`。
fn make_engine(verbose: bool, quiet: bool) -> Engine {
let mut config = KernelConfig::standard();
if verbose {
config.log_levels = [Level::Info; SUBSYSTEM_COUNT];
} else if quiet {
config.log_levels = [Level::Off; SUBSYSTEM_COUNT];
} else {
config.log_levels = [Level::Error; SUBSYSTEM_COUNT];
}
Engine::new(config)
}
fn eval(code: &str, engine: &Engine, trace: bool, profile: bool, print_result: bool) -> ExitCode {
let module = match engine.compile(code) {
Ok(m) => m,
Err(err) => {
compiler_error!("compile error: {}", err);
eprintln!("{}", Red.paint(err));
return ExitCode::FAILURE;
}
};
let mut guard = engine.spawn();
guard.vm_mut().set_instruction_trace(trace);
let exec_start = Instant::now();
match guard.vm_mut().run(&module) {
Ok(result) => {
// --profile 开启时聚合现有计数器,每指标一行输出到 stderr(stdout 保持纯结果)。
if profile {
let p = bench::profile::ProfileOutput::collect(guard.vm(), exec_start.elapsed().as_micros() as u64);
for (name, value) in p.iter_metrics() {
eprintln!("{name} = {value}");
}
}
// eval 臂按 REPL 语义打印完成值;run 臂脚本只输出自身产生内容。
if print_result {
let kernel = engine.kernel();
format_result(guard.vm(), kernel.perm_interner().as_ref(), kernel.shape_forge().as_ref(), result);
}
// 主线程事件循环:消费 worker 消息、清理已断开 worker,直至无活跃
// worker(脚本建了 Worker 不 terminate 时恒转,符合页面寿命语义)。
run_worker_event_loop(guard.vm_mut());
ExitCode::SUCCESS
}
Err(err) => {
vm_error!("runtime error: {}", err);
eprintln!("{}", Red.paint(err));
ExitCode::FAILURE
}
}
}
/// 主线程事件循环:消费 worker 消息并清理已断开 worker,循环直至无活跃 worker。
///
/// # 步骤
/// 1. 循环条件:仍有活跃 worker。
/// 2. `deliver_worker_messages` 交付 worker 消息到 `onmessage`。
/// 3. `cleanup_disconnected_workers` 清理已退出 worker(通道断开即 join 加移除)。
/// 4. 本轮未交付消息时 1ms 轮询(避免忙等)。
///
/// # 边界与前提
/// - 终止条件为无活跃 worker;用户脚本建 Worker 不 terminate 时恒转(有意,
/// 页面寿命 = worker 寿命)。
fn run_worker_event_loop(vm: &mut Vm) {
while !vm.active_workers().is_empty() {
let delivered = vm.deliver_worker_messages();
vm.cleanup_disconnected_workers();
if !delivered {
std::thread::sleep(Duration::from_millis(1));
}
}
}
fn format_result(vm: &oxide_vm::vm::Vm, string_forge: &PermInterner, shape_forge: &ShapeForge, val: JsValue) {
println!("{}", format_js_value(vm, string_forge, shape_forge, val));
}
fn run(file: &str, engine: &Engine, trace: bool, profile: bool) -> ExitCode {
match fs::read_to_string(file) {
Ok(source) => eval(&source, engine, trace, profile, false),
Err(err) => {
kernel_error!("cannot read {}: {}", file, err);
eprintln!("{}", Red.paint(format!("Cannot read {file}: {err}")));
ExitCode::FAILURE
}
}
}
fn compile(engine: &Engine, expr: Option<String>, file: Option<String>, no_dce: bool, no_regalloc: bool) -> ExitCode {
let source = if let Some(code) = expr {
code
} else if let Some(path) = file {
match fs::read_to_string(&path) {
Ok(s) => s,
Err(err) => {
kernel_error!("cannot read {}: {}", path, err);
eprintln!("{}", Red.paint(format!("Cannot read {path}: {err}")));
return ExitCode::FAILURE;
}
}
} else {
kernel_error!("compile requires -e '<code>' or a file path");
eprintln!("{}", Red.paint("compile requires -e '<code>' or a file path"));
return ExitCode::FAILURE;
};
// 旗标面经自定义编译器透传(无缺省臂,漏传即旗标失效)。
let compiler = Compiler::new().with_dce(!no_dce).with_regalloc(!no_regalloc);
match engine.compile_with(&source, &compiler) {
Ok(module) => {
print!("{module}");
ExitCode::SUCCESS
}
Err(err) => {
compiler_error!("compile error: {}", err);
eprintln!("{}", Red.paint(err));
ExitCode::FAILURE
}
}
}
fn bracket_balance(line: &str) -> i32 {
let mut count = 0i32;
for ch in line.chars() {
match ch {
'(' | '[' | '{' => count += 1,
')' | ']' | '}' => count -= 1,
_ => {}
}
}
count
}
fn repl() -> ExitCode {
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
let mut rl = match DefaultEditor::new() {
Ok(editor) => editor,
Err(err) => {
kernel_error!("failed to start REPL: {}", err);
eprintln!("{}", Red.paint(format!("Failed to start REPL: {err}")));
return ExitCode::FAILURE;
}
};
let engine = make_engine(false, false);
// 常驻 Vm 不经池;动态编译服务由 `new_vm` 同口径注入。
let mut vm = engine.new_vm();
let mut input_buf = String::new();
loop {
let prompt = if input_buf.is_empty() { "oxide> " } else { "...> " };
match rl.readline(prompt) {
Ok(line) => {
let trimmed = line.trim().to_string();
if trimmed.is_empty() {
continue;
}
if trimmed == ".exit" || trimmed == ".quit" {
println!("exit");
return ExitCode::SUCCESS;
}
rl.add_history_entry(&trimmed).ok();
if !input_buf.is_empty() {
input_buf.push('\n');
}
input_buf.push_str(&trimmed);
let balance = bracket_balance(&input_buf);
if balance > 0 {
continue;
}
let result = eval_repl(&input_buf, &engine, &mut vm);
input_buf.clear();
if result == ExitCode::FAILURE {
// eval_repl 已打印错误,此处仅保持缓冲区已清空,避免重复输出。
}
}
Err(ReadlineError::Interrupted) => {
println!("^C");
return ExitCode::SUCCESS;
}
Err(ReadlineError::Eof) => {
println!("exit");
return ExitCode::SUCCESS;
}
Err(err) => {
vm_error!("REPL error: {}", err);
eprintln!("{}", Red.paint(format!("REPL error: {err}")));
return ExitCode::FAILURE;
}
}
}
}
fn eval_repl(code: &str, engine: &Engine, vm: &mut Vm) -> ExitCode {
// REPL 持久模式:顶层 let/const 也写全局对象属性,经缓存编译。
let compiler = Compiler::new().with_repl_persist(true);
let module = match engine.compile_with(code, &compiler) {
Ok(m) => m,
Err(err) => {
compiler_error!("compile error: {}", err);
eprintln!("{}", Red.paint(err));
return ExitCode::FAILURE;
}
};
match vm.run(&module) {
Ok(result) => {
let kernel = engine.kernel();
format_result(vm, kernel.perm_interner().as_ref(), kernel.shape_forge().as_ref(), result);
ExitCode::SUCCESS
}
Err(err) => {
vm_error!("runtime error: {}", err);
eprintln!("{}", Red.paint(err));
ExitCode::FAILURE
}
}
}
fn not_implemented(command: &str) -> ExitCode {
use ansi_term::Colour::Yellow;
kernel_info!("command not yet implemented: {}", command);
eprintln!("{}", Yellow.paint(format!("'{command}' is not yet implemented")));
// 2 = 未实现,与 0=成功、1=运行失败区分,供脚本与 CI 识别。
ExitCode::from(2)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
/// 取解析结果的 start 臂 host 字段(两臂共享的 --host 旗标)。
fn parsed_host(args: &[&str]) -> IpAddr {
let cli = Cli::try_parse_from(args).expect("解析应成功");
let Some(Commands::Server { command }) = cli.command else {
panic!("应解析出 server 臂");
};
let ServerCommands::Start { host, .. } = command else {
panic!("应解析出 start 臂");
};
host
}
/// --host 缺省取 loopback:不带旗标解析得 127.0.0.1,显式旗标原样解析,
/// 非法取值走 clap 错误。
#[test]
fn start_host_flag_parses() {
assert_eq!(
parsed_host(&["oxide", "server", "start"]),
IpAddr::V4(Ipv4Addr::LOCALHOST),
"缺省 host 应为 loopback"
);
assert_eq!(
parsed_host(&["oxide", "server", "start", "--host", "0.0.0.0"]),
"0.0.0.0".parse::<IpAddr>().unwrap(),
"显式 host 应原样解析"
);
assert!(
Cli::try_parse_from(["oxide", "server", "start", "--host", "not-an-ip"]).is_err(),
"非法 host 应走 clap 错误"
);
}
}