#![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, #[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, file: Option, #[arg(long)] no_dce: bool, /// 关闭 liveness/精确 DCE/RegAlloc 链,vreg 原样当物理号(调试降级路径)。 #[arg(long)] no_regalloc: bool, }, Bench { #[arg(default_value = "js")] mode: Option, #[arg(short, long)] filter: Option, #[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, }, /// 持久 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, /// TCP 监听端口;0 表示动态端口(操作系统分配空闲端口),缺省取 /// well-known 端口。 #[arg(long)] port: Option, /// 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, /// 显示最后 N 行(缺省全部)。 #[arg(long)] lines: Option, }, /// 查询内部 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, }, /// 监控 server 存活并在崩溃后自动重启(前台进程,60 秒窗口 5 次崩溃即停)。 Watchdog { /// 常驻 worker 线程数;缺省取宿主核数。 #[arg(long)] workers: Option, }, } /// 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 } => { Some(format!("版本:{version}\nendpoint:{socket_path}\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, cache_cleared: bool, lookup: Option, ) -> 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) -> 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, 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, lines: Option) -> 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, file: Option, 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 '' or a file path"); eprintln!("{}", Red.paint("compile requires -e '' 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::().unwrap(), "显式 host 应原样解析" ); assert!( Cli::try_parse_from(["oxide", "server", "start", "--host", "not-an-ip"]).is_err(), "非法 host 应走 clap 错误" ); } }