From 13a406d0fe2802ccc234264baab113c19249c7a9 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Wed, 7 Oct 2026 01:32:59 +0900 Subject: [PATCH] Start alpha development with an opt-in Whale IR backend Signed-off-by: LunaStev --- Cargo.lock | 28 ++- Cargo.toml | 4 +- README.md | 30 +++ VERSION.md | 12 +- src/cli.rs | 150 +++++++++++- src/lib.rs | 2 +- src/runner.rs | 32 +++ tests/whale_backend.rs | 317 +++++++++++++++++++++++++ tools/ci/nightly.py | 2 +- tools/ci/nightly_installers.json | 4 +- whale/Cargo.toml | 12 + whale/src/codegen/expression.rs | 303 ++++++++++++++++++++++++ whale/src/codegen/mod.rs | 383 +++++++++++++++++++++++++++++++ whale/src/codegen/walk.rs | 240 +++++++++++++++++++ whale/src/lib.rs | 21 ++ 15 files changed, 1522 insertions(+), 18 deletions(-) create mode 100644 tests/whale_backend.rs create mode 100644 whale/Cargo.toml create mode 100644 whale/src/codegen/expression.rs create mode 100644 whale/src/codegen/mod.rs create mode 100644 whale/src/codegen/walk.rs create mode 100644 whale/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2b339512..a56fbe2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,14 @@ dependencies = [ "syn", ] +[[package]] +name = "ir" +version = "0.1.0" +source = "git+https://github.com/wavefnd/Whale.git?branch=master#4f45fdb53d07882932cc251856316206a3bb3947" +dependencies = [ + "whale-target", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -204,7 +212,7 @@ version = "0.1.0" [[package]] name = "wavec" -version = "0.2.1-pre-beta" +version = "0.0.1-alpha-dev" dependencies = [ "error", "hir", @@ -212,4 +220,22 @@ dependencies = [ "llvm", "parser", "utils", + "whale", +] + +[[package]] +name = "whale" +version = "0.1.0" +dependencies = [ + "error", + "hir", + "ir", + "lexer", + "parser", + "utils", ] + +[[package]] +name = "whale-target" +version = "0.1.0" +source = "git+https://github.com/wavefnd/Whale.git?branch=master#4f45fdb53d07882932cc251856316206a3bb3947" diff --git a/Cargo.toml b/Cargo.toml index 8b73ff27..f4ba2624 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wavec" -version = "0.2.1-pre-beta" +version = "0.0.1-alpha-dev" edition = "2021" [lib] @@ -14,6 +14,7 @@ parser = { path = "front/parser" } hir = { path = "front/hir" } error = { path = "front/error" } llvm = { path = "./llvm", default-features = false } +whale = { path = "./whale" } [features] default = ["llvm-target-core64", "llvm-target-loongarch", "llvm-target-wasm"] @@ -31,6 +32,7 @@ members = [ "front/parser", "front/hir", "llvm", + "whale", "front/error", "utils", ".", diff --git a/README.md b/README.md index e4ce6a0e..52250bd7 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,36 @@ wavec print target-spec --target riscv64-unknown-linux-gnu --format=json Run `wavec --help` for the complete CLI contract. +## Experimental Whale backend + +Development is now `0.0.1-alpha-dev`. LLVM remains the default backend. The +existing `--whale` flag selects the initial Linux amd64, O0 integration: + +```shell +wavec --whale check sample.wave +wavec --whale build sample.wave --emit=ir --out-dir output +``` + +The second command writes verified Whale IR to `output/sample.wir`. This path +supports scalar functions and direct calls, initialized local variables, +assignment, `if`/`else`, `while`, and scalar arithmetic and comparisons. Wave's +typed HIR supplies calculation types and ordered conversions. Short-circuit +logical operators preserve conditional evaluation. + +This integration does not yet generate executables, objects, or assembly. +Aggregates, pointers, async, foreign calls, I/O, integers wider than 128 bits, +checked division/shifts, and float-to-integer conversions are rejected explicitly. +Unsupported requests never fall back to LLVM. `--whale check` also verifies +backend support, whereas the default `check` performs frontend validation. + +The root `whale/` workspace crate owns the Whale backend alongside `llvm/`. +The compiler driver in `src/` selects and invokes these backends. The `whale/` +crate uses upstream Whale IR as a Rust Git dependency from the official +repository's `master` branch. +`Cargo.lock` records the tested commit; `cargo build --locked` reuses it. Advancing +Whale uses `cargo update -p ir`, followed by integration tests and review of the +lockfile change. Wave does not require a separately installed Whale executable. + ## Target families | Architecture | Hosted targets | Freestanding target | diff --git a/VERSION.md b/VERSION.md index 636d8357..c01a5bac 100644 --- a/VERSION.md +++ b/VERSION.md @@ -43,11 +43,15 @@ example, after the final `pre-beta` release, the first `alpha` release starts at ## Current Sequence -`v0.2.0-pre-beta` is the latest public `pre-beta` release. +`v0.2.1-pre-beta` is the final public `pre-beta` release. -The repository uses `v0.2.1-pre-beta-dev` while the next release is under -development. The `-dev` suffix is removed when the release candidate is ready -to publish as `v0.2.1-pre-beta`. +The repository uses `v0.0.1-alpha-dev` while LLVM and Whale integration is under +development. LLVM remains the default backend; Whale is selected explicitly +with `--whale`. The `-dev` suffix is removed when the release is ready to publish +as `v0.0.1-alpha`. + +The stage transition resets the release counter. Installers must determine the +latest published release by publication order, not by comparing version numbers. ## Rules diff --git a/src/cli.rs b/src/cli.rs index bff7c7d5..b479bd1d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -274,12 +274,6 @@ where } fn dispatch(mut global: Global, cmd: CliCommand) -> Result<(), CliError> { - if global.whale.enabled { - return Err(CliError::usage( - "TODO: --whale backend is reserved but not implemented yet", - )); - } - if global.std_root.is_some() && matches!(&cmd, CliCommand::Build(_) | CliCommand::Print { .. }) { let root = parser::import::ResolvedStdRoot::resolve(global.std_root.as_deref()) @@ -289,7 +283,7 @@ fn dispatch(mut global: Global, cmd: CliCommand) -> Result<(), CliError> { match cmd { CliCommand::Version => { - print_version(); + print_version_for_backend(global.whale.enabled); Ok(()) } CliCommand::Help => { @@ -311,6 +305,129 @@ fn dispatch(mut global: Global, cmd: CliCommand) -> Result<(), CliError> { } } +/// Whale currently produces verified IR only. Keep its capability checks out of +/// LLVM target resolution: an LLVM installation must not decide Whale support. +fn dispatch_whale_build(global: &Global, build: &BuildRequest) -> Result<(), CliError> { + let mut global = global.clone(); + let target = global + .llvm + .target + .get_or_insert_with(|| "x86_64-unknown-linux-gnu".into()); + if target != "x86_64-unknown-linux-gnu" { + return Err(CliError::usage( + "Whale currently supports only --target x86_64-unknown-linux-gnu", + )); + } + if global.opt != "-O0" { + return Err(CliError::usage("Whale currently supports only -O0")); + } + if !build.emit.is_check() + && !matches!(build.emit.as_set(), Some(set) if set.len() == 1 && set.contains(&EmitKind::Ir)) + { + return Err(CliError::usage("Whale currently supports check and --emit=ir (.wir) only; executable, object, assembly and bitcode output are not available")); + } + let llvm = &global.llvm; + if build.run + || build.link_only + || build.shared + || build.static_link + || build.pie.is_some() + || build.freestanding + || build.entry.is_some() + || build.linker_script.is_some() + || build.no_start_files + || !global.link.libs.is_empty() + || !global.link.paths.is_empty() + || llvm.cpu.is_some() + || llvm.features.is_some() + || llvm.abi.is_some() + || llvm.isa.is_some() + || llvm.code_model.is_some() + || llvm.relocation_model.is_some() + || llvm.sysroot.is_some() + || llvm.linker.is_some() + || !llvm.link_args.is_empty() + || llvm.no_default_libs + || global.debug.mc + || global.debug.hex + { + return Err(CliError::usage("Whale IR does not support execution, linking, LLVM tuning, or machine-code debug options")); + } + let classified = classify_inputs(build)?; + if classified.is_empty() || classified.iter().any(|i| i.kind != InputKind::Wave) { + return Err(CliError::usage("Whale requires Wave source inputs")); + } + if build.emit.is_check() && (build.output.is_some() || build.out_dir.is_some()) { + return Err(CliError::usage( + "--emit=check does not produce artifacts; remove -o/--out-dir", + )); + } + if build.output.is_some() && (classified.len() != 1 || build.out_dir.is_some()) { + return Err(CliError::usage( + "Whale -o requires one input and cannot be combined with --out-dir", + )); + } + if global.dep.resolved_std_root.is_none() { + global.dep.resolved_std_root = Some(parser::import::ResolvedStdRoot::resolve(None)); + } + let outputs: Vec<_> = if build.emit.is_check() { + vec![] + } else { + classified + .iter() + .enumerate() + .map(|(i, input)| { + let output = build.output.clone().unwrap_or_else(|| { + resolve_extra_emit_output_path(build, input, EmitKind::Ir, i, classified.len()) + .with_extension("wir") + }); + (output, None) + }) + .collect() + }; + let mut inputs = build.inputs.clone(); + for input in &classified { + inputs.extend(runner::wave_input_paths( + &input.path, + &global.dep, + &global.llvm, + )); + } + crate::output_guard::validate(&inputs, &outputs)?; + if build.dry_run { + for (i, input) in classified.iter().enumerate() { + println!( + "Whale / Linux amd64 / O0: {} -> {}", + input.path.display(), + outputs + .get(i) + .map(|(p, _)| p.display().to_string()) + .unwrap_or_else(|| "verify only".into()) + ); + } + return Ok(()); + } + // Lower every source before publishing any output, including multi-input requests. + let texts: Vec<_> = classified + .iter() + .map(|input| { + runner::emit_whale_ir_text(&input.path, &global.debug, &global.dep, &global.llvm) + }) + .collect(); + if global.debug.ir { + for text in &texts { + println!("{text}"); + } + } + for ((output, _), text) in outputs.iter().zip(texts) { + ensure_parent_dir(output)?; + let pending = PendingOutput::new(output)?; + fs::write(pending.path(), text)?; + pending.commit()?; + } + Ok(()) +} + fn dispatch_build(global: &Global, build: &BuildRequest) -> Result<(), CliError> { let mut build = build.clone(); if global.error_format == ErrorFormat::Json { @@ -318,6 +435,10 @@ fn dispatch_build(global: &Global, build: &BuildRequest) -> Result<(), CliError> } configure_wave_error_format(build.error_format); + if global.whale.enabled { + return dispatch_whale_build(global, &build); + } + let mut effective_global = effective_global_for_build(global, &build); resolve_target_configuration(&mut effective_global.llvm)?; resolve_build_sysroot(&mut effective_global.llvm, &build); @@ -441,6 +562,9 @@ fn dispatch_print( target_arg: Option<&str>, format: PrintFormat, ) -> Result<(), CliError> { + if global.whale.enabled { + return Err(CliError::usage("Whale capability queries are not yet available; supported target: x86_64-unknown-linux-gnu, optimization: O0, output: IR")); + } let target = target_arg .map(|s| s.to_string()) .or_else(|| global.llvm.target.clone()) @@ -4432,6 +4556,10 @@ pub fn print_usage() { } pub fn print_version() { + print_version_for_backend(false); +} + +fn print_version_for_backend(whale: bool) { let os = format!("({})", get_os_pretty_name()).color("117,117,117"); println!( @@ -4441,7 +4569,9 @@ pub fn print_version() { os ); - if let Some(backend) = llvm::backend() { + if whale { + println!(" backend: Whale IR (experimental, Linux amd64, O0)"); + } else if let Some(backend) = llvm::backend() { println!(" backend: {}", backend.color("117,117,117")); } else { println!("{}", " backend: unknown backend".color("117,117,117")); @@ -4490,6 +4620,10 @@ pub fn print_help() { ); println!(" {:<22} {}", "--help".color("38,139,235"), "Show help"); + println!("\nBackend selection:"); + println!( + " --whale Experimental Whale IR (Linux amd64, O0); LLVM is the default" + ); println!("\nBuild options:"); println!( " {:<24} {}", diff --git a/src/lib.rs b/src/lib.rs index bd43413c..806c7b73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ //! //! This crate owns command-line planning, source preparation, diagnostics, and //! link orchestration. Language parsing lives in the frontend crates, while -//! target-specific lowering lives in the `llvm` crate. +//! target-specific lowering lives in the `llvm` and `whale` crates. // CLI tables and compiler phase boundaries intentionally favor explicit data. #![allow( diff --git a/src/runner.rs b/src/runner.rs index 7fb966f8..76497466 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -620,6 +620,38 @@ fn frontend_prepare_wave_hir( (code, hir) } +pub(crate) fn emit_whale_ir_text( + file_path: &Path, + debug: &DebugFlags, + dep: &DepFlags, + target: &LlvmFlags, +) -> String { + let (source, program) = frontend_prepare_wave_hir(file_path, debug, dep, Some(target)); + match whale::emit_ir(&program) { + Ok(text) => text, + Err(diagnostic) => { + let diagnostic_source = diagnostic + .span + .as_ref() + .and_then(|s| fs::read_to_string(&s.file).ok()) + .unwrap_or(source); + WaveError::new( + WaveErrorKind::InvalidStatement(diagnostic.message.clone()), + diagnostic.message, + file_path.display().to_string(), + 0, + 0, + ) + .with_code("E4001") + .with_source_code(diagnostic_source) + .with_span(diagnostic.span.as_ref()) + .with_context("Whale IR lowering") + .display_auto(); + process::exit(1); + } + } +} + pub(crate) unsafe fn check_wave_file( file_path: &Path, debug: &DebugFlags, diff --git a/tests/whale_backend.rs b/tests/whale_backend.rs new file mode 100644 index 00000000..c0dd81f1 --- /dev/null +++ b/tests/whale_backend.rs @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: MPL-2.0 +use std::{ + fs, + path::PathBuf, + process::{Command, Output}, +}; +use utils::wson; + +struct Case { + root: PathBuf, +} +impl Case { + fn new(name: &str, source: &str) -> Self { + let root = std::env::temp_dir().join(format!("wave-whale-{name}-{}", std::process::id())); + fs::create_dir_all(&root).unwrap(); + fs::write(root.join("main.wave"), source).unwrap(); + Self { root } + } + fn run(&self, args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_wavec")) + .current_dir(&self.root) + .args(args) + .output() + .unwrap() + } + fn ir(&self) -> String { + let output = self.run(&["--whale", "build", "main.wave", "--emit=ir"]); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + fs::read_to_string(self.root.join("main.wir")).unwrap() + } +} +impl Drop for Case { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +#[test] +fn scalar_control_flow_and_calls_produce_verified_whale_ir() { + let case = Case::new( + "control", + r#" +fun inc(n: i32) -> i32 { return n + 1; } +fun main() -> i32 { + var count: i32 = 0; + while (count < 10) { + count = inc(count); + if (count == 3) { continue; } + if (count > 6) { break; } + if (count == 5) { + var count: i32 = count + 1; + if (count != 6) { return 1; } + } + } + if (count == 7 && (inc(count) == 8 || count == 0)) { return 0; } + else if (count == 8) { return 2; } + else { return 3; } +} +"#, + ); + let ir = case.ir(); + assert!(ir.contains("x86_64-whale-linux"), "{ir}"); + assert!(ir.contains("phi"), "{ir}"); + assert!(ir.contains("call"), "{ir}"); + assert!(ir.contains("while.test"), "{ir}"); + assert!(!case.root.join("main.ll").exists()); + let check = case.run(&["--whale", "check", "main.wave"]); + assert!( + check.status.success(), + "{}", + String::from_utf8_lossy(&check.stderr) + ); +} + +#[test] +fn ordered_casts_and_contextual_arithmetic_survive_lowering() { + let case = Case::new( + "casts", + r#" +fun narrow(n: i32) -> i32 { return (n as u8) as i32; } +fun signedness(n: u32) -> i32 { return n as i32; } +fun bound(value: u64) -> bool { return value <= 4294967296 - 32; } +fun wide() -> u128 { return 340282366920938463463374607431768211455; } +fun main() -> i32 { return narrow(257); } +"#, + ); + let ir = case.ir(); + let trunc = ir.find("trunc").expect(&ir); + let extend = ir.find("zext").expect(&ir); + assert!(trunc < extend, "{ir}"); + assert!(ir.contains("bitcast"), "{ir}"); + assert!(ir.contains("4294967296"), "{ir}"); + assert!( + ir.contains("340282366920938463463374607431768211455"), + "{ir}" + ); + assert!(ir.contains("ule"), "{ir}"); +} + +#[test] +fn floating_bool_uses_unordered_nonzero_and_preserves_negative_zero() { + let case = Case::new( + "float", + r#" +fun truth(n: f64) -> bool { return n as bool; } +fun not_int(n: i32) -> bool { return !n; } +fun neg(n: f64) -> f64 { return -n; } +fun zero() -> f64 { return -0.0; } +fun mixed(n: i32, f: f64) -> bool { return n != f; } +fun main() -> i32 { if (truth(0.0)) { return 1; } return 0; } +"#, + ); + let ir = case.ir(); + assert!(ir.contains("une"), "{ir}"); + assert!(ir.contains("0x8000000000000000"), "{ir}"); +} + +#[test] +fn unsupported_source_has_json_location_and_preserves_previous_output() { + let case = Case::new("unsupported", "fun main() -> i32 {\n return 8 / 2;\n}\n"); + let output = case.root.join("main.wir"); + fs::write(&output, "previous verified IR").unwrap(); + let result = case.run(&[ + "--whale", + "build", + "main.wave", + "--emit=ir", + "--error-format=json", + ]); + assert!(!result.status.success()); + let stderr = String::from_utf8_lossy(&result.stderr); + let json = wson::parse_json(stderr.trim()).unwrap(); + assert!( + stderr.contains("Whale backend does not yet support operation Divide"), + "{json:?}" + ); + assert!(stderr.contains("main.wave"), "{json:?}"); + assert!(stderr.contains("E4001"), "{json:?}"); + assert_eq!(fs::read_to_string(output).unwrap(), "previous verified IR"); + let targets = case.run(&["print", "target-list"]); + assert!(targets.status.success()); + let targets = String::from_utf8(targets.stdout).unwrap(); + let llvm = case.run(&[ + "check", + "main.wave", + "--target", + targets.lines().next().unwrap(), + ]); + assert!( + llvm.status.success(), + "{}", + String::from_utf8_lossy(&llvm.stderr) + ); + let whale = case.run(&["--whale", "check", "main.wave"]); + assert!(!whale.status.success()); +} + +#[test] +fn unsupported_requests_fail_without_creating_outputs() { + let case = Case::new("options", "fun main() -> i32 { return 0; }"); + for args in [ + vec!["--whale", "build", "main.wave"], + vec!["--whale", "-O2", "build", "main.wave", "--emit=ir"], + vec![ + "--whale", + "--target=aarch64-unknown-linux-gnu", + "check", + "main.wave", + ], + vec!["--whale", "build", "main.wave", "--emit=ir", "--run"], + vec!["--whale", "build", "main.wave", "--emit=obj"], + vec!["--whale", "print", "supported-targets"], + ] { + let result = case.run(&args); + assert!(!result.status.success(), "{args:?}"); + assert!( + String::from_utf8_lossy(&result.stderr).contains("Whale"), + "{args:?}: {}", + String::from_utf8_lossy(&result.stderr) + ); + } + assert!(!case.root.join("main.wir").exists()); + assert!(!case.root.join("main.ll").exists()); + let dry = case.run(&["--whale", "build", "main.wave", "--emit=ir", "--dry-run"]); + assert!( + dry.status.success(), + "{}", + String::from_utf8_lossy(&dry.stderr) + ); + assert!(!case.root.join("main.wir").exists()); +} + +#[test] +fn whale_output_cannot_overwrite_source_or_imported_input() { + let source = "fun main() -> i32 { return 0; }"; + let case = Case::new("output", source); + let result = case.run(&[ + "--whale", + "build", + "main.wave", + "--emit=ir", + "-o", + "main.wave", + ]); + assert!(!result.status.success()); + assert_eq!( + fs::read_to_string(case.root.join("main.wave")).unwrap(), + source + ); + fs::write( + case.root.join("helper.wave"), + "pub fun helper() -> i32 { return 1; }", + ) + .unwrap(); + fs::write( + case.root.join("main.wave"), + "import(\"./helper.wave\")::{helper}; fun main() -> i32 { return helper(); }", + ) + .unwrap(); + let original = fs::read_to_string(case.root.join("helper.wave")).unwrap(); + let result = case.run(&[ + "--whale", + "build", + "main.wave", + "--emit=ir", + "-o", + "helper.wave", + ]); + assert!(!result.status.success()); + assert!( + String::from_utf8_lossy(&result.stderr).contains("aliases compiler input"), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + assert_eq!( + fs::read_to_string(case.root.join("helper.wave")).unwrap(), + original + ); +} + +#[test] +fn llvm_stays_default_and_whale_help_is_available() { + let case = Case::new("default", "fun main() -> i32 { return 0; }"); + if cfg!(feature = "llvm-target-x86") { + let output = case.run(&[ + "--target=x86_64-unknown-linux-gnu", + "build", + "main.wave", + "--emit=ir", + ]); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(fs::read_to_string(case.root.join("main.ll")) + .unwrap() + .contains("target triple")); + assert!(!case.root.join("main.wir").exists()); + } + let help = case.run(&["--whale", "--help"]); + assert!(help.status.success()); + assert!(String::from_utf8_lossy(&help.stdout).contains("LLVM is the default")); + let version = case.run(&["--version"]); + assert!(version.status.success()); + assert!(String::from_utf8_lossy(&version.stdout).contains(env!("CARGO_PKG_VERSION"))); + let version = case.run(&["--whale", "--version"]); + assert!(version.status.success()); + assert!(String::from_utf8_lossy(&version.stdout).contains("backend: Whale IR")); +} + +#[test] +fn whale_expression_depth_keeps_the_frontend_boundary() { + let case = Case::new("depth", ""); + for kind in ["binary", "cast", "call"] { + for depth in [128, 129] { + let expression = match kind { + "binary" => format!("1{}", " + 1".repeat(depth)), + "cast" => format!("1{}", " as i32".repeat(depth)), + _ => format!("{}1{}", "id(".repeat(depth), ")".repeat(depth)), + }; + fs::write(case.root.join("main.wave"), format!( + "fun id(n: i32) -> i32 {{ return n; }}\nfun main() -> i32 {{ return {expression}; }}" + )).unwrap(); + #[cfg(target_os = "linux")] + let mut command = { + let mut command = Command::new("sh"); + command + .args(["-c", "ulimit -s 1024 && exec \"$@\"", "whale-depth"]) + .arg(env!("CARGO_BIN_EXE_wavec")); + command + }; + #[cfg(not(target_os = "linux"))] + let mut command = Command::new(env!("CARGO_BIN_EXE_wavec")); + let result = command + .current_dir(&case.root) + .args(["--whale", "check", "main.wave", "--error-format=json"]) + .output() + .unwrap(); + if depth == 128 { + assert!( + result.status.success(), + "{kind}/{depth}: {}", + String::from_utf8_lossy(&result.stderr) + ); + } else { + assert_eq!(result.status.code(), Some(1), "{kind}/{depth}"); + assert!(String::from_utf8_lossy(&result.stderr) + .contains("expression nesting exceeds the maximum of 128 levels")); + } + } + } +} diff --git a/tools/ci/nightly.py b/tools/ci/nightly.py index 67fdc12f..c5e4151b 100644 --- a/tools/ci/nightly.py +++ b/tools/ci/nightly.py @@ -162,7 +162,7 @@ def installers_ready(): f"cannot verify deployed installer {url}: {error}" ) from error if len(data) > 1024 * 1024 or hashlib.sha256(data).hexdigest() != expected: - raise ValueError(f"versioned-only installer is not deployed: {url}") + raise ValueError(f"reviewed latest-only installer is not deployed: {url}") def file_record(path): diff --git a/tools/ci/nightly_installers.json b/tools/ci/nightly_installers.json index 80c9df79..2e912a05 100644 --- a/tools/ci/nightly_installers.json +++ b/tools/ci/nightly_installers.json @@ -1,4 +1,4 @@ { - "https://wave-lang.dev/install.sh": "d0ad7ae9eab7fac33189e972dea7d30397759524e4e12abb6d1929ec641a5781", - "https://wave-lang.dev/install.ps1": "2e62d2dc3636c6b45ca5635193b4693e0e4bc7633024611ec0b77228da03beb6" + "https://wave-lang.dev/install.sh": "861356ad9822868ed3e5aa25438c2158ca503c62012639ea24b68c54ff274921", + "https://wave-lang.dev/install.ps1": "2dc19d0e8cfe9907d11b1a204e1812e0e53955ffc755774b4936e9eb21106995" } diff --git a/whale/Cargo.toml b/whale/Cargo.toml new file mode 100644 index 00000000..1496bb9d --- /dev/null +++ b/whale/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "whale" +version = "0.1.0" +edition = "2021" + +[dependencies] +parser = { path = "../front/parser" } +hir = { path = "../front/hir" } +lexer = { path = "../front/lexer" } +error = { path = "../front/error" } +utils = { path = "../utils" } +whale-ir = { package = "ir", git = "https://github.com/wavefnd/Whale.git", branch = "master", default-features = false } diff --git a/whale/src/codegen/expression.rs b/whale/src/codegen/expression.rs new file mode 100644 index 00000000..823b5b84 --- /dev/null +++ b/whale/src/codegen/expression.rs @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: MPL-2.0 +use super::*; +use hir::conversions::{integer_width, unsigned, ConversionInfo, ConversionKind}; +use parser::ast::{Literal, Operator}; +use utils::const_int::ConstInt; + +impl Lowerer<'_> { + pub(super) fn effect(&mut self, e: &Expression) -> Result<()> { + if let Expression::FunctionCall { name, args, .. } = e.unspanned() { + self.call(name, args).map(|_| ()).map_err(|mut error| { + if error.span.is_none() { + error.span = self + .program + .expression_id(e) + .and_then(|id| self.program.expression_span(id)) + .cloned() + .or_else(|| e.span().cloned()); + } + error + }) + } else { + self.expr(e).map(|_| ()) + } + } + pub(super) fn expr(&mut self, e: &Expression) -> Result { + self.expression(e).map_err(|mut error| { + if error.span.is_none() { + error.span = self + .program + .expression_id(e) + .and_then(|id| self.program.expression_span(id)) + .cloned() + .or_else(|| e.span().cloned()); + } + error + }) + } + pub(super) fn unary_value( + &mut self, + operator: &Operator, + operand: Value, + fact: &hir::conversions::NumericExpressionInfo, + ) -> Result { + let ty = scalar(&fact.evaluation_type)?; + Ok(match operator { + Operator::Neg if matches!(ty, Type::F32 | Type::F64) => { + // Multiplication by -1 preserves both signs of zero. + let minus_one = self.literal(&Literal::Float(-1.0), &fact.evaluation_type)?; + self.bin(BinOp::FMul, ty.clone(), operand, minus_one)? + } + Operator::Neg => { + let zero = self.literal(&Literal::Int("0".into()), &fact.evaluation_type)?; + self.bin(BinOp::Sub, ty.clone(), zero, operand)? + } + Operator::LogicalNot | Operator::Not if ty == Type::Bool => { + let input = fact + .computation_type + .as_ref() + .ok_or_else(|| ice("missing unary computation type"))?; + if *input == WaveType::Bool { + let zero = self.constant(Type::Bool, ConstValue::Bool(false)); + self.icmp(ICmpPred::Eq, operand, zero)? + } else if matches!(input, WaveType::Float(_)) { + let zero = self.literal(&Literal::Float(0.0), input)?; + self.fcmp(FCmpPred::Oeq, operand, zero)? + } else { + let zero = self.literal(&Literal::Int("0".into()), input)?; + self.icmp(ICmpPred::Eq, operand, zero)? + } + } + Operator::BitwiseNot => { + let value = self.value(ty.clone()); + self.emit(Instruction::Not { + dst: value.id, + ty: ty.clone(), + src: operand.id, + }); + value + } + _ => return Err(unsupported("this unary operation")), + }) + } + fn constant(&mut self, ty: Type, literal: ConstValue) -> Value { + let value = self.value(ty.clone()); + self.emit(Instruction::Const { + dst: value.id, + ty, + value: literal, + }); + value + } + pub(super) fn literal(&mut self, literal: &Literal, ty: &WaveType) -> Result { + let ir_ty = scalar(ty)?; + let constant = match literal { + Literal::Bool(v) if *ty == WaveType::Bool => ConstValue::Bool(*v), + Literal::Float(v) if matches!(ty, WaveType::Float(_)) => { + let WaveType::Float(bits) = ty else { + unreachable!() + }; + ConstValue::F(FloatBits::from_f64(*bits, *v).map_err(ice)?) + } + Literal::Int(raw) if matches!(ty, WaveType::Float(_)) => { + let WaveType::Float(bits) = ty else { + unreachable!() + }; + let n = hir::integer_literal_float(raw, *bits) + .ok_or_else(|| ice("invalid typed floating literal"))?; + ConstValue::F(FloatBits::from_f64(*bits, n).map_err(ice)?) + } + Literal::Int(raw) => { + let token = lexer::number::IntegerLiteral::parse(raw) + .ok_or_else(|| ice("invalid integer token"))?; + let mut n = ConstInt::from_digits(&token.digits, token.radix) + .ok_or_else(|| ice("invalid integer digits"))?; + if token.negative { + n = n.negated(); + } + let bits = integer_width(ty) + .ok_or_else(|| ice("integer literal with non-integer HIR type"))?; + let words = n.normalize(bits, !unsigned(ty)).to_le_words(128); + let raw = u128::from(words[0]) | (u128::from(words[1]) << 64); + if unsigned(ty) { + ConstValue::U(raw) + } else { + ConstValue::I(raw as i128) + } + } + _ => return Err(unsupported("this literal")), + }; + Ok(self.constant(ir_ty, constant)) + } + pub(super) fn convert(&mut self, input: Value, conversion: &ConversionInfo) -> Result { + let src_ty = scalar(&conversion.source_type)?; + let dst_ty = scalar(&conversion.target_type)?; + if input.ty != src_ty { + return Err(ice("conversion source type mismatch")); + } + let op = match conversion.kind { + ConversionKind::Identity => return Ok(input), + ConversionKind::ReinterpretInteger => CastOp::Bitcast, + ConversionKind::SignExtend => CastOp::SExt, + ConversionKind::ZeroExtend => CastOp::ZExt, + ConversionKind::Truncate => CastOp::Trunc, + ConversionKind::FloatExtend => CastOp::FExt, + ConversionKind::FloatTruncate => CastOp::FTrunc, + ConversionKind::SignedToFloat => CastOp::IToF_S, + ConversionKind::UnsignedToFloat => CastOp::IToF_U, + ConversionKind::IntegerToBool => { + let zero = self.literal(&Literal::Int("0".into()), &conversion.source_type)?; + return self.icmp(ICmpPred::Ne, input, zero); + } + ConversionKind::FloatToBool => { + let zero = self.literal(&Literal::Float(0.0), &conversion.source_type)?; + return self.fcmp(FCmpPred::Une, input, zero); + } + // Raw Whale FToI instructions do not implement Wave's checked range + // contract. Reject until the adapter emits those checks explicitly. + _ => return Err(unsupported(format!("conversion {:?}", conversion.kind))), + }; + let value = self.value(dst_ty.clone()); + self.emit(Instruction::Cast { + dst: value.id, + op, + dst_ty, + src_ty, + src: input.id, + }); + Ok(value) + } + fn call(&mut self, name: &str, args: &[Expression]) -> Result> { + let mut values = Vec::with_capacity(args.len()); + for arg in args { + values.push(self.expr(arg)?); + } + self.call_values(name, values) + } + pub(super) fn call_values(&mut self, name: &str, args: Vec) -> Result> { + let (id, signature) = self + .functions + .get(name) + .cloned() + .ok_or_else(|| unsupported(format!("external or intrinsic call `{name}`")))?; + if args.len() != signature.params.len() { + return Err(unsupported("omitted default arguments")); + } + if args.iter().zip(&signature.params).any(|(v, t)| &v.ty != t) { + return Err(ice("call argument disagrees with HIR signature")); + } + let value = (signature.ret != Type::Void).then(|| self.value(signature.ret.clone())); + self.emit(Instruction::Call { + convention: CallingConvention::Whale, + dst: value.as_ref().map(|v| v.id), + ret_ty: signature.ret, + callee: Callee::Direct(id), + args: args.into_iter().map(|v| v.id).collect(), + }); + Ok(value) + } + fn bin(&mut self, op: BinOp, ty: Type, lhs: Value, rhs: Value) -> Result { + if lhs.ty != ty || rhs.ty != ty { + return Err(ice( + "arithmetic operands disagree with HIR computation type", + )); + } + let value = self.value(ty.clone()); + self.emit(Instruction::Bin { + dst: value.id, + op, + ty, + lhs: lhs.id, + rhs: rhs.id, + }); + Ok(value) + } + fn icmp(&mut self, pred: ICmpPred, lhs: Value, rhs: Value) -> Result { + if lhs.ty != rhs.ty { + return Err(ice("comparison operand type mismatch")); + } + let value = self.value(Type::Bool); + self.emit(Instruction::ICmp { + dst: value.id, + pred, + ty: lhs.ty, + lhs: lhs.id, + rhs: rhs.id, + }); + Ok(value) + } + fn fcmp(&mut self, pred: FCmpPred, lhs: Value, rhs: Value) -> Result { + if lhs.ty != rhs.ty { + return Err(ice("comparison operand type mismatch")); + } + let value = self.value(Type::Bool); + self.emit(Instruction::FCmp { + dst: value.id, + pred, + ty: lhs.ty, + lhs: lhs.id, + rhs: rhs.id, + }); + Ok(value) + } + pub(super) fn binary_values( + &mut self, + lhs: Value, + op: &Operator, + rhs: Value, + computation: &WaveType, + result: &Type, + ) -> Result { + use Operator::*; + // These operations require additional Wave-specific checked lowering. + if matches!(op, ShiftLeft | ShiftRight | Divide | Remainder) { + return Err(unsupported(format!("operation {op:?}"))); + } + let ty = scalar(computation)?; + if lhs.ty != ty || rhs.ty != ty { + return Err(ice("binary operands disagree with HIR computation type")); + } + let float = matches!(ty, Type::F32 | Type::F64); + if *result == Type::Bool { + if float { + let pred = match op { + Equal => FCmpPred::Oeq, + NotEqual => FCmpPred::Une, + Less => FCmpPred::Olt, + LessEqual => FCmpPred::Ole, + Greater => FCmpPred::Ogt, + GreaterEqual => FCmpPred::Oge, + _ => return Err(unsupported(format!("comparison {op:?}"))), + }; + return self.fcmp(pred, lhs, rhs); + } + let pred = match (op, unsigned(computation)) { + (Equal, _) => ICmpPred::Eq, + (NotEqual, _) => ICmpPred::Ne, + (Less, true) => ICmpPred::Ult, + (LessEqual, true) => ICmpPred::Ule, + (Greater, true) => ICmpPred::Ugt, + (GreaterEqual, true) => ICmpPred::Uge, + (Less, false) => ICmpPred::Slt, + (LessEqual, false) => ICmpPred::Sle, + (Greater, false) => ICmpPred::Sgt, + (GreaterEqual, false) => ICmpPred::Sge, + _ => return Err(unsupported(format!("comparison {op:?}"))), + }; + return self.icmp(pred, lhs, rhs); + } + let op = match (op, float) { + (Add, false) => BinOp::Add, + (Subtract, false) => BinOp::Sub, + (Multiply, false) => BinOp::Mul, + (Add, true) => BinOp::FAdd, + (Subtract, true) => BinOp::FSub, + (Multiply, true) => BinOp::FMul, + (BitwiseAnd, false) => BinOp::And, + (BitwiseOr, false) => BinOp::Or, + (BitwiseXor, false) => BinOp::Xor, + _ => return Err(unsupported(format!("operation {op:?}"))), + }; + self.bin(op, ty, lhs, rhs) + } +} diff --git a/whale/src/codegen/mod.rs b/whale/src/codegen/mod.rs new file mode 100644 index 00000000..d2d642be --- /dev/null +++ b/whale/src/codegen/mod.rs @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Experimental Wave typed HIR -> Whale IR adapter. No LLVM fallback or ABI +//! inference belongs here. Unsupported Wave constructs fail before publication. +mod expression; +mod walk; + +use error::SourceSpan; +use hir::TypedProgram; +use parser::ast::{ASTNode, Expression, FunctionNode, Mutability, StatementNode, WaveType}; +use std::collections::HashMap; +use whale_ir::*; + +#[derive(Debug)] +pub struct LowerError { + pub message: String, + pub span: Option, +} +type Result = std::result::Result; + +fn unsupported(message: impl Into) -> LowerError { + LowerError { + message: format!("Whale backend does not yet support {}", message.into()), + span: None, + } +} +fn ice(message: impl Into) -> LowerError { + LowerError { + message: format!( + "internal compiler error in Whale lowering: {}", + message.into() + ), + span: None, + } +} +fn scalar(ty: &WaveType) -> Result { + Ok(match ty { + WaveType::Int(8) => Type::I8, + WaveType::Int(16) => Type::I16, + WaveType::Int(32) => Type::I32, + WaveType::Int(64) => Type::I64, + WaveType::Int(128) => Type::I128, + WaveType::Uint(8) | WaveType::Byte => Type::U8, + WaveType::Uint(16) => Type::U16, + WaveType::Uint(32) => Type::U32, + WaveType::Uint(64) => Type::U64, + WaveType::Uint(128) => Type::U128, + WaveType::Float(32) => Type::F32, + WaveType::Float(64) => Type::F64, + WaveType::Bool => Type::Bool, + WaveType::Void => Type::Void, + _ => return Err(unsupported(format!("type {ty:?}"))), + }) +} + +pub(crate) fn lower(program: &TypedProgram) -> Result { + program.verify_conversions().map_err(|e| LowerError { + message: format!( + "internal compiler error: invalid HIR conversions: {}", + e.message + ), + span: e.span, + })?; + let mut module = Module::new("x86_64-whale-linux", DataLayout::default_64bit_le()); + let mut functions = HashMap::new(); + for node in program.syntax() { + let result = (|| { + let ASTNode::Function(f) = node.unspanned() else { + return Err(unsupported("non-function top-level declarations")); + }; + if f.is_async || f.export.is_some() { + return Err(unsupported("async or exported functions")); + } + let id = FunctionId(module.declarations.len() as u32); + let signature = FunctionSignature::whale( + f.parameters + .iter() + .map(|p| scalar(&p.param_type)) + .collect::>()?, + scalar(f.return_type.as_ref().unwrap_or(&WaveType::Void))?, + ); + functions.insert(f.name.clone(), (id, signature.clone())); + module.declarations.push(FunctionDecl { + id, + name: f.name.clone(), + signature, + linkage: Linkage::Internal, + link_name: None, + }); + Ok(()) + })(); + result.map_err(|mut e: LowerError| { + e.span = program + .node_id(node) + .and_then(|id| program.node_span(id)) + .cloned() + .or_else(|| node.span().cloned()); + e + })?; + } + for node in program.syntax() { + let ASTNode::Function(f) = node.unspanned() else { + unreachable!() + }; + let function = Lowerer::function(program, &functions, f).map_err(|mut e| { + if e.span.is_none() { + e.span = f.span.clone().or_else(|| node.span().cloned()); + } + e + })?; + module.functions.push(function); + } + verify_module(&module).map_err(|e| ice(format!("Whale IR verification failed: {e:?}")))?; + Ok(module) +} + +#[derive(Clone)] +struct Slot { + ptr: ValueId, + ty: Type, +} +#[derive(Clone)] +struct Value { + id: ValueId, + ty: Type, +} +struct Lowerer<'a> { + program: &'a TypedProgram, + functions: &'a HashMap, + function: Function, + block: usize, + scopes: Vec>, + loops: Vec<(BlockId, BlockId)>, +} +impl<'a> Lowerer<'a> { + fn function( + program: &'a TypedProgram, + functions: &'a HashMap, + source: &FunctionNode, + ) -> Result { + let (id, signature) = &functions[&source.name]; + let mut this = Self { + program, + functions, + function: Function { + id: *id, + name: source.name.clone(), + params: vec![], + ret_ty: signature.ret.clone(), + blocks: vec![BasicBlock::new(BlockId(0), "entry")], + entry: BlockId(0), + value_types: vec![], + }, + block: 0, + scopes: vec![HashMap::new()], + loops: vec![], + }; + for (param, ty) in source.parameters.iter().zip(&signature.params) { + let value = this.value(ty.clone()); + this.function.params.push(Param { + name: param.name.clone(), + id: value.id, + ty: ty.clone(), + }); + this.bind(¶m.name, ty.clone(), Some(value))?; + } + this.body(&source.body)?; + if !this.terminated() { + if this.function.ret_ty != Type::Void { + return Err(unsupported( + "implicit non-void returns; add an explicit return", + )); + } + this.terminate(Terminator::Ret { + ty: Type::Void, + value: None, + }); + } + Ok(this.function) + } + fn value(&mut self, ty: Type) -> Value { + let id = ValueId(self.function.value_types.len() as u32); + self.function.value_types.push((id, ty.clone())); + Value { id, ty } + } + fn emit(&mut self, instruction: Instruction) { + self.function.blocks[self.block] + .instructions + .push(instruction); + } + fn terminate(&mut self, terminator: Terminator) { + self.function.blocks[self.block].terminator = Some(terminator); + } + fn terminated(&self) -> bool { + self.function.blocks[self.block].is_terminated() + } + fn new_block(&mut self, name: &str) -> BlockId { + let id = BlockId(self.function.blocks.len() as u32); + self.function.blocks.push(BasicBlock::new(id, name)); + id + } + fn switch(&mut self, block: BlockId) { + self.block = block.0 as usize; + } + fn branch_if_open(&mut self, target: BlockId) { + if !self.terminated() { + self.terminate(Terminator::Br { target }); + } + } + fn slot(&self, name: &str) -> Result { + self.scopes + .iter() + .rev() + .find_map(|scope| scope.get(name)) + .cloned() + .ok_or_else(|| unsupported(format!("binding `{name}`"))) + } + fn bind(&mut self, name: &str, ty: Type, init: Option) -> Result<()> { + let ptr = self.value(Type::ptr_to(ty.clone())).id; + // All local storage belongs to the entry block, including loop locals. + self.function.blocks[0] + .instructions + .push(Instruction::Alloca { + dst: ptr, + ty: ty.clone(), + align: 1, + }); + let slot = Slot { ptr, ty }; + if let Some(value) = init { + self.store(&slot, value)?; + } + self.scopes.last_mut().unwrap().insert(name.into(), slot); + Ok(()) + } + fn store(&mut self, slot: &Slot, value: Value) -> Result<()> { + if value.ty != slot.ty { + return Err(ice("store disagrees with HIR destination type")); + } + self.emit(Instruction::Store { + ty: slot.ty.clone(), + value: value.id, + ptr: slot.ptr, + align: 1, + }); + Ok(()) + } + fn body(&mut self, body: &[ASTNode]) -> Result<()> { + self.scopes.push(HashMap::new()); + for node in body { + if self.terminated() { + break; + } + self.node(node).map_err(|mut e| { + if e.span.is_none() { + e.span = self + .program + .node_id(node) + .and_then(|id| self.program.node_span(id)) + .cloned() + .or_else(|| node.span().cloned()); + } + e + })?; + } + self.scopes.pop(); + Ok(()) + } + fn node(&mut self, node: &ASTNode) -> Result<()> { + match node.unspanned() { + ASTNode::Variable(v) => { + if v.mutability == Mutability::Static { + return Err(unsupported("static local storage")); + } + let ty = scalar(&v.type_name)?; + // Evaluate before binding: a shadowing initializer sees the outer scope. + let init = v.initial_value.as_ref().map(|e| self.expr(e)).transpose()?; + if init.is_none() { + return Err(unsupported("uninitialized locals")); + } + self.bind(&v.name, ty, init) + } + ASTNode::Expression(e) | ASTNode::Statement(StatementNode::Expression(e)) => { + self.effect(e) + } + ASTNode::Statement(StatementNode::Assign { variable, value }) => { + let value = self.expr(value)?; + self.store(&self.slot(variable)?, value) + } + ASTNode::Statement(StatementNode::Return(expr)) => { + let value = expr.as_ref().map(|e| self.expr(e)).transpose()?; + if value.as_ref().map(|v| &v.ty).unwrap_or(&Type::Void) != &self.function.ret_ty { + return Err(ice("return disagrees with HIR signature")); + } + self.terminate(Terminator::Ret { + ty: self.function.ret_ty.clone(), + value: value.map(|v| v.id), + }); + Ok(()) + } + ASTNode::Statement(StatementNode::If { + condition, + body, + else_if_blocks, + else_block, + }) => { + let mut branches = vec![(condition, body.as_slice())]; + if let Some(blocks) = else_if_blocks { + branches.extend(blocks.iter().map(|(c, b)| (c, b.as_slice()))); + } + let join = self.new_block("if.end"); + let mut falls_through = false; + for (condition, body) in branches { + let cond = self.condition(condition)?; + let then_bb = self.new_block("if.then"); + let else_bb = self.new_block("if.else"); + self.terminate(Terminator::CBr { + cond, + then_bb, + else_bb, + }); + self.switch(then_bb); + self.body(body)?; + falls_through |= !self.terminated(); + self.branch_if_open(join); + self.switch(else_bb); + } + if let Some(body) = else_block { + self.body(body)?; + } + falls_through |= !self.terminated(); + self.branch_if_open(join); + self.switch(join); + if !falls_through { + self.terminate(Terminator::Trap { + reason: "unreachable if continuation".into(), + }); + } + Ok(()) + } + ASTNode::Statement(StatementNode::While { condition, body }) => { + let test = self.new_block("while.test"); + let loop_body = self.new_block("while.body"); + let end = self.new_block("while.end"); + self.terminate(Terminator::Br { target: test }); + self.switch(test); + let cond = self.condition(condition)?; + self.terminate(Terminator::CBr { + cond, + then_bb: loop_body, + else_bb: end, + }); + self.switch(loop_body); + self.loops.push((test, end)); + self.body(body)?; + self.loops.pop(); + self.branch_if_open(test); + self.switch(end); + Ok(()) + } + ASTNode::Statement(statement @ (StatementNode::Break | StatementNode::Continue)) => { + let &(test, end) = self + .loops + .last() + .ok_or_else(|| ice("loop control outside loop"))?; + self.terminate(Terminator::Br { + target: if matches!(statement, StatementNode::Break) { + end + } else { + test + }, + }); + Ok(()) + } + _ => Err(unsupported("this statement")), + } + } + fn condition(&mut self, e: &Expression) -> Result { + let value = self.expr(e)?; + if value.ty != Type::Bool { + return Err(ice("condition missing HIR bool conversion")); + } + Ok(value.id) + } +} diff --git a/whale/src/codegen/walk.rs b/whale/src/codegen/walk.rs new file mode 100644 index 00000000..944675fc --- /dev/null +++ b/whale/src/codegen/walk.rs @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Iterative, source-ordered expression lowering. Even the maximum permitted +//! nesting depth must work on a 1 MiB stack in debug builds. +use super::*; +use parser::ast::{AssignOperator, Operator}; + +enum Action { + Enter, + Convert, + Unary, + Binary, + Assign, + Call, + LogicalLeft, + LogicalRight { + lhs: ValueId, + lhs_block: BlockId, + end: BlockId, + }, +} +fn pop(values: &mut Vec) -> Result { + values + .pop() + .ok_or_else(|| ice("expression value stack underflow")) +} + +impl Lowerer<'_> { + pub(super) fn expression(&mut self, root: &Expression) -> Result { + let mut pending = vec![(root, Action::Enter)]; + let mut values = Vec::new(); + while let Some((expr, action)) = pending.pop() { + let result = self.step(expr, action, &mut pending, &mut values); + result.map_err(|mut e| { + if e.span.is_none() { + e.span = self + .program + .expression_id(expr) + .and_then(|id| self.program.expression_span(id)) + .cloned(); + } + e + })?; + } + if values.len() != 1 { + return Err(ice("expression value stack imbalance")); + } + pop(&mut values) + } + fn step<'e>( + &mut self, + expr: &'e Expression, + action: Action, + pending: &mut Vec<(&'e Expression, Action)>, + values: &mut Vec, + ) -> Result<()> { + let program = self.program; + let fact = program + .numeric_expression_of(expr) + .ok_or_else(|| unsupported("non-scalar expressions"))?; + let ty = scalar(&fact.evaluation_type)?; + match action { + Action::Enter => { + pending.push((expr, Action::Convert)); + match expr.unspanned() { + Expression::Literal(literal) => { + values.push(self.literal(literal, &fact.evaluation_type)?) + } + Expression::Variable(name) => { + let slot = self.slot(name)?; + let value = self.value(slot.ty.clone()); + self.emit(Instruction::Load { + dst: value.id, + ty: slot.ty, + ptr: slot.ptr, + align: 1, + }); + values.push(value); + } + Expression::Cast { expr, .. } | Expression::Grouped(expr) => { + pending.push((expr, Action::Enter)) + } + Expression::Assignment { target, value } + | Expression::AssignOperation { + target, + operator: AssignOperator::Assign, + value, + } => { + if !matches!(target.unspanned(), Expression::Variable(_)) { + return Err(unsupported("non-local assignment")); + } + pending.push((expr, Action::Assign)); + pending.push((value, Action::Enter)); + } + Expression::FunctionCall { args, .. } => { + pending.push((expr, Action::Call)); + for arg in args.iter().rev() { + pending.push((arg, Action::Enter)); + } + } + Expression::Unary { expr: operand, .. } => { + pending.push((expr, Action::Unary)); + pending.push((operand, Action::Enter)); + } + Expression::BinaryExpression { + left, + operator, + right, + } => { + if matches!(operator, Operator::LogicalAnd | Operator::LogicalOr) { + pending.push((expr, Action::LogicalLeft)); + } else { + pending.push((expr, Action::Binary)); + pending.push((right, Action::Enter)); + } + pending.push((left, Action::Enter)); + } + _ => return Err(unsupported("this expression")), + } + } + Action::Convert => { + let mut value = pop(values)?; + if value.ty != ty { + return Err(ice(format!( + "expression produced {} but HIR requires {ty}", + value.ty + ))); + } + for conversion in &fact.conversions { + value = self.convert(value, conversion).map_err(|mut e| { + e.span = Some(conversion.span.clone()); + e + })?; + } + values.push(value); + } + Action::Unary => { + let Expression::Unary { operator, .. } = expr.unspanned() else { + unreachable!() + }; + let operand = pop(values)?; + values.push(self.unary_value(operator, operand, fact)?); + } + Action::Binary => { + let Expression::BinaryExpression { operator, .. } = expr.unspanned() else { + unreachable!() + }; + let rhs = pop(values)?; + let lhs = pop(values)?; + let computation = fact + .computation_type + .as_ref() + .ok_or_else(|| ice("missing computation type"))?; + values.push(self.binary_values(lhs, operator, rhs, computation, &ty)?); + } + Action::Assign => { + let (Expression::Assignment { target, .. } + | Expression::AssignOperation { target, .. }) = expr.unspanned() + else { + unreachable!() + }; + let Expression::Variable(name) = target.unspanned() else { + unreachable!() + }; + let value = pop(values)?; + self.store(&self.slot(name)?, value.clone())?; + values.push(value); + } + Action::Call => { + let Expression::FunctionCall { name, args, .. } = expr.unspanned() else { + unreachable!() + }; + let start = values + .len() + .checked_sub(args.len()) + .ok_or_else(|| ice("call value stack underflow"))?; + let args = values.split_off(start); + values.push( + self.call_values(name, args)? + .ok_or_else(|| unsupported("void calls in value expressions"))?, + ); + } + Action::LogicalLeft => { + let Expression::BinaryExpression { + operator, right, .. + } = expr.unspanned() + else { + unreachable!() + }; + let lhs = pop(values)?; + if lhs.ty != Type::Bool { + return Err(ice("logical operand missing HIR bool conversion")); + } + let lhs_block = BlockId(self.block as u32); + let rhs = self.new_block("logical.rhs"); + let end = self.new_block("logical.end"); + let (then_bb, else_bb) = if matches!(operator, Operator::LogicalAnd) { + (rhs, end) + } else { + (end, rhs) + }; + self.terminate(Terminator::CBr { + cond: lhs.id, + then_bb, + else_bb, + }); + self.switch(rhs); + pending.push(( + expr, + Action::LogicalRight { + lhs: lhs.id, + lhs_block, + end, + }, + )); + pending.push((right, Action::Enter)); + } + Action::LogicalRight { + lhs, + lhs_block, + end, + } => { + let rhs = pop(values)?; + if rhs.ty != Type::Bool { + return Err(ice("logical operand missing HIR bool conversion")); + } + let rhs_exit = BlockId(self.block as u32); + self.terminate(Terminator::Br { target: end }); + self.switch(end); + let value = self.value(Type::Bool); + self.emit(Instruction::Phi { + dst: value.id, + ty: Type::Bool, + incomings: vec![(lhs, lhs_block), (rhs.id, rhs_exit)], + }); + values.push(value); + } + } + Ok(()) + } +} diff --git a/whale/src/lib.rs b/whale/src/lib.rs new file mode 100644 index 00000000..345179ef --- /dev/null +++ b/whale/src/lib.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Whale backend for Wave, alongside the LLVM backend crate. +//! +//! This crate owns typed HIR lowering, Whale IR verification and serialization, +//! and the upstream Whale dependency. CLI dispatch and source preparation belong +//! to the compiler driver. + +// Source diagnostics retain their complete span at the backend boundary. +#![allow(clippy::result_large_err)] + +mod codegen; + +pub use codegen::LowerError; + +/// Lower verified Wave HIR to verified textual Whale IR. +/// +/// Unsupported constructs return a source diagnostic. This backend never +/// delegates lowering to LLVM. +pub fn emit_ir(program: &hir::TypedProgram) -> Result { + codegen::lower(program).map(|module| whale_ir::print_module(&module)) +}