From d5b69216c7cce431561167becc5f61ef68727b23 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 18 Jun 2026 22:53:09 -0500 Subject: [PATCH] feat: WAT flag and runtime debug emits --- crates/cli/src/args.rs | 11 +- crates/cli/src/commands.rs | 158 +++++++++++++- crates/cli/src/commands/builder.rs | 40 +++- crates/cli/src/commands/compiler.rs | 78 +++++-- crates/cli/tests/build.rs | 193 +++++++++++++++++- .../specs/17_cli_and_build_outputs.md | 25 ++- .../tasks/17_cli_and_build_outputs.md | 8 +- docs/website/development/projects.md | 4 +- docs/website/guide/usage/cli.md | 8 +- .../reference/cli-and-build-outputs.md | 11 +- docs/website/reference/compiling-projects.md | 3 + 11 files changed, 503 insertions(+), 36 deletions(-) diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 27c45b9..3915d1b 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -28,6 +28,9 @@ pub enum Command { /// Select emitted artifacts. #[arg(long, value_enum, value_delimiter = ',', default_value = "wasm")] emit: Vec, + /// Also write the generated WebAssembly text format. + #[arg(long)] + wat: Option>, /// Write compiler debug dumps to this directory. #[arg(long)] dump_dir: Option, @@ -182,11 +185,16 @@ pub enum Emit { Resolved, Typed, Ir, + Runtime, + Abi, } impl Emit { pub fn is_debug(self) -> bool { - matches!(self, Self::Ast | Self::Resolved | Self::Typed | Self::Ir) + matches!( + self, + Self::Ast | Self::Resolved | Self::Typed | Self::Ir | Self::Runtime | Self::Abi + ) } pub fn is_pre_lower_debug(self) -> bool { @@ -194,6 +202,7 @@ impl Emit { } } +#[derive(Clone, Copy)] pub struct DebugOptions { pub ts: bool, pub ast: bool, diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index 94c2e50..622e9c1 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -3,6 +3,7 @@ mod compiler; mod debug; mod runner; +use std::fmt::Write as _; use std::fs; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -47,9 +48,9 @@ impl CompiledModule { pub fn run(command: Command) -> ExitCode { match command { - Command::Build { project, output, out_dir, target, emit, dump_dir, verbose, json } => { + Command::Build { project, output, out_dir, target, emit, wat, dump_dir, verbose, json } => { let mut builder = - Builder { input: project.as_deref(), output, out_dir, target, emit, dump_dir, verbose, json }; + Builder { input: project.as_deref(), output, out_dir, target, emit, wat, dump_dir, verbose, json }; builder.build() } @@ -341,6 +342,159 @@ fn artifact_path(out_dir: Option<&Path>, wasm_path: &Path, artifact_base: &str, .unwrap_or_else(|| wasm_path.with_extension(ext)) } +fn runtime_debug_dump() -> String { + use compiler_core::runtime::{ObjectTag, RuntimeConfig, WASM_PAGE_SIZE}; + + let config = RuntimeConfig::DEFAULT; + let layout = config.layout; + let mut out = String::new(); + writeln!(&mut out, "runtime layout:").expect("write runtime debug dump"); + writeln!(&mut out, " word_size: {}", layout.word_size).expect("write runtime debug dump"); + writeln!(&mut out, " alignment: {}", layout.alignment).expect("write runtime debug dump"); + writeln!(&mut out, " header_size: {}", layout.header_size).expect("write runtime debug dump"); + writeln!(&mut out).expect("write runtime debug dump"); + writeln!(&mut out, "memory:").expect("write runtime debug dump"); + writeln!(&mut out, " wasm_page_size: {WASM_PAGE_SIZE}").expect("write runtime debug dump"); + writeln!(&mut out, " static_data_start: {}", config.static_data_start).expect("write runtime debug dump"); + writeln!(&mut out, " heap_start: {}", config.heap_start).expect("write runtime debug dump"); + writeln!(&mut out, " memory_max_pages: {}", config.memory_max_pages).expect("write runtime debug dump"); + writeln!(&mut out, " memory_limit_bytes: {}", config.memory_limit_bytes()).expect("write runtime debug dump"); + writeln!(&mut out).expect("write runtime debug dump"); + writeln!(&mut out, "object tags:").expect("write runtime debug dump"); + for tag in [ + ObjectTag::String, + ObjectTag::ListCons, + ObjectTag::Tuple, + ObjectTag::Record, + ObjectTag::Custom, + ObjectTag::Closure, + ObjectTag::BitArray, + ObjectTag::Opaque, + ObjectTag::Error, + ObjectTag::Panic, + ] { + writeln!(&mut out, " {:?}: {}", tag, u32::from(tag)).expect("write runtime debug dump"); + } + writeln!(&mut out).expect("write runtime debug dump"); + writeln!(&mut out, "sample object sizes:").expect("write runtime debug dump"); + writeln!(&mut out, " string(5): {}", layout.string_size(5)).expect("write runtime debug dump"); + writeln!(&mut out, " bit_array(13): {}", layout.bit_array_size(13)).expect("write runtime debug dump"); + writeln!(&mut out, " list_cons: {}", layout.list_cons_size(8)).expect("write runtime debug dump"); + writeln!(&mut out, " tuple(2): {}", layout.tuple_size(2, 8)).expect("write runtime debug dump"); + writeln!(&mut out, " record(2): {}", layout.record_size(2, 8)).expect("write runtime debug dump"); + writeln!(&mut out, " custom(2): {}", layout.custom_size(2, 8)).expect("write runtime debug dump"); + writeln!(&mut out, " closure(2): {}", layout.closure_size(2)).expect("write runtime debug dump"); + writeln!(&mut out, " opaque: {}", layout.opaque_size()).expect("write runtime debug dump"); + out +} + +fn abi_debug_dump(module: &compiler_core::ir::Module, target: CompileTarget) -> String { + use compiler_core::ir::{CallBoundary, ExportKind}; + + let mut out = String::new(); + writeln!(&mut out, "target: {target:?}").expect("write ABI debug dump"); + writeln!(&mut out).expect("write ABI debug dump"); + + let mut imports = module + .functions + .iter() + .filter(|function| { + matches!( + function.abi.boundary, + CallBoundary::HostImport { .. } | CallBoundary::ModuleImport { .. } + ) + }) + .collect::>(); + imports.sort_by_key(|function| function.name.as_str()); + writeln!(&mut out, "imports:").expect("write ABI debug dump"); + if imports.is_empty() { + writeln!(&mut out, " none").expect("write ABI debug dump"); + } + for function in imports { + writeln!( + &mut out, + " {} {}", + function.name, + function_abi_signature(&function.params, &function.return_type) + ) + .expect("write ABI debug dump"); + writeln!(&mut out, " boundary: {}", call_boundary(&function.abi.boundary)).expect("write ABI debug dump"); + write_call_abi(&mut out, &function.abi); + } + + let mut exports = module + .exports + .iter() + .filter(|export| export.kind == ExportKind::Function) + .collect::>(); + exports.sort_by_key(|export| export.name.as_str()); + writeln!(&mut out).expect("write ABI debug dump"); + writeln!(&mut out, "exports:").expect("write ABI debug dump"); + if exports.is_empty() { + writeln!(&mut out, " none").expect("write ABI debug dump"); + } + for export in exports { + let Some(function) = module + .functions + .iter() + .find(|function| function.name == export.backend_name()) + else { + writeln!( + &mut out, + " {} -> {} (missing function)", + export.name, + export.backend_name() + ) + .expect("write ABI debug dump"); + continue; + }; + writeln!( + &mut out, + " {} -> {} {}", + export.name, + export.backend_name(), + function_abi_signature(&function.params, &function.return_type) + ) + .expect("write ABI debug dump"); + write_call_abi(&mut out, &function.abi); + } + + out +} + +fn function_abi_signature(params: &[compiler_core::ir::Local], return_type: &Type) -> String { + let params = params + .iter() + .map(|param| format!("{}: {:?}", param.name, param.type_)) + .collect::>() + .join(", "); + format!("({params}) -> {return_type:?}") +} + +fn write_call_abi(out: &mut String, abi: &compiler_core::ir::CallAbi) { + writeln!(out, " params:").expect("write ABI debug dump"); + if abi.params.is_empty() { + writeln!(out, " none").expect("write ABI debug dump"); + } + for (index, param) in abi.params.iter().enumerate() { + writeln!(out, " {index}: {:?} as {:?}", param.type_, param.representation).expect("write ABI debug dump"); + } + match &abi.return_ { + Some(return_) => writeln!(out, " result: {:?} as {:?}", return_.type_, return_.representation) + .expect("write ABI debug dump"), + None => writeln!(out, " result: none").expect("write ABI debug dump"), + } +} + +fn call_boundary(boundary: &compiler_core::ir::CallBoundary) -> String { + match boundary { + compiler_core::ir::CallBoundary::Internal => "internal".into(), + compiler_core::ir::CallBoundary::ModuleExport => "module export".into(), + compiler_core::ir::CallBoundary::ModuleImport { module, name } => format!("module import {module}.{name}"), + compiler_core::ir::CallBoundary::HostImport { module, name } => format!("host import {module}.{name}"), + } +} + fn write_file(path: &Path, contents: &[u8]) -> std::io::Result<()> { if let Some(parent) = path.parent() && !parent.as_os_str().is_empty() diff --git a/crates/cli/src/commands/builder.rs b/crates/cli/src/commands/builder.rs index bed2557..b7bfa49 100644 --- a/crates/cli/src/commands/builder.rs +++ b/crates/cli/src/commands/builder.rs @@ -15,6 +15,7 @@ pub struct Builder<'a> { pub out_dir: Option, pub target: Option, pub emit: Vec, + pub wat: Option>, pub dump_dir: Option, pub verbose: bool, pub json: bool, @@ -25,6 +26,9 @@ impl Builder<'_> { if self.json { return echo::fail("build", "--json", "machine-readable output is not implemented yet"); } + if self.wat.is_some() && !self.emit.contains(&Emit::Wat) { + self.emit.push(Emit::Wat); + } let input = self.input.unwrap_or_else(|| Path::new(".")); let verbose = self.verbose; let mut progress = move |event| print_project_load_progress(event, verbose); @@ -79,6 +83,7 @@ impl Builder<'_> { &artifact_base, &self.emit, dump_all, + target, ) { return echo::fail("write", "debug dumps", error); @@ -97,6 +102,7 @@ impl Builder<'_> { &artifact_base, &self.emit, dump_all, + target, ) { return echo::fail("write", "debug dumps", error); @@ -106,6 +112,18 @@ impl Builder<'_> { Ok(wasm) => wasm, Err(diagnostics) => return echo::fail_with_diagnostics("emit wasm", project.root.display(), &diagnostics), }; + if let Some(debug_dir) = debug_dir.as_deref() + && (dump_all || self.emit.contains(&Emit::Runtime) || self.emit.contains(&Emit::Abi)) + && let Err(error) = ProjectDebugArtifacts::with(&typed, &ir, &wasm).write_project_debug_dumps( + debug_dir, + &artifact_base, + &self.emit, + dump_all, + target, + ) + { + return echo::fail("write", "debug dumps", error); + } if self.emit.contains(&Emit::Wasm) { if let Err(error) = super::write_file(&output, &wasm.bytes) { @@ -133,7 +151,11 @@ impl Builder<'_> { } } if self.emit.contains(&Emit::Wat) { - let wat_path = super::artifact_path(self.out_dir.as_deref(), &output, &artifact_base, "wat"); + let wat_path = self + .wat + .clone() + .flatten() + .unwrap_or_else(|| super::artifact_path(self.out_dir.as_deref(), &output, &artifact_base, "wat")); if let Err(error) = super::write_file(&wat_path, wasm.wat.as_bytes()) { return echo::fail("write", wat_path.display(), error); } @@ -145,6 +167,7 @@ impl Builder<'_> { &artifact_base, &self.emit, true, + target, ) { return echo::fail("write", "debug dumps", error); @@ -182,6 +205,7 @@ impl<'a> ProjectDebugArtifacts<'a> { impl ProjectDebugArtifacts<'_> { fn write_project_debug_dumps( self, dump_dir: &Path, artifact_base: &str, emit: &[args::Emit], dump_all: bool, + target: compiler_core::target::CompileTarget, ) -> std::io::Result<()> { fs::create_dir_all(dump_dir)?; @@ -214,6 +238,20 @@ impl ProjectDebugArtifacts<'_> { { fs::write(dump_dir.join(format!("{artifact_base}.wat")), &wasm.wat)?; } + if dump_all || emit.contains(&args::Emit::Runtime) { + fs::write( + dump_dir.join(format!("{artifact_base}.runtime.txt")), + super::runtime_debug_dump(), + )?; + } + if let Some(ir) = self.ir + && (dump_all || emit.contains(&args::Emit::Abi)) + { + fs::write( + dump_dir.join(format!("{artifact_base}.abi.txt")), + super::abi_debug_dump(ir, target), + )?; + } Ok(()) } } diff --git a/crates/cli/src/commands/compiler.rs b/crates/cli/src/commands/compiler.rs index 41f04c0..0cc5c48 100644 --- a/crates/cli/src/commands/compiler.rs +++ b/crates/cli/src/commands/compiler.rs @@ -41,12 +41,6 @@ impl Compiler<'_> { Err(diagnostics) => return echo::fail_with_diagnostics("compile", self.input.display(), &diagnostics), }; - if let Some(dump_dir) = self.dump_dir.clone() - && let Err(error) = write_debug_dumps(&dump_dir, &compiled) - { - return echo::fail("write", "debug dumps", error); - } - let artifact_base = self .input .file_stem() @@ -60,6 +54,29 @@ impl Compiler<'_> { (None, Some(dir)) => dir.join(format!("{artifact_base}.wasm")), (None, None) => self.input.with_extension("wasm"), }; + let debug_dir = self.dump_dir.clone().or_else(|| { + self.emit.iter().any(|emit| emit.is_debug()).then(|| { + output + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .map(Path::to_path_buf) + .unwrap_or_else(|| PathBuf::from(".")) + }) + }); + let dump_all = self.dump_dir.is_some(); + + if let Some(debug_dir) = debug_dir.as_deref() + && let Err(error) = write_debug_dumps( + debug_dir, + artifact_base, + &compiled, + self.target.into(), + &self.emit, + dump_all, + ) + { + return echo::fail("write", "debug dumps", error); + } if self.emit.contains(&Emit::Wasm) { if let Err(error) = super::write_file(&output, &compiled.wasm.bytes) { @@ -101,12 +118,49 @@ impl Compiler<'_> { } } -fn write_debug_dumps(dump_dir: &Path, compiled: &super::CompiledModule) -> std::io::Result<()> { +fn write_debug_dumps( + dump_dir: &Path, artifact_base: &str, compiled: &super::CompiledModule, + target: compiler_core::target::CompileTarget, emit: &[Emit], dump_all: bool, +) -> std::io::Result<()> { fs::create_dir_all(dump_dir)?; - fs::write(dump_dir.join("ast.txt"), format!("{:#?}\n", compiled.ast))?; - fs::write(dump_dir.join("resolved.txt"), format!("{:#?}\n", compiled.resolved))?; - fs::write(dump_dir.join("typed.txt"), format!("{:#?}\n", compiled.typed))?; - fs::write(dump_dir.join("ir.txt"), format!("{:#?}\n", compiled.ir))?; - fs::write(dump_dir.join("wat.wat"), &compiled.wasm.wat)?; + if dump_all || emit.contains(&Emit::Ast) { + fs::write( + dump_dir.join(format!("{artifact_base}.ast.txt")), + format!("{:#?}\n", compiled.ast), + )?; + } + if dump_all || emit.contains(&Emit::Resolved) { + fs::write( + dump_dir.join(format!("{artifact_base}.resolved.txt")), + format!("{:#?}\n", compiled.resolved), + )?; + } + if dump_all || emit.contains(&Emit::Typed) { + fs::write( + dump_dir.join(format!("{artifact_base}.typed.txt")), + format!("{:#?}\n", compiled.typed), + )?; + } + if dump_all || emit.contains(&Emit::Ir) { + fs::write( + dump_dir.join(format!("{artifact_base}.ir.txt")), + format!("{:#?}\n", compiled.ir), + )?; + } + if dump_all { + fs::write(dump_dir.join(format!("{artifact_base}.wat")), &compiled.wasm.wat)?; + } + if dump_all || emit.contains(&Emit::Runtime) { + fs::write( + dump_dir.join(format!("{artifact_base}.runtime.txt")), + super::runtime_debug_dump(), + )?; + } + if dump_all || emit.contains(&Emit::Abi) { + fs::write( + dump_dir.join(format!("{artifact_base}.abi.txt")), + super::abi_debug_dump(&compiled.ir, target), + )?; + } Ok(()) } diff --git a/crates/cli/tests/build.rs b/crates/cli/tests/build.rs index de6c945..2736ddf 100644 --- a/crates/cli/tests/build.rs +++ b/crates/cli/tests/build.rs @@ -94,6 +94,148 @@ fn run_decodes_managed_string_return_before_arena_reset() { let _ = fs::remove_dir_all(temp); } +#[test] +fn exec_alias_runs_wasmtime_exports() { + let temp = unique_temp_dir("regulus_cli_exec_alias"); + fs::create_dir_all(&temp).expect("create temp dir"); + let input = temp.join("app.gleam"); + fs::write( + &input, + r#"pub fn add(left: Int, right: Int) -> Int { left + right } +pub fn pair() -> #(Int, String) { #(7, "moons") } +"#, + ) + .expect("write Gleam input"); + + let add = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("exec") + .arg(&input) + .arg("--function") + .arg("add") + .arg("40") + .arg("2") + .output() + .expect("run reggie exec add"); + + assert!( + add.status.success(), + "exec add failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&add.stdout), + String::from_utf8_lossy(&add.stderr) + ); + assert_eq!(String::from_utf8_lossy(&add.stdout), "42\n"); + + let pair = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("exec") + .arg(&input) + .arg("--function") + .arg("pair") + .output() + .expect("run reggie exec pair"); + + assert!( + pair.status.success(), + "exec pair failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&pair.stdout), + String::from_utf8_lossy(&pair.stderr) + ); + assert_eq!(String::from_utf8_lossy(&pair.stdout), "#(7, \"moons\")\n"); + + let _ = fs::remove_dir_all(temp); +} + +#[test] +fn run_renders_managed_return_shapes() { + let temp = unique_temp_dir("regulus_cli_run_managed_shapes"); + fs::create_dir_all(&temp).expect("create temp dir"); + let input = temp.join("app.gleam"); + fs::write( + &input, + r#"import gleam/option.{Some} +import gleam/result.{Ok} + +pub type User { + User(name: String, age: Int) +} + +pub fn tuple() -> #(Int, String) { #(7, "moons") } +pub fn list() -> List(Int) { [1, 2] } +pub fn record() -> User { User(name: "Ada", age: 36) } +pub fn option() -> Option(String) { Some("Ada") } +pub fn result() -> Result(String, Int) { Ok("Ada") } +"#, + ) + .expect("write Gleam input"); + + let cases = [ + ("tuple", vec!["#(7, \"moons\")"]), + ("list", vec!["[1 | [2 | []]]"]), + ("record", vec!["Custom#", "\"Ada\"", "36"]), + ("option", vec!["Custom#", "\"Ada\""]), + ("result", vec!["Custom#", "\"Ada\""]), + ]; + + for (function, expected_parts) in cases { + let output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("run") + .arg(&input) + .arg("--function") + .arg(function) + .output() + .unwrap_or_else(|error| panic!("run reggie run {function}: {error}")); + + assert!( + output.status.success(), + "run {function} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + for expected in expected_parts { + assert!( + stdout.contains(expected), + "expected {function} output to contain {expected:?}, got {stdout:?}" + ); + } + } + + let _ = fs::remove_dir_all(temp); +} + +#[test] +fn build_wat_flag_matches_single_file_compile() { + let fixture = workspace_root().join("examples/scalar_project"); + let temp = unique_temp_dir("regulus_cli_build_wat_flag"); + fs::create_dir_all(&temp).expect("create temp dir"); + let wat_path = temp.join("scalar_project.custom.wat"); + + let output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("build") + .arg(&fixture) + .arg("--out-dir") + .arg(temp.join("out")) + .arg("--wat") + .arg(&wat_path) + .output() + .expect("run reggie build --wat"); + + assert!( + output.status.success(), + "build failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(temp.join("out/scalar_project.wasm").is_file()); + assert!(wat_path.is_file(), "expected WAT at {}", wat_path.display()); + assert!( + fs::read_to_string(&wat_path).expect("read WAT").contains("(module"), + "expected WAT module text" + ); + + let _ = fs::remove_dir_all(temp); +} + #[test] fn build_emit_writes_wat_and_debug_artifacts_without_wasm() { let fixture = workspace_root().join("fixtures/projects/generated_names/dependency_module_overlap"); @@ -106,7 +248,7 @@ fn build_emit_writes_wat_and_debug_artifacts_without_wasm() { .arg("--out-dir") .arg(&out_dir) .arg("--emit") - .arg("wat,ast,resolved,typed,ir") + .arg("wat,ast,resolved,typed,ir,runtime,abi") .output() .expect("run reggie build"); @@ -119,6 +261,8 @@ fn build_emit_writes_wat_and_debug_artifacts_without_wasm() { assert!(!out_dir.join("dependency_module_overlap.wasm").exists()); assert!(out_dir.join("dependency_module_overlap.wat").is_file()); assert!(out_dir.join("dependency_module_overlap.ir.txt").is_file()); + assert!(out_dir.join("dependency_module_overlap.runtime.txt").is_file()); + assert!(out_dir.join("dependency_module_overlap.abi.txt").is_file()); assert!( out_dir .join("dependency_module_overlap.dependency__module__overlap.main.ast.txt") @@ -135,9 +279,56 @@ fn build_emit_writes_wat_and_debug_artifacts_without_wasm() { .is_file() ); + let runtime = + fs::read_to_string(out_dir.join("dependency_module_overlap.runtime.txt")).expect("read runtime debug artifact"); + assert!(runtime.contains("runtime layout:"), "{runtime}"); + assert!(runtime.contains("object tags:"), "{runtime}"); + + let abi = fs::read_to_string(out_dir.join("dependency_module_overlap.abi.txt")).expect("read ABI debug artifact"); + assert!(abi.contains("target: Wasmtime"), "{abi}"); + assert!(abi.contains("exports:"), "{abi}"); + let _ = fs::remove_dir_all(out_dir); } +#[test] +fn compile_emit_writes_runtime_and_abi_debug_artifacts() { + let temp = unique_temp_dir("regulus_cli_compile_runtime_abi"); + let out_dir = temp.join("out"); + fs::create_dir_all(&out_dir).expect("create output dir"); + let input = temp.join("app.gleam"); + fs::write(&input, "pub fn answer() -> Int { 42 }\n").expect("write Gleam input"); + + let output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("compile") + .arg(&input) + .arg("--out-dir") + .arg(&out_dir) + .arg("--emit") + .arg("runtime,abi") + .output() + .expect("run reggie compile"); + + assert!( + output.status.success(), + "compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(!out_dir.join("app.wasm").exists()); + assert!(out_dir.join("app.runtime.txt").is_file()); + assert!(out_dir.join("app.abi.txt").is_file()); + + let runtime = fs::read_to_string(out_dir.join("app.runtime.txt")).expect("read runtime artifact"); + assert!(runtime.contains("memory_limit_bytes:"), "{runtime}"); + + let abi = fs::read_to_string(out_dir.join("app.abi.txt")).expect("read ABI artifact"); + assert!(abi.contains("answer -> answer"), "{abi}"); + assert!(abi.contains("result: Int as Scalar(I64)"), "{abi}"); + + let _ = fs::remove_dir_all(temp); +} + #[test] fn debug_alias_prints_tree_sitter_tree() { let temp = unique_temp_dir("regulus_cli_debug_ts"); diff --git a/docs/internal/specs/17_cli_and_build_outputs.md b/docs/internal/specs/17_cli_and_build_outputs.md index 1de4a80..0373c13 100644 --- a/docs/internal/specs/17_cli_and_build_outputs.md +++ b/docs/internal/specs/17_cli_and_build_outputs.md @@ -28,16 +28,25 @@ Remaining responsibilities: ## Commands Single-file compilation remains useful for fixtures and small examples. Project -compilation should reuse the same flags where possible. +compilation reuses the same output, target, WAT, emit, dump, verbose, and JSON +reservation flags where possible. -Important command concerns: +Current command surface: -- output path selection -- optional WAT output -- optional debug dump directory -- target selection for Wasmtime, browser, and WASI -- JS host profile selection for browser, bundler, and Node.js -- clear exit codes for success, diagnostics, and command misuse +- `build [project]` compiles a Gleam project. +- `compile ` compiles one source file. +- `run ` compiles one source file and executes an export with Wasmtime. +- `exec` is an alias for `run`. +- `debug`/`dbg` inspect compiler-internal views. +- `list [project]` prints discovered project modules. + +`build` and `compile` both support output path selection, optional WAT output, +optional debug dump directories, target selection, and explicit artifact +selection. `run` and `exec` support Wasmtime execution for scalar arguments and +ABI-aware rendering of scalar and managed return values. + +Commands should continue to return clear exit codes for success, diagnostics, +and command misuse. ## Artifacts diff --git a/docs/internal/tasks/17_cli_and_build_outputs.md b/docs/internal/tasks/17_cli_and_build_outputs.md index 34ade8a..3a747dc 100644 --- a/docs/internal/tasks/17_cli_and_build_outputs.md +++ b/docs/internal/tasks/17_cli_and_build_outputs.md @@ -13,9 +13,9 @@ Make compiler commands predictable and generated artifacts useful. - [x] Add target selection for supported runtimes and host profiles: Wasmtime, browser, bundler, Node.js, and WASI where implemented. - [x] Return useful exit codes for success, diagnostics, and command misuse. -- [ ] Keep project compilation flags consistent with single-file compilation. -- [ ] Add CLI `run` or `exec` integration tests for Wasmtime execution. -- [ ] Add ABI-aware rendering for managed `run` return values such as strings, +- [x] Keep project compilation flags consistent with single-file compilation. +- [x] Add CLI `run`/`exec` (aliases for one another) integration tests for Wasmtime execution. +- [x] Add ABI-aware rendering for managed `run` return values such as strings, tuples, lists, records, `Result`, and `Option`. ### Artifacts @@ -28,7 +28,7 @@ Make compiler commands predictable and generated artifacts useful. - [x] Emit deterministic `.mjs` adapter files when Wasm output is requested for browser, bundler, and Node.js targets. - [x] Expose JS host import and export metadata needed by checked host calls. -- [ ] Add optional runtime layout and ABI debug output where helpful. +- [x] Add optional runtime layout and ABI debug output where helpful. - [x] Include import and export metadata in explicit debug output where useful. - [x] Emit or package deterministic browser and Node.js host adapter files when requested. diff --git a/docs/website/development/projects.md b/docs/website/development/projects.md index 661809b..8c76c8e 100644 --- a/docs/website/development/projects.md +++ b/docs/website/development/projects.md @@ -40,8 +40,8 @@ Build and compile share flags where their meanings match: - `--out-dir ` writes compiler-named artifacts into a directory. - `--target ` selects `wasmtime`, `browser`, `bundler`, `nodejs`, or `wasi`. -- `--emit ` selects `wasm`, `wat`, `ast`, `resolved`, `typed`, or - `ir`. +- `--emit ` selects `wasm`, `wat`, `ast`, `resolved`, `typed`, + `ir`, `runtime`, or `abi`. - `--dump-dir ` writes debug dumps to a separate directory. - `-v, --verbose` prints module and dependency details. - `--json` is reserved for future machine-readable output. diff --git a/docs/website/guide/usage/cli.md b/docs/website/guide/usage/cli.md index 8bc5ba2..938330b 100644 --- a/docs/website/guide/usage/cli.md +++ b/docs/website/guide/usage/cli.md @@ -98,6 +98,7 @@ host glue and profile-specific APIs are still in progress. ```sh reggie build examples/scalar_project --emit wasm,wat reggie build examples/scalar_project --emit wat,ast,resolved,typed,ir +reggie build examples/scalar_project --emit runtime,abi ``` Supported emit values are: @@ -110,6 +111,8 @@ Supported emit values are: | `resolved` | Per-module resolved AST debug dumps. | | `typed` | Per-module typed-module debug dumps. | | `ir` | Linked IR debug dump. | +| `runtime` | Runtime layout and object tag summary. | +| `abi` | Import/export ABI boundary summary. | `wasm` is the default. `wat` writes next to the Wasm output, or into `--out-dir` when that option is used. Debug emit values write deterministic @@ -121,8 +124,9 @@ Use `--dump-dir` to write all compiler debug dumps into a separate directory: reggie build examples/multi_module_project --dump-dir build/dumps ``` -Single-file dumps include AST, resolved AST, typed output, IR, and WAT. Project -dumps include per-module AST, resolved AST, typed output, linked IR, and WAT. +Single-file dumps include AST, resolved AST, typed output, IR, WAT, runtime +layout, and ABI output. Project dumps include per-module AST, resolved AST, +typed output, linked IR, WAT, runtime layout, and ABI output. If compilation fails, Regulus does not write the final Wasm artifact. Debug artifacts are only written when the requested compiler phase completes. diff --git a/docs/website/reference/cli-and-build-outputs.md b/docs/website/reference/cli-and-build-outputs.md index 54dc590..c4a1248 100644 --- a/docs/website/reference/cli-and-build-outputs.md +++ b/docs/website/reference/cli-and-build-outputs.md @@ -13,7 +13,9 @@ The command writes `build/.wasm` by default. `--output` writes the final Wasm artifact to an exact path. `--out-dir` writes compiler-named artifacts such as `.wasm` and `.wat` into the given directory. `--emit` accepts comma-separated artifact kinds: `wasm`, `wat`, -`ast`, `resolved`, `typed`, and `ir`. +`ast`, `resolved`, `typed`, `ir`, `runtime`, and `abi`. +`--wat` is a compatibility alias for emitting WAT, matching `compile`. +Passing `--wat` without a path uses the `.wat` path matching the output file. See [Project compilation and dependencies][project-compilation] for dependency loading, linked output, and current project limits. @@ -65,8 +67,11 @@ and checks that host imports are valid for the selected target. ## Debug dumps `--dump-dir ` writes deterministic debug files. Single-file compilation -writes AST, resolved AST, typed output, IR, and WAT dumps. Project builds write -per-module AST, resolved AST, typed output, linked IR, and WAT dumps. +writes AST, resolved AST, typed output, IR, WAT, runtime layout, and ABI dumps. +Project builds write per-module AST, resolved AST, typed output, linked IR, +WAT, runtime layout, and ABI dumps. The `runtime` and `abi` emit kinds write +runtime layout and ABI boundary summaries without requiring a full dump +directory. These dumps are for contributor inspection. Normal CLI output stays focused on the final artifact path, optional WAT path, and diagnostics. diff --git a/docs/website/reference/compiling-projects.md b/docs/website/reference/compiling-projects.md index 9b20546..d82c4b2 100644 --- a/docs/website/reference/compiling-projects.md +++ b/docs/website/reference/compiling-projects.md @@ -112,6 +112,7 @@ reggie build examples/scalar_project --out-dir build/examples ```sh reggie build examples/scalar_project --emit wasm,wat reggie build examples/scalar_project --emit wat,ast,resolved,typed,ir +reggie build examples/scalar_project --emit runtime,abi ``` Supported project artifact kinds are: @@ -124,6 +125,8 @@ Supported project artifact kinds are: | `resolved` | Per-module resolved AST debug dumps. | | `typed` | Per-module typed-module debug dumps. | | `ir` | Linked IR debug dump. | +| `runtime` | Runtime layout and object tag summary. | +| `abi` | Import/export ABI boundary summary. | Use `--dump-dir` to write debug dumps into a separate directory. If compilation fails, Regulus does not write the final Wasm artifact. Debug -- 2.51.2