From 47a7aa2abbafd82aa84a87d4cb2fbb1cb8839761 Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Mon, 23 Jun 2025 13:29:15 +0200 Subject: [PATCH 01/16] chore: add llvm to dev env --- jargon.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 jargon.txt diff --git a/jargon.txt b/jargon.txt new file mode 100644 index 000000000..492f1de04 --- /dev/null +++ b/jargon.txt @@ -0,0 +1,3 @@ +dev +env +llvm \ No newline at end of file From 4d5b3db26f1cbbc665ed1675fd9cd7992980f84a Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Mon, 23 Jun 2025 15:12:06 +0200 Subject: [PATCH 02/16] chore: add llvm bindings, to be used in core --- Cargo.lock | 109 +++++++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 28 ++++++++------ flake.nix | 2 +- jargon.txt | 3 -- 4 files changed, 118 insertions(+), 24 deletions(-) delete mode 100644 jargon.txt diff --git a/Cargo.lock b/Cargo.lock index c48dbc2fb..7f38eeb7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "ansi_term" @@ -61,6 +61,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "assert_cmd" version = "0.10.2" @@ -102,6 +108,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f861d9ce359f56dbcb6e0c2a1cb84e52ad732cadb57b806adeb3c7668caccbd8" +[[package]] +name = "cc" +version = "1.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -225,6 +240,31 @@ dependencies = [ "libc", ] +[[package]] +name = "inkwell" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67349bd7578d4afebbe15eaa642a80b884e8623db74b1716611b131feb1deef" +dependencies = [ + "either", + "inkwell_internals", + "libc", + "llvm-sys", + "once_cell", + "thiserror", +] + +[[package]] +name = "inkwell_internals" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f365c8de536236cfdebd0ba2130de22acefed18b1fb99c32783b3840aec5fb46" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -246,6 +286,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.174" @@ -258,6 +304,20 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "llvm-sys" +version = "181.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320f9d2723c97d4b78f9190a61ed25cc7cfbe456668c08e6e7dd8e50ceb8500" +dependencies = [ + "anyhow", + "cc", + "lazy_static", + "libc", + "regex-lite", + "semver", +] + [[package]] name = "log" version = "0.4.27" @@ -283,12 +343,13 @@ dependencies = [ "assert_cmd", "clap", "glob", + "inkwell", "itertools", "log", "loggerv", "pathdiff", - "permutate", "python-parser", + "tempfile", "test-case", "tests_util", ] @@ -367,12 +428,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3bf70094d203e07844da868b634207e71bfab254fe713171fae9a6e751ccf31" -[[package]] -name = "permutate" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b7d5b19a715ffab38693a9dd44b067fdfa2b18eef65bd93562dfe507022fae" - [[package]] name = "predicates" version = "1.0.8" @@ -438,6 +493,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "rustix" version = "1.0.7" @@ -457,6 +518,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" @@ -489,6 +556,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "strsim" version = "0.11.1" @@ -568,6 +641,26 @@ dependencies = [ "tempfile", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index f250973dd..25391864b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,19 +12,23 @@ repository = "https://github.com/JSAbrahams/mamba" keywords = ["python", "transpiler", "compiler"] categories = ["parsing"] +[features] +default = ["machine-code"] +machine-code = ["inkwell"] + [dependencies] -ansi_term = "0.12.1" -assert_cmd = "0.10" -clap = { version = "4.5.40", features = ["derive"] } -glob = "0.3.0" -itertools = "0.8.2" -log = "0.4" -loggerv = "0.7" -pathdiff = "0.1.0" -permutate = "0.3.2" -python-parser = "0.1.0" +ansi_term = "0.12.1" # Colors in the terminal +clap = { version = "4.5.40", features = ["derive"] } # Compiler frontend +glob = "0.3.0" # Make traversing directories (Mamba projects) easier +inkwell = { version = "0.6.0", features = ["llvm18-1"], optional = true } # Relevant for generating machine code +itertools = "0.8.2" # Tools to make iterating over collections easier +log = "0.4" # Log frontend +loggerv = "0.7" # Logger which accepts -vvv flag +pathdiff = "0.1.0" # Traverse directories (Mamba projects) +python-parser = "0.1.0" # Parse Python, both in tests and to generate Mamba IR [dev-dependencies] -test-case = "3.3.1" -# dependencies: test -> test util -> mamba +assert_cmd = "0.10" # Assert commands work +tempfile = "3.1.0" # Create temporary files for intermediate results +test-case = "3.3.1" # Parameterize tests tests_util = { path = "./tests_util" } diff --git a/flake.nix b/flake.nix index 4c0863d47..98a5865ca 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,7 @@ git # Version control tool less # Used under the hood by git more # Nice to have next to 'less' - + clang # C++ tooling llvmPackages.bintools # llvmPackages_latest.llvm # LLVM build tools (also provides llvm-cov/llvm-profdata, see LLVM_COV below) diff --git a/jargon.txt b/jargon.txt deleted file mode 100644 index 492f1de04..000000000 --- a/jargon.txt +++ /dev/null @@ -1,3 +0,0 @@ -dev -env -llvm \ No newline at end of file From 00735c2d0cc432e3193c0eb944662bc273574ebd Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Tue, 25 Aug 2026 16:50:30 +0200 Subject: [PATCH 03/16] chore: specify backend, python --- src/backend/mod.rs | 15 + src/{generate => backend/python}/README.md | 6 +- src/{generate => backend/python}/ast/mod.rs | 188 ++++++------ src/{generate => backend/python}/ast/node.rs | 232 +++++++-------- .../python}/convert/builder.rs | 20 +- .../python}/convert/call.rs | 16 +- .../python}/convert/class.rs | 262 ++++++++-------- .../python}/convert/common.rs | 10 +- .../python}/convert/control_flow.rs | 92 +++--- .../python}/convert/definition.rs | 142 ++++----- .../python}/convert/handle.rs | 22 +- .../python}/convert/mod.rs | 279 +++++++++--------- .../python}/convert/range_slice.rs | 28 +- .../python}/convert/state.rs | 36 +-- src/{generate => backend/python}/mod.rs | 30 +- src/{generate => backend/python}/name.rs | 18 +- src/{generate => backend/python}/result.rs | 4 +- src/lib.rs | 5 +- tests_util/src/lib.rs | 5 +- 19 files changed, 724 insertions(+), 686 deletions(-) create mode 100644 src/backend/mod.rs rename src/{generate => backend/python}/README.md (78%) rename src/{generate => backend/python}/ast/mod.rs (66%) rename src/{generate => backend/python}/ast/node.rs (57%) rename src/{generate => backend/python}/convert/builder.rs (80%) rename src/{generate => backend/python}/convert/call.rs (56%) rename src/{generate => backend/python}/convert/class.rs (66%) rename src/{generate => backend/python}/convert/common.rs (52%) rename src/{generate => backend/python}/convert/control_flow.rs (84%) rename src/{generate => backend/python}/convert/definition.rs (81%) rename src/{generate => backend/python}/convert/handle.rs (82%) rename src/{generate => backend/python}/convert/mod.rs (75%) rename src/{generate => backend/python}/convert/range_slice.rs (74%) rename src/{generate => backend/python}/convert/state.rs (82%) rename src/{generate => backend/python}/mod.rs (71%) rename src/{generate => backend/python}/name.rs (84%) rename src/{generate => backend/python}/result.rs (91%) diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 000000000..6455f5038 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,15 @@ +pub mod cranelift; +pub mod python; + +/// Which backend the pipeline should target. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum Backend { + /// Transpile to Python source (the original, default behavior). + #[default] + Python, + /// Compile and link a native executable via the Cranelift backend. + /// + /// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`) passed on to + /// Cranelift; if `None`, the host triple is used. + Bin { target: Option }, +} diff --git a/src/generate/README.md b/src/backend/python/README.md similarity index 78% rename from src/generate/README.md rename to src/backend/python/README.md index 375ecd07c..39aa52cd9 100644 --- a/src/generate/README.md +++ b/src/backend/python/README.md @@ -12,7 +12,7 @@ Converts `AST` to Python code. Errors if: ## Convert -First step, converts `AST` to a simpler internal `Core` representation, which is closer to the Python language. +First step, converts `AST` to a simpler internal `PythonCore` representation, which is closer to the Python language. ### State And Imports @@ -22,7 +22,7 @@ traverses the AST. This is to desugar certain language constructs in a certain m Imports are also kept track of and added to the final output. This includes Mamba built-in types which have to be explicitly imported in Python. I.e. tuples are imported as `from typing import Tuples` when one annotates the output. -## Core +## PythonCore -A set of simple `Core` nodes, which are very close to Python constructs. These may almost directly be converted to a +A set of simple `PythonCore` nodes, which are very close to Python constructs. These may almost directly be converted to a Python string. This step also keeps track of code blocks and relevant indentation and dedents. diff --git a/src/generate/ast/mod.rs b/src/backend/python/ast/mod.rs similarity index 66% rename from src/generate/ast/mod.rs rename to src/backend/python/ast/mod.rs index e623c0d5a..068456359 100644 --- a/src/generate/ast/mod.rs +++ b/src/backend/python/ast/mod.rs @@ -1,18 +1,18 @@ use std::fmt::{Display, Formatter, Write}; +use crate::backend::python::ast::node::PythonCore; use crate::common::delimit::{comma_delm, custom_delimited}; -use crate::generate::ast::node::Core; pub mod node; pub const IND_SPACES: usize = 4; -impl Display for Core { - /// Convert [Core](mamba::generate.ast::construct::Core) to a String which represent +impl Display for PythonCore { + /// Convert [PythonCore](mamba::backend::python::ast::node::PythonCore) to a String which represent /// python source code. /// - /// Takes [Core](mamba::generate.ast::construct::Core) nodes as-is, meaning that this - /// should never panic, unless a certain generate.ast construct can still not be + /// Takes [PythonCore](mamba::backend::python::ast::node::PythonCore) nodes as-is, meaning that this + /// should never panic, unless a certain backend::python::ast construct can still not be /// converted. /// /// # Examples @@ -20,8 +20,8 @@ impl Display for Core { /// Writing a Return statement: /// /// ``` - /// # use mamba::generate::ast::node::Core; - /// let core_node = Core::Return { expr: Box::from(Core::None) }; + /// # use mamba::backend::python::ast::node::PythonCore; + /// let core_node = PythonCore::Return { expr: Box::from(PythonCore::None) }; /// let py_source = format!("{core_node}"); /// /// assert_eq!(py_source, "return None\n"); @@ -30,11 +30,11 @@ impl Display for Core { /// Writing an If statement: /// /// ``` - /// # use mamba::generate::ast::node::Core; - /// let core_node = Core::IfElse { - /// cond: Box::from(Core::Id { lit: String::from("a") }), - /// then: Box::from(Core::Str { string: String::from("b") }), - /// el: Box::from(Core::Str { string: String::from("c") }) + /// # use mamba::backend::python::ast::node::PythonCore; + /// let core_node = PythonCore::IfElse { + /// cond: Box::from(PythonCore::Id { lit: String::from("a") }), + /// then: Box::from(PythonCore::Str { string: String::from("b") }), + /// el: Box::from(PythonCore::Str { string: String::from("c") }) /// }; /// /// assert_eq!(format!("{core_node}"), "if a: \n \"b\"\nelse: \n \"c\"\n"); @@ -44,9 +44,9 @@ impl Display for Core { } } -fn to_py(core: &Core, ind: usize) -> String { +fn to_py(core: &PythonCore, ind: usize) -> String { match core { - Core::Import { + PythonCore::Import { from, import, alias, @@ -64,28 +64,30 @@ fn to_py(core: &Core, ind: usize) -> String { String::from("") } ), - Core::Id { lit } => lit.clone(), - Core::Type { lit, generics } => { + PythonCore::Id { lit } => lit.clone(), + PythonCore::Type { lit, generics } => { if generics.is_empty() { lit.clone() } else { format!("{}[{}]", lit, comma_delimited(generics, ind)) } } - Core::ExpressionType { expr, ty } => format!("{}: {}", to_py(expr, ind), to_py(ty, ind)), - Core::DocStr { string } => format!("\"\"\"{string}\"\"\""), - Core::Str { string } => format!("\"{string}\""), - Core::FStr { string } => format!("f\"{string}\""), - Core::Int { int } => int.clone(), - Core::ENum { num, exp } => format!("({num} * 10 ** {exp})"), - Core::Float { float } => float.clone(), - Core::Bool { boolean } => String::from(if *boolean { "True" } else { "False" }), + PythonCore::ExpressionType { expr, ty } => { + format!("{}: {}", to_py(expr, ind), to_py(ty, ind)) + } + PythonCore::DocStr { string } => format!("\"\"\"{string}\"\"\""), + PythonCore::Str { string } => format!("\"{string}\""), + PythonCore::FStr { string } => format!("f\"{string}\""), + PythonCore::Int { int } => int.clone(), + PythonCore::ENum { num, exp } => format!("({num} * 10 ** {exp})"), + PythonCore::Float { float } => float.clone(), + PythonCore::Bool { boolean } => String::from(if *boolean { "True" } else { "False" }), - Core::FunDefOp { op, arg, ty, body } => { + PythonCore::FunDefOp { op, arg, ty, body } => { let id = format!("{op}"); let dec = vec![]; to_py( - &Core::FunDef { + &PythonCore::FunDef { dec, id, arg: arg.clone(), @@ -95,16 +97,16 @@ fn to_py(core: &Core, ind: usize) -> String { ind, ) } - Core::FunDef { + PythonCore::FunDef { dec, id, arg, ty, body, } => { - let dec: Vec = dec + let dec: Vec = dec .iter() - .map(|d| Core::Id { + .map(|d| PythonCore::Id { lit: format!("@{d}"), }) .collect(); @@ -130,10 +132,10 @@ fn to_py(core: &Core, ind: usize) -> String { ) } - Core::Assign { left, right, op } => { + PythonCore::Assign { left, right, op } => { format!("{} {op} {}", to_py(left, ind), to_py(right, ind)) } - Core::VarDef { var, expr, ty } => format!( + PythonCore::VarDef { var, expr, ty } => format!( "{}{} = {}", to_py(var, ind), if let Some(ty) = ty { @@ -148,7 +150,7 @@ fn to_py(core: &Core, ind: usize) -> String { } ), - Core::FunArg { + PythonCore::FunArg { vararg, var, ty, @@ -169,7 +171,7 @@ fn to_py(core: &Core, ind: usize) -> String { } ), - Core::AnonFun { args, body } => format!( + PythonCore::AnonFun { args, body } => format!( "lambda{}: {}", if args.is_empty() { String::new() @@ -179,16 +181,16 @@ fn to_py(core: &Core, ind: usize) -> String { to_py(body, ind) ), - Core::Block { statements } => newline_delimited(statements, ind), + PythonCore::Block { statements } => newline_delimited(statements, ind), - Core::PropertyCall { object, property } => { + PythonCore::PropertyCall { object, property } => { format!("{}.{}", to_py(object, ind), to_py(property, ind)) } - Core::FunctionCall { function, args } => { + PythonCore::FunctionCall { function, args } => { format!("{}({})", to_py(function, ind), comma_delimited(args, ind)) } - Core::DictComprehension { + PythonCore::DictComprehension { from, to, col, @@ -201,7 +203,7 @@ fn to_py(core: &Core, ind: usize) -> String { to_py(col, ind) ) } - Core::DictComprehension { + PythonCore::DictComprehension { from, to, col, @@ -216,10 +218,10 @@ fn to_py(core: &Core, ind: usize) -> String { custom_delimited(conds, " and ", "") ) } - Core::Comprehension { expr, col, conds } if conds.is_empty() => { + PythonCore::Comprehension { expr, col, conds } if conds.is_empty() => { format!("{} for {}", to_py(expr, ind), to_py(col, ind)) } - Core::Comprehension { expr, col, conds } => { + PythonCore::Comprehension { expr, col, conds } => { let conds: Vec = conds.iter().map(|cond| to_py(cond, ind)).collect(); format!( "{} for {} if {}", @@ -229,54 +231,56 @@ fn to_py(core: &Core, ind: usize) -> String { ) } - Core::Tuple { elements } => format!("({})", comma_delimited(elements, ind)), - Core::TupleLiteral { elements } => comma_delimited(elements, ind), - Core::Dictionary { elements } => { + PythonCore::Tuple { elements } => format!("({})", comma_delimited(elements, ind)), + PythonCore::TupleLiteral { elements } => comma_delimited(elements, ind), + PythonCore::Dictionary { elements } => { let elements: Vec = elements .iter() .map(|(from, to)| format!("{}: {}", to_py(from, ind), to_py(to, ind))) .collect(); format!("{{{}}}", comma_delm(elements)) } - Core::Set { elements } => format!("{{{}}}", comma_delimited(elements, ind)), - Core::List { elements } => format!("[{}]", comma_delimited(elements, ind)), + PythonCore::Set { elements } => format!("{{{}}}", comma_delimited(elements, ind)), + PythonCore::List { elements } => format!("[{}]", comma_delimited(elements, ind)), - Core::Match { expr, cases } => { + PythonCore::Match { expr, cases } => { format!( "match {}:\n{}", to_py(expr, ind), newline_delimited(cases, ind + 1) ) } - Core::Case { expr, body } => { + PythonCore::Case { expr, body } => { format!("case {}: {}", to_py(expr, ind), newline_if_body(body, ind)) } - Core::KeyValue { key, value } => format!("{}: {}", to_py(key, ind), to_py(value, ind)), + PythonCore::KeyValue { key, value } => { + format!("{}: {}", to_py(key, ind), to_py(value, ind)) + } - Core::UnderScore => String::from("_"), + PythonCore::UnderScore => String::from("_"), - Core::Ge { left, right } => { + PythonCore::Ge { left, right } => { format!( "{} > {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Geq { left, right } => { + PythonCore::Geq { left, right } => { format!( "{} >= {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Le { left, right } => { + PythonCore::Le { left, right } => { format!( "{} < {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Leq { left, right } => { + PythonCore::Leq { left, right } => { format!( "{} <= {}", to_py(left.as_ref(), ind), @@ -284,129 +288,129 @@ fn to_py(core: &Core, ind: usize) -> String { ) } - Core::Not { expr } => format!("not {}", to_py(expr.as_ref(), ind)), - Core::And { left, right } => { + PythonCore::Not { expr } => format!("not {}", to_py(expr.as_ref(), ind)), + PythonCore::And { left, right } => { format!( "{} and {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Or { left, right } => { + PythonCore::Or { left, right } => { format!( "{} or {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Eq { left, right } => { + PythonCore::Eq { left, right } => { format!( "{} == {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Neq { left, right } => { + PythonCore::Neq { left, right } => { format!( "{} != {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::AddU { expr } => format!("+{}", to_py(expr, ind)), - Core::Add { left, right } => { + PythonCore::AddU { expr } => format!("+{}", to_py(expr, ind)), + PythonCore::Add { left, right } => { format!( "{} + {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::SubU { expr } => format!("-{}", to_py(expr, ind)), - Core::Sub { left, right } => { + PythonCore::SubU { expr } => format!("-{}", to_py(expr, ind)), + PythonCore::Sub { left, right } => { format!( "{} - {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Mul { left, right } => { + PythonCore::Mul { left, right } => { format!( "{} * {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Div { left, right } => { + PythonCore::Div { left, right } => { format!( "{} / {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::FDiv { left, right } => { + PythonCore::FDiv { left, right } => { format!( "{} // {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Pow { left, right } => { + PythonCore::Pow { left, right } => { format!( "{} ** {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Mod { left, right } => { + PythonCore::Mod { left, right } => { format!( "{} % {}", to_py(left.as_ref(), ind), to_py(right.as_ref(), ind) ) } - Core::Sqrt { expr } => format!("math.sqrt({})", to_py(expr.as_ref(), ind)), + PythonCore::Sqrt { expr } => format!("math.sqrt({})", to_py(expr.as_ref(), ind)), - Core::Return { expr } => format!("return {}", to_py(expr.as_ref(), ind)), + PythonCore::Return { expr } => format!("return {}", to_py(expr.as_ref(), ind)), - Core::For { expr, col, body } => format!( + PythonCore::For { expr, col, body } => format!( "for {} in {}:{}", to_py(expr.as_ref(), ind), to_py(col.as_ref(), ind), newline_if_body(body, ind) ), - Core::In { left, right } => format! {"{} in {}", to_py(left, ind), to_py(right, ind)}, - Core::Index { item, range } => format!("{}[{}]", to_py(item, ind), to_py(range, ind)), - Core::If { cond, then } => { + PythonCore::In { left, right } => format! {"{} in {}", to_py(left, ind), to_py(right, ind)}, + PythonCore::Index { item, range } => format!("{}[{}]", to_py(item, ind), to_py(range, ind)), + PythonCore::If { cond, then } => { format!( "if {}:{}", to_py(cond.as_ref(), ind), newline_if_body(then, ind) ) } - Core::IfElse { cond, then, el } => format!( + PythonCore::IfElse { cond, then, el } => format!( "if {}: {}\n{}else: {}", to_py(cond.as_ref(), ind), newline_if_body(then, ind), indent(ind), newline_if_body(el, ind) ), - Core::Ternary { cond, then, el } => format!( + PythonCore::Ternary { cond, then, el } => format!( "{} if {} else {}", to_py(then.as_ref(), ind), to_py(cond.as_ref(), ind + 1), to_py(el.as_ref(), ind + 1) ), - Core::While { cond, body } => { + PythonCore::While { cond, body } => { format!( "while {}:{}", to_py(cond.as_ref(), ind), newline_if_body(body, ind) ) } - Core::Continue => String::from("continue"), - Core::Break => String::from("break"), + PythonCore::Continue => String::from("continue"), + PythonCore::Break => String::from("break"), - Core::ClassDef { + PythonCore::ClassDef { name, parent_names, body, @@ -421,18 +425,18 @@ fn to_py(core: &Core, ind: usize) -> String { newline_if_body(body, ind) ), - Core::Pass => String::from("pass"), - Core::None => String::from("None"), - Core::Empty => String::new(), + PythonCore::Pass => String::from("pass"), + PythonCore::None => String::from("None"), + PythonCore::Empty => String::new(), - Core::With { resource, expr } => { + PythonCore::With { resource, expr } => { format!( "with {}: {}", to_py(resource, ind), newline_if_body(expr, ind) ) } - Core::WithAs { + PythonCore::WithAs { resource, alias, expr, @@ -443,7 +447,7 @@ fn to_py(core: &Core, ind: usize) -> String { newline_if_body(expr, ind) ), - Core::TryExcept { + PythonCore::TryExcept { setup, attempt, except, @@ -457,18 +461,18 @@ fn to_py(core: &Core, ind: usize) -> String { newline_if_body(attempt, ind), newline_delimited(except, ind) ), - Core::ExceptId { id, class, body } => { + PythonCore::ExceptId { id, class, body } => { let (id, class) = (to_py(id, ind), to_py(class, ind)); let body = newline_if_body(body, ind); format!("except {class} as {id}: {body}") } - Core::Except { class, body } => { + PythonCore::Except { class, body } => { let class = to_py(class, ind); let body = newline_if_body(body, ind); format!("except {class}: {body}") } - Core::Raise { error } => format!("raise {}", to_py(error, ind)), + PythonCore::Raise { error } => format!("raise {}", to_py(error, ind)), } } @@ -476,14 +480,14 @@ fn indent(amount: usize) -> String { " ".repeat(IND_SPACES * amount) } -fn newline_if_body(core: &Core, ind: usize) -> String { +fn newline_if_body(core: &PythonCore, ind: usize) -> String { match core { - Core::Block { .. } => format!("\n{}", to_py(core, ind + 1)), + PythonCore::Block { .. } => format!("\n{}", to_py(core, ind + 1)), _ => format!("\n{}{}", indent(ind + 1), to_py(core, ind + 1)), } } -fn newline_delimited(items: &[Core], ind: usize) -> String { +fn newline_delimited(items: &[PythonCore], ind: usize) -> String { let mut s = String::new(); items .iter() @@ -491,7 +495,7 @@ fn newline_delimited(items: &[Core], ind: usize) -> String { s } -fn comma_delimited(items: &[Core], ind: usize) -> String { +fn comma_delimited(items: &[PythonCore], ind: usize) -> String { let mut s = String::new(); items .iter() diff --git a/src/generate/ast/node.rs b/src/backend/python/ast/node.rs similarity index 57% rename from src/generate/ast/node.rs rename to src/backend/python/ast/node.rs index 503511a90..079ae0dd2 100644 --- a/src/generate/ast/node.rs +++ b/src/backend/python/ast/node.rs @@ -1,77 +1,77 @@ use std::convert::TryFrom; use std::fmt::{Display, Formatter}; +use crate::backend::python::result::UnimplementedErr; use crate::check::context::function; -use crate::generate::result::UnimplementedErr; use crate::parse::ast::node_op::NodeOp; use crate::ASTTy; #[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum Core { +pub enum PythonCore { Import { - from: Option>, - import: Vec, - alias: Vec, + from: Option>, + import: Vec, + alias: Vec, }, ClassDef { - name: Box, - parent_names: Vec, - body: Box, + name: Box, + parent_names: Vec, + body: Box, }, FunctionCall { - function: Box, - args: Vec, + function: Box, + args: Vec, }, PropertyCall { - object: Box, - property: Box, + object: Box, + property: Box, }, Id { lit: String, }, Type { lit: String, - generics: Vec, + generics: Vec, }, ExpressionType { - expr: Box, - ty: Box, + expr: Box, + ty: Box, }, Assign { - left: Box, - right: Box, + left: Box, + right: Box, op: CoreOp, }, VarDef { - var: Box, - ty: Option>, - expr: Option>, + var: Box, + ty: Option>, + expr: Option>, }, FunDefOp { op: CoreFunOp, - arg: Vec, - ty: Option>, - body: Box, + arg: Vec, + ty: Option>, + body: Box, }, FunDef { dec: Vec, id: String, - arg: Vec, - ty: Option>, - body: Box, + arg: Vec, + ty: Option>, + body: Box, }, FunArg { vararg: bool, - var: Box, - ty: Option>, - default: Option>, + var: Box, + ty: Option>, + default: Option>, }, AnonFun { - args: Vec, - body: Box, + args: Vec, + body: Box, }, Block { - statements: Vec, + statements: Vec, }, Float { float: String, @@ -96,180 +96,180 @@ pub enum Core { boolean: bool, }, Tuple { - elements: Vec, + elements: Vec, }, TupleLiteral { - elements: Vec, + elements: Vec, }, DictComprehension { - from: Box, - to: Box, - col: Box, - conds: Vec, + from: Box, + to: Box, + col: Box, + conds: Vec, }, Comprehension { - expr: Box, - col: Box, - conds: Vec, + expr: Box, + col: Box, + conds: Vec, }, Dictionary { - elements: Vec<(Core, Core)>, + elements: Vec<(PythonCore, PythonCore)>, }, Set { - elements: Vec, + elements: Vec, }, List { - elements: Vec, + elements: Vec, }, Index { - item: Box, - range: Box, + item: Box, + range: Box, }, Ge { - left: Box, - right: Box, + left: Box, + right: Box, }, Geq { - left: Box, - right: Box, + left: Box, + right: Box, }, Le { - left: Box, - right: Box, + left: Box, + right: Box, }, Leq { - left: Box, - right: Box, + left: Box, + right: Box, }, Not { - expr: Box, + expr: Box, }, Eq { - left: Box, - right: Box, + left: Box, + right: Box, }, Neq { - left: Box, - right: Box, + left: Box, + right: Box, }, And { - left: Box, - right: Box, + left: Box, + right: Box, }, Or { - left: Box, - right: Box, + left: Box, + right: Box, }, Add { - left: Box, - right: Box, + left: Box, + right: Box, }, AddU { - expr: Box, + expr: Box, }, Sub { - left: Box, - right: Box, + left: Box, + right: Box, }, SubU { - expr: Box, + expr: Box, }, Mul { - left: Box, - right: Box, + left: Box, + right: Box, }, Mod { - left: Box, - right: Box, + left: Box, + right: Box, }, Pow { - left: Box, - right: Box, + left: Box, + right: Box, }, Div { - left: Box, - right: Box, + left: Box, + right: Box, }, FDiv { - left: Box, - right: Box, + left: Box, + right: Box, }, Sqrt { - expr: Box, + expr: Box, }, For { - expr: Box, - col: Box, - body: Box, + expr: Box, + col: Box, + body: Box, }, If { - cond: Box, - then: Box, + cond: Box, + then: Box, }, IfElse { - cond: Box, - then: Box, - el: Box, + cond: Box, + then: Box, + el: Box, }, Match { - expr: Box, - cases: Vec, + expr: Box, + cases: Vec, }, Case { - expr: Box, - body: Box, + expr: Box, + body: Box, }, Ternary { - cond: Box, - then: Box, - el: Box, + cond: Box, + then: Box, + el: Box, }, KeyValue { - key: Box, - value: Box, + key: Box, + value: Box, }, While { - cond: Box, - body: Box, + cond: Box, + body: Box, }, In { - left: Box, - right: Box, + left: Box, + right: Box, }, Break, Continue, Return { - expr: Box, + expr: Box, }, UnderScore, Pass, None, Empty, TryExcept { - setup: Option>, - attempt: Box, - except: Vec, + setup: Option>, + attempt: Box, + except: Vec, }, ExceptId { - id: Box, - class: Box, - body: Box, + id: Box, + class: Box, + body: Box, }, Except { - class: Box, - body: Box, + class: Box, + body: Box, }, Raise { - error: Box, + error: Box, }, With { - resource: Box, - expr: Box, + resource: Box, + expr: Box, }, WithAs { - resource: Box, - alias: Box, - expr: Box, + resource: Box, + alias: Box, + expr: Box, }, } diff --git a/src/generate/convert/builder.rs b/src/backend/python/convert/builder.rs similarity index 80% rename from src/generate/convert/builder.rs rename to src/backend/python/convert/builder.rs index f8395ff5a..4870e2142 100644 --- a/src/generate/convert/builder.rs +++ b/src/backend/python/convert/builder.rs @@ -1,9 +1,9 @@ +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::convert::common::convert_vec; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::result::{GenResult, UnimplementedErr}; use crate::check::ast::NodeTy; -use crate::generate::ast::node::Core; -use crate::generate::convert::common::convert_vec; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::result::{GenResult, UnimplementedErr}; use crate::{ASTTy, Context}; pub fn convert_builder(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) -> GenResult { @@ -22,7 +22,7 @@ pub fn convert_builder(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Cont .expect("Unreachable"); let conds = convert_vec(conds, imp, state, ctx)?; let col = Box::from(convert_node(col, imp, state, ctx)?); - Ok(Core::DictComprehension { + Ok(PythonCore::DictComprehension { from, to, col, @@ -41,8 +41,8 @@ pub fn convert_builder(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Cont .expect("Unreachable"); let conds = convert_vec(conds, imp, state, ctx)?; let col = Box::from(convert_node(col, imp, state, ctx)?); - Ok(Core::List { - elements: vec![Core::Comprehension { expr, col, conds }], + Ok(PythonCore::List { + elements: vec![PythonCore::Comprehension { expr, col, conds }], }) } else { Err(Box::from(UnimplementedErr::new(ast, "Cannot be empty"))) @@ -57,8 +57,8 @@ pub fn convert_builder(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Cont .expect("Unreachable"); let conds = convert_vec(conds, imp, state, ctx)?; let col = Box::from(convert_node(col, imp, state, ctx)?); - Ok(Core::Set { - elements: vec![Core::Comprehension { expr, col, conds }], + Ok(PythonCore::Set { + elements: vec![PythonCore::Comprehension { expr, col, conds }], }) } else { Err(Box::from(UnimplementedErr::new(ast, "Cannot be empty"))) diff --git a/src/generate/convert/call.rs b/src/backend/python/convert/call.rs similarity index 56% rename from src/generate/convert/call.rs rename to src/backend/python/convert/call.rs index 61b61f98c..28ede9cca 100644 --- a/src/generate/convert/call.rs +++ b/src/backend/python/convert/call.rs @@ -1,19 +1,19 @@ +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::convert::common::convert_vec; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::name::ToPy; +use crate::backend::python::result::{GenResult, UnimplementedErr}; use crate::check::ast::NodeTy; -use crate::generate::ast::node::Core; -use crate::generate::convert::common::convert_vec; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::name::ToPy; -use crate::generate::result::{GenResult, UnimplementedErr}; use crate::{ASTTy, Context}; pub fn convert_call(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) -> GenResult { Ok(match &ast.node { - NodeTy::PropertyCall { instance, property } => Core::PropertyCall { + NodeTy::PropertyCall { instance, property } => PythonCore::PropertyCall { object: Box::from(convert_node(instance, imp, state, ctx)?), property: Box::from(convert_node(property, imp, state, ctx)?), }, - NodeTy::FunctionCall { name, args } => Core::FunctionCall { + NodeTy::FunctionCall { name, args } => PythonCore::FunctionCall { function: Box::from(name.to_py(imp)), args: convert_vec(args, imp, state, ctx)?, }, diff --git a/src/generate/convert/class.rs b/src/backend/python/convert/class.rs similarity index 66% rename from src/generate/convert/class.rs rename to src/backend/python/convert/class.rs index 4c3e42b94..5dfc87e65 100644 --- a/src/generate/convert/class.rs +++ b/src/backend/python/convert/class.rs @@ -3,17 +3,17 @@ use std::ops::Deref; use itertools::Itertools; +use crate::backend::python::ast::node::{CoreOp, PythonCore}; +use crate::backend::python::convert::common::convert_vec; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::name::ToPy; +use crate::backend::python::result::{GenResult, UnimplementedErr}; use crate::check::ast::NodeTy; use crate::check::context::clss::Class; use crate::check::context::{arg, function, LookupClass}; use crate::check::name::string_name::StringName; use crate::common::position::Position; -use crate::generate::ast::node::{Core, CoreOp}; -use crate::generate::convert::common::convert_vec; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::name::ToPy; -use crate::generate::result::{GenResult, UnimplementedErr}; use crate::{ASTTy, Context}; /// Desugar a class. @@ -29,13 +29,13 @@ pub fn convert_class(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Contex imp.add_from_import("typing", "NewType"); let lit = ty.name.clone(); - Ok(Core::Assign { - left: Box::new(Core::Id { lit: lit.clone() }), - right: Box::new(Core::FunctionCall { - function: Box::new(Core::Id { + Ok(PythonCore::Assign { + left: Box::new(PythonCore::Id { lit: lit.clone() }), + right: Box::new(PythonCore::FunctionCall { + function: Box::new(PythonCore::Id { lit: String::from("NewType"), }), - args: vec![Core::Str { string: lit }, isa.to_py(imp)], + args: vec![PythonCore::Str { string: lit }, isa.to_py(imp)], }), op: CoreOp::Assign, }) @@ -65,7 +65,7 @@ pub fn convert_class(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Contex } NodeTy::Parent { ty, args } if args.is_empty() => Ok(ty.to_py(imp)), - NodeTy::Parent { ty, args } => Ok(Core::FunctionCall { + NodeTy::Parent { ty, args } => Ok(PythonCore::FunctionCall { function: Box::from(ty.to_py(imp)), args: convert_vec(args, imp, state, ctx)?, }), @@ -89,7 +89,7 @@ fn extract_class( ty: &StringName, body: &Option>, args: &[ASTTy], - parents: &[Core], + parents: &[PythonCore], imp: &mut Imports, state: &State, ctx: &Context, @@ -102,8 +102,8 @@ fn extract_class( } else { None }; - let mut body_name_stmts: HashMap = match body { - Some(Core::Block { statements }) => statements, + let mut body_name_stmts: HashMap = match body { + Some(PythonCore::Block { statements }) => statements, Some(other) => vec![other], None => vec![], } @@ -112,17 +112,17 @@ fn extract_class( .map(|(i, stmt)| { // function two further to leave place for init let (pos, key) = match stmt { - Core::FunDef { id, .. } => (i + 2, Core::Id { lit: id.clone() }), - Core::FunDefOp { op, .. } => ( + PythonCore::FunDef { id, .. } => (i + 2, PythonCore::Id { lit: id.clone() }), + PythonCore::FunDefOp { op, .. } => ( i + 2, - Core::Id { + PythonCore::Id { lit: format!("{op}"), }, ), - Core::VarDef { var, .. } => (i, var.deref().clone()), + PythonCore::VarDef { var, .. } => (i, var.deref().clone()), _ => ( i, - Core::Id { + PythonCore::Id { lit: String::from("@"), }, ), @@ -141,10 +141,10 @@ fn extract_class( let old_init = body_name_stmts .iter() - .find(|(name, _)| matches!(name, Core::Id { lit } if *lit == function::python::INIT)) + .find(|(name, _)| matches!(name, PythonCore::Id { lit } if *lit == function::python::INIT)) .map(|(_, (_, function))| function); if let Some(new_init) = init(&old_init, &args, parents, hoisted)? { - let init = Core::Id { + let init = PythonCore::Id { lit: String::from(function::python::INIT), }; let pos = if let Some((pos, _)) = body_name_stmts.get(&init) { @@ -152,7 +152,7 @@ fn extract_class( } else { body_name_stmts .values() - .filter(|(_, stmt)| matches!(stmt, Core::VarDef { .. })) + .filter(|(_, stmt)| matches!(stmt, PythonCore::VarDef { .. })) .map(|(pos, _)| *pos + 1) .max() .unwrap_or(0) // otherwise always first @@ -164,20 +164,20 @@ fn extract_class( let parent_names = parents .iter() .map(|parent| match parent.clone() { - Core::FunctionCall { function, .. } => match *function { - Core::Type { lit, .. } => Ok(Core::Id { lit }), + PythonCore::FunctionCall { function, .. } => match *function { + PythonCore::Type { lit, .. } => Ok(PythonCore::Id { lit }), other => panic!("Expected type in parent, was {other}"), }, - Core::Type { .. } => Ok(parent.clone()), + PythonCore::Type { .. } => Ok(parent.clone()), other => panic!("Expected type in parent, was {other}"), }) - .collect::>>()?; + .collect::>>()?; let parent_names = if state.interface && !has_abstract_parent(&class, ctx) { imp.add_from_import("abc", "ABC"); parent_names .into_iter() - .chain(vec![Core::Id { + .chain(vec![PythonCore::Id { lit: String::from("ABC"), }]) .collect() @@ -185,22 +185,22 @@ fn extract_class( parent_names }; - let body_stmts: Vec = body_name_stmts + let body_stmts: Vec = body_name_stmts .values() .sorted_by_key(|(pos, _)| *pos) .map(|(_, stmt)| stmt.clone()) .collect(); let statements = if body_stmts.is_empty() { - vec![Core::Pass] + vec![PythonCore::Pass] } else { body_stmts }; - let body = Core::Block { statements }; + let body = PythonCore::Block { statements }; - if let Core::Type { lit, .. } = ty.to_py(imp) { - let name = Box::from(Core::Id { lit }); - Ok(Core::ClassDef { + if let PythonCore::Type { lit, .. } = ty.to_py(imp) { + let name = Box::from(PythonCore::Id { lit }); + Ok(PythonCore::ClassDef { name, parent_names, body: Box::from(body), @@ -216,24 +216,24 @@ fn extract_class( /// A field declaration keeps its class-level slot with `None` in place of the initializer; /// any other statement (e.g. a bare `print(self.a)`) is moved wholesale. fn hoist_constructor_dependent_stmts( - body_name_stmts: &mut HashMap, + body_name_stmts: &mut HashMap, self_name: &HashSet, -) -> Vec { - let mut hoisted: Vec<(usize, Core)> = vec![]; +) -> Vec { + let mut hoisted: Vec<(usize, PythonCore)> = vec![]; let mut to_remove = vec![]; for (key, (pos, stmt)) in body_name_stmts.iter_mut() { match stmt { - Core::VarDef { + PythonCore::VarDef { var, expr: Some(expr), .. } if references_free_var(expr, self_name) => { hoisted.push(( *pos, - Core::Assign { - left: Box::from(Core::PropertyCall { - object: Box::from(Core::Id { + PythonCore::Assign { + left: Box::from(PythonCore::PropertyCall { + object: Box::from(PythonCore::Id { lit: String::from(arg::python::SELF), }), property: var.clone(), @@ -242,14 +242,14 @@ fn hoist_constructor_dependent_stmts( op: CoreOp::Assign, }, )); - *expr = Box::from(Core::None); + *expr = Box::from(PythonCore::None); } // A docstring must stay a literal first statement in the class body, not move into // `__init__`. - Core::FunDef { .. } - | Core::FunDefOp { .. } - | Core::VarDef { .. } - | Core::DocStr { .. } => {} + PythonCore::FunDef { .. } + | PythonCore::FunDefOp { .. } + | PythonCore::VarDef { .. } + | PythonCore::DocStr { .. } => {} // Any other class-body statement (e.g. a bare `print(...)`) runs once per instance, // like the rest of the constructor — not once at class-definition time — so it // always moves into `__init__`, whether or not it happens to reference `self`. @@ -272,7 +272,7 @@ fn hoist_constructor_dependent_stmts( /// This could still be `None` at that point. /// /// A statement referencing its *own* field is exempt, since that reads the constructor-arg auto-assignment, not a hoisted default. -fn order_by_self_field_deps(mut hoisted: Vec<(usize, Core)>) -> Vec { +fn order_by_self_field_deps(mut hoisted: Vec<(usize, PythonCore)>) -> Vec { hoisted.sort_by_key(|(pos, _)| *pos); let names: Vec> = hoisted @@ -323,15 +323,19 @@ fn order_by_self_field_deps(mut hoisted: Vec<(usize, Core)>) -> Vec { } /// The field name of a hoisted `self. = ...` assignment, if `stmt` is one. -fn assigned_self_field(stmt: &Core) -> Option { +fn assigned_self_field(stmt: &PythonCore) -> Option { match stmt { - Core::Assign { left, .. } => match left.as_ref() { - Core::PropertyCall { object, property } => match (object.as_ref(), property.as_ref()) { - (Core::Id { lit: obj }, Core::Id { lit: prop }) if obj == arg::python::SELF => { - Some(prop.clone()) + PythonCore::Assign { left, .. } => match left.as_ref() { + PythonCore::PropertyCall { object, property } => { + match (object.as_ref(), property.as_ref()) { + (PythonCore::Id { lit: obj }, PythonCore::Id { lit: prop }) + if obj == arg::python::SELF => + { + Some(prop.clone()) + } + _ => None, } - _ => None, - }, + } _ => None, }, _ => None, @@ -339,74 +343,74 @@ fn assigned_self_field(stmt: &Core) -> Option { } /// Applies `test` to `core` and every sub-expression, depth-first. Not exhaustive over every -/// `Core` variant; a missed variant just means a match goes undetected. -fn any_node(core: &Core, test: &impl Fn(&Core) -> bool) -> bool { +/// `PythonCore` variant; a missed variant just means a match goes undetected. +fn any_node(core: &PythonCore, test: &impl Fn(&PythonCore) -> bool) -> bool { if test(core) { return true; } match core { - Core::PropertyCall { object, .. } => any_node(object, test), - Core::FunctionCall { function, args } => { + PythonCore::PropertyCall { object, .. } => any_node(object, test), + PythonCore::FunctionCall { function, args } => { any_node(function, test) || args.iter().any(|a| any_node(a, test)) } - Core::Index { item, range } => any_node(item, test) || any_node(range, test), - Core::KeyValue { key, value } => any_node(key, test) || any_node(value, test), - Core::Ge { left, right } - | Core::Geq { left, right } - | Core::Le { left, right } - | Core::Leq { left, right } - | Core::Eq { left, right } - | Core::Neq { left, right } - | Core::And { left, right } - | Core::Or { left, right } - | Core::Add { left, right } - | Core::Sub { left, right } - | Core::Mul { left, right } - | Core::Mod { left, right } - | Core::Pow { left, right } - | Core::Div { left, right } - | Core::FDiv { left, right } - | Core::In { left, right } => any_node(left, test) || any_node(right, test), - Core::Not { expr } - | Core::AddU { expr } - | Core::SubU { expr } - | Core::Sqrt { expr } - | Core::Return { expr } - | Core::Raise { error: expr } => any_node(expr, test), - Core::If { cond, then } => any_node(cond, test) || any_node(then, test), - Core::IfElse { cond, then, el } | Core::Ternary { cond, then, el } => { + PythonCore::Index { item, range } => any_node(item, test) || any_node(range, test), + PythonCore::KeyValue { key, value } => any_node(key, test) || any_node(value, test), + PythonCore::Ge { left, right } + | PythonCore::Geq { left, right } + | PythonCore::Le { left, right } + | PythonCore::Leq { left, right } + | PythonCore::Eq { left, right } + | PythonCore::Neq { left, right } + | PythonCore::And { left, right } + | PythonCore::Or { left, right } + | PythonCore::Add { left, right } + | PythonCore::Sub { left, right } + | PythonCore::Mul { left, right } + | PythonCore::Mod { left, right } + | PythonCore::Pow { left, right } + | PythonCore::Div { left, right } + | PythonCore::FDiv { left, right } + | PythonCore::In { left, right } => any_node(left, test) || any_node(right, test), + PythonCore::Not { expr } + | PythonCore::AddU { expr } + | PythonCore::SubU { expr } + | PythonCore::Sqrt { expr } + | PythonCore::Return { expr } + | PythonCore::Raise { error: expr } => any_node(expr, test), + PythonCore::If { cond, then } => any_node(cond, test) || any_node(then, test), + PythonCore::IfElse { cond, then, el } | PythonCore::Ternary { cond, then, el } => { any_node(cond, test) || any_node(then, test) || any_node(el, test) } - Core::Tuple { elements } - | Core::TupleLiteral { elements } - | Core::Set { elements } - | Core::List { elements } => elements.iter().any(|e| any_node(e, test)), - Core::Dictionary { elements } => elements + PythonCore::Tuple { elements } + | PythonCore::TupleLiteral { elements } + | PythonCore::Set { elements } + | PythonCore::List { elements } => elements.iter().any(|e| any_node(e, test)), + PythonCore::Dictionary { elements } => elements .iter() .any(|(k, v)| any_node(k, test) || any_node(v, test)), - Core::Assign { right, .. } => any_node(right, test), - Core::VarDef { + PythonCore::Assign { right, .. } => any_node(right, test), + PythonCore::VarDef { expr: Some(expr), .. } => any_node(expr, test), - Core::Block { statements } => statements.iter().any(|s| any_node(s, test)), + PythonCore::Block { statements } => statements.iter().any(|s| any_node(s, test)), _ => false, } } /// Whether `core` contains a free reference to any name in `names`. -fn references_free_var(core: &Core, names: &HashSet) -> bool { +fn references_free_var(core: &PythonCore, names: &HashSet) -> bool { any_node( core, - &|c| matches!(c, Core::Id { lit } if names.contains(lit)), + &|c| matches!(c, PythonCore::Id { lit } if names.contains(lit)), ) } /// Whether `core` reads `self.` anywhere. -fn references_self_field(core: &Core, field: &str) -> bool { +fn references_self_field(core: &PythonCore, field: &str) -> bool { any_node(core, &|c| match c { - Core::PropertyCall { object, property } => { - matches!(object.as_ref(), Core::Id { lit } if lit == arg::python::SELF) - && matches!(property.as_ref(), Core::Id { lit } if lit == field) + PythonCore::PropertyCall { object, property } => { + matches!(object.as_ref(), PythonCore::Id { lit } if lit == arg::python::SELF) + && matches!(property.as_ref(), PythonCore::Id { lit } if lit == field) } _ => false, }) @@ -436,33 +440,33 @@ fn is_abstract(clss: &Option, ctx: &Context) -> bool { } fn init( - old_init: &Option<&Core>, - class_args: &[Core], - parents: &[Core], - mut extra_statements: Vec, -) -> GenResult> { - let (parent_inits, parent_args): (Vec, Vec>) = parents + old_init: &Option<&PythonCore>, + class_args: &[PythonCore], + parents: &[PythonCore], + mut extra_statements: Vec, +) -> GenResult> { + let (parent_inits, parent_args): (Vec, Vec>) = parents .iter() .map(|parent| { let (lit, mut arg) = match parent { - Core::FunctionCall { function, args } => match function.deref() { - Core::Type { lit, .. } => (lit.clone(), args.clone()), + PythonCore::FunctionCall { function, args } => match function.deref() { + PythonCore::Type { lit, .. } => (lit.clone(), args.clone()), _ => (String::from(""), args.clone()), }, - Core::Type { lit, .. } => (lit.clone(), vec![]), + PythonCore::Type { lit, .. } => (lit.clone(), vec![]), _ => (String::from(""), vec![]), }; - let mut args = vec![Core::Id { + let mut args = vec![PythonCore::Id { lit: String::from(arg::python::SELF), }]; args.append(&mut arg); ( - Core::PropertyCall { - object: Box::from(Core::Id { lit }), - property: Box::new(Core::FunctionCall { - function: Box::new(Core::Id { + PythonCore::PropertyCall { + object: Box::from(PythonCore::Id { lit }), + property: Box::new(PythonCore::FunctionCall { + function: Box::new(PythonCore::Id { lit: String::from(function::python::INIT), }), args: args.clone(), @@ -476,8 +480,8 @@ fn init( // Parent calls from parents let (mut args, mut statements) = if let Some(old_init) = old_init { let (mut old_stmts, args) = match old_init { - Core::FunDef { body, arg, .. } => match body.deref() { - Core::Block { statements } => (statements.clone(), arg.clone()), + PythonCore::FunDef { body, arg, .. } => match body.deref() { + PythonCore::Block { statements } => (statements.clone(), arg.clone()), other => (vec![other.clone()], arg.clone()), }, _ => (vec![], vec![]), @@ -495,7 +499,7 @@ fn init( &mut class_args .iter() .flat_map(|arg| match arg { - Core::FunArg { var, .. } => Some(var.deref().clone()), + PythonCore::FunArg { var, .. } => Some(var.deref().clone()), _ => None, }) .filter(|arg| { @@ -503,9 +507,9 @@ fn init( .iter() .any(|p_args| p_args.iter().any(|p_arg| p_arg == arg)) }) - .map(|var| Core::Assign { - left: Box::from(Core::PropertyCall { - object: Box::from(Core::Id { + .map(|var| PythonCore::Assign { + left: Box::from(PythonCore::PropertyCall { + object: Box::from(PythonCore::Id { lit: String::from(arg::python::SELF), }), property: Box::from(var.clone()), @@ -520,8 +524,8 @@ fn init( let first_is_self = args .first() .map(|arg| match arg { - Core::FunArg { var, .. } => { - if let Core::Id { lit } = var.deref() { + PythonCore::FunArg { var, .. } => { + if let PythonCore::Id { lit } = var.deref() { lit == arg::python::SELF } else { false @@ -533,7 +537,7 @@ fn init( let args = if first_is_self { args } else { - let mut new_args = vec![Core::Id { + let mut new_args = vec![PythonCore::Id { lit: String::from(arg::python::SELF), }]; new_args.append(&mut args); @@ -543,12 +547,12 @@ fn init( let id = String::from(function::python::INIT); Ok(if !statements.is_empty() { let dec = vec![]; - Some(Core::FunDef { + Some(PythonCore::FunDef { dec, id, arg: args, ty: None, - body: Box::new(Core::Block { statements }), + body: Box::new(PythonCore::Block { statements }), }) } else { None @@ -557,9 +561,9 @@ fn init( #[cfg(test)] mod tests { + use crate::backend::python::ast::node::PythonCore; + use crate::backend::python::gen; use crate::common::position::Position; - use crate::generate::ast::node::Core; - use crate::generate::gen; use crate::parse::ast::{Node, AST}; use crate::ASTTy; @@ -598,7 +602,7 @@ mod tests { }); let (from, import, alias) = match gen(&ASTTy::from(&*import)) { - Ok(Core::Import { + Ok(PythonCore::Import { from, import, alias, @@ -606,17 +610,17 @@ mod tests { other => panic!("Expected import but got {other:?}"), }; - assert_eq!(*from.unwrap(), Core::Break); + assert_eq!(*from.unwrap(), PythonCore::Break); assert_eq!( import[0], - Core::ENum { + PythonCore::ENum { num: String::from("a"), exp: String::from("100") } ); assert_eq!( import[1], - Core::Float { + PythonCore::Float { float: String::from("3000.5") } ); diff --git a/src/generate/convert/common.rs b/src/backend/python/convert/common.rs similarity index 52% rename from src/generate/convert/common.rs rename to src/backend/python/convert/common.rs index 44622fdce..b9b261aa0 100644 --- a/src/generate/convert/common.rs +++ b/src/backend/python/convert/common.rs @@ -1,7 +1,7 @@ -use crate::generate::ast::node::Core; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::result::GenResult; +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::result::GenResult; use crate::{ASTTy, Context}; pub fn convert_vec( @@ -9,7 +9,7 @@ pub fn convert_vec( imp: &mut Imports, state: &State, ctx: &Context, -) -> GenResult> { +) -> GenResult> { let mut result = vec![]; for ast in node_vec { result.push(convert_node(ast, imp, state, ctx)?) diff --git a/src/generate/convert/control_flow.rs b/src/backend/python/convert/control_flow.rs similarity index 84% rename from src/generate/convert/control_flow.rs rename to src/backend/python/convert/control_flow.rs index 6e5e4688c..35ce98f78 100644 --- a/src/generate/convert/control_flow.rs +++ b/src/backend/python/convert/control_flow.rs @@ -1,8 +1,8 @@ +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::result::{GenResult, UnimplementedErr}; use crate::check::ast::NodeTy; -use crate::generate::ast::node::Core; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::result::{GenResult, UnimplementedErr}; use crate::{ASTTy, Context}; pub fn convert_cntrl_flow( @@ -31,20 +31,20 @@ pub fn convert_cntrl_flow( .remove_ret(true) .must_assign_to(None, None); - Core::Ternary { + PythonCore::Ternary { cond, then: Box::from(convert_node(then, imp, &state, ctx)?), el: Box::from(convert_node(el, imp, &state, ctx)?), } } else { - Core::IfElse { + PythonCore::IfElse { cond, then: Box::from(convert_node(then, imp, state, ctx)?), el: Box::from(convert_node(el, imp, state, ctx)?), } } } - None => Core::If { + None => PythonCore::If { cond, then: Box::from(convert_node(then, imp, state, ctx)?), }, @@ -65,7 +65,7 @@ pub fn convert_cntrl_flow( for case in match_cases { if let NodeTy::Case { cond, body } = &case.node { if let NodeTy::ExpressionType { expr, .. } = &cond.node { - cases.push(Core::Case { + cases.push(PythonCore::Case { expr: Box::from(convert_node( expr.as_ref(), imp, @@ -78,19 +78,19 @@ pub fn convert_cntrl_flow( } } - Core::Match { expr, cases } + PythonCore::Match { expr, cases } } - NodeTy::While { cond, body } => Core::While { + NodeTy::While { cond, body } => PythonCore::While { cond: Box::from(convert_node(cond, imp, state, ctx)?), body: Box::from(convert_node(body, imp, state, ctx)?), }, - NodeTy::For { expr, col, body } => Core::For { + NodeTy::For { expr, col, body } => PythonCore::For { expr: Box::from(convert_node(expr, imp, state, ctx)?), col: Box::from(convert_node(col, imp, state, ctx)?), body: Box::from(convert_node(body, imp, state, ctx)?), }, - NodeTy::Break => Core::Break, - NodeTy::Continue => Core::Continue, + NodeTy::Break => PythonCore::Break, + NodeTy::Continue => PythonCore::Continue, other => { let msg = format!("Expected control flow but was: {other:?}."); return Err(Box::from(UnimplementedErr::new(ast, &msg))); @@ -123,9 +123,9 @@ fn is_expr_valid_in_ternary(node: &ASTTy) -> bool { #[cfg(test)] mod tests { + use crate::backend::python::ast::node::PythonCore; + use crate::backend::python::gen; use crate::common::position::Position; - use crate::generate::ast::node::Core; - use crate::generate::gen; use crate::parse::ast::Node; use crate::parse::ast::AST; use crate::ASTTy; @@ -160,19 +160,19 @@ mod tests { }); let (core_cond, core_then) = match gen(&ASTTy::from(&if_stmt)) { - Ok(Core::If { cond, then }) => (cond, then), + Ok(PythonCore::If { cond, then }) => (cond, then), other => panic!("Expected reassign but was {other:?}"), }; assert_eq!( *core_cond, - Core::Id { + PythonCore::Id { lit: String::from("cond") } ); assert_eq!( *core_then, - Core::Id { + PythonCore::Id { lit: String::from("then") } ); @@ -196,25 +196,25 @@ mod tests { }); let (core_cond, core_then, core_else) = match gen(&ASTTy::from(&if_stmt)) { - Ok(Core::IfElse { cond, then, el }) => (cond, then, el), + Ok(PythonCore::IfElse { cond, then, el }) => (cond, then, el), other => panic!("Expected reassign but was {other:?}"), }; assert_eq!( *core_cond, - Core::Id { + PythonCore::Id { lit: String::from("cond") } ); assert_eq!( *core_then, - Core::Id { + PythonCore::Id { lit: String::from("then") } ); assert_eq!( *core_else, - Core::Id { + PythonCore::Id { lit: String::from("else") } ); @@ -232,19 +232,19 @@ mod tests { let while_stmt = to_pos!(Node::While { cond, body }); let (core_cond, core_body) = match gen(&ASTTy::from(&while_stmt)) { - Ok(Core::While { cond, body }) => (cond, body), + Ok(PythonCore::While { cond, body }) => (cond, body), other => panic!("Expected reassign but was {other:?}"), }; assert_eq!( *core_cond, - Core::Id { + PythonCore::Id { lit: String::from("cond") } ); assert_eq!( *core_body, - Core::ENum { + PythonCore::ENum { num: String::from("num"), exp: String::from("0") } @@ -265,25 +265,25 @@ mod tests { let for_stmt = to_pos!(Node::For { expr, col, body }); let (core_expr, core_col, core_body) = match gen(&ASTTy::from(&for_stmt)) { - Ok(Core::For { expr, col, body }) => (expr, col, body), + Ok(PythonCore::For { expr, col, body }) => (expr, col, body), other => panic!("Expected for but was {other:?}"), }; assert_eq!( *core_expr, - Core::Id { + PythonCore::Id { lit: String::from("expr_1") } ); assert_eq!( *core_col, - Core::Id { + PythonCore::Id { lit: String::from("col") } ); assert_eq!( *core_body, - Core::Id { + PythonCore::Id { lit: String::from("body") } ); @@ -305,10 +305,10 @@ mod tests { }); let (from, to, step) = match gen(&ASTTy::from(&range)) { - Ok(Core::FunctionCall { function, args }) => { + Ok(PythonCore::FunctionCall { function, args }) => { assert_eq!( *function, - Core::Id { + PythonCore::Id { lit: String::from("range") } ); @@ -319,19 +319,19 @@ mod tests { assert_eq!( from, - Core::Id { + PythonCore::Id { lit: String::from("a") } ); assert_eq!( to, - Core::Id { + PythonCore::Id { lit: String::from("b") } ); assert_eq!( step, - Core::Int { + PythonCore::Int { int: String::from("1") } ); @@ -353,10 +353,10 @@ mod tests { }); let (from, to, step) = match gen(&ASTTy::from(&range)) { - Ok(Core::FunctionCall { function, args }) => { + Ok(PythonCore::FunctionCall { function, args }) => { assert_eq!( *function, - Core::Id { + PythonCore::Id { lit: String::from("range") } ); @@ -367,24 +367,24 @@ mod tests { assert_eq!( from, - Core::Id { + PythonCore::Id { lit: String::from("a") } ); assert_eq!( to, - Core::Add { - left: Box::from(Core::Id { + PythonCore::Add { + left: Box::from(PythonCore::Id { lit: String::from("b") }), - right: Box::from(Core::Int { + right: Box::from(PythonCore::Int { int: String::from("1") }), } ); assert_eq!( step, - Core::Int { + PythonCore::Int { int: String::from("1") } ); @@ -409,10 +409,10 @@ mod tests { }); let (from, to, step) = match gen(&ASTTy::from(&range)) { - Ok(Core::FunctionCall { function, args }) => { + Ok(PythonCore::FunctionCall { function, args }) => { assert_eq!( *function, - Core::Id { + PythonCore::Id { lit: String::from("range") } ); @@ -423,19 +423,19 @@ mod tests { assert_eq!( from, - Core::Id { + PythonCore::Id { lit: String::from("a") } ); assert_eq!( to, - Core::Id { + PythonCore::Id { lit: String::from("b") } ); assert_eq!( step, - Core::Id { + PythonCore::Id { lit: String::from("c") } ); diff --git a/src/generate/convert/definition.rs b/src/backend/python/convert/definition.rs similarity index 81% rename from src/generate/convert/definition.rs rename to src/backend/python/convert/definition.rs index e37c2e606..0a3366751 100644 --- a/src/generate/convert/definition.rs +++ b/src/backend/python/convert/definition.rs @@ -1,14 +1,14 @@ use std::ops::Deref; +use crate::backend::python::ast::node::{CoreFunOp, PythonCore}; +use crate::backend::python::convert::common::convert_vec; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::name::ToPy; +use crate::backend::python::result::{GenResult, UnimplementedErr}; use crate::check::ast::NodeTy; use crate::check::context::arg::python::SELF; use crate::check::context::function; -use crate::generate::ast::node::{Core, CoreFunOp}; -use crate::generate::convert::common::convert_vec; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::name::ToPy; -use crate::generate::result::{GenResult, UnimplementedErr}; use crate::{ASTTy, Context}; pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) -> GenResult { @@ -16,12 +16,13 @@ pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) NodeTy::VariableDef { var, expr, ty, .. } => { let var = convert_node(var, imp, &state.tuple_literal(), ctx)?; let state = state.in_tup(match var.clone() { - Core::Tuple { elements } => elements.len(), + PythonCore::Tuple { elements } => elements.len(), _ => 1, }); - let annotate = - state.annotate && state.expand_ty && !matches!(var, Core::TupleLiteral { .. }); + let annotate = state.annotate + && state.expand_ty + && !matches!(var, PythonCore::TupleLiteral { .. }); let ty = match (ty, expr) { (Some(ty), _) if annotate => Some(Box::from(ty.to_py(imp))), (_, Some(expr)) if annotate => { @@ -37,7 +38,7 @@ pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) } None => None, }; - Core::FunArg { + PythonCore::FunArg { vararg: false, var: Box::from(var), ty, @@ -46,19 +47,21 @@ pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) } else { let expr = match (&var, expr) { (_, Some(expr)) => match convert_node(expr, imp, &state, ctx)? { - Core::IfElse { .. } | Core::Match { .. } => { + PythonCore::IfElse { .. } | PythonCore::Match { .. } => { // redo convert but with assign to state let state = state.must_assign_to(Some(&var.clone()), expr.ty.clone()); return convert_node(expr, imp, &state, ctx); } other => Some(Box::from(other)), }, - (Core::TupleLiteral { elements }, None) => Some(Box::from(Core::Tuple { - elements: vec![Core::None; elements.len()], - })), + (PythonCore::TupleLiteral { elements }, None) => { + Some(Box::from(PythonCore::Tuple { + elements: vec![PythonCore::None; elements.len()], + })) + } (_, None) => None, }; - Core::VarDef { + PythonCore::VarDef { var: Box::from(var), ty, expr, @@ -79,7 +82,10 @@ pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) }; let (dec, body) = if state.interface && expression.is_none() { imp.add_from_import("abc", "abstractmethod"); - (vec![String::from("abstractmethod")], Box::from(Core::Pass)) + ( + vec![String::from("abstractmethod")], + Box::from(PythonCore::Pass), + ) } else { ( vec![], @@ -90,18 +96,18 @@ pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) &state.expand_ty(true).is_last_must_be_ret(ty.is_some()), ctx, )?, - None => Core::Pass, + None => PythonCore::Pass, }), ) }; let c_id = Box::from(convert_node(id, imp, state, ctx)?); match c_id.deref() { - Core::Id { lit } => Ok(if let Some(op) = CoreFunOp::from(lit.as_str()) { - Core::FunDefOp { op, arg, ty, body } + PythonCore::Id { lit } => Ok(if let Some(op) = CoreFunOp::from(lit.as_str()) { + PythonCore::FunDefOp { op, arg, ty, body } } else { let id = match c_id.as_ref() { - Core::Id { ref lit, .. } => match lit.as_str() { + PythonCore::Id { ref lit, .. } => match lit.as_str() { "size" => String::from("__size__"), function::python::INIT => String::from("__init__"), other => String::from(other), @@ -112,7 +118,7 @@ pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) } }; - Core::FunDef { + PythonCore::FunDef { dec, id, arg, @@ -134,11 +140,11 @@ pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) let annotate = state.annotate && state.expand_ty && var - != Core::Id { + != PythonCore::Id { lit: String::from(SELF), }; - Ok(Core::FunArg { + Ok(PythonCore::FunArg { vararg: *vararg, var: Box::from(var), ty: if annotate { @@ -161,9 +167,9 @@ pub fn convert_def(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) #[cfg(test)] mod test { + use crate::backend::python::ast::node::{CoreOp, PythonCore}; + use crate::backend::python::gen; use crate::common::position::Position; - use crate::generate::ast::node::{Core, CoreOp}; - use crate::generate::gen; use crate::parse::ast::node_op::NodeOp; use crate::parse::ast::Node; use crate::parse::ast::AST; @@ -199,19 +205,19 @@ mod test { }); let (left, right, op) = match gen(&ASTTy::from(&reassign)) { - Ok(Core::Assign { left, right, op }) => (left, right, op), + Ok(PythonCore::Assign { left, right, op }) => (left, right, op), other => panic!("Expected reassign but was {other:?}"), }; assert_eq!( *left, - Core::Id { + PythonCore::Id { lit: String::from("something") } ); assert_eq!( *right, - Core::Id { + PythonCore::Id { lit: String::from("other") } ); @@ -233,20 +239,20 @@ mod test { }); let (var, ty, expr) = match gen(&ASTTy::from(&definition)) { - Ok(Core::VarDef { var, ty, expr }) => (var, ty, expr), + Ok(PythonCore::VarDef { var, ty, expr }) => (var, ty, expr), other => panic!("Expected var def but got: {other:?}."), }; assert_eq!(ty, None); assert_eq!( var, - Box::from(Core::Id { + Box::from(PythonCore::Id { lit: String::from("d") }) ); assert_eq!( expr, - Some(Box::from(Core::Int { + Some(Box::from(PythonCore::Int { int: String::from("98") })) ); @@ -267,20 +273,20 @@ mod test { }); let (var, ty, expr) = match gen(&ASTTy::from(&definition)) { - Ok(Core::VarDef { var, ty, expr }) => (var, ty, expr), + Ok(PythonCore::VarDef { var, ty, expr }) => (var, ty, expr), other => panic!("Expected var def but got: {other:?}."), }; assert_eq!(ty, None); assert_eq!( var, - Box::from(Core::Id { + Box::from(PythonCore::Id { lit: String::from("d") }) ); assert_eq!( expr, - Some(Box::from(Core::Int { + Some(Box::from(PythonCore::Int { int: String::from("98") })) ); @@ -315,31 +321,31 @@ mod test { }); let (var, ty, expr) = match gen(&ASTTy::from(&definition)) { - Ok(Core::VarDef { var, ty, expr }) => (var, ty, expr), + Ok(PythonCore::VarDef { var, ty, expr }) => (var, ty, expr), other => panic!("Expected var def but got: {other:?}."), }; assert_eq!(ty, None); let elements = vec![ - Core::Id { + PythonCore::Id { lit: String::from("a"), }, - Core::Id { + PythonCore::Id { lit: String::from("b"), }, ]; - assert_eq!(var, Box::from(Core::TupleLiteral { elements })); + assert_eq!(var, Box::from(PythonCore::TupleLiteral { elements })); let expressions = vec![ - Core::Id { + PythonCore::Id { lit: String::from("c"), }, - Core::Id { + PythonCore::Id { lit: String::from("d"), }, ]; assert_eq!( expr, - Some(Box::from(Core::Tuple { + Some(Box::from(PythonCore::Tuple { elements: expressions })) ); @@ -358,14 +364,14 @@ mod test { }); let (var, ty, expr) = match gen(&ASTTy::from(&definition)) { - Ok(Core::VarDef { var, ty, expr }) => (var, ty, expr), + Ok(PythonCore::VarDef { var, ty, expr }) => (var, ty, expr), other => panic!("Expected var def but got: {other:?}."), }; assert_eq!(ty, None); assert_eq!( var, - Box::from(Core::Id { + Box::from(PythonCore::Id { lit: String::from("d") }) ); @@ -391,24 +397,24 @@ mod test { }); let (var, ty, expr) = match gen(&ASTTy::from(&definition)) { - Ok(Core::VarDef { var, ty, expr }) => (var, ty, expr), + Ok(PythonCore::VarDef { var, ty, expr }) => (var, ty, expr), other => panic!("Expected var def but got: {other:?}."), }; assert_eq!(ty, None); let elements = vec![ - Core::Id { + PythonCore::Id { lit: String::from("a"), }, - Core::Id { + PythonCore::Id { lit: String::from("b"), }, ]; - assert_eq!(var, Box::from(Core::TupleLiteral { elements })); + assert_eq!(var, Box::from(PythonCore::TupleLiteral { elements })); assert_eq!( expr, - Some(Box::from(Core::Tuple { - elements: vec![Core::None, Core::None] + Some(Box::from(PythonCore::Tuple { + elements: vec![PythonCore::None, PythonCore::None] })) ); } @@ -446,7 +452,7 @@ mod test { }); let (id, args, body) = match gen(&ASTTy::from(&definition)) { - Ok(Core::FunDef { id, arg, body, .. }) => (id, arg, body), + Ok(PythonCore::FunDef { id, arg, body, .. }) => (id, arg, body), other => panic!("Expected fun def but got: {other:?}."), }; @@ -455,9 +461,9 @@ mod test { assert_eq!(args.len(), 2); assert_eq!( args[0], - Core::FunArg { + PythonCore::FunArg { vararg: false, - var: Box::from(Core::Id { + var: Box::from(PythonCore::Id { lit: String::from("arg1") }), ty: None, @@ -466,16 +472,16 @@ mod test { ); assert_eq!( args[1], - Core::FunArg { + PythonCore::FunArg { vararg: true, - var: Box::from(Core::Id { + var: Box::from(PythonCore::Id { lit: String::from("arg2") }), ty: None, default: None, } ); - assert_eq!(*body, Core::Pass); + assert_eq!(*body, PythonCore::Pass); } #[test] @@ -503,7 +509,7 @@ mod test { }); let (id, args, body) = match gen(&ASTTy::from(&definition)) { - Ok(Core::FunDef { id, arg, body, .. }) => (id, arg, body), + Ok(PythonCore::FunDef { id, arg, body, .. }) => (id, arg, body), other => panic!("Expected fun def but got: {other:?}."), }; @@ -512,18 +518,18 @@ mod test { assert_eq!(args.len(), 1); assert_eq!( args[0], - Core::FunArg { + PythonCore::FunArg { vararg: false, - var: Box::from(Core::Id { + var: Box::from(PythonCore::Id { lit: String::from("arg1") }), ty: None, - default: Some(Box::from(Core::Str { + default: Some(Box::from(PythonCore::Str { string: String::from("asdf") })), } ); - assert_eq!(*body, Core::Pass); + assert_eq!(*body, PythonCore::Pass); } #[test] @@ -549,7 +555,7 @@ mod test { }); let (id, args, body) = match gen(&ASTTy::from(&definition)) { - Ok(Core::FunDef { id, arg, body, .. }) => (id, arg, body), + Ok(PythonCore::FunDef { id, arg, body, .. }) => (id, arg, body), other => panic!("Expected fun def but got: {other:?}."), }; @@ -558,19 +564,19 @@ mod test { assert_eq!(args.len(), 2); assert_eq!( args[0], - Core::Id { + PythonCore::Id { lit: String::from("arg1") } ); assert_eq!( args[1], - Core::Id { + PythonCore::Id { lit: String::from("arg2") } ); assert_eq!( *body, - Core::Float { + PythonCore::Float { float: String::from("2.4") } ); @@ -594,26 +600,26 @@ mod test { }); let (args, body) = match gen(&ASTTy::from(&anon_fun)) { - Ok(Core::AnonFun { args, body }) => (args, body), + Ok(PythonCore::AnonFun { args, body }) => (args, body), other => panic!("Expected anon fun but got: {other:?}."), }; assert_eq!(args.len(), 2); assert_eq!( args[0], - Core::Id { + PythonCore::Id { lit: String::from("first") } ); assert_eq!( args[1], - Core::Id { + PythonCore::Id { lit: String::from("second") } ); assert_eq!( *body, - Core::Str { + PythonCore::Str { string: String::from("this_string") } ); diff --git a/src/generate/convert/handle.rs b/src/backend/python/convert/handle.rs similarity index 82% rename from src/generate/convert/handle.rs rename to src/backend/python/convert/handle.rs index f299fe763..80b7b8469 100644 --- a/src/generate/convert/handle.rs +++ b/src/backend/python/convert/handle.rs @@ -1,14 +1,14 @@ +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::name::ToPy; +use crate::backend::python::result::{GenResult, UnimplementedErr}; use crate::check::ast::NodeTy; -use crate::generate::ast::node::Core; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::name::ToPy; -use crate::generate::result::{GenResult, UnimplementedErr}; use crate::{ASTTy, Context}; pub fn convert_handle(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context) -> GenResult { Ok(match &ast.node { - NodeTy::Raise { error } => Core::Raise { + NodeTy::Raise { error } => PythonCore::Raise { error: Box::from(convert_node(error, imp, state, ctx)?), }, @@ -24,9 +24,9 @@ pub fn convert_handle(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Conte }; let assign_state = state.must_assign_to(var.as_deref(), expr_or_stmt.ty.clone()); - Core::TryExcept { + PythonCore::TryExcept { setup: var.map(|var| { - Box::from(Core::VarDef { + Box::from(PythonCore::VarDef { var, ty, expr: None, @@ -53,10 +53,10 @@ pub fn convert_handle(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Conte )); let body = Box::from(convert_node(body, imp, &assign_state, ctx)?); - except.push(if *expr == Core::UnderScore { - Core::Except { class, body } + except.push(if *expr == PythonCore::UnderScore { + PythonCore::Except { class, body } } else { - Core::ExceptId { + PythonCore::ExceptId { id: expr, class, body, diff --git a/src/generate/convert/mod.rs b/src/backend/python/convert/mod.rs similarity index 75% rename from src/generate/convert/mod.rs rename to src/backend/python/convert/mod.rs index e54ff2032..bcf988d97 100644 --- a/src/generate/convert/mod.rs +++ b/src/backend/python/convert/mod.rs @@ -1,20 +1,20 @@ use std::convert::TryFrom; +use crate::backend::python::ast::node::{CoreOp, PythonCore}; +use crate::backend::python::convert::builder::convert_builder; +use crate::backend::python::convert::call::convert_call; +use crate::backend::python::convert::class::convert_class; +use crate::backend::python::convert::common::convert_vec; +use crate::backend::python::convert::control_flow::convert_cntrl_flow; +use crate::backend::python::convert::definition::convert_def; +use crate::backend::python::convert::handle::convert_handle; +use crate::backend::python::convert::range_slice::convert_range_slice; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::name::ToPy; +use crate::backend::python::result::{GenResult, UnimplementedErr}; use crate::check::ast::NodeTy; use crate::check::context::clss::concrete_to_python; use crate::check::name::Name; -use crate::generate::ast::node::{Core, CoreOp}; -use crate::generate::convert::builder::convert_builder; -use crate::generate::convert::call::convert_call; -use crate::generate::convert::class::convert_class; -use crate::generate::convert::common::convert_vec; -use crate::generate::convert::control_flow::convert_cntrl_flow; -use crate::generate::convert::definition::convert_def; -use crate::generate::convert::handle::convert_handle; -use crate::generate::convert::range_slice::convert_range_slice; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::name::ToPy; -use crate::generate::result::{GenResult, UnimplementedErr}; use crate::{ASTTy, Context}; mod builder; @@ -41,7 +41,7 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context from, import, alias, - } => Core::Import { + } => PythonCore::Import { from: if let Some(from) = from { Some(Box::from(convert_node(from, imp, state, ctx)?)) } else { @@ -54,19 +54,19 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context NodeTy::VariableDef { .. } | NodeTy::FunDef { .. } | NodeTy::FunArg { .. } => { convert_def(ast, imp, state, ctx)? } - NodeTy::Reassign { left, right, op } => Core::Assign { + NodeTy::Reassign { left, right, op } => PythonCore::Assign { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), op: CoreOp::try_from((ast, op))?, }, - NodeTy::Block { statements } => Core::Block { + NodeTy::Block { statements } => PythonCore::Block { statements: convert_vec(statements, imp, state, ctx)?, }, - NodeTy::Int { lit } => Core::Int { int: lit.clone() }, - NodeTy::Real { lit } => Core::Float { float: lit.clone() }, - NodeTy::ENum { num, exp } => Core::ENum { + NodeTy::Int { lit } => PythonCore::Int { int: lit.clone() }, + NodeTy::Real { lit } => PythonCore::Float { float: lit.clone() }, + NodeTy::ENum { num, exp } => PythonCore::ENum { num: num.clone(), exp: if exp.is_empty() { String::from("0") @@ -74,32 +74,32 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context exp.clone() }, }, - NodeTy::DocStr { lit } => Core::DocStr { + NodeTy::DocStr { lit } => PythonCore::DocStr { string: lit.clone(), }, - NodeTy::Str { lit, expressions } if expressions.is_empty() => Core::Str { + NodeTy::Str { lit, expressions } if expressions.is_empty() => PythonCore::Str { string: lit.clone(), }, - NodeTy::Str { lit, .. } => Core::FStr { + NodeTy::Str { lit, .. } => PythonCore::FStr { string: lit.clone(), }, - NodeTy::Undefined => Core::None, + NodeTy::Undefined => PythonCore::None, NodeTy::ExpressionType { expr, .. } => { convert_node(expr, imp, &state.expand_ty(true), ctx)? } - NodeTy::Id { lit } => Core::Id { + NodeTy::Id { lit } => PythonCore::Id { lit: concrete_to_python(lit), }, - NodeTy::Bool { lit } => Core::Bool { boolean: *lit }, + NodeTy::Bool { lit } => PythonCore::Bool { boolean: *lit }, - NodeTy::Tuple { elements } if state.tup_lit => Core::TupleLiteral { + NodeTy::Tuple { elements } if state.tup_lit => PythonCore::TupleLiteral { elements: convert_vec(elements, imp, state, ctx)?, }, - NodeTy::Tuple { elements } => Core::Tuple { + NodeTy::Tuple { elements } => PythonCore::Tuple { elements: convert_vec(elements, imp, state, ctx)?, }, - NodeTy::List { elements } => Core::List { + NodeTy::List { elements } => PythonCore::List { elements: convert_vec(elements, imp, state, ctx)?, }, NodeTy::Dict { elements } => { @@ -109,14 +109,14 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context let to = convert_node(to, imp, state, ctx)?; converted.push((from, to)); } - Core::Dictionary { + PythonCore::Dictionary { elements: converted, } } - NodeTy::Set { elements } => Core::Set { + NodeTy::Set { elements } => PythonCore::Set { elements: convert_vec(elements, imp, state, ctx)?, }, - NodeTy::Index { item, range } => Core::Index { + NodeTy::Index { item, range } => PythonCore::Index { item: Box::from(convert_node(item, imp, state, ctx)?), range: Box::from(convert_node(range, imp, state, ctx)?), }, @@ -125,13 +125,13 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context NodeTy::ListBuilder { .. } => convert_builder(ast, imp, state, ctx)?, NodeTy::SetBuilder { .. } => convert_builder(ast, imp, state, ctx)?, - NodeTy::ReturnEmpty => Core::Return { - expr: Box::from(Core::None), + NodeTy::ReturnEmpty => PythonCore::Return { + expr: Box::from(PythonCore::None), }, NodeTy::Return { expr } if state.is_remove_last_ret => { convert_node(expr, imp, &state.remove_ret(false), ctx)? } - NodeTy::Return { expr } => Core::Return { + NodeTy::Return { expr } => PythonCore::Return { expr: Box::from(convert_node(expr, imp, state, ctx)?), }, @@ -141,80 +141,80 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context convert_cntrl_flow(ast, imp, state, ctx)? } - NodeTy::Not { expr } => Core::Not { + NodeTy::Not { expr } => PythonCore::Not { expr: Box::from(convert_node(expr, imp, state, ctx)?), }, - NodeTy::And { left, right } => Core::And { + NodeTy::And { left, right } => PythonCore::And { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Or { left, right } => Core::Or { + NodeTy::Or { left, right } => PythonCore::Or { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Eq { left, right } => Core::Eq { + NodeTy::Eq { left, right } => PythonCore::Eq { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Neq { left, right } => Core::Neq { + NodeTy::Neq { left, right } => PythonCore::Neq { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Add { left, right } => Core::Add { + NodeTy::Add { left, right } => PythonCore::Add { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Sub { left, right } => Core::Sub { + NodeTy::Sub { left, right } => PythonCore::Sub { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Mul { left, right } => Core::Mul { + NodeTy::Mul { left, right } => PythonCore::Mul { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Div { left, right } => Core::Div { + NodeTy::Div { left, right } => PythonCore::Div { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::FDiv { left, right } => Core::FDiv { + NodeTy::FDiv { left, right } => PythonCore::FDiv { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Mod { left, right } => Core::Mod { + NodeTy::Mod { left, right } => PythonCore::Mod { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Pow { left, right } => Core::Pow { + NodeTy::Pow { left, right } => PythonCore::Pow { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::AddU { expr } => Core::AddU { + NodeTy::AddU { expr } => PythonCore::AddU { expr: Box::from(convert_node(expr, imp, state, ctx)?), }, - NodeTy::SubU { expr } => Core::SubU { + NodeTy::SubU { expr } => PythonCore::SubU { expr: Box::from(convert_node(expr, imp, state, ctx)?), }, NodeTy::Sqrt { expr } => { imp.add_import("math"); - Core::Sqrt { + PythonCore::Sqrt { expr: Box::from(convert_node(expr, imp, state, ctx)?), } } - NodeTy::Le { left, right } => Core::Le { + NodeTy::Le { left, right } => PythonCore::Le { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Leq { left, right } => Core::Leq { + NodeTy::Leq { left, right } => PythonCore::Leq { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Ge { left, right } => Core::Ge { + NodeTy::Ge { left, right } => PythonCore::Ge { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, - NodeTy::Geq { left, right } => Core::Geq { + NodeTy::Geq { left, right } => PythonCore::Geq { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, @@ -222,19 +222,19 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context NodeTy::FunctionCall { .. } | NodeTy::PropertyCall { .. } => { convert_call(ast, imp, state, ctx)? } - NodeTy::AnonFun { args, body } => Core::AnonFun { + NodeTy::AnonFun { args, body } => PythonCore::AnonFun { args: convert_vec(args, imp, &state.expand_ty(false), ctx)?, body: Box::from(convert_node(body, imp, state, ctx)?), }, - NodeTy::In { left, right } => Core::In { + NodeTy::In { left, right } => PythonCore::In { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, NodeTy::Range { .. } | NodeTy::Slice { .. } => convert_range_slice(ast, imp, state, ctx)?, - NodeTy::Underscore => Core::UnderScore, - NodeTy::Question { left, right } => Core::Or { + NodeTy::Underscore => PythonCore::UnderScore, + NodeTy::Question { left, right } => PythonCore::Or { left: Box::from(convert_node(left, imp, state, ctx)?), right: Box::from(convert_node(right, imp, state, ctx)?), }, @@ -251,20 +251,20 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context resource, alias: Some((alias, ..)), expr, - } => Core::WithAs { + } => PythonCore::WithAs { resource: Box::from(convert_node(resource, imp, state, ctx)?), alias: Box::from(convert_node(alias, imp, &state.expand_ty(false), ctx)?), expr: Box::from(convert_node(expr, imp, state, ctx)?), }, - NodeTy::With { resource, expr, .. } => Core::With { + NodeTy::With { resource, expr, .. } => PythonCore::With { resource: Box::from(convert_node(resource, imp, state, ctx)?), expr: Box::from(convert_node(expr, imp, state, ctx)?), }, NodeTy::Raise { .. } | NodeTy::Handle { .. } => convert_handle(ast, imp, state, ctx)?, - NodeTy::Pass => Core::Pass, - _ => Core::Empty, + NodeTy::Pass => PythonCore::Pass, + _ => PythonCore::Empty, }; let core = if let Some((assign_to, name)) = must_assign_to { @@ -282,39 +282,44 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context Ok(core) } -fn append_assign(core: &Core, assign_to: &Core, name: &Option, imp: &mut Imports) -> Core { +fn append_assign( + core: &PythonCore, + assign_to: &PythonCore, + name: &Option, + imp: &mut Imports, +) -> PythonCore { match &core { - Core::Block { ref statements } => match statements.last() { + PythonCore::Block { ref statements } => match statements.last() { Some(last) => { let last = append_assign(last, assign_to, name, imp); - let (mut statements, idx): (Vec, usize) = + let (mut statements, idx): (Vec, usize) = (statements.clone(), statements.len() - 1); statements[idx] = last; - Core::Block { statements } + PythonCore::Block { statements } } None => core.clone(), }, - Core::IfElse { cond, then, el } => Core::IfElse { + PythonCore::IfElse { cond, then, el } => PythonCore::IfElse { cond: cond.clone(), then: Box::from(append_assign(then, assign_to, name, imp)), el: Box::from(append_assign(el, assign_to, name, imp)), }, - Core::Match { expr, cases } => Core::Match { + PythonCore::Match { expr, cases } => PythonCore::Match { expr: expr.clone(), cases: cases .iter() .map(|c| append_assign(c, assign_to, name, imp)) .collect(), }, - Core::Case { expr, body } => Core::Case { + PythonCore::Case { expr, body } => PythonCore::Case { expr: expr.clone(), body: Box::from(append_assign(body, assign_to, name, imp)), }, - Core::TryExcept { + PythonCore::TryExcept { setup, attempt, except, - } => Core::TryExcept { + } => PythonCore::TryExcept { setup: setup.clone(), attempt: Box::from(append_assign(attempt, assign_to, name, imp)), except: except @@ -322,17 +327,17 @@ fn append_assign(core: &Core, assign_to: &Core, name: &Option, imp: &mut I .map(|e| append_assign(e, assign_to, name, imp)) .collect(), }, - Core::ExceptId { id, class, body } => Core::ExceptId { + PythonCore::ExceptId { id, class, body } => PythonCore::ExceptId { id: id.clone(), class: class.clone(), body: Box::from(append_assign(body, assign_to, name, imp)), }, - Core::Except { class, body } => Core::Except { + PythonCore::Except { class, body } => PythonCore::Except { class: class.clone(), body: Box::from(append_assign(body, assign_to, name, imp)), }, expr if skip_assign(expr) => core.clone(), - _ => Core::VarDef { + _ => PythonCore::VarDef { var: Box::from(assign_to.clone()), ty: name.clone().map(|name| Box::from(name.to_py(imp))), expr: Option::from(Box::from(core.clone())), @@ -340,73 +345,73 @@ fn append_assign(core: &Core, assign_to: &Core, name: &Option, imp: &mut I } } -fn append_ret(core: &Core) -> Core { +fn append_ret(core: &PythonCore) -> PythonCore { match core { - Core::Block { ref statements } => match statements.last() { + PythonCore::Block { ref statements } => match statements.last() { Some(last) => { let last = append_ret(last); - let (mut statements, idx): (Vec, usize) = + let (mut statements, idx): (Vec, usize) = (statements.clone(), statements.len() - 1); statements[idx] = last; - Core::Block { statements } + PythonCore::Block { statements } } - None => Core::Block { - statements: vec![Core::Return { - expr: Box::from(Core::None), + None => PythonCore::Block { + statements: vec![PythonCore::Return { + expr: Box::from(PythonCore::None), }], }, }, - Core::IfElse { cond, then, el } => Core::IfElse { + PythonCore::IfElse { cond, then, el } => PythonCore::IfElse { cond: cond.clone(), then: Box::from(append_ret(then)), el: Box::from(append_ret(el)), }, - Core::Match { expr, cases } => Core::Match { + PythonCore::Match { expr, cases } => PythonCore::Match { expr: expr.clone(), cases: cases.iter().map(append_ret).collect(), }, - Core::Case { expr, body } => Core::Case { + PythonCore::Case { expr, body } => PythonCore::Case { expr: expr.clone(), body: Box::from(append_ret(body)), }, - Core::TryExcept { + PythonCore::TryExcept { setup, attempt, except, - } => Core::TryExcept { + } => PythonCore::TryExcept { setup: setup.clone(), attempt: Box::from(append_ret(attempt)), except: except.iter().map(append_ret).collect(), }, - Core::ExceptId { id, class, body } => Core::ExceptId { + PythonCore::ExceptId { id, class, body } => PythonCore::ExceptId { id: id.clone(), class: class.clone(), body: Box::from(append_ret(body)), }, - Core::Except { class, body } => Core::Except { + PythonCore::Except { class, body } => PythonCore::Except { class: class.clone(), body: Box::from(append_ret(body)), }, core if skip_return(core) => core.clone(), - _ => Core::Return { + _ => PythonCore::Return { expr: Box::from(core.clone()), }, } } -fn skip_assign(core: &Core) -> bool { - skip_return(core) || matches!(core, Core::VarDef { .. } | Core::Assign { .. }) +fn skip_assign(core: &PythonCore) -> bool { + skip_return(core) || matches!(core, PythonCore::VarDef { .. } | PythonCore::Assign { .. }) } -fn skip_return(core: &Core) -> bool { - matches!(core, Core::Return { .. } | Core::Raise { .. }) +fn skip_return(core: &PythonCore) -> bool { + matches!(core, PythonCore::Return { .. } | PythonCore::Raise { .. }) } #[cfg(test)] mod tests { + use crate::backend::python::ast::node::PythonCore; + use crate::backend::python::gen; use crate::common::position::Position; - use crate::generate::ast::node::Core; - use crate::generate::gen; use crate::parse::ast::Node; use crate::parse::ast::AST; use crate::ASTTy; @@ -429,19 +434,19 @@ mod tests { #[test] fn break_verify() { let _break = to_pos!(Node::Break); - assert_eq!(gen(&ASTTy::from(&_break)).unwrap(), Core::Break); + assert_eq!(gen(&ASTTy::from(&_break)).unwrap(), PythonCore::Break); } #[test] fn continue_verify() { let _continue = to_pos!(Node::Continue); - assert_eq!(gen(&ASTTy::from(&_continue)).unwrap(), Core::Continue); + assert_eq!(gen(&ASTTy::from(&_continue)).unwrap(), PythonCore::Continue); } #[test] fn pass_verify() { let pass = to_pos!(Node::Pass); - assert_eq!(gen(&ASTTy::from(&pass)).unwrap(), Core::Pass); + assert_eq!(gen(&ASTTy::from(&pass)).unwrap(), PythonCore::Pass); } #[test] @@ -454,8 +459,8 @@ mod tests { assert_eq!( gen(&ASTTy::from(&print_stmt)).unwrap(), - Core::Return { - expr: Box::from(Core::Str { + PythonCore::Return { + expr: Box::from(PythonCore::Str { string: String::from("a") }) } @@ -467,8 +472,8 @@ mod tests { let print_stmt = to_pos!(Node::ReturnEmpty); assert_eq!( gen(&ASTTy::from(&print_stmt)).unwrap(), - Core::Return { - expr: Box::from(Core::None) + PythonCore::Return { + expr: Box::from(PythonCore::None) } ); } @@ -487,12 +492,12 @@ mod tests { assert_eq!( gen(&ASTTy::from(&_break)).unwrap(), - Core::Import { + PythonCore::Import { from: None, - import: vec![Core::Id { + import: vec![PythonCore::Id { lit: String::from("a") }], - alias: vec![Core::Id { + alias: vec![PythonCore::Id { lit: String::from("b") }], } @@ -513,19 +518,19 @@ mod tests { }); let (left, right) = match gen(&ASTTy::from(&add_node)) { - Ok(Core::$ast { left, right }) => (left, right), + Ok(PythonCore::$ast { left, right }) => (left, right), other => panic!("Expected binary operation but was {:?}", other), }; assert_eq!( *left, - Core::Id { + PythonCore::Id { lit: String::from("left") } ); assert_eq!( *right, - Core::Id { + PythonCore::Id { lit: String::from("right") } ); @@ -540,13 +545,13 @@ mod tests { let add_node = to_pos!(Node::$ast { expr }); let expr_des = match gen(&ASTTy::from(&add_node)) { - Ok(Core::$ast { expr }) => expr, + Ok(PythonCore::$ast { expr }) => expr, other => panic!("Expected unary operation but was {:?}", other), }; assert_eq!( *expr_des, - Core::Id { + PythonCore::Id { lit: String::from("expression") } ); @@ -601,15 +606,15 @@ mod tests { let add_node = to_pos!(Node::Sqrt { expr }); let (import, expr_des) = match gen(&ASTTy::from(&add_node)) { - Ok(Core::Block { statements }) => (statements[0].clone(), statements[1].clone()), + Ok(PythonCore::Block { statements }) => (statements[0].clone(), statements[1].clone()), other => panic!("Expected unary operation but was {other:?}"), }; assert_eq!( import, - Core::Import { + PythonCore::Import { from: None, - import: vec![Core::Id { + import: vec![PythonCore::Id { lit: String::from("math") }], alias: vec![], @@ -617,8 +622,8 @@ mod tests { ); assert_eq!( expr_des, - Core::Sqrt { - expr: Box::from(Core::Id { + PythonCore::Sqrt { + expr: Box::from(PythonCore::Id { lit: String::from("expression") }) } @@ -680,20 +685,20 @@ mod tests { let core = gen(&ASTTy::from(&tuple)); let core_elements = match core { - Ok(Core::Tuple { elements }) => elements, + Ok(PythonCore::Tuple { elements }) => elements, other => panic!("Expected tuple but got {other:?}"), }; assert_eq!( core_elements[0], - Core::ENum { + PythonCore::ENum { num: String::from("a"), exp: String::from("100") } ); assert_eq!( core_elements[1], - Core::Float { + PythonCore::Float { float: String::from("3000.5") } ); @@ -713,17 +718,17 @@ mod tests { let core = gen(&ASTTy::from(&set)); let core_elements = match core { - Ok(Core::Set { elements }) => elements, + Ok(PythonCore::Set { elements }) => elements, other => panic!("Expected set but got {other:?}"), }; assert_eq!( core_elements[0], - Core::Id { + PythonCore::Id { lit: String::from("a") } ); - assert_eq!(core_elements[1], Core::Bool { boolean: true }); + assert_eq!(core_elements[1], PythonCore::Bool { boolean: true }); } #[test] @@ -741,20 +746,20 @@ mod tests { let core = gen(&ASTTy::from(&tuple)); let core_elements = match core { - Ok(Core::List { elements }) => elements, + Ok(PythonCore::List { elements }) => elements, other => panic!("Expected tuple but got {other:?}"), }; assert_eq!( core_elements[0], - Core::ENum { + PythonCore::ENum { num: String::from("a"), exp: String::from("100") } ); assert_eq!( core_elements[1], - Core::Float { + PythonCore::Float { float: String::from("3000.5") } ); @@ -805,7 +810,7 @@ mod tests { expr }); - let Ok(Core::WithAs { + let Ok(PythonCore::WithAs { resource, alias, expr, @@ -816,19 +821,19 @@ mod tests { assert_eq!( *resource, - Core::Id { + PythonCore::Id { lit: String::from("my_resource") } ); assert_eq!( *alias, - Core::Id { + PythonCore::Id { lit: String::from("other") } ); assert_eq!( *expr, - Core::Int { + PythonCore::Int { int: String::from("9") } ); @@ -849,19 +854,19 @@ mod tests { }); let (resource, expr) = match gen(&ASTTy::from(&with)) { - Ok(Core::With { resource, expr }) => (resource, expr), + Ok(PythonCore::With { resource, expr }) => (resource, expr), other => panic!("Expected with but was {other:?}"), }; assert_eq!( *resource, - Core::Id { + PythonCore::Id { lit: String::from("other") } ); assert_eq!( *expr, - Core::Int { + PythonCore::Int { int: String::from("2341") } ); @@ -878,7 +883,7 @@ mod tests { }); let (setup, _try, except) = match gen(&ASTTy::from(&handle)) { - Ok(Core::TryExcept { + Ok(PythonCore::TryExcept { setup, attempt, except, @@ -889,7 +894,7 @@ mod tests { assert_eq!(setup, None); assert_eq!( *_try, - Core::Id { + PythonCore::Id { lit: String::from("my_fun") } ); @@ -922,7 +927,7 @@ mod tests { cases: vec![case] }); - let Ok(Core::TryExcept { + let Ok(PythonCore::TryExcept { setup, attempt, except, @@ -937,31 +942,31 @@ mod tests { assert_eq!(setup, None); assert_eq!( *attempt, - Core::Id { + PythonCore::Id { lit: String::from("my_fun") } ); assert_eq!(except.len(), 1); - let Core::ExceptId { id, class, body } = &except[0] else { + let PythonCore::ExceptId { id, class, body } = &except[0] else { panic!("Expected except case but was {:?}", except[0]) }; assert_eq!( *id, - Box::from(Core::Id { + Box::from(PythonCore::Id { lit: String::from("err") }) ); assert_eq!( *class, - Box::from(Core::Type { + Box::from(PythonCore::Type { lit: String::from("my_type"), generics: vec![] }) ); assert_eq!( *body, - Box::from(Core::Int { + Box::from(PythonCore::Int { int: String::from("9999") }) ); diff --git a/src/generate/convert/range_slice.rs b/src/backend/python/convert/range_slice.rs similarity index 74% rename from src/generate/convert/range_slice.rs rename to src/backend/python/convert/range_slice.rs index 77fffee1a..6f9d2f271 100644 --- a/src/generate/convert/range_slice.rs +++ b/src/backend/python/convert/range_slice.rs @@ -1,9 +1,9 @@ +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::result::{GenResult, UnimplementedErr}; use crate::check::ast::NodeTy; use crate::check::context::clss; -use crate::generate::ast::node::Core; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::result::{GenResult, UnimplementedErr}; use crate::{ASTTy, Context}; pub fn convert_range_slice( @@ -18,16 +18,16 @@ pub fn convert_range_slice( to, inclusive, step, - } => Ok(Core::FunctionCall { - function: Box::from(Core::Id { + } => Ok(PythonCore::FunctionCall { + function: Box::from(PythonCore::Id { lit: String::from(clss::python::RANGE), }), args: vec![ convert_node(from, imp, state, ctx)?, if *inclusive { - Core::Add { + PythonCore::Add { left: Box::from(convert_node(to, imp, state, ctx)?), - right: Box::from(Core::Int { + right: Box::from(PythonCore::Int { int: String::from("1"), }), } @@ -37,7 +37,7 @@ pub fn convert_range_slice( if let Some(step) = step { convert_node(step, imp, state, ctx)? } else { - Core::Int { + PythonCore::Int { int: String::from("1"), } }, @@ -48,16 +48,16 @@ pub fn convert_range_slice( to, inclusive, step, - } => Ok(Core::FunctionCall { - function: Box::from(Core::Id { + } => Ok(PythonCore::FunctionCall { + function: Box::from(PythonCore::Id { lit: String::from(clss::python::SLICE), }), args: vec![ convert_node(from, imp, state, ctx)?, if !inclusive { - Core::Sub { + PythonCore::Sub { left: Box::from(convert_node(to, imp, state, ctx)?), - right: Box::from(Core::Int { + right: Box::from(PythonCore::Int { int: String::from("1"), }), } @@ -67,7 +67,7 @@ pub fn convert_range_slice( if let Some(step) = step { convert_node(step, imp, state, ctx)? } else { - Core::Int { + PythonCore::Int { int: String::from("1"), } }, diff --git a/src/generate/convert/state.rs b/src/backend/python/convert/state.rs similarity index 82% rename from src/generate/convert/state.rs rename to src/backend/python/convert/state.rs index a7867ad3b..cce76a24b 100644 --- a/src/generate/convert/state.rs +++ b/src/backend/python/convert/state.rs @@ -2,9 +2,9 @@ use std::collections::BTreeMap; use itertools::Itertools; +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::GenArguments; use crate::check::name::Name; -use crate::generate::ast::node::Core; -use crate::generate::GenArguments; #[derive(Clone, Debug)] pub struct State { @@ -19,7 +19,7 @@ pub struct State { pub annotate: bool, pub is_last_must_be_ret: bool, - pub must_assign_to: Option<(Core, Option)>, + pub must_assign_to: Option<(PythonCore, Option)>, pub is_remove_last_ret: bool, } @@ -96,7 +96,7 @@ impl State { } } - pub fn must_assign_to(&self, must_assign_to: Option<&Core>, name: Option) -> State { + pub fn must_assign_to(&self, must_assign_to: Option<&PythonCore>, name: Option) -> State { if let Some(must_assign_to) = must_assign_to { State { must_assign_to: Some((must_assign_to.clone(), name)), @@ -112,8 +112,8 @@ impl State { } pub struct Imports { - imports: Vec, - from_imports: BTreeMap, + imports: Vec, + from_imports: BTreeMap, } impl Default for Imports { @@ -131,9 +131,9 @@ impl Imports { } pub fn add_import(&mut self, import: &str) { - let import = Core::Import { + let import = PythonCore::Import { from: None, - import: vec![Core::Id { + import: vec![PythonCore::Id { lit: String::from(import), }], alias: vec![], @@ -144,29 +144,29 @@ impl Imports { } pub fn add_from_import(&mut self, from: &str, import: &str) { - if let Some(Core::Import { + if let Some(PythonCore::Import { import: imports, alias, .. }) = self.from_imports.get(&String::from(from)) { - let new = Core::Id { + let new = PythonCore::Id { lit: String::from(import), }; - let imports: Vec = if !imports.contains(&new) { + let imports: Vec = if !imports.contains(&new) { imports.clone().into_iter().chain(vec![new]).collect() } else { imports.to_vec() }; - let import = Core::Import { - from: Some(Box::from(Core::Id { + let import = PythonCore::Import { + from: Some(Box::from(PythonCore::Id { lit: String::from(from), })), import: imports .iter() .sorted_by_key(|c| match c { - Core::Id { lit } => lit.clone(), + PythonCore::Id { lit } => lit.clone(), _ => String::from(""), }) .cloned() @@ -177,11 +177,11 @@ impl Imports { return; } - let import = Core::Import { - from: Some(Box::from(Core::Id { + let import = PythonCore::Import { + from: Some(Box::from(PythonCore::Id { lit: String::from(from), })), - import: vec![Core::Id { + import: vec![PythonCore::Id { lit: String::from(import), }], alias: vec![], @@ -193,7 +193,7 @@ impl Imports { self.imports.is_empty() && self.from_imports.is_empty() } - pub fn imports(&self) -> Vec { + pub fn imports(&self) -> Vec { let mut statements = self.imports.clone(); statements.append(&mut self.from_imports.clone().into_values().collect()); statements diff --git a/src/generate/mod.rs b/src/backend/python/mod.rs similarity index 71% rename from src/generate/mod.rs rename to src/backend/python/mod.rs index 1d2ff36dc..ba707fe10 100644 --- a/src/generate/mod.rs +++ b/src/backend/python/mod.rs @@ -1,8 +1,8 @@ +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::convert::convert_node; +use crate::backend::python::convert::state::{Imports, State}; +use crate::backend::python::result::GenResult; use crate::check::ast::ASTTy; -use crate::generate::ast::node::Core; -use crate::generate::convert::convert_node; -use crate::generate::convert::state::{Imports, State}; -use crate::generate::result::GenResult; use crate::{Context, PipelineArguments}; mod convert; @@ -25,12 +25,12 @@ impl From<&PipelineArguments> for GenArguments { } } -/// Consumes the given [AST](mamba::parser::ast::AST) and produces -/// a [Core](mamba::generate.ast::construct::Core) node. +/// Consumes the given [AST](mamba::parse::ast::AST) and produces +/// a [PythonCore](mamba::backend::python::ast::node::PythonCore) node. /// -/// Note that the given [AST](mamba::parser::ast::AST) must be +/// Note that the given [AST](mamba::parse::ast::AST) must be /// correctly formed. Therefore, malformed -/// [AST](mamba::parser::ast::AST)'s should be caught by either +/// [AST](mamba::parse::ast::AST)'s should be caught by either /// the parser or the type checker. /// /// # Examples @@ -39,15 +39,15 @@ impl From<&PipelineArguments> for GenArguments { /// # use mamba::check::ast::ASTTy; /// # use mamba::parse::ast::Node; /// # use mamba::parse::ast::AST; -/// # use mamba::generate::ast::node::Core; +/// # use mamba::backend::python::ast::node::PythonCore; /// # use mamba::common::position::{CaretPos, Position}; -/// # use mamba::generate::gen; +/// # use mamba::backend::python::gen; /// let node = Node::ReturnEmpty; /// let ast = AST::new(Position::new(CaretPos::new(1, 1), CaretPos::new(1, 5)), node); /// let ast_ty = ASTTy::from(&ast); /// let core_result = gen(&ast_ty).unwrap(); /// -/// assert_eq!(core_result, Core::Return { expr: Box::from(Core::None) }); +/// assert_eq!(core_result, PythonCore::Return { expr: Box::from(PythonCore::None) }); /// ``` /// /// # Failures @@ -58,9 +58,9 @@ impl From<&PipelineArguments> for GenArguments { /// # use mamba::check::ast::ASTTy; /// # use mamba::parse::ast::Node; /// # use mamba::parse::ast::AST; -/// # use mamba::generate::ast::node::Core; +/// # use mamba::backend::python::ast::node::PythonCore; /// # use mamba::common::position::{CaretPos, Position}; -/// # use mamba::generate::gen; +/// # use mamba::backend::python::gen; /// let cond_node = Node::Int { lit: String::from("56") }; /// let cond_pos = AST::new(Position::new(CaretPos::new(0, 0), CaretPos::new(0, 5)), cond_node); /// let node = Node::Condition { cond: Box::from(cond_pos), el: None }; @@ -80,10 +80,10 @@ pub fn gen_arguments(ast_ty: &ASTTy, gen_args: &GenArguments, ctx: &Context) -> let import = &mut Imports::new(); match convert_node(ast_ty, import, &state, ctx)? { - Core::Block { statements } => Ok(Core::Block { + PythonCore::Block { statements } => Ok(PythonCore::Block { statements: import.imports().into_iter().chain(statements).collect(), }), - other if !import.is_empty() => Ok(Core::Block { + other if !import.is_empty() => Ok(PythonCore::Block { statements: import.imports().into_iter().chain(vec![other]).collect(), }), other => Ok(other), diff --git a/src/generate/name.rs b/src/backend/python/name.rs similarity index 84% rename from src/generate/name.rs rename to src/backend/python/name.rs index 053263c7d..e87ff26de 100644 --- a/src/generate/name.rs +++ b/src/backend/python/name.rs @@ -1,20 +1,20 @@ use itertools::Itertools; +use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::convert::state::Imports; use crate::check::context::clss; use crate::check::context::clss::concrete_to_python; use crate::check::context::clss::python::{ANY, CALLABLE, TUPLE, UNION}; use crate::check::name::string_name::StringName; use crate::check::name::true_name::TrueName; use crate::check::name::{Empty, Name, Nullable, Union}; -use crate::generate::ast::node::Core; -use crate::generate::convert::state::Imports; pub trait ToPy { - fn to_py(&self, imp: &mut Imports) -> Core; + fn to_py(&self, imp: &mut Imports) -> PythonCore; } impl ToPy for Name { - fn to_py(&self, imp: &mut Imports) -> Core { + fn to_py(&self, imp: &mut Imports) -> PythonCore { if self.names.len() > 1 { imp.add_from_import("typing", UNION); let generics: Vec = self.names.iter().sorted().map(Name::from).collect(); @@ -22,13 +22,13 @@ impl ToPy for Name { } else if let Some(name) = self.names.iter().next() { name.to_py(imp) } else { - Core::Empty + PythonCore::Empty } } } impl ToPy for TrueName { - fn to_py(&self, imp: &mut Imports) -> Core { + fn to_py(&self, imp: &mut Imports) -> PythonCore { if self.is_nullable() { imp.add_from_import("typing", "Optional"); core_type("Optional", &[Name::from(&self.variant)], imp) @@ -39,7 +39,7 @@ impl ToPy for TrueName { } impl ToPy for StringName { - fn to_py(&self, imp: &mut Imports) -> Core { + fn to_py(&self, imp: &mut Imports) -> PythonCore { match self.name.as_str() { clss::UNION => self .generics @@ -69,8 +69,8 @@ impl ToPy for StringName { } } -fn core_type(lit: &str, generics: &[Name], imp: &mut Imports) -> Core { - Core::Type { +fn core_type(lit: &str, generics: &[Name], imp: &mut Imports) -> PythonCore { + PythonCore::Type { lit: String::from(lit), generics: generics.iter().map(|core| core.to_py(imp)).collect(), } diff --git a/src/generate/result.rs b/src/backend/python/result.rs similarity index 91% rename from src/generate/result.rs rename to src/backend/python/result.rs index 9cd4feef3..78dd9d584 100644 --- a/src/generate/result.rs +++ b/src/backend/python/result.rs @@ -2,12 +2,12 @@ use std::fmt; use std::fmt::{Display, Formatter}; use std::path::PathBuf; +use crate::backend::python::ast::node::PythonCore; use crate::common::position::Position; use crate::common::result::{format_err, WithSource}; -use crate::generate::ast::node::Core; use crate::ASTTy; -pub type GenResult = Result>; +pub type GenResult = Result>; const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/lib.rs b/src/lib.rs index 70e7e7ed7..1cfbe30bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,18 +4,19 @@ use std::path::{Path, PathBuf}; use log::{info, trace}; +use crate::backend::python::{gen_arguments, GenArguments}; +use crate::backend::Backend; use crate::check::ast::ASTTy; use crate::check::check; use crate::check::context::Context; use crate::check::result::TypeErr; use crate::common::result::WithSource; -use crate::generate::{gen_arguments, GenArguments}; use crate::parse::ast::AST; pub mod common; +pub mod backend; pub mod check; -pub mod generate; pub mod parse; pub mod cli; diff --git a/tests_util/src/lib.rs b/tests_util/src/lib.rs index 99ef9eab3..55f946889 100644 --- a/tests_util/src/lib.rs +++ b/tests_util/src/lib.rs @@ -37,7 +37,10 @@ impl Debug for OutTestErr { /// Test directory with default set to annotate output. pub fn test_directory(valid: bool, input: &[&str], output: &[&str], file_name: &str) -> OutTestRet { - let args = Arguments { annotate: true }; + let args = Arguments { + annotate: true, + ..Arguments::default() + }; test_directory_args(valid, input, output, file_name, &args) } From 298147b105a64ca53b0b526be27b8cdaaf393e7e Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Tue, 25 Aug 2026 17:00:05 +0200 Subject: [PATCH 04/16] feat: add simple compile to binary --- Cargo.lock | 383 +++++++++--- Cargo.toml | 13 +- README.md | 42 +- src/backend/cranelift/link.rs | 23 + src/backend/cranelift/lower.rs | 578 ++++++++++++++++++ src/backend/cranelift/mod.rs | 70 +++ src/backend/cranelift/result.rs | 69 +++ src/backend/cranelift/types.rs | 58 ++ src/cli.rs | 29 +- src/lib.rs | 152 +++-- src/main.rs | 10 + tests/README.md | 15 + tests/check/valid.rs | 3 +- tests/execution.rs | 58 ++ .../resource/valid/function/hello_world.mamba | 1 + tests/resource/valid/function/hello_world.py | 1 + tests_util/src/lib.rs | 22 + 17 files changed, 1370 insertions(+), 157 deletions(-) create mode 100644 src/backend/cranelift/link.rs create mode 100644 src/backend/cranelift/lower.rs create mode 100644 src/backend/cranelift/mod.rs create mode 100644 src/backend/cranelift/result.rs create mode 100644 src/backend/cranelift/types.rs create mode 100644 tests/execution.rs create mode 100644 tests/resource/valid/function/hello_world.mamba create mode 100644 tests/resource/valid/function/hello_world.py diff --git a/Cargo.lock b/Cargo.lock index 7f38eeb7b..c11bc332a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check 0.9.5", + "zerocopy", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -67,6 +79,12 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "assert_cmd" version = "0.10.2" @@ -103,19 +121,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] -name = "bytecount" -version = "0.3.2" +name = "bumpalo" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f861d9ce359f56dbcb6e0c2a1cb84e52ad732cadb57b806adeb3c7668caccbd8" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] -name = "cc" -version = "1.2.29" +name = "bytecount" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" -dependencies = [ - "shlex", -] +checksum = "f861d9ce359f56dbcb6e0c2a1cb84e52ad732cadb57b806adeb3c7668caccbd8" [[package]] name = "cfg-if" @@ -169,6 +184,142 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "cranelift-bforest" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ba4f80548f22dc9c43911907b5e322c5555544ee85f785115701e6a28c9abe1" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005884e3649c3e5ff2dc79e8a94b138f11569cc08a91244a292714d2a86e9156" + +[[package]] +name = "cranelift-codegen" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4036255ec33ce9a37495dfbcfc4e1118fd34e693eff9a1e106336b7cd16a9b" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.14.5", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7ca74f4b68319da11d39e894437cb6e20ec7c2e11fbbda823c3bf207beedff7" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e54f433a0269c4187871aa06d452214d5515d228d5bdc22219585e9eef895" + +[[package]] +name = "cranelift-control" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29cb4018f5bf59fb53f515fa9d80e6f8c5ce19f198dc538984ebd23ecf8965ec" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305399fd781a2953ac78c1396f02ff53144f39c33eb7fc7789cf4e8936d13a96" +dependencies = [ + "cranelift-bitset", +] + +[[package]] +name = "cranelift-frontend" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9230b460a128d53653456137751d27baf567947a3ab8c0c4d6e31fd08036d81e" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b961e24ae3ec9813a24a15ae64bbd2a42e4de4d79a7f3225a412e3b94e78d1c8" + +[[package]] +name = "cranelift-module" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f20b0b51ba962dac30fc7e812b86e4390d908acd4f59bcc8ac7610a8f3e0977" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", +] + +[[package]] +name = "cranelift-native" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5bd76df6c9151188dfa428c863b33da5b34561b67f43c0cf3f24a794f9fa1f" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-object" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee231640a7ecceedd0f1f2782d9288db6a6908cc70675ed9427e3bf0ea6daacd" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", + "cranelift-module", + "log", + "object", + "target-lexicon", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + [[package]] name = "difference" version = "2.0.0" @@ -181,6 +332,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.13" @@ -201,12 +358,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "getrandom" version = "0.3.3" @@ -219,12 +388,47 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -241,28 +445,13 @@ dependencies = [ ] [[package]] -name = "inkwell" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67349bd7578d4afebbe15eaa642a80b884e8623db74b1716611b131feb1deef" -dependencies = [ - "either", - "inkwell_internals", - "libc", - "llvm-sys", - "once_cell", - "thiserror", -] - -[[package]] -name = "inkwell_internals" -version = "0.11.0" +name = "indexmap" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f365c8de536236cfdebd0ba2130de22acefed18b1fb99c32783b3840aec5fb46" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ - "proc-macro2", - "quote", - "syn", + "equivalent", + "hashbrown 0.17.1", ] [[package]] @@ -286,12 +475,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.174" @@ -304,20 +487,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" -[[package]] -name = "llvm-sys" -version = "181.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d320f9d2723c97d4b78f9190a61ed25cc7cfbe456668c08e6e7dd8e50ceb8500" -dependencies = [ - "anyhow", - "cc", - "lazy_static", - "libc", - "regex-lite", - "semver", -] - [[package]] name = "log" version = "0.4.27" @@ -342,13 +511,18 @@ dependencies = [ "ansi_term", "assert_cmd", "clap", + "cranelift-codegen", + "cranelift-frontend", + "cranelift-module", + "cranelift-native", + "cranelift-object", "glob", - "inkwell", "itertools", "log", "loggerv", "pathdiff", "python-parser", + "target-lexicon", "tempfile", "test-case", "tests_util", @@ -367,7 +541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" dependencies = [ "memchr", - "version_check", + "version_check 0.1.5", ] [[package]] @@ -410,6 +584,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -494,10 +680,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "regex-lite" -version = "0.1.6" +name = "regalloc2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "12908dbeb234370af84d0579b9f68258a0f67e201412dd9a2814e6f45b2fc0f0" +dependencies = [ + "hashbrown 0.14.5", + "log", + "rustc-hash", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustix" @@ -518,12 +717,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - [[package]] name = "serde" version = "1.0.219" @@ -557,10 +750,22 @@ dependencies = [ ] [[package]] -name = "shlex" -version = "1.3.0" +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -579,6 +784,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.20.0" @@ -641,26 +852,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "unicode-ident" version = "1.0.18" @@ -691,6 +882,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -882,3 +1079,23 @@ name = "wtf8" version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b9309a86639c488a8eb2b5331cb5127cc9feb0a94a0db4b5d1ab5b84977956" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 25391864b..8992047f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,23 +12,24 @@ repository = "https://github.com/JSAbrahams/mamba" keywords = ["python", "transpiler", "compiler"] categories = ["parsing"] -[features] -default = ["machine-code"] -machine-code = ["inkwell"] - [dependencies] ansi_term = "0.12.1" # Colors in the terminal clap = { version = "4.5.40", features = ["derive"] } # Compiler frontend +cranelift-codegen = "0.114.0" # Machine code IR, instruction selection and register allocation +cranelift-frontend = "0.114.0" # Build Cranelift IR function bodies (SSA construction) +cranelift-module = "0.114.0" # Function/data declaration shared across Cranelift backends +cranelift-native = "0.114.0" # Detect the host target when --target is not given +cranelift-object = "0.114.0" # Emit a native object file from compiled Cranelift IR glob = "0.3.0" # Make traversing directories (Mamba projects) easier -inkwell = { version = "0.6.0", features = ["llvm18-1"], optional = true } # Relevant for generating machine code itertools = "0.8.2" # Tools to make iterating over collections easier log = "0.4" # Log frontend loggerv = "0.7" # Logger which accepts -vvv flag pathdiff = "0.1.0" # Traverse directories (Mamba projects) python-parser = "0.1.0" # Parse Python, both in tests and to generate Mamba IR +target-lexicon = "0.12.16" # Parse a --target triple for the Cranelift backend +tempfile = "3.1.0" # Stage object files before linking them into a --bin executable [dev-dependencies] assert_cmd = "0.10" # Assert commands work -tempfile = "3.1.0" # Create temporary files for intermediate results test-case = "3.3.1" # Parameterize tests tests_util = { path = "./tests_util" } diff --git a/README.md b/README.md index d828975c6..15b1ad216 100644 --- a/README.md +++ b/README.md @@ -753,31 +753,23 @@ end ## 💻 The Command Line Interface ``` -USAGE: - mamba.exe [FLAGS] [OPTIONS] - -FLAGS: - -a, --annotate Enable type annotation of the output source. - Currently still buggy feature. - -d, --debug Add line numbers to log statements - -h, --help Prints help information - -l, --level Print log level - --no-module-path Disable the module path in the log statements - --no-color Disable colorized output - -v Set level of verbosity - - v : info, error, warning printed to stderr (Default) - - vv : debug messages are printed - - vvv : trace messages are printed - -V, --version Prints version information - -OPTIONS: - -i, --input Input file or directory. - If file, file taken as input. - If directory, recursively search all sub-directories for *.mamba files. - If no input given, current directory used as input directory. - -o, --output Output directory to store Python files. - Output directory structure reflects input directory structure. - If no output given, 'target' directory created in current directory. +Transpile Mamba to Python code, or compile it to a native binary. + +Usage: mamba [OPTIONS] + +Options: + -i, --input Input file or directory. If file, file taken as input. If directory, recursively search all sub-directories for *.mamba files. If no input given, current directory used as input directory + -o, --output Output location. With `--python` (the default): output directory to store Python files, structured to reflect the input directory; if not given, a 'target' directory is created in the current directory. With `--bin`: path of the linked executable to produce; if not given, 'a.out' is created in the current directory + --python Output Python source (the default) + --bin Compile and link a native executable via the Cranelift backend, instead of outputting Python source. Only a small subset of the language is currentlysupported: literals, arithmetic and comparison operators, if/else, top-level function definitions and calls, and `print` + --target Target triple to pass to Cranelift, e.g. `x86_64-unknown-linux-gnu` (only meaningful with `--bin`; defaults to the host triple) + -v... Set level of verbosity: - `-v` : info, error, warning printed to stderr (default) - `-vv` : debug messages are printed - `-vvv` : trace messages are printed + -d, --debug Add line numbers to log statements + --no-module-path Disable the module path in the log statements + --no-color Disable colorized output + -l, --level Print log level + -a, --annotate Enable type annotation of the output source. Currently still buggy feature + -h, --help Print help ``` You can type `mamba -help` for a message containing roughly the above information. diff --git a/src/backend/cranelift/link.rs b/src/backend/cranelift/link.rs new file mode 100644 index 000000000..5980566fb --- /dev/null +++ b/src/backend/cranelift/link.rs @@ -0,0 +1,23 @@ +use std::path::Path; +use std::process::Command; + +/// Link the given object files into a single executable at `output`, by shelling out to the +/// system `cc` -- the same approach `rustc` itself uses, rather than reimplementing a linker. +pub fn link(object_paths: &[impl AsRef], output: &Path) -> Result<(), String> { + let mut cmd = Command::new("cc"); + cmd.args(object_paths.iter().map(AsRef::as_ref)); + cmd.arg("-o").arg(output); + + let result = cmd + .output() + .map_err(|e| format!("Could not run 'cc' to link the executable: {e}"))?; + + if result.status.success() { + Ok(()) + } else { + Err(format!( + "Linking failed:\n{}", + String::from_utf8_lossy(&result.stderr) + )) + } +} diff --git a/src/backend/cranelift/lower.rs b/src/backend/cranelift/lower.rs new file mode 100644 index 000000000..14e290e8f --- /dev/null +++ b/src/backend/cranelift/lower.rs @@ -0,0 +1,578 @@ +use std::collections::HashMap; + +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::{ + types, AbiParam, Function, InstBuilder, Signature, UserFuncName, Value, +}; +use cranelift_codegen::isa::CallConv; +use cranelift_codegen::Context as ClifContext; +use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; +use cranelift_module::{DataDescription, FuncId, Linkage, Module}; +use cranelift_object::ObjectModule; + +use crate::check::ast::{ASTTy, NodeTy}; +use crate::check::context::function::PRINT; +use crate::check::name::Name; +use crate::Context; + +use crate::backend::cranelift::result::{BackendErr, BackendResult}; +use crate::backend::cranelift::types::{cranelift_type, cranelift_type_of_name}; + +/// Declared user functions, keyed by their Mamba name -- shared across every function body so +/// forward references (function `a` calling function `b` defined later in the same file) work. +type Funcs = HashMap; + +/// Lower an entire checked file (a top-level `NodeTy::Block`) into `module`. +/// +/// Top-level `FunDef`s become real Cranelift functions. Every other top-level statement is +/// collected into a synthetic `main`, mirroring how a `.mamba` file's top-level statements run +/// top-to-bottom as a script in the Python backend -- machine code needs an explicit entry point, +/// which Python's linear script execution doesn't. +pub fn lower_program( + ast_ty: &ASTTy, + _ctx: &Context, + module: &mut ObjectModule, +) -> BackendResult<()> { + let statements = match &ast_ty.node { + NodeTy::Block { statements } => statements, + _ => std::slice::from_ref(ast_ty), + }; + + let call_conv = module.isa().default_call_conv(); + + // Pass 1: declare every top-level function's signature, so calls to a function defined later + // in the file still resolve. + let mut funcs = Funcs::new(); + for statement in statements { + if let NodeTy::FunDef { id, args, ret, .. } = &statement.node { + let name = fun_name(id)?; + let sig = fun_signature(args, ret.as_ref(), call_conv, statement)?; + let func_id = module + .declare_function(&name, Linkage::Export, &sig) + .map_err(|e| BackendErr::new(statement.pos, &e.to_string()))?; + funcs.insert(name, func_id); + } + } + + // Pass 2: define each function's body, plus a synthetic `main` for everything else. + let mut main_body = vec![]; + for statement in statements { + match &statement.node { + NodeTy::FunDef { + id, + args, + ret, + body, + .. + } => { + let name = fun_name(id)?; + let func_id = *funcs.get(&name).expect("declared in pass 1"); + let sig = fun_signature(args, ret.as_ref(), call_conv, statement)?; + define_function(module, func_id, sig, args, body.as_deref(), &funcs)?; + } + _ => main_body.push(statement.clone()), + } + } + + let main_sig = Signature { + params: vec![], + returns: vec![AbiParam::new(types::I32)], + call_conv, + }; + let main_id = module + .declare_function("main", Linkage::Export, &main_sig) + .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; + define_main(module, main_id, main_sig, &main_body, &funcs)?; + + Ok(()) +} + +fn fun_name(id: &ASTTy) -> BackendResult { + match &id.node { + NodeTy::Id { lit } => Ok(lit.clone()), + other => Err(BackendErr::unimplemented( + id, + &format!("{other:?} function name"), + )), + } +} + +fn fun_signature( + args: &[ASTTy], + ret: Option<&Name>, + call_conv: CallConv, + pos_ast: &ASTTy, +) -> BackendResult { + let mut params = vec![]; + for arg in args { + params.push(AbiParam::new(arg_type(arg)?)); + } + let returns = match ret { + Some(ret) => vec![AbiParam::new(cranelift_type_of_name(ret, pos_ast.pos)?)], + None => vec![], + }; + Ok(Signature { + params, + returns, + call_conv, + }) +} + +/// The Cranelift type of a `FunArg`'s declared parameter type -- which lives in the `FunArg` +/// variant's own `ty` field, not in the surrounding node's resolved type (see +/// `types::cranelift_type_of_name`'s doc comment). +fn arg_type(arg: &ASTTy) -> BackendResult { + match &arg.node { + NodeTy::FunArg { ty: Some(ty), .. } => cranelift_type_of_name(ty, arg.pos), + NodeTy::FunArg { ty: None, .. } => Err(BackendErr::new( + arg.pos, + "Function argument must have a type", + )), + other => Err(BackendErr::unimplemented( + arg, + &format!("{other:?} argument"), + )), + } +} + +/// Declare the two external `libc` functions `print` may need. Declared lazily -- and re-declared +/// per function via `Module::declare_function`, which is idempotent (merges with the existing +/// declaration of the same name) -- rather than threading a single shared declaration through. +fn declare_libc(module: &mut ObjectModule, call_conv: CallConv) -> BackendResult<(FuncId, FuncId)> { + let pointer_type = module.isa().pointer_type(); + + let puts_sig = Signature { + params: vec![AbiParam::new(pointer_type)], + returns: vec![AbiParam::new(types::I32)], + call_conv, + }; + let puts_id = module + .declare_function("puts", Linkage::Import, &puts_sig) + .map_err(|e| { + BackendErr::new( + crate::common::position::Position::invisible(), + &e.to_string(), + ) + })?; + + let printf_sig = Signature { + params: vec![AbiParam::new(pointer_type), AbiParam::new(types::I64)], + returns: vec![AbiParam::new(types::I32)], + call_conv, + }; + let printf_id = module + .declare_function("printf", Linkage::Import, &printf_sig) + .map_err(|e| { + BackendErr::new( + crate::common::position::Position::invisible(), + &e.to_string(), + ) + })?; + + Ok((puts_id, printf_id)) +} + +fn define_function( + module: &mut ObjectModule, + func_id: FuncId, + sig: Signature, + args: &[ASTTy], + body: Option<&ASTTy>, + funcs: &Funcs, +) -> BackendResult<()> { + let mut ctx = ClifContext::new(); + ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), sig); + let mut fb_ctx = FunctionBuilderContext::new(); + + { + let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx); + let entry = builder.create_block(); + builder.append_block_params_for_function_params(entry); + builder.switch_to_block(entry); + + let (puts_id, printf_id) = declare_libc(module, builder.func.signature.call_conv)?; + let mut lower = FnLower { + builder, + module, + vars: HashMap::new(), + var_seq: 0, + funcs, + puts_id, + printf_id, + }; + + let block_params: Vec = lower.builder.block_params(entry).to_vec(); + for (arg, value) in args.iter().zip(block_params) { + lower.bind_arg(arg, value)?; + } + + match body { + Some(body) => lower.lower_tail(body)?, + None => { + lower.builder.ins().return_(&[]); + } + } + + lower.builder.seal_all_blocks(); + lower.builder.finalize(); + } + + module.define_function(func_id, &mut ctx).map_err(|e| { + BackendErr::new( + crate::common::position::Position::invisible(), + &e.to_string(), + ) + }) +} + +fn define_main( + module: &mut ObjectModule, + func_id: FuncId, + sig: Signature, + statements: &[ASTTy], + funcs: &Funcs, +) -> BackendResult<()> { + let mut ctx = ClifContext::new(); + ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), sig); + let mut fb_ctx = FunctionBuilderContext::new(); + + { + let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx); + let entry = builder.create_block(); + builder.switch_to_block(entry); + + let (puts_id, printf_id) = declare_libc(module, builder.func.signature.call_conv)?; + let mut lower = FnLower { + builder, + module, + vars: HashMap::new(), + var_seq: 0, + funcs, + puts_id, + printf_id, + }; + + for statement in statements { + lower.lower_stmt(statement)?; + } + let zero = lower.builder.ins().iconst(types::I32, 0); + lower.builder.ins().return_(&[zero]); + + lower.builder.seal_all_blocks(); + lower.builder.finalize(); + } + + module.define_function(func_id, &mut ctx).map_err(|e| { + BackendErr::new( + crate::common::position::Position::invisible(), + &e.to_string(), + ) + }) +} + +/// Per-function-body lowering state. +struct FnLower<'a> { + builder: FunctionBuilder<'a>, + module: &'a mut ObjectModule, + vars: HashMap, + var_seq: u32, + funcs: &'a Funcs, + puts_id: FuncId, + printf_id: FuncId, +} + +impl<'a> FnLower<'a> { + fn new_var(&mut self, ty: cranelift_codegen::ir::Type) -> Variable { + let var = Variable::from_u32(self.var_seq); + self.var_seq += 1; + self.builder.declare_var(var, ty); + var + } + + fn bind_arg(&mut self, arg: &ASTTy, value: Value) -> BackendResult<()> { + let name = match &arg.node { + NodeTy::FunArg { var, .. } => fun_name(var)?, + _ => return Err(BackendErr::unimplemented(arg, "non-identifier argument")), + }; + let ty = arg_type(arg)?; + let var = self.new_var(ty); + self.builder.def_var(var, value); + self.vars.insert(name, (var, ty)); + Ok(()) + } + + /// Lower `ast` as a statement: for side effects only, its value (if any) is discarded. + fn lower_stmt(&mut self, ast: &ASTTy) -> BackendResult<()> { + match &ast.node { + NodeTy::VariableDef { + var, + expr: Some(expr), + .. + } => { + let name = fun_name(var)?; + let ty = cranelift_type(expr)?; + let value = self.lower_expr(expr)?; + let var = self.new_var(ty); + self.builder.def_var(var, value); + self.vars.insert(name, (var, ty)); + Ok(()) + } + NodeTy::IfElse { cond, then, el } => { + let cond_value = self.lower_expr(cond)?; + let then_block = self.builder.create_block(); + let merge_block = self.builder.create_block(); + let else_block = if el.is_some() { + self.builder.create_block() + } else { + merge_block + }; + + self.builder + .ins() + .brif(cond_value, then_block, &[], else_block, &[]); + + self.builder.switch_to_block(then_block); + self.lower_stmt(then)?; + self.builder.ins().jump(merge_block, &[]); + + if let Some(el) = el { + self.builder.switch_to_block(else_block); + self.lower_stmt(el)?; + self.builder.ins().jump(merge_block, &[]); + } + + self.builder.switch_to_block(merge_block); + Ok(()) + } + NodeTy::Block { statements } => { + for statement in statements { + self.lower_stmt(statement)?; + } + Ok(()) + } + NodeTy::FunctionCall { name, .. } if name.name == PRINT => { + self.lower_print(ast).map(|_| ()) + } + NodeTy::FunctionCall { .. } => self.lower_expr(ast).map(|_| ()), + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} statement"), + )), + } + } + + /// Lower `ast` as the tail of a function body: it must end the current block with a `return` + /// (possibly by recursing into `Block`'s last statement, or into each arm of an `IfElse`). + fn lower_tail(&mut self, ast: &ASTTy) -> BackendResult<()> { + match &ast.node { + NodeTy::Return { expr } => { + let value = self.lower_expr(expr)?; + self.builder.ins().return_(&[value]); + Ok(()) + } + NodeTy::ReturnEmpty => { + self.builder.ins().return_(&[]); + Ok(()) + } + NodeTy::Block { statements } => match statements.split_last() { + Some((last, init)) => { + for statement in init { + self.lower_stmt(statement)?; + } + self.lower_tail(last) + } + None => { + self.builder.ins().return_(&[]); + Ok(()) + } + }, + NodeTy::IfElse { cond, then, el } => { + let cond_value = self.lower_expr(cond)?; + let then_block = self.builder.create_block(); + let else_block = self.builder.create_block(); + + self.builder + .ins() + .brif(cond_value, then_block, &[], else_block, &[]); + + self.builder.switch_to_block(then_block); + self.lower_tail(then)?; + + self.builder.switch_to_block(else_block); + match el { + Some(el) => self.lower_tail(el)?, + None => { + self.builder.ins().return_(&[]); + } + } + Ok(()) + } + _ => { + let value = self.lower_expr(ast)?; + self.builder.ins().return_(&[value]); + Ok(()) + } + } + } + + /// Lower `ast` as a value-producing expression. + fn lower_expr(&mut self, ast: &ASTTy) -> BackendResult { + match &ast.node { + // Int/Bool literals' own resolved `ty` can come back widened to a union (e.g. a + // literal argument to `print`, whose parameter accepts several printable types + // unifies to that broader union rather than staying just `Int`) -- but the node + // variant itself already tells us the literal's true type, so there's no need to + // consult `ast.ty` at all here. + NodeTy::Int { lit } => { + let value: i64 = lit.parse().map_err(|_| { + BackendErr::new(ast.pos, &format!("Invalid int literal '{lit}'")) + })?; + Ok(self.builder.ins().iconst(types::I64, value)) + } + NodeTy::Bool { lit } => Ok(self.builder.ins().iconst(types::I8, i64::from(*lit))), + NodeTy::Id { lit } => { + let (var, _) = self.vars.get(lit).ok_or_else(|| { + BackendErr::new(ast.pos, &format!("Undefined variable '{lit}'")) + })?; + Ok(self.builder.use_var(*var)) + } + NodeTy::Add { left, right } => { + self.lower_arith(ast, left, right, |b, a, c| b.ins().iadd(a, c)) + } + NodeTy::Sub { left, right } => { + self.lower_arith(ast, left, right, |b, a, c| b.ins().isub(a, c)) + } + NodeTy::Mul { left, right } => { + self.lower_arith(ast, left, right, |b, a, c| b.ins().imul(a, c)) + } + NodeTy::Div { left, right } => { + self.lower_arith(ast, left, right, |b, a, c| b.ins().sdiv(a, c)) + } + NodeTy::Le { left, right } => self.lower_cmp(left, right, IntCC::SignedLessThan), + NodeTy::Leq { left, right } => { + self.lower_cmp(left, right, IntCC::SignedLessThanOrEqual) + } + NodeTy::Ge { left, right } => self.lower_cmp(left, right, IntCC::SignedGreaterThan), + NodeTy::Geq { left, right } => { + self.lower_cmp(left, right, IntCC::SignedGreaterThanOrEqual) + } + NodeTy::Eq { left, right } => self.lower_cmp(left, right, IntCC::Equal), + NodeTy::Neq { left, right } => self.lower_cmp(left, right, IntCC::NotEqual), + NodeTy::FunctionCall { name, args } => { + let func_id = *self.funcs.get(&name.name).ok_or_else(|| { + BackendErr::new(ast.pos, &format!("Undefined function '{}'", name.name)) + })?; + let local = self.module.declare_func_in_func(func_id, self.builder.func); + let mut arg_values = vec![]; + for arg in args { + arg_values.push(self.lower_expr(arg)?); + } + let call = self.builder.ins().call(local, &arg_values); + self.builder + .inst_results(call) + .first() + .copied() + .ok_or_else(|| { + BackendErr::new( + ast.pos, + &format!("'{}' does not return a value", name.name), + ) + }) + } + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} expression"), + )), + } + } + + fn lower_arith( + &mut self, + ast: &ASTTy, + left: &ASTTy, + right: &ASTTy, + op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, + ) -> BackendResult { + cranelift_type(ast)?; // reject non-primitive-typed arithmetic early, with a clear error + let l = self.lower_expr(left)?; + let r = self.lower_expr(right)?; + Ok(op(&mut self.builder, l, r)) + } + + fn lower_cmp(&mut self, left: &ASTTy, right: &ASTTy, cc: IntCC) -> BackendResult { + let l = self.lower_expr(left)?; + let r = self.lower_expr(right)?; + Ok(self.builder.ins().icmp(cc, l, r)) + } + + /// `print(...)`: a string literal goes through `puts` (which appends its own newline, like + /// Mamba/Python's `print`); a primitive value goes through `printf` with a fixed `%lld\n` + /// format. Anything else (interpolated strings, non-primitive values, multiple arguments) is + /// out of scope for this backend. + fn lower_print(&mut self, ast: &ASTTy) -> BackendResult> { + let args = match &ast.node { + NodeTy::FunctionCall { args, .. } => args, + _ => unreachable!("only called for a FunctionCall node"), + }; + let arg = match args.as_slice() { + [arg] => arg, + _ => return Err(BackendErr::unimplemented(ast, "print with != 1 argument")), + }; + + match &arg.node { + NodeTy::Str { lit, expressions } if expressions.is_empty() => { + let data = format!("{lit}\0").into_bytes().into_boxed_slice(); + let data_id = self + .module + .declare_anonymous_data(false, false) + .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; + let mut desc = DataDescription::new(); + desc.define(data); + self.module + .define_data(data_id, &desc) + .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; + + let gv = self.module.declare_data_in_func(data_id, self.builder.func); + let pointer_type = self.module.isa().pointer_type(); + let ptr = self.builder.ins().global_value(pointer_type, gv); + let callee = self + .module + .declare_func_in_func(self.puts_id, self.builder.func); + self.builder.ins().call(callee, &[ptr]); + Ok(None) + } + NodeTy::Str { .. } => Err(BackendErr::unimplemented( + ast, + "print of an interpolated string", + )), + _ => { + let value = self.lower_expr(arg)?; + let fmt = format!("{}\0", "%lld\n").into_bytes().into_boxed_slice(); + let data_id = self + .module + .declare_anonymous_data(false, false) + .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; + let mut desc = DataDescription::new(); + desc.define(fmt); + self.module + .define_data(data_id, &desc) + .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; + + let gv = self.module.declare_data_in_func(data_id, self.builder.func); + let pointer_type = self.module.isa().pointer_type(); + let ptr = self.builder.ins().global_value(pointer_type, gv); + // Widen a narrower-than-i64 value (e.g. Bool, stored as i8) to match `%lld`. + let value = if self.builder.func.dfg.value_type(value) != types::I64 { + self.builder.ins().sextend(types::I64, value) + } else { + value + }; + let callee = self + .module + .declare_func_in_func(self.printf_id, self.builder.func); + self.builder.ins().call(callee, &[ptr, value]); + Ok(None) + } + } + } +} diff --git a/src/backend/cranelift/mod.rs b/src/backend/cranelift/mod.rs new file mode 100644 index 000000000..16a0e8e08 --- /dev/null +++ b/src/backend/cranelift/mod.rs @@ -0,0 +1,70 @@ +use std::str::FromStr; + +use cranelift_codegen::isa::{self, OwnedTargetIsa}; +use cranelift_codegen::settings::{self}; +use cranelift_module::default_libcall_names; +use cranelift_object::{ObjectBuilder, ObjectModule}; +use target_lexicon::Triple; + +use crate::backend::cranelift::result::{BackendErr, BackendResult}; +use crate::check::ast::ASTTy; +use crate::common::position::Position; +use crate::Context; + +mod lower; +mod types; + +pub mod link; +pub mod result; + +/// Compile a single checked Mamba file to the bytes of a native object file. +/// +/// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`); if `None`, the host +/// triple is used. +pub fn compile(ast_ty: &ASTTy, ctx: &Context, target: Option<&str>) -> BackendResult> { + let isa = build_isa(target)?; + let builder = ObjectBuilder::new(isa, "mamba", default_libcall_names()) + .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; + let mut module = ObjectModule::new(builder); + + lower::lower_program(ast_ty, ctx, &mut module)?; + + module + .finish() + .emit() + .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string())) +} + +fn build_isa(target: Option<&str>) -> BackendResult { + let flag_builder = settings::builder(); + let flags = settings::Flags::new(flag_builder); + + match target { + Some(target) => { + let triple = Triple::from_str(target).map_err(|e| { + BackendErr::new( + Position::invisible(), + &format!("Invalid target '{target}': {e}"), + ) + })?; + isa::lookup(triple) + .map_err(|e| { + BackendErr::new( + Position::invisible(), + &format!("Unsupported target '{target}': {e}"), + ) + })? + .finish(flags) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string())) + } + None => cranelift_native::builder() + .map_err(|e| { + BackendErr::new( + Position::invisible(), + &format!("Unsupported host target: {e}"), + ) + })? + .finish(flags) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string())), + } +} diff --git a/src/backend/cranelift/result.rs b/src/backend/cranelift/result.rs new file mode 100644 index 000000000..bd8a6d4a1 --- /dev/null +++ b/src/backend/cranelift/result.rs @@ -0,0 +1,69 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; + +use crate::check::ast::ASTTy; +use crate::common::position::Position; +use crate::common::result::{format_err, WithSource}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub type BackendResult = Result>; + +#[derive(Debug, Clone)] +pub struct BackendErr { + pub position: Position, + pub msg: String, + pub source: Option, + pub path: Option, +} + +impl BackendErr { + /// A construct that the Cranelift backend doesn't (yet) support -- unlike the Python backend, + /// which aims to eventually support the whole language, this one is intentionally scoped to a + /// small subset (see `src/backend/cranelift/lower.rs`), so this is expected long-term, not + /// just a temporary gap. + pub fn unimplemented(ast: &ASTTy, msg: &str) -> Box { + let msg = format!( + "The {msg} construct is not supported by the machine-code backend (v{VERSION})" + ); + Box::from(BackendErr { + position: ast.pos, + msg, + source: None, + path: None, + }) + } + + pub fn new(position: Position, msg: &str) -> Box { + Box::from(BackendErr { + position, + msg: String::from(msg), + source: None, + path: None, + }) + } +} + +impl WithSource for BackendErr { + fn with_source(self, source: &Option, path: &Option) -> BackendErr { + BackendErr { + source: source.clone(), + path: path.clone(), + ..self + } + } +} + +impl Display for BackendErr { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + format_err( + f, + &self.msg, + &self.path, + Some(self.position), + &self.source, + &[], + ) + } +} diff --git a/src/backend/cranelift/types.rs b/src/backend/cranelift/types.rs new file mode 100644 index 000000000..542d34c32 --- /dev/null +++ b/src/backend/cranelift/types.rs @@ -0,0 +1,58 @@ +use cranelift_codegen::ir::types; +use cranelift_codegen::ir::Type; + +use crate::check::ast::ASTTy; +use crate::check::context::clss; +use crate::check::name::Name; +use crate::common::position::Position; + +use crate::backend::cranelift::result::{BackendErr, BackendResult}; + +/// The Cranelift type a resolved Mamba primitive lowers to. +/// +/// Scoped to exactly the primitives the Cranelift backend supports (see `lower.rs`): `Int`, +/// `Bool`, and `Float`. Anything else (a class, a collection, a union of more than one name, +/// an unresolved type) is out of scope for this backend. +pub fn cranelift_type(ast: &ASTTy) -> BackendResult { + let name = ast + .ty + .as_ref() + .ok_or_else(|| BackendErr::new(ast.pos, "Expression has no resolved type"))?; + cranelift_type_of_name(name, ast.pos) +} + +/// As [cranelift_type], but for a [Name] that isn't attached to an [ASTTy] node's own `ty` field +/// -- e.g. a `FunArg`'s declared parameter type, which lives in the `FunArg` variant itself +/// rather than in the surrounding node's resolved type. +pub fn cranelift_type_of_name(name: &Name, pos: Position) -> BackendResult { + primitive_name(name) + .and_then(|name| match name { + clss::INT => Some(types::I64), + clss::BOOL => Some(types::I8), + clss::FLOAT => Some(types::F64), + _ => None, + }) + .ok_or_else(|| { + BackendErr::new( + pos, + &format!("The '{name}' type is not supported by the machine-code backend"), + ) + }) +} + +/// The single primitive class name a resolved [Name] refers to, if it is exactly one non-generic, +/// non-nullable name -- i.e. not a union of multiple types, and not a generic instantiation like +/// `List[Int]`. Every type this backend supports is shaped this way. Mutability is *not* +/// disqualifying -- it's a property of the binding (and Mamba function arguments are mutable by +/// default), not of the underlying machine representation, which is identical either way. +fn primitive_name(name: &Name) -> Option<&str> { + let mut names = name.names.iter(); + let true_name = names.next()?; + if names.next().is_some() { + return None; // union of more than one type + } + if true_name.is_nullable || !true_name.variant.generics.is_empty() { + return None; + } + Some(true_name.variant.name.as_str()) +} diff --git a/src/cli.rs b/src/cli.rs index cc367c35e..4c21c9e1d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,11 +1,11 @@ use clap::{ArgAction, Parser}; -/// Transpile Mamba to Python code. +/// Transpile Mamba to Python code, or compile it to a native binary. #[derive(Debug, Parser)] #[command( name = "Mamba", author = "Joël Abrahams", - about = "Transpile Mamba to Python code." + about = "Transpile Mamba to Python code, or compile it to a native binary." )] pub struct Cli { /// Input file or directory. @@ -15,12 +15,31 @@ pub struct Cli { #[arg(short = 'i', long = "input", value_name = "INPUT", value_parser)] pub input: Option, - /// Output directory to store Python files. - /// Output directory structure reflects input directory structure. - /// If no output given, 'target' directory created in current directory. + /// Output location. + /// With `--python` (the default): output directory to store Python files, structured to + /// reflect the input directory; if not given, a 'target' directory is created in the current + /// directory. + /// With `--bin`: path of the linked executable to produce; if not given, 'a.out' is created + /// in the current directory. #[arg(short = 'o', long = "output", value_name = "OUTPUT", value_parser)] pub output: Option, + /// Output Python source (the default). + #[arg(long = "python", action = ArgAction::SetTrue, conflicts_with = "bin")] + pub python: bool, + + /// Compile and link a native executable via the Cranelift backend, instead of outputting + /// Python source. + /// Only a small subset of the language is currently supported: literals, arithmetic and + /// comparison operators, if/else, top-level function definitions and calls, and `print`. + #[arg(long = "bin", action = ArgAction::SetTrue, conflicts_with = "python")] + pub bin: bool, + + /// Target triple to pass to Cranelift, e.g. `x86_64-unknown-linux-gnu` (only meaningful with + /// `--bin`; defaults to the host triple). + #[arg(long = "target", value_name = "TARGET", requires = "bin")] + pub target: Option, + /// Set level of verbosity: /// - `-v` : info, error, warning printed to stderr (default) /// - `-vv` : debug messages are printed diff --git a/src/lib.rs b/src/lib.rs index 1cfbe30bb..e95144273 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ const SOURCE: &str = "src"; #[derive(Default)] pub struct Arguments { pub annotate: bool, + pub backend: Backend, } /// Convert `*.mamba` files to `*.py`. @@ -63,15 +64,8 @@ pub fn transpile_dir( return Err(vec![msg]); } - let out_dir = dir.join(target.unwrap_or(TARGET)); - if !out_dir.exists() { - create_dir(&out_dir).map_err(|e| vec![e.to_string()])?; - } - info!("Input is '{}'", src_path.display()); - info!("Output will be stored in '{}'", out_dir.display()); - let relative_paths = io::relative_files(src_path.as_path()).map_err(|error| vec![error])?; - let in_absolute_paths = if src_path.is_dir() { + let in_absolute_paths: Vec = if src_path.is_dir() { relative_paths .iter() .map(|os_string| src_path.join(os_string)) @@ -79,19 +73,11 @@ pub fn transpile_dir( } else { vec![src_path.clone()] }; - let out_absolute_paths: Vec = relative_paths - .iter() - .map(|os_string| out_dir.join(os_string)) - .collect(); info!( - "Transpiling {} file {}", - out_absolute_paths.len(), - if out_absolute_paths.len() > 1 { - "s" - } else { - "" - } + "Compiling {} file{}", + in_absolute_paths.len(), + if in_absolute_paths.len() > 1 { "s" } else { "" } ); let mut sources = vec![]; @@ -105,15 +91,61 @@ pub fn transpile_dir( .map(|(source, path)| (source.clone(), Some(path.clone()))) .collect(); - let pipeline_arg = PipelineArguments::from(arguments); - let mamba_source = mamba_to_python(source_option_pairs.as_slice(), &src_path, &pipeline_arg)?; + match &arguments.backend { + Backend::Python => { + let out_dir = dir.join(target.unwrap_or(TARGET)); + if !out_dir.exists() { + create_dir(&out_dir).map_err(|e| vec![e.to_string()])?; + } + info!("Output will be stored in '{}'", out_dir.display()); - for (source, out_path) in mamba_source.iter().zip(out_absolute_paths) { - let out_path = out_path.with_extension("py"); - io::write_source(source, &out_path).map_err(|error| vec![error])?; - } + let out_absolute_paths: Vec = relative_paths + .iter() + .map(|os_string| out_dir.join(os_string)) + .collect(); + + let pipeline_arg = PipelineArguments::from(arguments); + let mamba_source = + mamba_to_python(source_option_pairs.as_slice(), &src_path, &pipeline_arg)?; - Ok(out_dir) + for (source, out_path) in mamba_source.iter().zip(out_absolute_paths) { + let out_path = out_path.with_extension("py"); + io::write_source(source, &out_path).map_err(|error| vec![error])?; + } + + Ok(out_dir) + } + Backend::Bin { target: triple } => { + let out_file = dir.join(target.unwrap_or("a.out")); + if let Some(parent) = out_file.parent() { + if !parent.exists() { + create_dir(parent).map_err(|e| vec![e.to_string()])?; + } + } + info!( + "Output executable will be stored at '{}'", + out_file.display() + ); + + let objects = + mamba_to_object(source_option_pairs.as_slice(), &src_path, triple.as_deref())?; + + let mut object_files = vec![]; + for object in &objects { + let mut file = tempfile::Builder::new() + .suffix(".o") + .tempfile() + .map_err(|e| vec![e.to_string()])?; + std::io::Write::write_all(&mut file, object).map_err(|e| vec![e.to_string()])?; + object_files.push(file.into_temp_path()); + } + + backend::cranelift::link::link(&object_files, &out_file) + .map_err(|error| vec![error])?; + + Ok(out_file) + } + } } pub struct PipelineArguments { @@ -128,16 +160,11 @@ impl From<&Arguments> for PipelineArguments { } } -/// Convert mamba source to python source. -/// -/// For each mamba source, a path can optionally be given for display in error -/// messages. This path is not necessary however. -pub fn mamba_to_python( +/// Strip each source's path down to be relative to `source_dir`, for nicer error messages. +fn strip_source_paths( source: &[(String, Option)], source_dir: &PathBuf, - pipeline_args: &PipelineArguments, -) -> Result, Vec> { - // Strip until source +) -> Vec<(String, Option)> { let strip_prefix = |p: PathBuf| { p.strip_prefix(source_dir) .map(|p| { @@ -145,11 +172,17 @@ pub fn mamba_to_python( }) .unwrap_or(p) }; - let source: Vec<(String, Option)> = source + source .iter() .map(|(src, dir)| (src.clone(), dir.clone().map(strip_prefix))) - .collect(); + .collect() +} +/// Parse and type-check `source`, shared by every backend -- parsing and type-checking don't +/// depend on which backend eventually turns the result into output. +fn check_sources( + source: &[(String, Option)], +) -> Result<(Context, Vec), Vec> { let (asts, parse_errs): (Vec<_>, Vec<_>) = source .iter() .map(|(src, path)| { @@ -170,7 +203,7 @@ pub fn mamba_to_python( .map_err(|errs| errs.iter().map(|e| format!("{e}")).collect::>())?; let (typed_ast, type_errs): (Vec<_>, Vec<_>) = asts .iter() - .zip(&source) + .zip(source) .map(|(ast, (src, path))| { check(ast, &ctx).map_err(|errs| { errs.iter() @@ -194,6 +227,20 @@ pub fn mamba_to_python( .collect::>(); trace!("Checked {} files", typed_ast.len()); + Ok((ctx, typed_ast)) +} + +/// Convert mamba source to python source. +/// +/// For each mamba source, a path can optionally be given for display in error +/// messages. This path is not necessary however. +pub fn mamba_to_python( + source: &[(String, Option)], + source_dir: &PathBuf, + pipeline_args: &PipelineArguments, +) -> Result, Vec> { + let source = strip_source_paths(source, source_dir); + let (ctx, typed_ast) = check_sources(&source)?; let gen_args = GenArguments::from(pipeline_args); let (py_sources, gen_errs): (Vec<_>, Vec<_>) = typed_ast @@ -216,3 +263,34 @@ pub fn mamba_to_python( Ok(py_sources) } + +/// Compile mamba source to native object files, one per source, via the Cranelift backend. +/// +/// `target`, if given, is a target triple passed on to Cranelift; if `None`, the host triple is +/// used. As with [`mamba_to_python`], a path can optionally be given per source for error +/// messages. +pub fn mamba_to_object( + source: &[(String, Option)], + source_dir: &PathBuf, + target: Option<&str>, +) -> Result>, Vec> { + let source = strip_source_paths(source, source_dir); + let (ctx, typed_ast) = check_sources(&source)?; + + let (objects, gen_errs): (Vec<_>, Vec<_>) = typed_ast + .iter() + .zip(&source) + .map(|(ast_ty, (src, path))| { + backend::cranelift::compile(ast_ty, &ctx, target) + .map_err(|err| err.with_source(&Some(src.clone()), &path.clone())) + }) + .partition(Result::is_ok); + + let gen_errs: Vec<_> = gen_errs.into_iter().map(Result::unwrap_err).collect(); + if !gen_errs.is_empty() { + return Err(gen_errs.iter().map(|err| format!("{err}")).collect()); + } + + trace!("Compiled {} files to object code", objects.len()); + Ok(objects.into_iter().map(Result::unwrap).collect()) +} diff --git a/src/main.rs b/src/main.rs index a407c143f..ba9edad1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use clap::Parser; use itertools::Itertools; use log::{self, error, info}; +use mamba::backend::Backend; use mamba::cli::Cli; use mamba::{transpile_dir, Arguments}; @@ -25,8 +26,17 @@ pub fn main() -> Result<(), String> { .init() .unwrap(); + let backend = if cli_input.bin { + Backend::Bin { + target: cli_input.target.clone(), + } + } else { + Backend::Python + }; + let arguments = Arguments { annotate: cli_input.annotate, + backend, }; info!("Mamba 🐍 {VERSION}"); diff --git a/tests/README.md b/tests/README.md index 1d9b190af..9fc0ff47b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,6 +6,21 @@ structure and conventions (fixture layout, `test_case` tables, how `to_python` d the root `CLAUDE.md` — this file is specifically about *what to test next* and *why some things can't be*. +## `tests/execution.rs`: actually running the output, not just diffing its AST + +Every other Python-backend test (`tests_util::test_directory`, used throughout +`tests/check/valid.rs`) only asserts that generated Python is *structurally* equivalent to a +reference `.py` file (a parsed-AST diff) — it never executes the result. `tests/execution.rs` is +deliberately different: it calls `transpile_dir` directly (both with the default `Backend::Python` +and with `Backend::Bin`), then actually runs what comes out — `tests_util::run_python` shells out +to `python3` and captures stdout; the `Backend::Bin` case runs the linked executable directly via +`std::process::Command` — and asserts on the captured output. Both tests currently share one +fixture, `tests/resource/valid/function/hello_world.mamba` (also registered as an ordinary +AST-diff test in `tests/check/valid.rs`, for coverage of the plain `print` path) — pick a fixture +within the Cranelift backend's supported subset (see `src/backend/cranelift/lower.rs`'s doc +comment) if adding more of these, since unlike the Python backend, it doesn't aim to eventually +support everything. + Regenerate coverage with the same exclusions CI/Codecov use (`.github/workflows/coverage.yml`), so your local numbers match what you see on the dashboard — without `--ignore-filename-regex`, `tests/*.rs` and `tests_util/src/lib.rs` count toward the total too, which inflates/dilutes the diff --git a/tests/check/valid.rs b/tests/check/valid.rs index 3b4d4e7e5..d75e308d4 100644 --- a/tests/check/valid.rs +++ b/tests/check/valid.rs @@ -106,6 +106,7 @@ use test_case::test_case; #[test_case("error", "with")] #[test_case("function", "callable_fun_arg")] #[test_case("function", "divmod")] +#[test_case("function", "hello_world")] #[test_case("function", "pure_function")] #[test_case("function", "infer_default_arg_type")] #[test_case("function", "calls")] @@ -162,7 +163,7 @@ fn to_python(input_dir: &str, file_name: &str) -> OutTestRet { tests_util::test_directory(true, &[input_dir], &[input_dir, "target"], file_name) } -#[test_case("collection", "tuple", Arguments { annotate: false })] +#[test_case("collection", "tuple", Arguments { annotate: false, ..Arguments::default() })] fn to_python_with_args(input_dir: &str, file_name: &str, args: Arguments) -> OutTestRet { tests_util::test_directory_args(true, &[input_dir], &[input_dir, "target"], file_name, &args) } diff --git a/tests/execution.rs b/tests/execution.rs new file mode 100644 index 000000000..fa8b36892 --- /dev/null +++ b/tests/execution.rs @@ -0,0 +1,58 @@ +//! Runtime execution tests: unlike the AST-diff tests in `tests/check/valid.rs` +//! (`tests_util::test_directory`), which only check that generated Python is *structurally* +//! equivalent to a reference file, these actually run the compiled output and assert on what it +//! prints -- for both backends. + +use std::path::Path; +use std::process::Command; + +use mamba::backend::Backend; +use mamba::{transpile_dir, Arguments}; +use tests_util::{resource_path, run_python}; + +#[test] +fn python_backend_prints_expected_output() -> Result<(), Box> { + let src_dir = resource_path(true, &["function"], ""); + let out_dir = tempfile::tempdir()?; + + let arguments = Arguments::default(); // backend defaults to `Backend::Python` + let output_dir = transpile_dir( + Path::new(&src_dir), + Some("hello_world.mamba"), + Some(out_dir.path().join("out").to_str().unwrap()), + &arguments, + ) + .map_err(|errs| format!("{errs:?}"))?; + + let stdout = run_python(&output_dir.join("hello_world.py"))?; + assert_eq!(stdout, "hello world\n"); + Ok(()) +} + +#[test] +fn bin_backend_prints_expected_output() -> Result<(), Box> { + let src_dir = resource_path(true, &["function"], ""); + let out_dir = tempfile::tempdir()?; + let bin_path = out_dir.path().join("hello_world_bin"); + + let arguments = Arguments { + annotate: false, + backend: Backend::Bin { target: None }, + }; + let produced = transpile_dir( + Path::new(&src_dir), + Some("hello_world.mamba"), + Some(bin_path.to_str().unwrap()), + &arguments, + ) + .map_err(|errs| format!("{errs:?}"))?; + + let output = Command::new(&produced).output()?; + assert!( + output.status.success(), + "executable exited with an error:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(String::from_utf8(output.stdout)?, "hello world\n"); + Ok(()) +} diff --git a/tests/resource/valid/function/hello_world.mamba b/tests/resource/valid/function/hello_world.mamba new file mode 100644 index 000000000..8cde7829c --- /dev/null +++ b/tests/resource/valid/function/hello_world.mamba @@ -0,0 +1 @@ +print("hello world") diff --git a/tests/resource/valid/function/hello_world.py b/tests/resource/valid/function/hello_world.py new file mode 100644 index 000000000..8cde7829c --- /dev/null +++ b/tests/resource/valid/function/hello_world.py @@ -0,0 +1 @@ +print("hello world") diff --git a/tests_util/src/lib.rs b/tests_util/src/lib.rs index 55f946889..0ceee1f66 100644 --- a/tests_util/src/lib.rs +++ b/tests_util/src/lib.rs @@ -19,6 +19,28 @@ pub static PYTHON: &str = "python3"; #[cfg(target_os = "windows")] pub static PYTHON: &str = "python"; +/// Run a Python file with [PYTHON] and return its captured stdout. +/// +/// Unlike [test_directory]/[fallable], which only diff the generated Python's *AST* against a +/// reference, this actually executes the file -- for asserting on runtime behavior (e.g. what a +/// program actually prints), not just structural equivalence to a reference. +pub fn run_python(path: &Path) -> Result { + let output = Command::new(PYTHON) + .arg(path) + .output() + .map_err(|e| format!("Could not run '{PYTHON} {}': {e}", path.display()))?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) + } else { + Err(format!( + "'{PYTHON} {}' exited with an error:\n{}", + path.display(), + String::from_utf8_lossy(&output.stderr) + )) + } +} + pub struct OutTestErr(Vec); pub type OutTestRet = Result; From b50ec4dec740928046552a49832d88ea1e7abb0d Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 10:28:14 +0200 Subject: [PATCH 05/16] refactor: encapsulate backend calls Logic specific to different backends lives in backend. Lib should be a relatively thin wrapper which just picks the backend. The actual logic of the backend should not leak to lib beyond a simple function call. --- src/backend/cranelift/link.rs | 4 +- src/backend/cranelift/lower.rs | 5 +- src/backend/cranelift/mod.rs | 88 ++++++++++- .../cranelift/{types.rs => primitive.rs} | 0 src/backend/cranelift/result.rs | 3 +- src/backend/python/mod.rs | 73 ++++++++- src/lib.rs | 140 +++--------------- 7 files changed, 176 insertions(+), 137 deletions(-) rename src/backend/cranelift/{types.rs => primitive.rs} (100%) diff --git a/src/backend/cranelift/link.rs b/src/backend/cranelift/link.rs index 5980566fb..323cf385d 100644 --- a/src/backend/cranelift/link.rs +++ b/src/backend/cranelift/link.rs @@ -1,8 +1,8 @@ use std::path::Path; use std::process::Command; -/// Link the given object files into a single executable at `output`, by shelling out to the -/// system `cc` -- the same approach `rustc` itself uses, rather than reimplementing a linker. +/// Link the given object files into a single executable at `output`. +/// By shelling out to the system `cc` -- the same approach `rustc` itself uses, rather than reimplementing a linker. pub fn link(object_paths: &[impl AsRef], output: &Path) -> Result<(), String> { let mut cmd = Command::new("cc"); cmd.args(object_paths.iter().map(AsRef::as_ref)); diff --git a/src/backend/cranelift/lower.rs b/src/backend/cranelift/lower.rs index 14e290e8f..0046e430f 100644 --- a/src/backend/cranelift/lower.rs +++ b/src/backend/cranelift/lower.rs @@ -10,14 +10,13 @@ use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; use cranelift_module::{DataDescription, FuncId, Linkage, Module}; use cranelift_object::ObjectModule; +use crate::backend::cranelift::primitive::{cranelift_type, cranelift_type_of_name}; +use crate::backend::cranelift::result::{BackendErr, BackendResult}; use crate::check::ast::{ASTTy, NodeTy}; use crate::check::context::function::PRINT; use crate::check::name::Name; use crate::Context; -use crate::backend::cranelift::result::{BackendErr, BackendResult}; -use crate::backend::cranelift::types::{cranelift_type, cranelift_type_of_name}; - /// Declared user functions, keyed by their Mamba name -- shared across every function body so /// forward references (function `a` calling function `b` defined later in the same file) work. type Funcs = HashMap; diff --git a/src/backend/cranelift/mod.rs b/src/backend/cranelift/mod.rs index 16a0e8e08..5a87fffd8 100644 --- a/src/backend/cranelift/mod.rs +++ b/src/backend/cranelift/mod.rs @@ -1,28 +1,102 @@ +use std::fs::create_dir; +use std::path::{Path, PathBuf}; use std::str::FromStr; use cranelift_codegen::isa::{self, OwnedTargetIsa}; use cranelift_codegen::settings::{self}; use cranelift_module::default_libcall_names; use cranelift_object::{ObjectBuilder, ObjectModule}; +use log::{info, trace}; use target_lexicon::Triple; use crate::backend::cranelift::result::{BackendErr, BackendResult}; use crate::check::ast::ASTTy; use crate::common::position::Position; -use crate::Context; +use crate::common::result::WithSource; +use crate::{check_sources, strip_source_paths, Context}; +mod link; mod lower; -mod types; - -pub mod link; +mod primitive; pub mod result; +/// Compile `source` to native object code and link it into an executable, written to disk. +/// +/// Output is written to `target` (relative to `dir`) if given, otherwise to `a.out` in `dir`. +/// `triple`, if given, is a target triple passed on to Cranelift; if `None`, the host triple is +/// used. +pub fn write_output( + dir: &Path, + target: Option<&str>, + source: &[(String, Option)], + src_path: &Path, + triple: Option<&str>, +) -> Result> { + let out_file = dir.join(target.unwrap_or("a.out")); + if let Some(parent) = out_file.parent() { + if !parent.exists() { + create_dir(parent).map_err(|e| vec![e.to_string()])?; + } + } + info!( + "Output executable will be stored at '{}'", + out_file.display() + ); + + let objects = mamba_to_object(source, &src_path.to_path_buf(), triple)?; + + let mut object_files = vec![]; + for object in &objects { + let mut file = tempfile::Builder::new() + .suffix(".o") + .tempfile() + .map_err(|e| vec![e.to_string()])?; + std::io::Write::write_all(&mut file, object).map_err(|e| vec![e.to_string()])?; + object_files.push(file.into_temp_path()); + } + + link::link(&object_files, &out_file).map_err(|error| vec![error])?; + + Ok(out_file) +} + +/// Compile mamba source to native object files, one per source, via the Cranelift backend. +/// +/// `target`, if given, is a target triple passed on to Cranelift; if `None`, the host triple is +/// used. A path can optionally be given per source for error messages. +fn mamba_to_object( + source: &[(String, Option)], + source_dir: &PathBuf, + target: Option<&str>, +) -> Result>, Vec> { + let source = strip_source_paths(source, source_dir); + let (ctx, typed_ast) = check_sources(&source)?; + + let (objects, gen_errs): (Vec<_>, Vec<_>) = typed_ast + .iter() + .zip(&source) + .map(|(ast_ty, (src, path))| { + compile(ast_ty, &ctx, target) + .map_err(|err| err.with_source(&Some(src.clone()), &path.clone())) + }) + .partition(Result::is_ok); + + let gen_errs: Vec<_> = gen_errs.into_iter().map(Result::unwrap_err).collect(); + if !gen_errs.is_empty() { + return Err(gen_errs.iter().map(|err| format!("{err}")).collect()); + } + + trace!("Compiled {} files to object code", objects.len()); + Ok(objects.into_iter().map(Result::unwrap).collect()) +} + /// Compile a single checked Mamba file to the bytes of a native object file. /// -/// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`); if `None`, the host -/// triple is used. +/// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`). +/// If `None`, the host triple is used. pub fn compile(ast_ty: &ASTTy, ctx: &Context, target: Option<&str>) -> BackendResult> { let isa = build_isa(target)?; + let builder = ObjectBuilder::new(isa, "mamba", default_libcall_names()) .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; let mut module = ObjectModule::new(builder); @@ -35,6 +109,8 @@ pub fn compile(ast_ty: &ASTTy, ctx: &Context, target: Option<&str>) -> BackendRe .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string())) } +/// Create target which is understood by cranelift. +/// If None, then default to host architecture as target. fn build_isa(target: Option<&str>) -> BackendResult { let flag_builder = settings::builder(); let flags = settings::Flags::new(flag_builder); diff --git a/src/backend/cranelift/types.rs b/src/backend/cranelift/primitive.rs similarity index 100% rename from src/backend/cranelift/types.rs rename to src/backend/cranelift/primitive.rs diff --git a/src/backend/cranelift/result.rs b/src/backend/cranelift/result.rs index bd8a6d4a1..dd8ff08a7 100644 --- a/src/backend/cranelift/result.rs +++ b/src/backend/cranelift/result.rs @@ -1,5 +1,4 @@ -use std::fmt; -use std::fmt::{Display, Formatter}; +use std::fmt::{self, Display, Formatter}; use std::path::PathBuf; use crate::check::ast::ASTTy; diff --git a/src/backend/python/mod.rs b/src/backend/python/mod.rs index ba707fe10..ef4dd7f33 100644 --- a/src/backend/python/mod.rs +++ b/src/backend/python/mod.rs @@ -1,9 +1,16 @@ +use std::ffi::OsString; +use std::fs::create_dir; +use std::path::{Path, PathBuf}; + +use log::{info, trace}; + use crate::backend::python::ast::node::PythonCore; use crate::backend::python::convert::convert_node; use crate::backend::python::convert::state::{Imports, State}; use crate::backend::python::result::GenResult; use crate::check::ast::ASTTy; -use crate::{Context, PipelineArguments}; +use crate::common::result::WithSource; +use crate::{check_sources, io, strip_source_paths, Context, PipelineArguments}; mod convert; @@ -12,6 +19,70 @@ pub mod name; pub mod result; +const TARGET: &str = "target"; + +/// Transpile `source` to Python and write the result to disk under `dir`. +/// +/// Output is written to `target` (relative to `dir`) if given, otherwise to a `target` +/// directory created in `dir`; the output directory structure mirrors `relative_paths`. +pub fn write_output( + dir: &Path, + target: Option<&str>, + relative_paths: &[OsString], + source: &[(String, Option)], + src_path: &Path, + pipeline_args: &PipelineArguments, +) -> Result> { + let out_dir = dir.join(target.unwrap_or(TARGET)); + if !out_dir.exists() { + create_dir(&out_dir).map_err(|e| vec![e.to_string()])?; + } + info!("Output will be stored in '{}'", out_dir.display()); + + let py_sources = mamba_to_python(source, &src_path.to_path_buf(), pipeline_args)?; + + for (source, relative_path) in py_sources.iter().zip(relative_paths) { + let out_path = out_dir.join(relative_path).with_extension("py"); + io::write_source(source, &out_path).map_err(|error| vec![error])?; + } + + Ok(out_dir) +} + +/// Convert mamba source to python source. +/// +/// For each mamba source, a path can optionally be given for display in error +/// messages. This path is not necessary however. +fn mamba_to_python( + source: &[(String, Option)], + source_dir: &PathBuf, + pipeline_args: &PipelineArguments, +) -> Result, Vec> { + let source = strip_source_paths(source, source_dir); + let (ctx, typed_ast) = check_sources(&source)?; + + let gen_args = GenArguments::from(pipeline_args); + let (py_sources, gen_errs): (Vec<_>, Vec<_>) = typed_ast + .iter() + .zip(&source) + .map(|(ast_ty, (src, path))| { + gen_arguments(ast_ty, &gen_args, &ctx) + .map_err(|err| err.with_source(&Some(src.clone()), &path.clone())) + .map(|core| format!("{core}")) + }) + .partition(Result::is_ok); + + let gen_errs: Vec<_> = gen_errs.into_iter().map(Result::unwrap_err).collect(); + if !gen_errs.is_empty() { + return Err(gen_errs.iter().map(|err| format!("{err}")).collect()); + } + + let py_sources: Vec = py_sources.into_iter().map(Result::unwrap).collect(); + trace!("Converted {} files to Python source", py_sources.len()); + + Ok(py_sources) +} + #[derive(Default)] pub struct GenArguments { pub annotate: bool, diff --git a/src/lib.rs b/src/lib.rs index e95144273..d0caf558c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,8 @@ use std::convert::TryFrom; -use std::fs::create_dir; use std::path::{Path, PathBuf}; use log::{info, trace}; -use crate::backend::python::{gen_arguments, GenArguments}; use crate::backend::Backend; use crate::check::ast::ASTTy; use crate::check::check; @@ -22,7 +20,6 @@ pub mod parse; pub mod cli; pub mod io; -const TARGET: &str = "target"; const SOURCE: &str = "src"; #[derive(Default)] @@ -92,59 +89,21 @@ pub fn transpile_dir( .collect(); match &arguments.backend { - Backend::Python => { - let out_dir = dir.join(target.unwrap_or(TARGET)); - if !out_dir.exists() { - create_dir(&out_dir).map_err(|e| vec![e.to_string()])?; - } - info!("Output will be stored in '{}'", out_dir.display()); - - let out_absolute_paths: Vec = relative_paths - .iter() - .map(|os_string| out_dir.join(os_string)) - .collect(); - - let pipeline_arg = PipelineArguments::from(arguments); - let mamba_source = - mamba_to_python(source_option_pairs.as_slice(), &src_path, &pipeline_arg)?; - - for (source, out_path) in mamba_source.iter().zip(out_absolute_paths) { - let out_path = out_path.with_extension("py"); - io::write_source(source, &out_path).map_err(|error| vec![error])?; - } - - Ok(out_dir) - } - Backend::Bin { target: triple } => { - let out_file = dir.join(target.unwrap_or("a.out")); - if let Some(parent) = out_file.parent() { - if !parent.exists() { - create_dir(parent).map_err(|e| vec![e.to_string()])?; - } - } - info!( - "Output executable will be stored at '{}'", - out_file.display() - ); - - let objects = - mamba_to_object(source_option_pairs.as_slice(), &src_path, triple.as_deref())?; - - let mut object_files = vec![]; - for object in &objects { - let mut file = tempfile::Builder::new() - .suffix(".o") - .tempfile() - .map_err(|e| vec![e.to_string()])?; - std::io::Write::write_all(&mut file, object).map_err(|e| vec![e.to_string()])?; - object_files.push(file.into_temp_path()); - } - - backend::cranelift::link::link(&object_files, &out_file) - .map_err(|error| vec![error])?; - - Ok(out_file) - } + Backend::Python => backend::python::write_output( + dir, + target, + &relative_paths, + source_option_pairs.as_slice(), + &src_path, + &PipelineArguments::from(arguments), + ), + Backend::Bin { target: triple } => backend::cranelift::write_output( + dir, + target, + source_option_pairs.as_slice(), + &src_path, + triple.as_deref(), + ), } } @@ -161,7 +120,7 @@ impl From<&Arguments> for PipelineArguments { } /// Strip each source's path down to be relative to `source_dir`, for nicer error messages. -fn strip_source_paths( +pub(crate) fn strip_source_paths( source: &[(String, Option)], source_dir: &PathBuf, ) -> Vec<(String, Option)> { @@ -180,7 +139,7 @@ fn strip_source_paths( /// Parse and type-check `source`, shared by every backend -- parsing and type-checking don't /// depend on which backend eventually turns the result into output. -fn check_sources( +pub(crate) fn check_sources( source: &[(String, Option)], ) -> Result<(Context, Vec), Vec> { let (asts, parse_errs): (Vec<_>, Vec<_>) = source @@ -229,68 +188,3 @@ fn check_sources( trace!("Checked {} files", typed_ast.len()); Ok((ctx, typed_ast)) } - -/// Convert mamba source to python source. -/// -/// For each mamba source, a path can optionally be given for display in error -/// messages. This path is not necessary however. -pub fn mamba_to_python( - source: &[(String, Option)], - source_dir: &PathBuf, - pipeline_args: &PipelineArguments, -) -> Result, Vec> { - let source = strip_source_paths(source, source_dir); - let (ctx, typed_ast) = check_sources(&source)?; - - let gen_args = GenArguments::from(pipeline_args); - let (py_sources, gen_errs): (Vec<_>, Vec<_>) = typed_ast - .iter() - .zip(&source) - .map(|(ast_ty, (src, path))| { - gen_arguments(ast_ty, &gen_args, &ctx) - .map_err(|err| err.with_source(&Some(src.clone()), &path.clone())) - .map(|core| format!("{core}")) - }) - .partition(Result::is_ok); - - let gen_errs: Vec<_> = gen_errs.into_iter().map(Result::unwrap_err).collect(); - if !gen_errs.is_empty() { - return Err(gen_errs.iter().map(|err| format!("{err}")).collect()); - } - - let py_sources: Vec = py_sources.into_iter().map(Result::unwrap).collect(); - trace!("Converted {} files to Python source", py_sources.len()); - - Ok(py_sources) -} - -/// Compile mamba source to native object files, one per source, via the Cranelift backend. -/// -/// `target`, if given, is a target triple passed on to Cranelift; if `None`, the host triple is -/// used. As with [`mamba_to_python`], a path can optionally be given per source for error -/// messages. -pub fn mamba_to_object( - source: &[(String, Option)], - source_dir: &PathBuf, - target: Option<&str>, -) -> Result>, Vec> { - let source = strip_source_paths(source, source_dir); - let (ctx, typed_ast) = check_sources(&source)?; - - let (objects, gen_errs): (Vec<_>, Vec<_>) = typed_ast - .iter() - .zip(&source) - .map(|(ast_ty, (src, path))| { - backend::cranelift::compile(ast_ty, &ctx, target) - .map_err(|err| err.with_source(&Some(src.clone()), &path.clone())) - }) - .partition(Result::is_ok); - - let gen_errs: Vec<_> = gen_errs.into_iter().map(Result::unwrap_err).collect(); - if !gen_errs.is_empty() { - return Err(gen_errs.iter().map(|err| format!("{err}")).collect()); - } - - trace!("Compiled {} files to object code", objects.len()); - Ok(objects.into_iter().map(Result::unwrap).collect()) -} From 1dfb712686b6212c7e6ae0b28a6eb6f16e86c160 Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 10:43:17 +0200 Subject: [PATCH 06/16] refactor: split lower into convert parts of ast --- src/backend/cranelift/convert/call.rs | 109 ++++ src/backend/cranelift/convert/common.rs | 13 + src/backend/cranelift/convert/control_flow.rs | 72 +++ src/backend/cranelift/convert/definition.rs | 225 +++++++ src/backend/cranelift/convert/mod.rs | 195 ++++++ src/backend/cranelift/convert/operation.rs | 61 ++ src/backend/cranelift/lower.rs | 577 ------------------ src/backend/cranelift/mod.rs | 13 +- src/backend/cranelift/primitive.rs | 2 +- 9 files changed, 684 insertions(+), 583 deletions(-) create mode 100644 src/backend/cranelift/convert/call.rs create mode 100644 src/backend/cranelift/convert/common.rs create mode 100644 src/backend/cranelift/convert/control_flow.rs create mode 100644 src/backend/cranelift/convert/definition.rs create mode 100644 src/backend/cranelift/convert/mod.rs create mode 100644 src/backend/cranelift/convert/operation.rs delete mode 100644 src/backend/cranelift/lower.rs diff --git a/src/backend/cranelift/convert/call.rs b/src/backend/cranelift/convert/call.rs new file mode 100644 index 000000000..f57a295ee --- /dev/null +++ b/src/backend/cranelift/convert/call.rs @@ -0,0 +1,109 @@ +use cranelift_codegen::ir::{types, InstBuilder, Value}; +use cranelift_module::{DataDescription, Module}; + +use crate::backend::cranelift::convert::FnLower; +use crate::backend::cranelift::result::{BackendErr, BackendResult}; +use crate::check::ast::{ASTTy, NodeTy}; + +impl<'a> FnLower<'a> { + /// Lower a (non-`print`) `FunctionCall` to a user-defined function. + pub(super) fn lower_call(&mut self, ast: &ASTTy) -> BackendResult { + let (name, args) = match &ast.node { + NodeTy::FunctionCall { name, args } => (name, args), + other => { + return Err(BackendErr::unimplemented( + ast, + &format!("{other:?} function call"), + )) + } + }; + + let func_id = *self.funcs.get(&name.name).ok_or_else(|| { + BackendErr::new(ast.pos, &format!("Undefined function '{}'", name.name)) + })?; + let local = self.module.declare_func_in_func(func_id, self.builder.func); + let mut arg_values = vec![]; + for arg in args { + arg_values.push(self.lower_expr(arg)?); + } + let call = self.builder.ins().call(local, &arg_values); + self.builder + .inst_results(call) + .first() + .copied() + .ok_or_else(|| { + BackendErr::new(ast.pos, &format!("'{}' does not return a value", name.name)) + }) + } + + /// `print(...)`: a string literal goes through `puts` (which appends its own newline, like + /// Mamba/Python's `print`); a primitive value goes through `printf` with a fixed `%lld\n` + /// format. Anything else (interpolated strings, non-primitive values, multiple arguments) is + /// out of scope for this backend. + pub(super) fn lower_print(&mut self, ast: &ASTTy) -> BackendResult> { + let args = match &ast.node { + NodeTy::FunctionCall { args, .. } => args, + _ => unreachable!("only called for a FunctionCall node"), + }; + let arg = match args.as_slice() { + [arg] => arg, + _ => return Err(BackendErr::unimplemented(ast, "print with != 1 argument")), + }; + + match &arg.node { + NodeTy::Str { lit, expressions } if expressions.is_empty() => { + let data = format!("{lit}\0").into_bytes().into_boxed_slice(); + let data_id = self + .module + .declare_anonymous_data(false, false) + .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; + let mut desc = DataDescription::new(); + desc.define(data); + self.module + .define_data(data_id, &desc) + .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; + + let gv = self.module.declare_data_in_func(data_id, self.builder.func); + let pointer_type = self.module.isa().pointer_type(); + let ptr = self.builder.ins().global_value(pointer_type, gv); + let callee = self + .module + .declare_func_in_func(self.puts_id, self.builder.func); + self.builder.ins().call(callee, &[ptr]); + Ok(None) + } + NodeTy::Str { .. } => Err(BackendErr::unimplemented( + ast, + "print of an interpolated string", + )), + _ => { + let value = self.lower_expr(arg)?; + let fmt = format!("{}\0", "%lld\n").into_bytes().into_boxed_slice(); + let data_id = self + .module + .declare_anonymous_data(false, false) + .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; + let mut desc = DataDescription::new(); + desc.define(fmt); + self.module + .define_data(data_id, &desc) + .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; + + let gv = self.module.declare_data_in_func(data_id, self.builder.func); + let pointer_type = self.module.isa().pointer_type(); + let ptr = self.builder.ins().global_value(pointer_type, gv); + // Widen a narrower-than-i64 value (e.g. Bool, stored as i8) to match `%lld`. + let value = if self.builder.func.dfg.value_type(value) != types::I64 { + self.builder.ins().sextend(types::I64, value) + } else { + value + }; + let callee = self + .module + .declare_func_in_func(self.printf_id, self.builder.func); + self.builder.ins().call(callee, &[ptr, value]); + Ok(None) + } + } + } +} diff --git a/src/backend/cranelift/convert/common.rs b/src/backend/cranelift/convert/common.rs new file mode 100644 index 000000000..614d85f3f --- /dev/null +++ b/src/backend/cranelift/convert/common.rs @@ -0,0 +1,13 @@ +use crate::backend::cranelift::result::{BackendErr, BackendResult}; +use crate::check::ast::{ASTTy, NodeTy}; + +/// The identifier an `Id` node holds, e.g. a `FunDef`'s name or a `FunArg`'s bound variable. +pub(super) fn fun_name(id: &ASTTy) -> BackendResult { + match &id.node { + NodeTy::Id { lit } => Ok(lit.clone()), + other => Err(BackendErr::unimplemented( + id, + &format!("{other:?} function name"), + )), + } +} diff --git a/src/backend/cranelift/convert/control_flow.rs b/src/backend/cranelift/convert/control_flow.rs new file mode 100644 index 000000000..988624a8d --- /dev/null +++ b/src/backend/cranelift/convert/control_flow.rs @@ -0,0 +1,72 @@ +use cranelift_codegen::ir::InstBuilder; + +use crate::backend::cranelift::convert::FnLower; +use crate::backend::cranelift::result::BackendResult; +use crate::check::ast::ASTTy; + +impl<'a> FnLower<'a> { + /// Lower an `IfElse` in statement position: both arms are lowered as statements, and control + /// re-joins in a shared `merge_block` afterwards (or falls straight through to it when there + /// is no `else`). + pub(super) fn lower_if_else_stmt( + &mut self, + cond: &ASTTy, + then: &ASTTy, + el: Option<&ASTTy>, + ) -> BackendResult<()> { + let cond_value = self.lower_expr(cond)?; + let then_block = self.builder.create_block(); + let merge_block = self.builder.create_block(); + let else_block = if el.is_some() { + self.builder.create_block() + } else { + merge_block + }; + + self.builder + .ins() + .brif(cond_value, then_block, &[], else_block, &[]); + + self.builder.switch_to_block(then_block); + self.lower_stmt(then)?; + self.builder.ins().jump(merge_block, &[]); + + if let Some(el) = el { + self.builder.switch_to_block(else_block); + self.lower_stmt(el)?; + self.builder.ins().jump(merge_block, &[]); + } + + self.builder.switch_to_block(merge_block); + Ok(()) + } + + /// Lower an `IfElse` in tail position: both arms must themselves end in a `return`, so unlike + /// [`Self::lower_if_else_stmt`] there is no shared merge block to jump back to. + pub(super) fn lower_if_else_tail( + &mut self, + cond: &ASTTy, + then: &ASTTy, + el: Option<&ASTTy>, + ) -> BackendResult<()> { + let cond_value = self.lower_expr(cond)?; + let then_block = self.builder.create_block(); + let else_block = self.builder.create_block(); + + self.builder + .ins() + .brif(cond_value, then_block, &[], else_block, &[]); + + self.builder.switch_to_block(then_block); + self.lower_tail(then)?; + + self.builder.switch_to_block(else_block); + match el { + Some(el) => self.lower_tail(el)?, + None => { + self.builder.ins().return_(&[]); + } + } + Ok(()) + } +} diff --git a/src/backend/cranelift/convert/definition.rs b/src/backend/cranelift/convert/definition.rs new file mode 100644 index 000000000..7b67f2790 --- /dev/null +++ b/src/backend/cranelift/convert/definition.rs @@ -0,0 +1,225 @@ +use std::collections::HashMap; + +use cranelift_codegen::ir::{ + types, AbiParam, Function, InstBuilder, Signature, UserFuncName, Value, +}; +use cranelift_codegen::isa::CallConv; +use cranelift_codegen::Context as ClifContext; +use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; +use cranelift_module::{FuncId, Linkage, Module}; +use cranelift_object::ObjectModule; + +use crate::backend::cranelift::convert::common::fun_name; +use crate::backend::cranelift::convert::{FnLower, Funcs}; +use crate::backend::cranelift::primitive::{cranelift_type, cranelift_type_of_name}; +use crate::backend::cranelift::result::{BackendErr, BackendResult}; +use crate::check::ast::{ASTTy, NodeTy}; +use crate::check::name::Name; +use crate::common::position::Position; + +/// A `FunDef`'s Cranelift signature, built from its declared argument types and return type. +pub(super) fn fun_signature( + args: &[ASTTy], + ret: Option<&Name>, + call_conv: CallConv, + pos_ast: &ASTTy, +) -> BackendResult { + let mut params = vec![]; + for arg in args { + params.push(AbiParam::new(arg_type(arg)?)); + } + let returns = match ret { + Some(ret) => vec![AbiParam::new(cranelift_type_of_name(ret, pos_ast.pos)?)], + None => vec![], + }; + Ok(Signature { + params, + returns, + call_conv, + }) +} + +/// The Cranelift type of a `FunArg`'s declared parameter type -- which lives in the `FunArg` +/// variant's own `ty` field, not in the surrounding node's resolved type (see +/// `name::cranelift_type_of_name`'s doc comment). +pub(super) fn arg_type(arg: &ASTTy) -> BackendResult { + match &arg.node { + NodeTy::FunArg { ty: Some(ty), .. } => cranelift_type_of_name(ty, arg.pos), + NodeTy::FunArg { ty: None, .. } => Err(BackendErr::new( + arg.pos, + "Function argument must have a type", + )), + other => Err(BackendErr::unimplemented( + arg, + &format!("{other:?} argument"), + )), + } +} + +/// Declare the two external `libc` functions `print` may need. Declared lazily -- and re-declared +/// per function via `Module::declare_function`, which is idempotent (merges with the existing +/// declaration of the same name) -- rather than threading a single shared declaration through. +pub(super) fn declare_libc( + module: &mut ObjectModule, + call_conv: CallConv, +) -> BackendResult<(FuncId, FuncId)> { + let pointer_type = module.isa().pointer_type(); + + let puts_sig = Signature { + params: vec![AbiParam::new(pointer_type)], + returns: vec![AbiParam::new(types::I32)], + call_conv, + }; + let puts_id = module + .declare_function("puts", Linkage::Import, &puts_sig) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string()))?; + + let printf_sig = Signature { + params: vec![AbiParam::new(pointer_type), AbiParam::new(types::I64)], + returns: vec![AbiParam::new(types::I32)], + call_conv, + }; + let printf_id = module + .declare_function("printf", Linkage::Import, &printf_sig) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string()))?; + + Ok((puts_id, printf_id)) +} + +/// Define a top-level `FunDef`'s body as a real Cranelift function. +pub(super) fn define_function( + module: &mut ObjectModule, + func_id: FuncId, + sig: Signature, + args: &[ASTTy], + body: Option<&ASTTy>, + funcs: &Funcs, +) -> BackendResult<()> { + let mut ctx = ClifContext::new(); + ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), sig); + let mut fb_ctx = FunctionBuilderContext::new(); + + { + let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx); + let entry = builder.create_block(); + builder.append_block_params_for_function_params(entry); + builder.switch_to_block(entry); + + let (puts_id, printf_id) = declare_libc(module, builder.func.signature.call_conv)?; + let mut lower = FnLower { + builder, + module, + vars: HashMap::new(), + var_seq: 0, + funcs, + puts_id, + printf_id, + }; + + let block_params = lower.builder.block_params(entry).to_vec(); + for (arg, value) in args.iter().zip(block_params) { + lower.bind_arg(arg, value)?; + } + + match body { + Some(body) => lower.lower_tail(body)?, + None => { + lower.builder.ins().return_(&[]); + } + } + + lower.builder.seal_all_blocks(); + lower.builder.finalize(); + } + + module + .define_function(func_id, &mut ctx) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string())) +} + +/// Define the synthetic `main` collecting every top-level statement that isn't a `FunDef`. +pub(super) fn define_main( + module: &mut ObjectModule, + func_id: FuncId, + sig: Signature, + statements: &[ASTTy], + funcs: &Funcs, +) -> BackendResult<()> { + let mut ctx = ClifContext::new(); + ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), sig); + let mut fb_ctx = FunctionBuilderContext::new(); + + { + let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx); + let entry = builder.create_block(); + builder.switch_to_block(entry); + + let (puts_id, printf_id) = declare_libc(module, builder.func.signature.call_conv)?; + let mut lower = FnLower { + builder, + module, + vars: HashMap::new(), + var_seq: 0, + funcs, + puts_id, + printf_id, + }; + + for statement in statements { + lower.lower_stmt(statement)?; + } + let zero = lower.builder.ins().iconst(types::I32, 0); + lower.builder.ins().return_(&[zero]); + + lower.builder.seal_all_blocks(); + lower.builder.finalize(); + } + + module + .define_function(func_id, &mut ctx) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string())) +} + +impl<'a> FnLower<'a> { + /// Lower a `VariableDef` with an initializer, binding a new Cranelift variable. + pub(super) fn lower_variable_def(&mut self, ast: &ASTTy) -> BackendResult<()> { + match &ast.node { + NodeTy::VariableDef { + var, + expr: Some(expr), + .. + } => { + let name = fun_name(var)?; + let ty = cranelift_type(expr)?; + let value = self.lower_expr(expr)?; + let var = self.new_var(ty); + self.builder.def_var(var, value); + self.vars.insert(name, (var, ty)); + Ok(()) + } + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} variable definition"), + )), + } + } + + pub(super) fn bind_arg(&mut self, arg: &ASTTy, value: Value) -> BackendResult<()> { + let name = match &arg.node { + NodeTy::FunArg { var, .. } => fun_name(var)?, + _ => return Err(BackendErr::unimplemented(arg, "non-identifier argument")), + }; + let ty = arg_type(arg)?; + let var = self.new_var(ty); + self.builder.def_var(var, value); + self.vars.insert(name, (var, ty)); + Ok(()) + } + + pub(super) fn new_var(&mut self, ty: cranelift_codegen::ir::Type) -> Variable { + let var = Variable::from_u32(self.var_seq); + self.var_seq += 1; + self.builder.declare_var(var, ty); + var + } +} diff --git a/src/backend/cranelift/convert/mod.rs b/src/backend/cranelift/convert/mod.rs new file mode 100644 index 000000000..d75167dde --- /dev/null +++ b/src/backend/cranelift/convert/mod.rs @@ -0,0 +1,195 @@ +use std::collections::HashMap; + +use cranelift_codegen::ir::{types, AbiParam, InstBuilder, Signature, Value}; +use cranelift_frontend::{FunctionBuilder, Variable}; +use cranelift_module::{FuncId, Linkage, Module}; +use cranelift_object::ObjectModule; + +use crate::backend::cranelift::convert::definition::{define_function, define_main, fun_signature}; +use crate::backend::cranelift::result::{BackendErr, BackendResult}; +use crate::check::ast::{ASTTy, NodeTy}; +use crate::check::context::function::PRINT; +use crate::Context; + +mod call; +mod common; +mod control_flow; +mod definition; +mod operation; + +/// Declared user functions, keyed by their Mamba name -- shared across every function body so +/// forward references (function `a` calling function `b` defined later in the same file) work. +type Funcs = HashMap; + +/// Lower an entire checked file (a top-level `NodeTy::Block`) into `module`. +/// +/// Top-level `FunDef`s become real Cranelift functions. Every other top-level statement is +/// collected into a synthetic `main`, mirroring how a `.mamba` file's top-level statements run +/// top-to-bottom as a script in the Python backend -- machine code needs an explicit entry point, +/// which Python's linear script execution doesn't. +pub(super) fn lower_program( + ast_ty: &ASTTy, + _ctx: &Context, + module: &mut ObjectModule, +) -> BackendResult<()> { + let statements = match &ast_ty.node { + NodeTy::Block { statements } => statements, + _ => std::slice::from_ref(ast_ty), + }; + + let call_conv = module.isa().default_call_conv(); + + // Pass 1: declare every top-level function's signature, so calls to a function defined later + // in the file still resolve. + let mut funcs = Funcs::new(); + for statement in statements { + if let NodeTy::FunDef { id, args, ret, .. } = &statement.node { + let name = common::fun_name(id)?; + let sig = fun_signature(args, ret.as_ref(), call_conv, statement)?; + let func_id = module + .declare_function(&name, Linkage::Export, &sig) + .map_err(|e| BackendErr::new(statement.pos, &e.to_string()))?; + funcs.insert(name, func_id); + } + } + + // Pass 2: define each function's body, plus a synthetic `main` for everything else. + let mut main_body = vec![]; + for statement in statements { + match &statement.node { + NodeTy::FunDef { + id, + args, + ret, + body, + .. + } => { + let name = common::fun_name(id)?; + let func_id = *funcs.get(&name).expect("declared in pass 1"); + let sig = fun_signature(args, ret.as_ref(), call_conv, statement)?; + define_function(module, func_id, sig, args, body.as_deref(), &funcs)?; + } + _ => main_body.push(statement.clone()), + } + } + + let main_sig = Signature { + params: vec![], + returns: vec![AbiParam::new(types::I32)], + call_conv, + }; + let main_id = module + .declare_function("main", Linkage::Export, &main_sig) + .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; + define_main(module, main_id, main_sig, &main_body, &funcs)?; + + Ok(()) +} + +/// Per-function-body lowering state. +struct FnLower<'a> { + builder: FunctionBuilder<'a>, + module: &'a mut ObjectModule, + vars: HashMap, + var_seq: u32, + funcs: &'a Funcs, + puts_id: FuncId, + printf_id: FuncId, +} + +impl<'a> FnLower<'a> { + /// Lower `ast` as a statement: for side effects only, its value (if any) is discarded. + fn lower_stmt(&mut self, ast: &ASTTy) -> BackendResult<()> { + match &ast.node { + NodeTy::VariableDef { .. } => self.lower_variable_def(ast), + NodeTy::IfElse { cond, then, el } => self.lower_if_else_stmt(cond, then, el.as_deref()), + NodeTy::Block { statements } => { + for statement in statements { + self.lower_stmt(statement)?; + } + Ok(()) + } + NodeTy::FunctionCall { name, .. } if name.name == PRINT => { + self.lower_print(ast).map(|_| ()) + } + NodeTy::FunctionCall { .. } => self.lower_expr(ast).map(|_| ()), + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} statement"), + )), + } + } + + /// Lower `ast` as the tail of a function body: it must end the current block with a `return` + /// (possibly by recursing into `Block`'s last statement, or into each arm of an `IfElse`). + fn lower_tail(&mut self, ast: &ASTTy) -> BackendResult<()> { + match &ast.node { + NodeTy::Return { expr } => { + let value = self.lower_expr(expr)?; + self.builder.ins().return_(&[value]); + Ok(()) + } + NodeTy::ReturnEmpty => { + self.builder.ins().return_(&[]); + Ok(()) + } + NodeTy::Block { statements } => match statements.split_last() { + Some((last, init)) => { + for statement in init { + self.lower_stmt(statement)?; + } + self.lower_tail(last) + } + None => { + self.builder.ins().return_(&[]); + Ok(()) + } + }, + NodeTy::IfElse { cond, then, el } => self.lower_if_else_tail(cond, then, el.as_deref()), + _ => { + let value = self.lower_expr(ast)?; + self.builder.ins().return_(&[value]); + Ok(()) + } + } + } + + /// Lower `ast` as a value-producing expression. + fn lower_expr(&mut self, ast: &ASTTy) -> BackendResult { + match &ast.node { + // Int/Bool literals' own resolved `ty` can come back widened to a union (e.g. a + // literal argument to `print`, whose parameter accepts several printable types + // unifies to that broader union rather than staying just `Int`) -- but the node + // variant itself already tells us the literal's true type, so there's no need to + // consult `ast.ty` at all here. + NodeTy::Int { lit } => { + let value: i64 = lit.parse().map_err(|_| { + BackendErr::new(ast.pos, &format!("Invalid int literal '{lit}'")) + })?; + Ok(self.builder.ins().iconst(types::I64, value)) + } + NodeTy::Bool { lit } => Ok(self.builder.ins().iconst(types::I8, i64::from(*lit))), + NodeTy::Id { lit } => { + let (var, _) = self.vars.get(lit).ok_or_else(|| { + BackendErr::new(ast.pos, &format!("Undefined variable '{lit}'")) + })?; + Ok(self.builder.use_var(*var)) + } + NodeTy::Add { .. } + | NodeTy::Sub { .. } + | NodeTy::Mul { .. } + | NodeTy::Div { .. } + | NodeTy::Le { .. } + | NodeTy::Leq { .. } + | NodeTy::Ge { .. } + | NodeTy::Geq { .. } + | NodeTy::Eq { .. } + | NodeTy::Neq { .. } => self.lower_operation(ast), + NodeTy::FunctionCall { .. } => self.lower_call(ast), + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} expression"), + )), + } + } +} diff --git a/src/backend/cranelift/convert/operation.rs b/src/backend/cranelift/convert/operation.rs new file mode 100644 index 000000000..84be28899 --- /dev/null +++ b/src/backend/cranelift/convert/operation.rs @@ -0,0 +1,61 @@ +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::{InstBuilder, Value}; +use cranelift_frontend::FunctionBuilder; + +use crate::backend::cranelift::convert::FnLower; +use crate::backend::cranelift::primitive::cranelift_type; +use crate::backend::cranelift::result::{BackendErr, BackendResult}; +use crate::check::ast::{ASTTy, NodeTy}; + +impl<'a> FnLower<'a> { + /// Lower a binary arithmetic or comparison operation. + pub(super) fn lower_operation(&mut self, ast: &ASTTy) -> BackendResult { + match &ast.node { + NodeTy::Add { left, right } => { + self.lower_arith(ast, left, right, |b, a, c| b.ins().iadd(a, c)) + } + NodeTy::Sub { left, right } => { + self.lower_arith(ast, left, right, |b, a, c| b.ins().isub(a, c)) + } + NodeTy::Mul { left, right } => { + self.lower_arith(ast, left, right, |b, a, c| b.ins().imul(a, c)) + } + NodeTy::Div { left, right } => { + self.lower_arith(ast, left, right, |b, a, c| b.ins().sdiv(a, c)) + } + NodeTy::Le { left, right } => self.lower_cmp(left, right, IntCC::SignedLessThan), + NodeTy::Leq { left, right } => { + self.lower_cmp(left, right, IntCC::SignedLessThanOrEqual) + } + NodeTy::Ge { left, right } => self.lower_cmp(left, right, IntCC::SignedGreaterThan), + NodeTy::Geq { left, right } => { + self.lower_cmp(left, right, IntCC::SignedGreaterThanOrEqual) + } + NodeTy::Eq { left, right } => self.lower_cmp(left, right, IntCC::Equal), + NodeTy::Neq { left, right } => self.lower_cmp(left, right, IntCC::NotEqual), + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} operation"), + )), + } + } + + fn lower_arith( + &mut self, + ast: &ASTTy, + left: &ASTTy, + right: &ASTTy, + op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, + ) -> BackendResult { + cranelift_type(ast)?; // reject non-primitive-typed arithmetic early, with a clear error + let l = self.lower_expr(left)?; + let r = self.lower_expr(right)?; + Ok(op(&mut self.builder, l, r)) + } + + fn lower_cmp(&mut self, left: &ASTTy, right: &ASTTy, cc: IntCC) -> BackendResult { + let l = self.lower_expr(left)?; + let r = self.lower_expr(right)?; + Ok(self.builder.ins().icmp(cc, l, r)) + } +} diff --git a/src/backend/cranelift/lower.rs b/src/backend/cranelift/lower.rs deleted file mode 100644 index 0046e430f..000000000 --- a/src/backend/cranelift/lower.rs +++ /dev/null @@ -1,577 +0,0 @@ -use std::collections::HashMap; - -use cranelift_codegen::ir::condcodes::IntCC; -use cranelift_codegen::ir::{ - types, AbiParam, Function, InstBuilder, Signature, UserFuncName, Value, -}; -use cranelift_codegen::isa::CallConv; -use cranelift_codegen::Context as ClifContext; -use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; -use cranelift_module::{DataDescription, FuncId, Linkage, Module}; -use cranelift_object::ObjectModule; - -use crate::backend::cranelift::primitive::{cranelift_type, cranelift_type_of_name}; -use crate::backend::cranelift::result::{BackendErr, BackendResult}; -use crate::check::ast::{ASTTy, NodeTy}; -use crate::check::context::function::PRINT; -use crate::check::name::Name; -use crate::Context; - -/// Declared user functions, keyed by their Mamba name -- shared across every function body so -/// forward references (function `a` calling function `b` defined later in the same file) work. -type Funcs = HashMap; - -/// Lower an entire checked file (a top-level `NodeTy::Block`) into `module`. -/// -/// Top-level `FunDef`s become real Cranelift functions. Every other top-level statement is -/// collected into a synthetic `main`, mirroring how a `.mamba` file's top-level statements run -/// top-to-bottom as a script in the Python backend -- machine code needs an explicit entry point, -/// which Python's linear script execution doesn't. -pub fn lower_program( - ast_ty: &ASTTy, - _ctx: &Context, - module: &mut ObjectModule, -) -> BackendResult<()> { - let statements = match &ast_ty.node { - NodeTy::Block { statements } => statements, - _ => std::slice::from_ref(ast_ty), - }; - - let call_conv = module.isa().default_call_conv(); - - // Pass 1: declare every top-level function's signature, so calls to a function defined later - // in the file still resolve. - let mut funcs = Funcs::new(); - for statement in statements { - if let NodeTy::FunDef { id, args, ret, .. } = &statement.node { - let name = fun_name(id)?; - let sig = fun_signature(args, ret.as_ref(), call_conv, statement)?; - let func_id = module - .declare_function(&name, Linkage::Export, &sig) - .map_err(|e| BackendErr::new(statement.pos, &e.to_string()))?; - funcs.insert(name, func_id); - } - } - - // Pass 2: define each function's body, plus a synthetic `main` for everything else. - let mut main_body = vec![]; - for statement in statements { - match &statement.node { - NodeTy::FunDef { - id, - args, - ret, - body, - .. - } => { - let name = fun_name(id)?; - let func_id = *funcs.get(&name).expect("declared in pass 1"); - let sig = fun_signature(args, ret.as_ref(), call_conv, statement)?; - define_function(module, func_id, sig, args, body.as_deref(), &funcs)?; - } - _ => main_body.push(statement.clone()), - } - } - - let main_sig = Signature { - params: vec![], - returns: vec![AbiParam::new(types::I32)], - call_conv, - }; - let main_id = module - .declare_function("main", Linkage::Export, &main_sig) - .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; - define_main(module, main_id, main_sig, &main_body, &funcs)?; - - Ok(()) -} - -fn fun_name(id: &ASTTy) -> BackendResult { - match &id.node { - NodeTy::Id { lit } => Ok(lit.clone()), - other => Err(BackendErr::unimplemented( - id, - &format!("{other:?} function name"), - )), - } -} - -fn fun_signature( - args: &[ASTTy], - ret: Option<&Name>, - call_conv: CallConv, - pos_ast: &ASTTy, -) -> BackendResult { - let mut params = vec![]; - for arg in args { - params.push(AbiParam::new(arg_type(arg)?)); - } - let returns = match ret { - Some(ret) => vec![AbiParam::new(cranelift_type_of_name(ret, pos_ast.pos)?)], - None => vec![], - }; - Ok(Signature { - params, - returns, - call_conv, - }) -} - -/// The Cranelift type of a `FunArg`'s declared parameter type -- which lives in the `FunArg` -/// variant's own `ty` field, not in the surrounding node's resolved type (see -/// `types::cranelift_type_of_name`'s doc comment). -fn arg_type(arg: &ASTTy) -> BackendResult { - match &arg.node { - NodeTy::FunArg { ty: Some(ty), .. } => cranelift_type_of_name(ty, arg.pos), - NodeTy::FunArg { ty: None, .. } => Err(BackendErr::new( - arg.pos, - "Function argument must have a type", - )), - other => Err(BackendErr::unimplemented( - arg, - &format!("{other:?} argument"), - )), - } -} - -/// Declare the two external `libc` functions `print` may need. Declared lazily -- and re-declared -/// per function via `Module::declare_function`, which is idempotent (merges with the existing -/// declaration of the same name) -- rather than threading a single shared declaration through. -fn declare_libc(module: &mut ObjectModule, call_conv: CallConv) -> BackendResult<(FuncId, FuncId)> { - let pointer_type = module.isa().pointer_type(); - - let puts_sig = Signature { - params: vec![AbiParam::new(pointer_type)], - returns: vec![AbiParam::new(types::I32)], - call_conv, - }; - let puts_id = module - .declare_function("puts", Linkage::Import, &puts_sig) - .map_err(|e| { - BackendErr::new( - crate::common::position::Position::invisible(), - &e.to_string(), - ) - })?; - - let printf_sig = Signature { - params: vec![AbiParam::new(pointer_type), AbiParam::new(types::I64)], - returns: vec![AbiParam::new(types::I32)], - call_conv, - }; - let printf_id = module - .declare_function("printf", Linkage::Import, &printf_sig) - .map_err(|e| { - BackendErr::new( - crate::common::position::Position::invisible(), - &e.to_string(), - ) - })?; - - Ok((puts_id, printf_id)) -} - -fn define_function( - module: &mut ObjectModule, - func_id: FuncId, - sig: Signature, - args: &[ASTTy], - body: Option<&ASTTy>, - funcs: &Funcs, -) -> BackendResult<()> { - let mut ctx = ClifContext::new(); - ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), sig); - let mut fb_ctx = FunctionBuilderContext::new(); - - { - let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx); - let entry = builder.create_block(); - builder.append_block_params_for_function_params(entry); - builder.switch_to_block(entry); - - let (puts_id, printf_id) = declare_libc(module, builder.func.signature.call_conv)?; - let mut lower = FnLower { - builder, - module, - vars: HashMap::new(), - var_seq: 0, - funcs, - puts_id, - printf_id, - }; - - let block_params: Vec = lower.builder.block_params(entry).to_vec(); - for (arg, value) in args.iter().zip(block_params) { - lower.bind_arg(arg, value)?; - } - - match body { - Some(body) => lower.lower_tail(body)?, - None => { - lower.builder.ins().return_(&[]); - } - } - - lower.builder.seal_all_blocks(); - lower.builder.finalize(); - } - - module.define_function(func_id, &mut ctx).map_err(|e| { - BackendErr::new( - crate::common::position::Position::invisible(), - &e.to_string(), - ) - }) -} - -fn define_main( - module: &mut ObjectModule, - func_id: FuncId, - sig: Signature, - statements: &[ASTTy], - funcs: &Funcs, -) -> BackendResult<()> { - let mut ctx = ClifContext::new(); - ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), sig); - let mut fb_ctx = FunctionBuilderContext::new(); - - { - let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx); - let entry = builder.create_block(); - builder.switch_to_block(entry); - - let (puts_id, printf_id) = declare_libc(module, builder.func.signature.call_conv)?; - let mut lower = FnLower { - builder, - module, - vars: HashMap::new(), - var_seq: 0, - funcs, - puts_id, - printf_id, - }; - - for statement in statements { - lower.lower_stmt(statement)?; - } - let zero = lower.builder.ins().iconst(types::I32, 0); - lower.builder.ins().return_(&[zero]); - - lower.builder.seal_all_blocks(); - lower.builder.finalize(); - } - - module.define_function(func_id, &mut ctx).map_err(|e| { - BackendErr::new( - crate::common::position::Position::invisible(), - &e.to_string(), - ) - }) -} - -/// Per-function-body lowering state. -struct FnLower<'a> { - builder: FunctionBuilder<'a>, - module: &'a mut ObjectModule, - vars: HashMap, - var_seq: u32, - funcs: &'a Funcs, - puts_id: FuncId, - printf_id: FuncId, -} - -impl<'a> FnLower<'a> { - fn new_var(&mut self, ty: cranelift_codegen::ir::Type) -> Variable { - let var = Variable::from_u32(self.var_seq); - self.var_seq += 1; - self.builder.declare_var(var, ty); - var - } - - fn bind_arg(&mut self, arg: &ASTTy, value: Value) -> BackendResult<()> { - let name = match &arg.node { - NodeTy::FunArg { var, .. } => fun_name(var)?, - _ => return Err(BackendErr::unimplemented(arg, "non-identifier argument")), - }; - let ty = arg_type(arg)?; - let var = self.new_var(ty); - self.builder.def_var(var, value); - self.vars.insert(name, (var, ty)); - Ok(()) - } - - /// Lower `ast` as a statement: for side effects only, its value (if any) is discarded. - fn lower_stmt(&mut self, ast: &ASTTy) -> BackendResult<()> { - match &ast.node { - NodeTy::VariableDef { - var, - expr: Some(expr), - .. - } => { - let name = fun_name(var)?; - let ty = cranelift_type(expr)?; - let value = self.lower_expr(expr)?; - let var = self.new_var(ty); - self.builder.def_var(var, value); - self.vars.insert(name, (var, ty)); - Ok(()) - } - NodeTy::IfElse { cond, then, el } => { - let cond_value = self.lower_expr(cond)?; - let then_block = self.builder.create_block(); - let merge_block = self.builder.create_block(); - let else_block = if el.is_some() { - self.builder.create_block() - } else { - merge_block - }; - - self.builder - .ins() - .brif(cond_value, then_block, &[], else_block, &[]); - - self.builder.switch_to_block(then_block); - self.lower_stmt(then)?; - self.builder.ins().jump(merge_block, &[]); - - if let Some(el) = el { - self.builder.switch_to_block(else_block); - self.lower_stmt(el)?; - self.builder.ins().jump(merge_block, &[]); - } - - self.builder.switch_to_block(merge_block); - Ok(()) - } - NodeTy::Block { statements } => { - for statement in statements { - self.lower_stmt(statement)?; - } - Ok(()) - } - NodeTy::FunctionCall { name, .. } if name.name == PRINT => { - self.lower_print(ast).map(|_| ()) - } - NodeTy::FunctionCall { .. } => self.lower_expr(ast).map(|_| ()), - other => Err(BackendErr::unimplemented( - ast, - &format!("{other:?} statement"), - )), - } - } - - /// Lower `ast` as the tail of a function body: it must end the current block with a `return` - /// (possibly by recursing into `Block`'s last statement, or into each arm of an `IfElse`). - fn lower_tail(&mut self, ast: &ASTTy) -> BackendResult<()> { - match &ast.node { - NodeTy::Return { expr } => { - let value = self.lower_expr(expr)?; - self.builder.ins().return_(&[value]); - Ok(()) - } - NodeTy::ReturnEmpty => { - self.builder.ins().return_(&[]); - Ok(()) - } - NodeTy::Block { statements } => match statements.split_last() { - Some((last, init)) => { - for statement in init { - self.lower_stmt(statement)?; - } - self.lower_tail(last) - } - None => { - self.builder.ins().return_(&[]); - Ok(()) - } - }, - NodeTy::IfElse { cond, then, el } => { - let cond_value = self.lower_expr(cond)?; - let then_block = self.builder.create_block(); - let else_block = self.builder.create_block(); - - self.builder - .ins() - .brif(cond_value, then_block, &[], else_block, &[]); - - self.builder.switch_to_block(then_block); - self.lower_tail(then)?; - - self.builder.switch_to_block(else_block); - match el { - Some(el) => self.lower_tail(el)?, - None => { - self.builder.ins().return_(&[]); - } - } - Ok(()) - } - _ => { - let value = self.lower_expr(ast)?; - self.builder.ins().return_(&[value]); - Ok(()) - } - } - } - - /// Lower `ast` as a value-producing expression. - fn lower_expr(&mut self, ast: &ASTTy) -> BackendResult { - match &ast.node { - // Int/Bool literals' own resolved `ty` can come back widened to a union (e.g. a - // literal argument to `print`, whose parameter accepts several printable types - // unifies to that broader union rather than staying just `Int`) -- but the node - // variant itself already tells us the literal's true type, so there's no need to - // consult `ast.ty` at all here. - NodeTy::Int { lit } => { - let value: i64 = lit.parse().map_err(|_| { - BackendErr::new(ast.pos, &format!("Invalid int literal '{lit}'")) - })?; - Ok(self.builder.ins().iconst(types::I64, value)) - } - NodeTy::Bool { lit } => Ok(self.builder.ins().iconst(types::I8, i64::from(*lit))), - NodeTy::Id { lit } => { - let (var, _) = self.vars.get(lit).ok_or_else(|| { - BackendErr::new(ast.pos, &format!("Undefined variable '{lit}'")) - })?; - Ok(self.builder.use_var(*var)) - } - NodeTy::Add { left, right } => { - self.lower_arith(ast, left, right, |b, a, c| b.ins().iadd(a, c)) - } - NodeTy::Sub { left, right } => { - self.lower_arith(ast, left, right, |b, a, c| b.ins().isub(a, c)) - } - NodeTy::Mul { left, right } => { - self.lower_arith(ast, left, right, |b, a, c| b.ins().imul(a, c)) - } - NodeTy::Div { left, right } => { - self.lower_arith(ast, left, right, |b, a, c| b.ins().sdiv(a, c)) - } - NodeTy::Le { left, right } => self.lower_cmp(left, right, IntCC::SignedLessThan), - NodeTy::Leq { left, right } => { - self.lower_cmp(left, right, IntCC::SignedLessThanOrEqual) - } - NodeTy::Ge { left, right } => self.lower_cmp(left, right, IntCC::SignedGreaterThan), - NodeTy::Geq { left, right } => { - self.lower_cmp(left, right, IntCC::SignedGreaterThanOrEqual) - } - NodeTy::Eq { left, right } => self.lower_cmp(left, right, IntCC::Equal), - NodeTy::Neq { left, right } => self.lower_cmp(left, right, IntCC::NotEqual), - NodeTy::FunctionCall { name, args } => { - let func_id = *self.funcs.get(&name.name).ok_or_else(|| { - BackendErr::new(ast.pos, &format!("Undefined function '{}'", name.name)) - })?; - let local = self.module.declare_func_in_func(func_id, self.builder.func); - let mut arg_values = vec![]; - for arg in args { - arg_values.push(self.lower_expr(arg)?); - } - let call = self.builder.ins().call(local, &arg_values); - self.builder - .inst_results(call) - .first() - .copied() - .ok_or_else(|| { - BackendErr::new( - ast.pos, - &format!("'{}' does not return a value", name.name), - ) - }) - } - other => Err(BackendErr::unimplemented( - ast, - &format!("{other:?} expression"), - )), - } - } - - fn lower_arith( - &mut self, - ast: &ASTTy, - left: &ASTTy, - right: &ASTTy, - op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, - ) -> BackendResult { - cranelift_type(ast)?; // reject non-primitive-typed arithmetic early, with a clear error - let l = self.lower_expr(left)?; - let r = self.lower_expr(right)?; - Ok(op(&mut self.builder, l, r)) - } - - fn lower_cmp(&mut self, left: &ASTTy, right: &ASTTy, cc: IntCC) -> BackendResult { - let l = self.lower_expr(left)?; - let r = self.lower_expr(right)?; - Ok(self.builder.ins().icmp(cc, l, r)) - } - - /// `print(...)`: a string literal goes through `puts` (which appends its own newline, like - /// Mamba/Python's `print`); a primitive value goes through `printf` with a fixed `%lld\n` - /// format. Anything else (interpolated strings, non-primitive values, multiple arguments) is - /// out of scope for this backend. - fn lower_print(&mut self, ast: &ASTTy) -> BackendResult> { - let args = match &ast.node { - NodeTy::FunctionCall { args, .. } => args, - _ => unreachable!("only called for a FunctionCall node"), - }; - let arg = match args.as_slice() { - [arg] => arg, - _ => return Err(BackendErr::unimplemented(ast, "print with != 1 argument")), - }; - - match &arg.node { - NodeTy::Str { lit, expressions } if expressions.is_empty() => { - let data = format!("{lit}\0").into_bytes().into_boxed_slice(); - let data_id = self - .module - .declare_anonymous_data(false, false) - .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; - let mut desc = DataDescription::new(); - desc.define(data); - self.module - .define_data(data_id, &desc) - .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; - - let gv = self.module.declare_data_in_func(data_id, self.builder.func); - let pointer_type = self.module.isa().pointer_type(); - let ptr = self.builder.ins().global_value(pointer_type, gv); - let callee = self - .module - .declare_func_in_func(self.puts_id, self.builder.func); - self.builder.ins().call(callee, &[ptr]); - Ok(None) - } - NodeTy::Str { .. } => Err(BackendErr::unimplemented( - ast, - "print of an interpolated string", - )), - _ => { - let value = self.lower_expr(arg)?; - let fmt = format!("{}\0", "%lld\n").into_bytes().into_boxed_slice(); - let data_id = self - .module - .declare_anonymous_data(false, false) - .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; - let mut desc = DataDescription::new(); - desc.define(fmt); - self.module - .define_data(data_id, &desc) - .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; - - let gv = self.module.declare_data_in_func(data_id, self.builder.func); - let pointer_type = self.module.isa().pointer_type(); - let ptr = self.builder.ins().global_value(pointer_type, gv); - // Widen a narrower-than-i64 value (e.g. Bool, stored as i8) to match `%lld`. - let value = if self.builder.func.dfg.value_type(value) != types::I64 { - self.builder.ins().sextend(types::I64, value) - } else { - value - }; - let callee = self - .module - .declare_func_in_func(self.printf_id, self.builder.func); - self.builder.ins().call(callee, &[ptr, value]); - Ok(None) - } - } - } -} diff --git a/src/backend/cranelift/mod.rs b/src/backend/cranelift/mod.rs index 5a87fffd8..48be5cf09 100644 --- a/src/backend/cranelift/mod.rs +++ b/src/backend/cranelift/mod.rs @@ -15,11 +15,14 @@ use crate::common::position::Position; use crate::common::result::WithSource; use crate::{check_sources, strip_source_paths, Context}; +mod convert; mod link; -mod lower; mod primitive; + pub mod result; +const TARGET: &str = "a.out"; + /// Compile `source` to native object code and link it into an executable, written to disk. /// /// Output is written to `target` (relative to `dir`) if given, otherwise to `a.out` in `dir`. @@ -32,7 +35,7 @@ pub fn write_output( src_path: &Path, triple: Option<&str>, ) -> Result> { - let out_file = dir.join(target.unwrap_or("a.out")); + let out_file = dir.join(target.unwrap_or(TARGET)); if let Some(parent) = out_file.parent() { if !parent.exists() { create_dir(parent).map_err(|e| vec![e.to_string()])?; @@ -76,7 +79,7 @@ fn mamba_to_object( .iter() .zip(&source) .map(|(ast_ty, (src, path))| { - compile(ast_ty, &ctx, target) + compile(ast_ty, target, &ctx) .map_err(|err| err.with_source(&Some(src.clone()), &path.clone())) }) .partition(Result::is_ok); @@ -94,14 +97,14 @@ fn mamba_to_object( /// /// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`). /// If `None`, the host triple is used. -pub fn compile(ast_ty: &ASTTy, ctx: &Context, target: Option<&str>) -> BackendResult> { +pub fn compile(ast_ty: &ASTTy, target: Option<&str>, ctx: &Context) -> BackendResult> { let isa = build_isa(target)?; let builder = ObjectBuilder::new(isa, "mamba", default_libcall_names()) .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; let mut module = ObjectModule::new(builder); - lower::lower_program(ast_ty, ctx, &mut module)?; + convert::lower_program(ast_ty, ctx, &mut module)?; module .finish() diff --git a/src/backend/cranelift/primitive.rs b/src/backend/cranelift/primitive.rs index 542d34c32..bc02fa73e 100644 --- a/src/backend/cranelift/primitive.rs +++ b/src/backend/cranelift/primitive.rs @@ -10,7 +10,7 @@ use crate::backend::cranelift::result::{BackendErr, BackendResult}; /// The Cranelift type a resolved Mamba primitive lowers to. /// -/// Scoped to exactly the primitives the Cranelift backend supports (see `lower.rs`): `Int`, +/// Scoped to exactly the primitives the Cranelift backend supports (see `convert/`): `Int`, /// `Bool`, and `Float`. Anything else (a class, a collection, a union of more than one name, /// an unresolved type) is out of scope for this backend. pub fn cranelift_type(ast: &ASTTy) -> BackendResult { From bf7defb211d3bb4cc88f2fd331af85f961cceb71 Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 11:00:23 +0200 Subject: [PATCH 07/16] feat: allow printing assembly --- README.md | 9 +-- src/backend/cranelift/convert/definition.rs | 28 ++++++-- src/backend/cranelift/convert/mod.rs | 28 ++++++-- src/backend/cranelift/mod.rs | 73 ++++++++++++++++++++- src/backend/mod.rs | 8 +++ src/cli.rs | 21 ++++-- src/lib.rs | 8 +++ src/main.rs | 4 ++ tests/execution.rs | 30 +++++++++ 9 files changed, 190 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 15b1ad216..95e79acfd 100644 --- a/README.md +++ b/README.md @@ -753,16 +753,17 @@ end ## 💻 The Command Line Interface ``` -Transpile Mamba to Python code, or compile it to a native binary. +Transpile Mamba to Python code, compile it to a native binary, or print its assembly. Usage: mamba [OPTIONS] Options: -i, --input Input file or directory. If file, file taken as input. If directory, recursively search all sub-directories for *.mamba files. If no input given, current directory used as input directory - -o, --output Output location. With `--python` (the default): output directory to store Python files, structured to reflect the input directory; if not given, a 'target' directory is created in the current directory. With `--bin`: path of the linked executable to produce; if not given, 'a.out' is created in the current directory + -o, --output Output location. With `--python` (the default): output directory to store Python files, structured to reflect the input directory; if not given, a 'target' directory is created in the current directory. With `--bin`: path of the linked executable to produce; if not given, 'a.out' is created in the current directory. Ignored with`--asm`, which always prints to stdout instead of writing a file --python Output Python source (the default) - --bin Compile and link a native executable via the Cranelift backend, instead of outputting Python source. Only a small subset of the language is currentlysupported: literals, arithmetic and comparison operators, if/else, top-level function definitions and calls, and `print` - --target Target triple to pass to Cranelift, e.g. `x86_64-unknown-linux-gnu` (only meaningful with `--bin`; defaults to the host triple) + --bin Compile and link a native executable via the Cranelift backend, instead of outputting Python source. Only a small subset of the language is currently supported: literals, arithmetic and comparison operators, if/else, top-level function definitions and calls, and `print` + --asm Compile via the Cranelift backend and print the resulting disassembly to stdout, instead of outputting Python source or linking an executable. No file is written -- pipe stdout (e.g. `> out.s`) if you want to save it. Same language subset as `--bin` (see its help). Printed in AT&T syntax (`movq %rsp, %rbp`, source before destination) -- Cranelift's own disassembler doesn't support switching to Intel syntax + --target Target triple to pass to Cranelift, e.g. `x86_64-unknown-linux-gnu` (only meaningful with `--bin`/`--asm`; defaults to the host triple) -v... Set level of verbosity: - `-v` : info, error, warning printed to stderr (default) - `-vv` : debug messages are printed - `-vvv` : trace messages are printed -d, --debug Add line numbers to log statements --no-module-path Disable the module path in the log statements diff --git a/src/backend/cranelift/convert/definition.rs b/src/backend/cranelift/convert/definition.rs index 7b67f2790..23941120c 100644 --- a/src/backend/cranelift/convert/definition.rs +++ b/src/backend/cranelift/convert/definition.rs @@ -87,6 +87,9 @@ pub(super) fn declare_libc( } /// Define a top-level `FunDef`'s body as a real Cranelift function. +/// +/// Returns the function's disassembly text when `want_asm` is set -- `None` otherwise, so callers +/// that don't need it (e.g. [`super::compile`]) don't pay for [`ClifContext::set_disasm`]. pub(super) fn define_function( module: &mut ObjectModule, func_id: FuncId, @@ -94,8 +97,10 @@ pub(super) fn define_function( args: &[ASTTy], body: Option<&ASTTy>, funcs: &Funcs, -) -> BackendResult<()> { + want_asm: bool, +) -> BackendResult> { let mut ctx = ClifContext::new(); + ctx.set_disasm(want_asm); ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), sig); let mut fb_ctx = FunctionBuilderContext::new(); @@ -134,18 +139,23 @@ pub(super) fn define_function( module .define_function(func_id, &mut ctx) - .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string())) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string()))?; + Ok(want_asm.then(|| disasm_text(&ctx))) } /// Define the synthetic `main` collecting every top-level statement that isn't a `FunDef`. +/// +/// Returns its disassembly text when `want_asm` is set -- see [`define_function`]'s doc comment. pub(super) fn define_main( module: &mut ObjectModule, func_id: FuncId, sig: Signature, statements: &[ASTTy], funcs: &Funcs, -) -> BackendResult<()> { + want_asm: bool, +) -> BackendResult> { let mut ctx = ClifContext::new(); + ctx.set_disasm(want_asm); ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), sig); let mut fb_ctx = FunctionBuilderContext::new(); @@ -177,7 +187,17 @@ pub(super) fn define_main( module .define_function(func_id, &mut ctx) - .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string())) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string()))?; + Ok(want_asm.then(|| disasm_text(&ctx))) +} + +/// The disassembly text Cranelift collected while compiling `ctx.func`, if any -- empty when the +/// backend couldn't produce one, which the `vcode` field's doc comment says can happen even with +/// [`ClifContext::set_disasm`] on. Only call this when disasm was actually requested. +fn disasm_text(ctx: &ClifContext) -> String { + ctx.compiled_code() + .and_then(|compiled| compiled.vcode.clone()) + .unwrap_or_default() } impl<'a> FnLower<'a> { diff --git a/src/backend/cranelift/convert/mod.rs b/src/backend/cranelift/convert/mod.rs index d75167dde..4a6a15bb4 100644 --- a/src/backend/cranelift/convert/mod.rs +++ b/src/backend/cranelift/convert/mod.rs @@ -27,11 +27,16 @@ type Funcs = HashMap; /// collected into a synthetic `main`, mirroring how a `.mamba` file's top-level statements run /// top-to-bottom as a script in the Python backend -- machine code needs an explicit entry point, /// which Python's linear script execution doesn't. +/// +/// When `want_asm` is set, returns each defined function's disassembly text, labeled by name +/// (`main` last) -- otherwise the returned `Vec` is always empty, and the (otherwise pointless) +/// cost of having Cranelift compute disassembly text is skipped entirely. pub(super) fn lower_program( ast_ty: &ASTTy, _ctx: &Context, module: &mut ObjectModule, -) -> BackendResult<()> { + want_asm: bool, +) -> BackendResult> { let statements = match &ast_ty.node { NodeTy::Block { statements } => statements, _ => std::slice::from_ref(ast_ty), @@ -55,6 +60,7 @@ pub(super) fn lower_program( // Pass 2: define each function's body, plus a synthetic `main` for everything else. let mut main_body = vec![]; + let mut asm = vec![]; for statement in statements { match &statement.node { NodeTy::FunDef { @@ -67,7 +73,18 @@ pub(super) fn lower_program( let name = common::fun_name(id)?; let func_id = *funcs.get(&name).expect("declared in pass 1"); let sig = fun_signature(args, ret.as_ref(), call_conv, statement)?; - define_function(module, func_id, sig, args, body.as_deref(), &funcs)?; + let text = define_function( + module, + func_id, + sig, + args, + body.as_deref(), + &funcs, + want_asm, + )?; + if let Some(text) = text { + asm.push((name, text)); + } } _ => main_body.push(statement.clone()), } @@ -81,9 +98,12 @@ pub(super) fn lower_program( let main_id = module .declare_function("main", Linkage::Export, &main_sig) .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; - define_main(module, main_id, main_sig, &main_body, &funcs)?; + let main_text = define_main(module, main_id, main_sig, &main_body, &funcs, want_asm)?; + if let Some(main_text) = main_text { + asm.push((String::from("main"), main_text)); + } - Ok(()) + Ok(asm) } /// Per-function-body lowering state. diff --git a/src/backend/cranelift/mod.rs b/src/backend/cranelift/mod.rs index 48be5cf09..364877934 100644 --- a/src/backend/cranelift/mod.rs +++ b/src/backend/cranelift/mod.rs @@ -93,6 +93,52 @@ fn mamba_to_object( Ok(objects.into_iter().map(Result::unwrap).collect()) } +/// Compile `source` and print its disassembly to stdout, one function at a time. +/// +/// No file is written -- pipe stdout (`> out.s`) if you want to save it. `triple`, if given, is a +/// target triple passed on to Cranelift; if `None`, the host triple is used. +pub fn print_asm( + source: &[(String, Option)], + src_path: &Path, + triple: Option<&str>, +) -> Result<(), Vec> { + let asm_sources = mamba_to_asm(source, &src_path.to_path_buf(), triple)?; + for asm in &asm_sources { + println!("{asm}"); + } + Ok(()) +} + +/// Compile mamba source to disassembly text, one string per source, via the Cranelift backend. +/// +/// `target`, if given, is a target triple passed on to Cranelift; if `None`, the host triple is +/// used. A path can optionally be given per source for error messages. +fn mamba_to_asm( + source: &[(String, Option)], + source_dir: &PathBuf, + target: Option<&str>, +) -> Result, Vec> { + let source = strip_source_paths(source, source_dir); + let (ctx, typed_ast) = check_sources(&source)?; + + let (asm_sources, gen_errs): (Vec<_>, Vec<_>) = typed_ast + .iter() + .zip(&source) + .map(|(ast_ty, (src, path))| { + disassemble(ast_ty, target, &ctx) + .map_err(|err| err.with_source(&Some(src.clone()), &path.clone())) + }) + .partition(Result::is_ok); + + let gen_errs: Vec<_> = gen_errs.into_iter().map(Result::unwrap_err).collect(); + if !gen_errs.is_empty() { + return Err(gen_errs.iter().map(|err| format!("{err}")).collect()); + } + + trace!("Disassembled {} files", asm_sources.len()); + Ok(asm_sources.into_iter().map(Result::unwrap).collect()) +} + /// Compile a single checked Mamba file to the bytes of a native object file. /// /// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`). @@ -104,7 +150,7 @@ pub fn compile(ast_ty: &ASTTy, target: Option<&str>, ctx: &Context) -> BackendRe .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; let mut module = ObjectModule::new(builder); - convert::lower_program(ast_ty, ctx, &mut module)?; + convert::lower_program(ast_ty, ctx, &mut module, false)?; module .finish() @@ -112,6 +158,31 @@ pub fn compile(ast_ty: &ASTTy, target: Option<&str>, ctx: &Context) -> BackendRe .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string())) } +/// Compile a single checked Mamba file and return its disassembly text, one block per function +/// (`main` last), instead of emitting an object file. +/// +/// Printed in AT&T syntax (source operand before destination, e.g. `movq %rsp, %rbp`) -- that's +/// what Cranelift's own disassembler (`CompiledCode::vcode`, via `Context::set_disasm`) always +/// produces. Real Intel-syntax output would mean re-disassembling the emitted machine code with +/// an external disassembler (e.g. capstone) instead, which isn't wired up here. +/// +/// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`). +/// If `None`, the host triple is used. +pub fn disassemble(ast_ty: &ASTTy, target: Option<&str>, ctx: &Context) -> BackendResult { + let isa = build_isa(target)?; + + let builder = ObjectBuilder::new(isa, "mamba", default_libcall_names()) + .map_err(|e| BackendErr::new(ast_ty.pos, &e.to_string()))?; + let mut module = ObjectModule::new(builder); + + let asm = convert::lower_program(ast_ty, ctx, &mut module, true)?; + Ok(asm + .into_iter() + .map(|(name, text)| format!("; -- {name} --\n{text}")) + .collect::>() + .join("\n")) +} + /// Create target which is understood by cranelift. /// If None, then default to host architecture as target. fn build_isa(target: Option<&str>) -> BackendResult { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 6455f5038..f9174aab6 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -12,4 +12,12 @@ pub enum Backend { /// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`) passed on to /// Cranelift; if `None`, the host triple is used. Bin { target: Option }, + /// Compile via the Cranelift backend and print the resulting disassembly to stdout, instead + /// of linking an executable. No file is written -- pipe stdout if you want to save it. + /// Printed in AT&T syntax -- Cranelift's own disassembler doesn't support switching to Intel + /// syntax. + /// + /// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`) passed on to + /// Cranelift; if `None`, the host triple is used. + Asm { target: Option }, } diff --git a/src/cli.rs b/src/cli.rs index 4c21c9e1d..a5bcce4dc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,11 +1,11 @@ use clap::{ArgAction, Parser}; -/// Transpile Mamba to Python code, or compile it to a native binary. +/// Transpile Mamba to Python code, compile it to a native binary, or print its assembly. #[derive(Debug, Parser)] #[command( name = "Mamba", author = "Joël Abrahams", - about = "Transpile Mamba to Python code, or compile it to a native binary." + about = "Transpile Mamba to Python code, compile it to a native binary, or print its assembly." )] pub struct Cli { /// Input file or directory. @@ -21,23 +21,32 @@ pub struct Cli { /// directory. /// With `--bin`: path of the linked executable to produce; if not given, 'a.out' is created /// in the current directory. + /// Ignored with `--asm`, which always prints to stdout instead of writing a file. #[arg(short = 'o', long = "output", value_name = "OUTPUT", value_parser)] pub output: Option, /// Output Python source (the default). - #[arg(long = "python", action = ArgAction::SetTrue, conflicts_with = "bin")] + #[arg(long = "python", action = ArgAction::SetTrue, conflicts_with_all = ["bin", "asm"])] pub python: bool, /// Compile and link a native executable via the Cranelift backend, instead of outputting /// Python source. /// Only a small subset of the language is currently supported: literals, arithmetic and /// comparison operators, if/else, top-level function definitions and calls, and `print`. - #[arg(long = "bin", action = ArgAction::SetTrue, conflicts_with = "python")] + #[arg(long = "bin", action = ArgAction::SetTrue, conflicts_with_all = ["python", "asm"])] pub bin: bool, + /// Compile via the Cranelift backend and print the resulting disassembly to stdout, instead + /// of outputting Python source or linking an executable. No file is written -- pipe stdout + /// (e.g. `> out.s`) if you want to save it. Same language subset as `--bin` (see its help). + /// Printed in AT&T syntax (`movq %rsp, %rbp`, source before destination) -- Cranelift's own + /// disassembler doesn't support switching to Intel syntax. + #[arg(long = "asm", action = ArgAction::SetTrue, conflicts_with_all = ["python", "bin"])] + pub asm: bool, + /// Target triple to pass to Cranelift, e.g. `x86_64-unknown-linux-gnu` (only meaningful with - /// `--bin`; defaults to the host triple). - #[arg(long = "target", value_name = "TARGET", requires = "bin")] + /// `--bin`/`--asm`; defaults to the host triple). + #[arg(long = "target", value_name = "TARGET")] pub target: Option, /// Set level of verbosity: diff --git a/src/lib.rs b/src/lib.rs index d0caf558c..e7975fd2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,14 @@ pub fn transpile_dir( &src_path, triple.as_deref(), ), + Backend::Asm { target: triple } => { + backend::cranelift::print_asm( + source_option_pairs.as_slice(), + &src_path, + triple.as_deref(), + )?; + Ok(dir.to_path_buf()) + } } } diff --git a/src/main.rs b/src/main.rs index ba9edad1c..45e5825fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,10 @@ pub fn main() -> Result<(), String> { Backend::Bin { target: cli_input.target.clone(), } + } else if cli_input.asm { + Backend::Asm { + target: cli_input.target.clone(), + } } else { Backend::Python }; diff --git a/tests/execution.rs b/tests/execution.rs index fa8b36892..d374683a0 100644 --- a/tests/execution.rs +++ b/tests/execution.rs @@ -6,6 +6,7 @@ use std::path::Path; use std::process::Command; +use assert_cmd::prelude::*; use mamba::backend::Backend; use mamba::{transpile_dir, Arguments}; use tests_util::{resource_path, run_python}; @@ -56,3 +57,32 @@ fn bin_backend_prints_expected_output() -> Result<(), Box assert_eq!(String::from_utf8(output.stdout)?, "hello world\n"); Ok(()) } + +#[test] +fn asm_backend_prints_disassembly_to_stdout() -> Result<(), Box> { + let src_dir = resource_path(true, &["function"], ""); + + let mut cmd = Command::main_binary()?; + let output = cmd + .current_dir(&src_dir) + .arg("--asm") + .arg("-i") + .arg("hello_world.mamba") + .output()?; + + assert!( + output.status.success(), + "mamba --asm exited with an error:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout)?; + assert!( + stdout.contains("; -- main --") && stdout.contains("ret"), + "expected disassembly on stdout, got:\n{stdout}" + ); + + // No output directory/file should have been written -- `--asm` only prints. + assert!(!Path::new(&src_dir).join("target").exists()); + assert!(!Path::new(&src_dir).join("a.out").exists()); + Ok(()) +} From de370bd2f738b325f280493291b08369d143df9f Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 11:23:29 +0200 Subject: [PATCH 08/16] feat: for loop and reassign in asm backend --- src/backend/cranelift/README.md | 54 +++++++++++ src/backend/cranelift/convert/control_flow.rs | 96 ++++++++++++++++++- src/backend/cranelift/convert/definition.rs | 29 ++++++ src/backend/cranelift/convert/mod.rs | 2 + src/backend/cranelift/convert/operation.rs | 15 +-- 5 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 src/backend/cranelift/README.md diff --git a/src/backend/cranelift/README.md b/src/backend/cranelift/README.md new file mode 100644 index 000000000..5ea06cf05 --- /dev/null +++ b/src/backend/cranelift/README.md @@ -0,0 +1,54 @@ +

+ Mamba logo +

+ +# Cranelift + +Compiles a checked `ASTTy` directly to native machine code via [Cranelift](https://cranelift.dev/), instead of +transpiling to Python source. Unlike the Python backend, there is no intermediate `PythonCore`-style tree: lowering +walks the `ASTTy` once and emits Cranelift IR straight into a `cranelift_object::ObjectModule` via imperative builder +calls, which Cranelift itself then turns into machine code. + +Three public entry points, all in `mod.rs`, mirroring the Python backend's `write_output`/`gen`/`gen_arguments` shape: + +- `write_output` -- compiles and links an executable, written to disk (the `--bin` CLI flag). +- `print_asm` -- compiles and prints the disassembly to stdout instead, no file written (the `--asm` CLI flag). +- `compile` / `disassemble` -- the single-file entry points those two build on: `compile` returns object bytes, + `disassemble` returns disassembly text (see "Assembly output" below). + +## Supported language subset + +Only a small slice of Mamba compiles down to machine code, enforced by simply erroring +(`BackendErr::unimplemented`) on anything else: + +- `Int`, `Bool`, `Float` primitives -- no collections, strings (beyond a `print` argument), classes, or traits. +- Arithmetic (`+ - * /`) and comparison (`< <= > >= == !=`) operators. +- `if`/`else`, both as a statement and in a function's tail (return) position. +- `for in .. ` / `..=` loops over `Int` ranges -- not arbitrary collections, since collections aren't + supported at all. +- Plain (`:=`) reassignment of an already-declared variable -- not compound assignment (`+=` and friends). +- Top-level function definitions and calls, including forward references within the same file. +- `print`, lowered directly to libc `puts` (string literal) or `printf` (primitive value). + +Every other top-level statement in a file is collected into a synthetic `main`, since machine code needs an explicit +entry point the way a `.mamba` file's top-to-bottom script execution doesn't. + +## Layout + +- `convert/` -- the lowering itself, split by AST category (`definition.rs`, `control_flow.rs`, `call.rs`, + `operation.rs`, plus a shared `common.rs`), the same way `backend::python::convert` is. `mod.rs` holds the entry + point (`lower_program`) and the three dispatchers a Mamba node can be lowered as: a statement (`lower_stmt`), the + tail of a function body (`lower_tail`), or a value-producing expression (`lower_expr`). +- `primitive.rs` -- resolves a checked `Name` to the one Cranelift `Type` it supports (`Int`/`Bool`/`Float`), the + same role `backend::python::name` plays for Python's richer type surface. +- `link.rs` -- shells out to the system `cc` to link object files into an executable, the same approach `rustc` + itself uses rather than reimplementing a linker. +- `result.rs` -- `BackendErr`/`BackendResult`, mirroring `backend::python::result`. + +## Assembly output + +`disassemble` asks Cranelift to compute disassembly text (`Context::set_disasm` + `CompiledCode::vcode`) while +lowering, gated behind a `want_asm: bool` threaded through `convert::lower_program` so `compile` (the `--bin` path) +never pays for it. It's printed in AT&T syntax (source operand before destination, e.g. `movq %rsp, %rbp`) -- +that's what Cranelift's own disassembler always produces; real Intel-syntax output would mean re-disassembling the +emitted machine code with an external disassembler (e.g. capstone) instead, which isn't wired up here. diff --git a/src/backend/cranelift/convert/control_flow.rs b/src/backend/cranelift/convert/control_flow.rs index 988624a8d..55fe00517 100644 --- a/src/backend/cranelift/convert/control_flow.rs +++ b/src/backend/cranelift/convert/control_flow.rs @@ -1,8 +1,11 @@ -use cranelift_codegen::ir::InstBuilder; +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::{types, InstBuilder}; +use crate::backend::cranelift::convert::common::fun_name; use crate::backend::cranelift::convert::FnLower; -use crate::backend::cranelift::result::BackendResult; -use crate::check::ast::ASTTy; +use crate::backend::cranelift::primitive::cranelift_type; +use crate::backend::cranelift::result::{BackendErr, BackendResult}; +use crate::check::ast::{ASTTy, NodeTy}; impl<'a> FnLower<'a> { /// Lower an `IfElse` in statement position: both arms are lowered as statements, and control @@ -41,6 +44,93 @@ impl<'a> FnLower<'a> { Ok(()) } + /// Lower a `For` loop in statement position. + /// + /// Only `for in .. ` / `for in ..= ` over `Int` bounds is + /// supported -- iterating a collection would need this backend to support collections at all, + /// which is out of scope (see the module-level docs). + /// + /// Classic three-block loop: `header` checks the bound and either enters `body` or falls + /// through to `exit`; `body` runs the loop body (with the loop variable shadowing any + /// outer binding of the same name for its duration) and increments before jumping back to + /// `header`. + pub(super) fn lower_for( + &mut self, + expr: &ASTTy, + col: &ASTTy, + body: &ASTTy, + ) -> BackendResult<()> { + let (from, to, inclusive, step) = match &col.node { + NodeTy::Range { + from, + to, + inclusive, + step, + } => (from, to, *inclusive, step), + other => { + return Err(BackendErr::unimplemented( + col, + &format!("{other:?} for-loop collection"), + )) + } + }; + let var_name = fun_name(expr)?; + + let ty = cranelift_type(from)?; + if ty != types::I64 { + return Err(BackendErr::new( + from.pos, + "For-loops are only supported over Int ranges", + )); + } + + let from_value = self.lower_expr(from)?; + let to_value = self.lower_expr(to)?; + let step_value = match step { + Some(step) => self.lower_expr(step)?, + None => self.builder.ins().iconst(types::I64, 1), + }; + + let loop_var = self.new_var(ty); + self.builder.def_var(loop_var, from_value); + + let header_block = self.builder.create_block(); + let body_block = self.builder.create_block(); + let exit_block = self.builder.create_block(); + self.builder.ins().jump(header_block, &[]); + + self.builder.switch_to_block(header_block); + let current = self.builder.use_var(loop_var); + let cc = if inclusive { + IntCC::SignedLessThanOrEqual + } else { + IntCC::SignedLessThan + }; + let cond = self.builder.ins().icmp(cc, current, to_value); + self.builder + .ins() + .brif(cond, body_block, &[], exit_block, &[]); + + self.builder.switch_to_block(body_block); + let shadowed = self.vars.insert(var_name.clone(), (loop_var, ty)); + self.lower_stmt(body)?; + match shadowed { + Some(shadowed) => { + self.vars.insert(var_name, shadowed); + } + None => { + self.vars.remove(&var_name); + } + } + let current = self.builder.use_var(loop_var); + let next = self.builder.ins().iadd(current, step_value); + self.builder.def_var(loop_var, next); + self.builder.ins().jump(header_block, &[]); + + self.builder.switch_to_block(exit_block); + Ok(()) + } + /// Lower an `IfElse` in tail position: both arms must themselves end in a `return`, so unlike /// [`Self::lower_if_else_stmt`] there is no shared merge block to jump back to. pub(super) fn lower_if_else_tail( diff --git a/src/backend/cranelift/convert/definition.rs b/src/backend/cranelift/convert/definition.rs index 23941120c..5062b1cc9 100644 --- a/src/backend/cranelift/convert/definition.rs +++ b/src/backend/cranelift/convert/definition.rs @@ -16,6 +16,7 @@ use crate::backend::cranelift::result::{BackendErr, BackendResult}; use crate::check::ast::{ASTTy, NodeTy}; use crate::check::name::Name; use crate::common::position::Position; +use crate::parse::ast::node_op::NodeOp; /// A `FunDef`'s Cranelift signature, built from its declared argument types and return type. pub(super) fn fun_signature( @@ -224,6 +225,34 @@ impl<'a> FnLower<'a> { } } + /// Lower a plain (`:=`) reassignment to an already-declared variable. Compound assignment + /// (`+=` and friends) is out of scope -- write `x := x + y` instead of `x += y`. + pub(super) fn lower_reassign(&mut self, ast: &ASTTy) -> BackendResult<()> { + match &ast.node { + NodeTy::Reassign { + left, + right, + op: NodeOp::Assign, + } => { + let name = fun_name(left)?; + let (var, _) = *self.vars.get(&name).ok_or_else(|| { + BackendErr::new(ast.pos, &format!("Undefined variable '{name}'")) + })?; + let value = self.lower_expr(right)?; + self.builder.def_var(var, value); + Ok(()) + } + NodeTy::Reassign { op, .. } => Err(BackendErr::unimplemented( + ast, + &format!("{op:?} compound reassignment"), + )), + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} reassignment"), + )), + } + } + pub(super) fn bind_arg(&mut self, arg: &ASTTy, value: Value) -> BackendResult<()> { let name = match &arg.node { NodeTy::FunArg { var, .. } => fun_name(var)?, diff --git a/src/backend/cranelift/convert/mod.rs b/src/backend/cranelift/convert/mod.rs index 4a6a15bb4..f8e6dcd94 100644 --- a/src/backend/cranelift/convert/mod.rs +++ b/src/backend/cranelift/convert/mod.rs @@ -122,7 +122,9 @@ impl<'a> FnLower<'a> { fn lower_stmt(&mut self, ast: &ASTTy) -> BackendResult<()> { match &ast.node { NodeTy::VariableDef { .. } => self.lower_variable_def(ast), + NodeTy::Reassign { .. } => self.lower_reassign(ast), NodeTy::IfElse { cond, then, el } => self.lower_if_else_stmt(cond, then, el.as_deref()), + NodeTy::For { expr, col, body } => self.lower_for(expr, col, body), NodeTy::Block { statements } => { for statement in statements { self.lower_stmt(statement)?; diff --git a/src/backend/cranelift/convert/operation.rs b/src/backend/cranelift/convert/operation.rs index 84be28899..cf733f618 100644 --- a/src/backend/cranelift/convert/operation.rs +++ b/src/backend/cranelift/convert/operation.rs @@ -12,16 +12,16 @@ impl<'a> FnLower<'a> { pub(super) fn lower_operation(&mut self, ast: &ASTTy) -> BackendResult { match &ast.node { NodeTy::Add { left, right } => { - self.lower_arith(ast, left, right, |b, a, c| b.ins().iadd(a, c)) + self.lower_arith(left, right, |b, a, c| b.ins().iadd(a, c)) } NodeTy::Sub { left, right } => { - self.lower_arith(ast, left, right, |b, a, c| b.ins().isub(a, c)) + self.lower_arith(left, right, |b, a, c| b.ins().isub(a, c)) } NodeTy::Mul { left, right } => { - self.lower_arith(ast, left, right, |b, a, c| b.ins().imul(a, c)) + self.lower_arith(left, right, |b, a, c| b.ins().imul(a, c)) } NodeTy::Div { left, right } => { - self.lower_arith(ast, left, right, |b, a, c| b.ins().sdiv(a, c)) + self.lower_arith(left, right, |b, a, c| b.ins().sdiv(a, c)) } NodeTy::Le { left, right } => self.lower_cmp(left, right, IntCC::SignedLessThan), NodeTy::Leq { left, right } => { @@ -42,12 +42,15 @@ impl<'a> FnLower<'a> { fn lower_arith( &mut self, - ast: &ASTTy, left: &ASTTy, right: &ASTTy, op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, ) -> BackendResult { - cranelift_type(ast)?; // reject non-primitive-typed arithmetic early, with a clear error + // Reject non-primitive-typed arithmetic early, with a clear error. Checked against + // `left`'s own type rather than the whole expression's -- e.g. as a `print(...)` argument, + // the *expression*'s resolved type widens to whatever union `print` accepts, even though + // the operands (and thus the actual machine type produced) stay concretely typed. + cranelift_type(left)?; let l = self.lower_expr(left)?; let r = self.lower_expr(right)?; Ok(op(&mut self.builder, l, r)) From 5b1fc89f910f001dad7c5ae2ad0c4b080cd64ef0 Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 11:24:52 +0200 Subject: [PATCH 09/16] test: simple arithmetic output In both python and assembly --- Cargo.lock | 1 + tests/execution.rs | 80 ++++--------------- .../valid/function/arithmetic_sum.mamba | 1 + .../valid/function/for_loop_sum.mamba | 3 + tests_util/Cargo.toml | 1 + tests_util/src/lib.rs | 75 +++++++++++++++++ 6 files changed, 95 insertions(+), 66 deletions(-) create mode 100644 tests/resource/valid/function/arithmetic_sum.mamba create mode 100644 tests/resource/valid/function/for_loop_sum.mamba diff --git a/Cargo.lock b/Cargo.lock index c11bc332a..7f87132dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -846,6 +846,7 @@ dependencies = [ name = "tests_util" version = "0.0.0" dependencies = [ + "assert_cmd", "itertools", "mamba", "python-parser", diff --git a/tests/execution.rs b/tests/execution.rs index d374683a0..fdc861fe7 100644 --- a/tests/execution.rs +++ b/tests/execution.rs @@ -3,79 +3,27 @@ //! equivalent to a reference file, these actually run the compiled output and assert on what it //! prints -- for both backends. +use std::error::Error; use std::path::Path; -use std::process::Command; -use assert_cmd::prelude::*; -use mamba::backend::Backend; -use mamba::{transpile_dir, Arguments}; -use tests_util::{resource_path, run_python}; - -#[test] -fn python_backend_prints_expected_output() -> Result<(), Box> { - let src_dir = resource_path(true, &["function"], ""); - let out_dir = tempfile::tempdir()?; - - let arguments = Arguments::default(); // backend defaults to `Backend::Python` - let output_dir = transpile_dir( - Path::new(&src_dir), - Some("hello_world.mamba"), - Some(out_dir.path().join("out").to_str().unwrap()), - &arguments, - ) - .map_err(|errs| format!("{errs:?}"))?; - - let stdout = run_python(&output_dir.join("hello_world.py"))?; - assert_eq!(stdout, "hello world\n"); - Ok(()) -} - -#[test] -fn bin_backend_prints_expected_output() -> Result<(), Box> { - let src_dir = resource_path(true, &["function"], ""); - let out_dir = tempfile::tempdir()?; - let bin_path = out_dir.path().join("hello_world_bin"); - - let arguments = Arguments { - annotate: false, - backend: Backend::Bin { target: None }, - }; - let produced = transpile_dir( - Path::new(&src_dir), - Some("hello_world.mamba"), - Some(bin_path.to_str().unwrap()), - &arguments, - ) - .map_err(|errs| format!("{errs:?}"))?; - - let output = Command::new(&produced).output()?; - assert!( - output.status.success(), - "executable exited with an error:\n{}", - String::from_utf8_lossy(&output.stderr) - ); - assert_eq!(String::from_utf8(output.stdout)?, "hello world\n"); - Ok(()) +use test_case::test_case; +use tests_util::{resource_path, run_cli, run_via_bin, run_via_python, Runner}; + +#[test_case(run_via_python, &["function"], "hello_world.mamba" => "hello world\n")] +#[test_case(run_via_bin, &["function"], "hello_world.mamba" => "hello world\n")] +#[test_case(run_via_python, &["function"], "arithmetic_sum.mamba" => "14\n")] +#[test_case(run_via_bin, &["function"], "arithmetic_sum.mamba" => "14\n")] +#[test_case(run_via_python, &["function"], "for_loop_sum.mamba" => "10\n")] +#[test_case(run_via_bin, &["function"], "for_loop_sum.mamba" => "10\n")] +fn execution(run: Runner, dirs: &[&str], file: &str) -> String { + run(dirs, file).unwrap() } #[test] -fn asm_backend_prints_disassembly_to_stdout() -> Result<(), Box> { +fn asm_prints_disassembly_to_stdout() -> Result<(), Box> { let src_dir = resource_path(true, &["function"], ""); - let mut cmd = Command::main_binary()?; - let output = cmd - .current_dir(&src_dir) - .arg("--asm") - .arg("-i") - .arg("hello_world.mamba") - .output()?; - - assert!( - output.status.success(), - "mamba --asm exited with an error:\n{}", - String::from_utf8_lossy(&output.stderr) - ); - let stdout = String::from_utf8(output.stdout)?; + let stdout = run_cli(Path::new(&src_dir), &["--asm", "-i", "hello_world.mamba"])?; assert!( stdout.contains("; -- main --") && stdout.contains("ret"), "expected disassembly on stdout, got:\n{stdout}" diff --git a/tests/resource/valid/function/arithmetic_sum.mamba b/tests/resource/valid/function/arithmetic_sum.mamba new file mode 100644 index 000000000..5875f0d79 --- /dev/null +++ b/tests/resource/valid/function/arithmetic_sum.mamba @@ -0,0 +1 @@ +print(10 + 4) diff --git a/tests/resource/valid/function/for_loop_sum.mamba b/tests/resource/valid/function/for_loop_sum.mamba new file mode 100644 index 000000000..d88084f4c --- /dev/null +++ b/tests/resource/valid/function/for_loop_sum.mamba @@ -0,0 +1,3 @@ +def total := 0 +for i in 0 ..= 4 do total := total + i end +print(total) diff --git a/tests_util/Cargo.toml b/tests_util/Cargo.toml index c31af3311..0051d1bac 100644 --- a/tests_util/Cargo.toml +++ b/tests_util/Cargo.toml @@ -10,3 +10,4 @@ mamba = { path = ".." } python-parser = "0.1.0" itertools = "0.8.2" tempfile = "3.1.0" +assert_cmd = "0.10" diff --git a/tests_util/src/lib.rs b/tests_util/src/lib.rs index 0ceee1f66..ddcf77191 100644 --- a/tests_util/src/lib.rs +++ b/tests_util/src/lib.rs @@ -1,14 +1,17 @@ use std::cmp::max; +use std::error::Error; use std::fmt::{Debug, Formatter}; use std::fs::{self, create_dir, File}; use std::io::Read; use std::path::{Path, PathBuf}; use std::process::Command; +use assert_cmd::prelude::*; use itertools::{EitherOrBoth, Itertools}; use python_parser::ast::Statement; use tempfile::tempdir_in; +use mamba::backend::Backend; use mamba::common::delimit::newline_delimited; use mamba::{transpile_dir, Arguments}; @@ -19,6 +22,10 @@ pub static PYTHON: &str = "python3"; #[cfg(target_os = "windows")] pub static PYTHON: &str = "python"; +/// A backend-driving test helper's signature -- [run_via_python] and [run_via_bin] both match it, +/// so a test can be parameterized over which backend it runs a fixture through. +pub type Runner = fn(&[&str], &str) -> Result>; + /// Run a Python file with [PYTHON] and return its captured stdout. /// /// Unlike [test_directory]/[fallable], which only diff the generated Python's *AST* against a @@ -41,6 +48,74 @@ pub fn run_python(path: &Path) -> Result { } } +/// Transpile `file` (relative to `subdirs`, under `tests/resource/valid`) to Python and run it, +/// returning its captured stdout. +/// +/// Unlike [run_python], which runs an already-generated Python file, this drives the whole +/// pipeline from `.mamba` source, the way a user invoking the CLI would. +pub fn run_via_python(subdirs: &[&str], file: &str) -> Result> { + let src_dir = resource_path(true, subdirs, ""); + let out_dir = tempfile::tempdir()?; + + let arguments = Arguments::default(); // backend defaults to `Backend::Python` + let output_dir = transpile_dir( + Path::new(&src_dir), + Some(file), + Some(out_dir.path().join("out").to_str().unwrap()), + &arguments, + ) + .map_err(|errs| format!("{errs:?}"))?; + + let py_file = Path::new(file).with_extension("py"); + Ok(run_python(&output_dir.join(py_file))?) +} + +/// Compile `file` (relative to `subdirs`, under `tests/resource/valid`) to a native binary via +/// the Cranelift backend and run it, returning its captured stdout. +pub fn run_via_bin(subdirs: &[&str], file: &str) -> Result> { + let src_dir = resource_path(true, subdirs, ""); + let out_dir = tempfile::tempdir()?; + let bin_path = out_dir.path().join("bin_out"); + + let arguments = Arguments { + annotate: false, + backend: Backend::Bin { target: None }, + }; + let produced = transpile_dir( + Path::new(&src_dir), + Some(file), + Some(bin_path.to_str().unwrap()), + &arguments, + ) + .map_err(|errs| format!("{errs:?}"))?; + + let output = Command::new(&produced).output()?; + if !output.status.success() { + return Err(format!( + "executable exited with an error:\n{}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + Ok(String::from_utf8(output.stdout)?) +} + +/// Run the `mamba` CLI binary itself with `args`, from within `cwd`, returning its captured +/// stdout. Fails if the process exits with an error. +pub fn run_cli(cwd: &Path, args: &[&str]) -> Result> { + let mut cmd = Command::main_binary()?; + let output = cmd.current_dir(cwd).args(args).output()?; + if !output.status.success() { + return Err(format!( + "mamba {} exited with an error:\n{}", + args.join(" "), + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + Ok(String::from_utf8(output.stdout)?) +} + pub struct OutTestErr(Vec); pub type OutTestRet = Result; From 346f8787a9076165432b43a2c566f46b9665a86f Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 11:49:54 +0200 Subject: [PATCH 10/16] doc: specify that we rely on ast.ty to compile - We do actually leverage the type system to generate machine code - Also reals properly compile to f64 We do not yet support arbitrary precision, which we may want to do. The philosophy of the language is being geared towards mathmaticians. --- src/backend/cranelift/README.md | 58 ++++++----- src/backend/cranelift/convert/call.rs | 14 ++- src/backend/cranelift/convert/mod.rs | 8 +- src/backend/cranelift/convert/operation.rs | 97 ++++++++++++++----- src/backend/cranelift/mod.rs | 6 ++ src/backend/mod.rs | 4 + src/cli.rs | 5 + tests/execution.rs | 13 +++ .../valid/function/float_arithmetic.mamba | 7 ++ .../function/float_print_unsupported.mamba | 1 + 10 files changed, 160 insertions(+), 53 deletions(-) create mode 100644 tests/resource/valid/function/float_arithmetic.mamba create mode 100644 tests/resource/valid/function/float_print_unsupported.mamba diff --git a/src/backend/cranelift/README.md b/src/backend/cranelift/README.md index 5ea06cf05..5c8b5a5cd 100644 --- a/src/backend/cranelift/README.md +++ b/src/backend/cranelift/README.md @@ -4,10 +4,9 @@ # Cranelift -Compiles a checked `ASTTy` directly to native machine code via [Cranelift](https://cranelift.dev/), instead of -transpiling to Python source. Unlike the Python backend, there is no intermediate `PythonCore`-style tree: lowering -walks the `ASTTy` once and emits Cranelift IR straight into a `cranelift_object::ObjectModule` via imperative builder -calls, which Cranelift itself then turns into machine code. +Compiles a checked `ASTTy` directly to native machine code via [Cranelift](https://cranelift.dev/), instead of transpiling to Python source. +Unlike the Python backend, there is no intermediate `PythonCore`-style tree: +lowering walks the `ASTTy` once and emits Cranelift IR straight into a `cranelift_object::ObjectModule` via imperative builder calls, which Cranelift itself then turns into machine code. Three public entry points, all in `mod.rs`, mirroring the Python backend's `write_output`/`gen`/`gen_arguments` shape: @@ -16,39 +15,52 @@ Three public entry points, all in `mod.rs`, mirroring the Python backend's `writ - `compile` / `disassemble` -- the single-file entry points those two build on: `compile` returns object bytes, `disassemble` returns disassembly text (see "Assembly output" below). +The general idea is that we are able to leverage the type checker so that we _know_ what the type of each node at compile time. +This means that we offer the flexibility of not having to exhaustively define types everywhere. +The type checker still verifies correctness and gives this information to us so that we are able to produce machine code. +Else, without knowing the type in advance, we would not be able to produce machine code except in the most trivial cases. + ## Supported language subset -Only a small slice of Mamba compiles down to machine code, enforced by simply erroring -(`BackendErr::unimplemented`) on anything else: +Only a small slice of Mamba compiles down to machine code, +enforced by simply erroring (`BackendErr::unimplemented`) on anything else: - `Int`, `Bool`, `Float` primitives -- no collections, strings (beyond a `print` argument), classes, or traits. -- Arithmetic (`+ - * /`) and comparison (`< <= > >= == !=`) operators. +- Arithmetic (`+ - * /`) and comparison (`< <= > >= == !=`) operators, over `Int` or `Float` -- `operation.rs`'s + `lower_arith`/`lower_cmp` check the *operand's* resolved type (not just that it's some supported primitive) to + pick `iadd`/`fadd` and friends, `icmp`/`fcmp`, since Cranelift has no single opcode for both. - `if`/`else`, both as a statement and in a function's tail (return) position. -- `for in .. ` / `..=` loops over `Int` ranges -- not arbitrary collections, since collections aren't - supported at all. +- `for in .. ` / `..=` loops over `Int` ranges -- not arbitrary collections, since collections aren't supported at all. - Plain (`:=`) reassignment of an already-declared variable -- not compound assignment (`+=` and friends). - Top-level function definitions and calls, including forward references within the same file. -- `print`, lowered directly to libc `puts` (string literal) or `printf` (primitive value). +- `print`, lowered directly to libc `puts` (string literal) or `printf` (an `Int`/`Bool` value). A `Float` value is + rejected -- `printf`'s `%lld` would read the raw float bits as an integer, and a `%f`-style call needs SysV + variadic-call ABI plumbing (setting `%al` to the vector-register count) this backend doesn't have yet. Every other top-level statement in a file is collected into a synthetic `main`, since machine code needs an explicit entry point the way a `.mamba` file's top-to-bottom script execution doesn't. ## Layout -- `convert/` -- the lowering itself, split by AST category (`definition.rs`, `control_flow.rs`, `call.rs`, - `operation.rs`, plus a shared `common.rs`), the same way `backend::python::convert` is. `mod.rs` holds the entry - point (`lower_program`) and the three dispatchers a Mamba node can be lowered as: a statement (`lower_stmt`), the - tail of a function body (`lower_tail`), or a value-producing expression (`lower_expr`). -- `primitive.rs` -- resolves a checked `Name` to the one Cranelift `Type` it supports (`Int`/`Bool`/`Float`), the - same role `backend::python::name` plays for Python's richer type surface. -- `link.rs` -- shells out to the system `cc` to link object files into an executable, the same approach `rustc` - itself uses rather than reimplementing a linker. +- `convert/` -- the lowering itself, split by AST category (`definition.rs`, `control_flow.rs`, `call.rs`, `operation.rs`, plus a shared `common.rs`), the same way `backend::python::convert` is. + `mod.rs` holds the entry point (`lower_program`) and the three dispatchers a Mamba node can be lowered as: + a statement (`lower_stmt`), the tail of a function body (`lower_tail`), or a value-producing expression (`lower_expr`). +- `primitive.rs` -- resolves a checked `Name` to the one Cranelift `Type` it supports (`Int`/`Bool`/`Float`), + the same role `backend::python::name` plays for Python's richer type surface. +- `link.rs` -- shells out to the system `cc` to link object files into an executable, + the same approach `rustc` itself uses rather than reimplementing a linker. - `result.rs` -- `BackendErr`/`BackendResult`, mirroring `backend::python::result`. ## Assembly output -`disassemble` asks Cranelift to compute disassembly text (`Context::set_disasm` + `CompiledCode::vcode`) while -lowering, gated behind a `want_asm: bool` threaded through `convert::lower_program` so `compile` (the `--bin` path) -never pays for it. It's printed in AT&T syntax (source operand before destination, e.g. `movq %rsp, %rbp`) -- -that's what Cranelift's own disassembler always produces; real Intel-syntax output would mean re-disassembling the -emitted machine code with an external disassembler (e.g. capstone) instead, which isn't wired up here. +`disassemble` asks Cranelift to compute disassembly text (`Context::set_disasm` + `CompiledCode::vcode`) while lowering, +gated behind a `want_asm: bool` threaded through `convert::lower_program` so `compile` (the `--bin` path) never pays for it. +It's printed in AT&T syntax (source operand before destination, e.g. `movq %rsp, %rbp`) as that's what Cranelift's own disassembler always produces; +real Intel-syntax output would mean re-disassembling the emitted machine code with an external disassembler (e.g. capstone) instead. +To keep things simple and to keep external dependencies to a minimum we opt not to do that. + +This is instructions only, not a full disassembly of the object, it doesn't cover the data section. +A string literal (e.g. a `print("...")` argument) is emitted as a separate anonymous data blob, +so it never appears in the output; +The instructions that reference it only show an opaque symbol (e.g. `load_ext_name userextname0+0, %rdi`). +This is similar to how import like `puts` shows up as a bare symbol rather than "the puts function". diff --git a/src/backend/cranelift/convert/call.rs b/src/backend/cranelift/convert/call.rs index f57a295ee..f3cacbcfc 100644 --- a/src/backend/cranelift/convert/call.rs +++ b/src/backend/cranelift/convert/call.rs @@ -37,9 +37,14 @@ impl<'a> FnLower<'a> { } /// `print(...)`: a string literal goes through `puts` (which appends its own newline, like - /// Mamba/Python's `print`); a primitive value goes through `printf` with a fixed `%lld\n` - /// format. Anything else (interpolated strings, non-primitive values, multiple arguments) is - /// out of scope for this backend. + /// Mamba/Python's `print`); an `Int`/`Bool` value goes through `printf` with a fixed + /// `%lld\n` format. Anything else (interpolated strings, a `Float` value, non-primitive + /// values, multiple arguments) is out of scope for this backend. + /// + /// A `Float` is deliberately rejected rather than attempted: `%lld` would read raw float bits + /// as an integer (garbage, not a crash), and doing this properly means both a `%f`-style + /// format string and setting `%al` to the SysV-mandated vector-register count for a variadic + /// call passing a float -- printf-only ABI plumbing this backend doesn't have yet. pub(super) fn lower_print(&mut self, ast: &ASTTy) -> BackendResult> { let args = match &ast.node { NodeTy::FunctionCall { args, .. } => args, @@ -78,6 +83,9 @@ impl<'a> FnLower<'a> { )), _ => { let value = self.lower_expr(arg)?; + if self.builder.func.dfg.value_type(value) == types::F64 { + return Err(BackendErr::unimplemented(ast, "print of a Float value")); + } let fmt = format!("{}\0", "%lld\n").into_bytes().into_boxed_slice(); let data_id = self .module diff --git a/src/backend/cranelift/convert/mod.rs b/src/backend/cranelift/convert/mod.rs index f8e6dcd94..962390dc7 100644 --- a/src/backend/cranelift/convert/mod.rs +++ b/src/backend/cranelift/convert/mod.rs @@ -179,7 +179,7 @@ impl<'a> FnLower<'a> { /// Lower `ast` as a value-producing expression. fn lower_expr(&mut self, ast: &ASTTy) -> BackendResult { match &ast.node { - // Int/Bool literals' own resolved `ty` can come back widened to a union (e.g. a + // Int/Bool/Real literals' own resolved `ty` can come back widened to a union (e.g. a // literal argument to `print`, whose parameter accepts several printable types // unifies to that broader union rather than staying just `Int`) -- but the node // variant itself already tells us the literal's true type, so there's no need to @@ -191,6 +191,12 @@ impl<'a> FnLower<'a> { Ok(self.builder.ins().iconst(types::I64, value)) } NodeTy::Bool { lit } => Ok(self.builder.ins().iconst(types::I8, i64::from(*lit))), + NodeTy::Real { lit } => { + let value: f64 = lit.parse().map_err(|_| { + BackendErr::new(ast.pos, &format!("Invalid float literal '{lit}'")) + })?; + Ok(self.builder.ins().f64const(value)) + } NodeTy::Id { lit } => { let (var, _) = self.vars.get(lit).ok_or_else(|| { BackendErr::new(ast.pos, &format!("Undefined variable '{lit}'")) diff --git a/src/backend/cranelift/convert/operation.rs b/src/backend/cranelift/convert/operation.rs index cf733f618..962eb1c48 100644 --- a/src/backend/cranelift/convert/operation.rs +++ b/src/backend/cranelift/convert/operation.rs @@ -1,5 +1,5 @@ -use cranelift_codegen::ir::condcodes::IntCC; -use cranelift_codegen::ir::{InstBuilder, Value}; +use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; +use cranelift_codegen::ir::{types, InstBuilder, Value}; use cranelift_frontend::FunctionBuilder; use crate::backend::cranelift::convert::FnLower; @@ -11,28 +11,52 @@ impl<'a> FnLower<'a> { /// Lower a binary arithmetic or comparison operation. pub(super) fn lower_operation(&mut self, ast: &ASTTy) -> BackendResult { match &ast.node { - NodeTy::Add { left, right } => { - self.lower_arith(left, right, |b, a, c| b.ins().iadd(a, c)) + NodeTy::Add { left, right } => self.lower_arith( + left, + right, + |b, a, c| b.ins().iadd(a, c), + |b, a, c| b.ins().fadd(a, c), + ), + NodeTy::Sub { left, right } => self.lower_arith( + left, + right, + |b, a, c| b.ins().isub(a, c), + |b, a, c| b.ins().fsub(a, c), + ), + NodeTy::Mul { left, right } => self.lower_arith( + left, + right, + |b, a, c| b.ins().imul(a, c), + |b, a, c| b.ins().fmul(a, c), + ), + NodeTy::Div { left, right } => self.lower_arith( + left, + right, + |b, a, c| b.ins().sdiv(a, c), + |b, a, c| b.ins().fdiv(a, c), + ), + NodeTy::Le { left, right } => { + self.lower_cmp(left, right, IntCC::SignedLessThan, FloatCC::LessThan) } - NodeTy::Sub { left, right } => { - self.lower_arith(left, right, |b, a, c| b.ins().isub(a, c)) + NodeTy::Leq { left, right } => self.lower_cmp( + left, + right, + IntCC::SignedLessThanOrEqual, + FloatCC::LessThanOrEqual, + ), + NodeTy::Ge { left, right } => { + self.lower_cmp(left, right, IntCC::SignedGreaterThan, FloatCC::GreaterThan) } - NodeTy::Mul { left, right } => { - self.lower_arith(left, right, |b, a, c| b.ins().imul(a, c)) + NodeTy::Geq { left, right } => self.lower_cmp( + left, + right, + IntCC::SignedGreaterThanOrEqual, + FloatCC::GreaterThanOrEqual, + ), + NodeTy::Eq { left, right } => self.lower_cmp(left, right, IntCC::Equal, FloatCC::Equal), + NodeTy::Neq { left, right } => { + self.lower_cmp(left, right, IntCC::NotEqual, FloatCC::NotEqual) } - NodeTy::Div { left, right } => { - self.lower_arith(left, right, |b, a, c| b.ins().sdiv(a, c)) - } - NodeTy::Le { left, right } => self.lower_cmp(left, right, IntCC::SignedLessThan), - NodeTy::Leq { left, right } => { - self.lower_cmp(left, right, IntCC::SignedLessThanOrEqual) - } - NodeTy::Ge { left, right } => self.lower_cmp(left, right, IntCC::SignedGreaterThan), - NodeTy::Geq { left, right } => { - self.lower_cmp(left, right, IntCC::SignedGreaterThanOrEqual) - } - NodeTy::Eq { left, right } => self.lower_cmp(left, right, IntCC::Equal), - NodeTy::Neq { left, right } => self.lower_cmp(left, right, IntCC::NotEqual), other => Err(BackendErr::unimplemented( ast, &format!("{other:?} operation"), @@ -40,25 +64,46 @@ impl<'a> FnLower<'a> { } } + /// Lower a binary arithmetic operation, picking `int_op` or `float_op` by `left`'s own + /// Cranelift type -- `Int` and `Float` need different opcodes entirely (`iadd` vs `fadd` and + /// so on), unlike comparisons where only the condition code differs. fn lower_arith( &mut self, left: &ASTTy, right: &ASTTy, - op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, + int_op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, + float_op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, ) -> BackendResult { // Reject non-primitive-typed arithmetic early, with a clear error. Checked against // `left`'s own type rather than the whole expression's -- e.g. as a `print(...)` argument, // the *expression*'s resolved type widens to whatever union `print` accepts, even though // the operands (and thus the actual machine type produced) stay concretely typed. - cranelift_type(left)?; + let ty = cranelift_type(left)?; let l = self.lower_expr(left)?; let r = self.lower_expr(right)?; - Ok(op(&mut self.builder, l, r)) + if ty == types::F64 { + Ok(float_op(&mut self.builder, l, r)) + } else { + Ok(int_op(&mut self.builder, l, r)) + } } - fn lower_cmp(&mut self, left: &ASTTy, right: &ASTTy, cc: IntCC) -> BackendResult { + /// Lower a comparison, picking `icmp`/`fcmp` (with the matching condition code) by `left`'s + /// own Cranelift type. + fn lower_cmp( + &mut self, + left: &ASTTy, + right: &ASTTy, + int_cc: IntCC, + float_cc: FloatCC, + ) -> BackendResult { + let ty = cranelift_type(left)?; let l = self.lower_expr(left)?; let r = self.lower_expr(right)?; - Ok(self.builder.ins().icmp(cc, l, r)) + if ty == types::F64 { + Ok(self.builder.ins().fcmp(float_cc, l, r)) + } else { + Ok(self.builder.ins().icmp(int_cc, l, r)) + } } } diff --git a/src/backend/cranelift/mod.rs b/src/backend/cranelift/mod.rs index 364877934..16a2e7308 100644 --- a/src/backend/cranelift/mod.rs +++ b/src/backend/cranelift/mod.rs @@ -166,6 +166,12 @@ pub fn compile(ast_ty: &ASTTy, target: Option<&str>, ctx: &Context) -> BackendRe /// produces. Real Intel-syntax output would mean re-disassembling the emitted machine code with /// an external disassembler (e.g. capstone) instead, which isn't wired up here. /// +/// This is `vcode` text, not a full disassembly of the object: it covers instructions only, not +/// the data section. A string literal (e.g. a `print("...")` argument) is emitted as a separate +/// anonymous data blob, so it never appears in this output -- the instructions that use it only +/// show an opaque symbol reference (e.g. `load_ext_name userextname0+0, %rdi`), the same way an +/// import like `puts` shows up as a bare symbol rather than "the puts function". +/// /// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`). /// If `None`, the host triple is used. pub fn disassemble(ast_ty: &ASTTy, target: Option<&str>, ctx: &Context) -> BackendResult { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index f9174aab6..91bdc6468 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -17,6 +17,10 @@ pub enum Backend { /// Printed in AT&T syntax -- Cranelift's own disassembler doesn't support switching to Intel /// syntax. /// + /// Only shows instructions, not the data section -- a string literal is compiled into the + /// object's data section, not the instruction stream, so it won't appear in this output at + /// all; the instructions will only show it being loaded by an opaque symbol name. + /// /// `target`, if given, is a target triple (e.g. `x86_64-unknown-linux-gnu`) passed on to /// Cranelift; if `None`, the host triple is used. Asm { target: Option }, diff --git a/src/cli.rs b/src/cli.rs index a5bcce4dc..53ad3491a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -41,6 +41,11 @@ pub struct Cli { /// (e.g. `> out.s`) if you want to save it. Same language subset as `--bin` (see its help). /// Printed in AT&T syntax (`movq %rsp, %rbp`, source before destination) -- Cranelift's own /// disassembler doesn't support switching to Intel syntax. + /// + /// Only shows instructions, not the data section: a string literal (e.g. a `print("...")` + /// argument) is compiled into the object's data section, not the instruction stream, so it + /// won't appear in this output at all -- the instructions will only show it being loaded by + /// an opaque symbol name (e.g. `load_ext_name userextname0+0, %rdi`). #[arg(long = "asm", action = ArgAction::SetTrue, conflicts_with_all = ["python", "bin"])] pub asm: bool, diff --git a/tests/execution.rs b/tests/execution.rs index fdc861fe7..3aafbec45 100644 --- a/tests/execution.rs +++ b/tests/execution.rs @@ -15,10 +15,23 @@ use tests_util::{resource_path, run_cli, run_via_bin, run_via_python, Runner}; #[test_case(run_via_bin, &["function"], "arithmetic_sum.mamba" => "14\n")] #[test_case(run_via_python, &["function"], "for_loop_sum.mamba" => "10\n")] #[test_case(run_via_bin, &["function"], "for_loop_sum.mamba" => "10\n")] +#[test_case(run_via_python, &["function"], "float_arithmetic.mamba" => "1\n")] +#[test_case(run_via_bin, &["function"], "float_arithmetic.mamba" => "1\n")] fn execution(run: Runner, dirs: &[&str], file: &str) -> String { run(dirs, file).unwrap() } +#[test] +fn bin_backend_rejects_float_print_cleanly() -> Result<(), Box> { + let err = run_via_bin(&["function"], "float_print_unsupported.mamba") + .expect_err("printing a Float isn't supported by the Cranelift backend yet"); + assert!( + err.to_string().contains("print of a Float value"), + "expected a graceful 'not supported' error, got: {err}" + ); + Ok(()) +} + #[test] fn asm_prints_disassembly_to_stdout() -> Result<(), Box> { let src_dir = resource_path(true, &["function"], ""); diff --git a/tests/resource/valid/function/float_arithmetic.mamba b/tests/resource/valid/function/float_arithmetic.mamba new file mode 100644 index 000000000..464c7aa66 --- /dev/null +++ b/tests/resource/valid/function/float_arithmetic.mamba @@ -0,0 +1,7 @@ +def compute(a: Float, b: Float) -> Float := do + def diff := a - b + def quot := a / b + return diff * quot +end + +if compute(10.0, 4.0) = 15.0 then print(1) else print(0) diff --git a/tests/resource/valid/function/float_print_unsupported.mamba b/tests/resource/valid/function/float_print_unsupported.mamba new file mode 100644 index 000000000..d9136a857 --- /dev/null +++ b/tests/resource/valid/function/float_print_unsupported.mamba @@ -0,0 +1 @@ +print(1.5 + 2.5) From 710868d353a33c5c21c8b6fdb114bf89975ad16f Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 12:27:29 +0200 Subject: [PATCH 11/16] test: full feature set of backend --- src/backend/cranelift/convert/call.rs | 30 ++++-- src/backend/cranelift/convert/control_flow.rs | 57 +++++++---- src/backend/cranelift/convert/definition.rs | 10 ++ src/backend/cranelift/convert/mod.rs | 17 +++- src/backend/cranelift/convert/operation.rs | 97 ++++++++++++++----- tests/execution.rs | 97 +++++++++++++++++-- .../valid/function/arithmetic_ops.mamba | 5 + .../valid/function/comparison_bool.mamba | 4 + .../valid/function/comparison_int.mamba | 7 ++ .../compound_reassign_unsupported.mamba | 3 + .../valid/function/float_var_decl.mamba | 2 + .../valid/function/floordiv_unsupported.mamba | 1 + .../function/for_loop_exclusive_range.mamba | 3 + .../valid/function/for_loop_shadow.mamba | 5 + .../function/for_over_list_unsupported.mamba | 1 + .../function/generic_arg_unsupported.mamba | 5 + .../valid/function/if_else_tail.mamba | 4 + .../valid/function/if_no_else_stmt.mamba | 3 + .../function/implicit_last_expr_return.mamba | 3 + .../function/no_initializer_unsupported.mamba | 3 + .../function/nullable_arg_unsupported.mamba | 5 + .../resource/valid/function/print_bool.mamba | 2 + .../print_interpolated_unsupported.mamba | 2 + .../function/print_two_args_unsupported.mamba | 1 + .../print_zero_args_unsupported.mamba | 1 + .../valid/function/str_arg_unsupported.mamba | 3 + .../function/void_function_call_stmt.mamba | 6 ++ tests_util/src/lib.rs | 30 +++++- 28 files changed, 336 insertions(+), 71 deletions(-) create mode 100644 tests/resource/valid/function/arithmetic_ops.mamba create mode 100644 tests/resource/valid/function/comparison_bool.mamba create mode 100644 tests/resource/valid/function/comparison_int.mamba create mode 100644 tests/resource/valid/function/compound_reassign_unsupported.mamba create mode 100644 tests/resource/valid/function/float_var_decl.mamba create mode 100644 tests/resource/valid/function/floordiv_unsupported.mamba create mode 100644 tests/resource/valid/function/for_loop_exclusive_range.mamba create mode 100644 tests/resource/valid/function/for_loop_shadow.mamba create mode 100644 tests/resource/valid/function/for_over_list_unsupported.mamba create mode 100644 tests/resource/valid/function/generic_arg_unsupported.mamba create mode 100644 tests/resource/valid/function/if_else_tail.mamba create mode 100644 tests/resource/valid/function/if_no_else_stmt.mamba create mode 100644 tests/resource/valid/function/implicit_last_expr_return.mamba create mode 100644 tests/resource/valid/function/no_initializer_unsupported.mamba create mode 100644 tests/resource/valid/function/nullable_arg_unsupported.mamba create mode 100644 tests/resource/valid/function/print_bool.mamba create mode 100644 tests/resource/valid/function/print_interpolated_unsupported.mamba create mode 100644 tests/resource/valid/function/print_two_args_unsupported.mamba create mode 100644 tests/resource/valid/function/print_zero_args_unsupported.mamba create mode 100644 tests/resource/valid/function/str_arg_unsupported.mamba create mode 100644 tests/resource/valid/function/void_function_call_stmt.mamba diff --git a/src/backend/cranelift/convert/call.rs b/src/backend/cranelift/convert/call.rs index f3cacbcfc..a8972680e 100644 --- a/src/backend/cranelift/convert/call.rs +++ b/src/backend/cranelift/convert/call.rs @@ -6,8 +6,26 @@ use crate::backend::cranelift::result::{BackendErr, BackendResult}; use crate::check::ast::{ASTTy, NodeTy}; impl<'a> FnLower<'a> { - /// Lower a (non-`print`) `FunctionCall` to a user-defined function. - pub(super) fn lower_call(&mut self, ast: &ASTTy) -> BackendResult { + /// Lower a (non-`print`) `FunctionCall` to a user-defined function, in statement position: + /// any return value is discarded, so a call to a function with no return type (`None` here) + /// is perfectly fine -- unlike [`Self::lower_call_expr`], which needs one. + pub(super) fn lower_call_stmt(&mut self, ast: &ASTTy) -> BackendResult<()> { + self.lower_call(ast).map(|_| ()) + } + + /// Lower a (non-`print`) `FunctionCall` to a user-defined function, in expression position: + /// errors if the callee has no return type to produce a value with. + pub(super) fn lower_call_expr(&mut self, ast: &ASTTy) -> BackendResult { + self.lower_call(ast)?.ok_or_else(|| { + let name = match &ast.node { + NodeTy::FunctionCall { name, .. } => name.name.as_str(), + _ => unreachable!("only called for a FunctionCall node"), + }; + BackendErr::new(ast.pos, &format!("'{name}' does not return a value")) + }) + } + + fn lower_call(&mut self, ast: &ASTTy) -> BackendResult> { let (name, args) = match &ast.node { NodeTy::FunctionCall { name, args } => (name, args), other => { @@ -27,13 +45,7 @@ impl<'a> FnLower<'a> { arg_values.push(self.lower_expr(arg)?); } let call = self.builder.ins().call(local, &arg_values); - self.builder - .inst_results(call) - .first() - .copied() - .ok_or_else(|| { - BackendErr::new(ast.pos, &format!("'{}' does not return a value", name.name)) - }) + Ok(self.builder.inst_results(call).first().copied()) } /// `print(...)`: a string literal goes through `puts` (which appends its own newline, like diff --git a/src/backend/cranelift/convert/control_flow.rs b/src/backend/cranelift/convert/control_flow.rs index 55fe00517..ce1cbc3ef 100644 --- a/src/backend/cranelift/convert/control_flow.rs +++ b/src/backend/cranelift/convert/control_flow.rs @@ -50,10 +50,25 @@ impl<'a> FnLower<'a> { /// supported -- iterating a collection would need this backend to support collections at all, /// which is out of scope (see the module-level docs). /// - /// Classic three-block loop: `header` checks the bound and either enters `body` or falls - /// through to `exit`; `body` runs the loop body (with the loop variable shadowing any - /// outer binding of the same name for its duration) and increments before jumping back to - /// `header`. + /// Rotated loop, entered through a pre-check: `entry_check` handles the (possibly empty + /// up-front) range and falls through to `exit` or into `body`; `body` runs the loop body, + /// computes the next value, and -- checking *that* -- either commits it and loops back into + /// `body` directly (skipping `entry_check` on every subsequent iteration) or falls through to + /// `exit` without committing it. + /// + /// Mamba (like Python, which this must match) has no block scoping, so the loop variable is + /// just an ordinary binding of its name -- if that name already existed, this loop + /// permanently overwrites it. Committing the next value only on the branch that's actually + /// going to use it (rather than unconditionally at the bottom of `body`, then discovering at + /// `header` that it was one too many and exiting anyway) is what makes the loop variable keep + /// whatever value it was last actually *used* with once the loop exits, instead of one past + /// it -- matching what Python's own `for` leaves its loop variable holding. + /// + /// One known gap: for a range that's empty from the start (e.g. `for i in 5 .. 5`), Python's + /// `i` never gets touched at all -- it keeps whatever it held *before* the loop -- whereas + /// this still binds it to `from` before the (never-taken) entry check. Shadowing an outer + /// variable with a loop that may run zero times is the only way this is observable, and isn't + /// worth the extra restructuring to close. pub(super) fn lower_for( &mut self, expr: &ASTTy, @@ -93,39 +108,39 @@ impl<'a> FnLower<'a> { let loop_var = self.new_var(ty); self.builder.def_var(loop_var, from_value); + self.vars.insert(var_name, (loop_var, ty)); - let header_block = self.builder.create_block(); - let body_block = self.builder.create_block(); - let exit_block = self.builder.create_block(); - self.builder.ins().jump(header_block, &[]); - - self.builder.switch_to_block(header_block); - let current = self.builder.use_var(loop_var); let cc = if inclusive { IntCC::SignedLessThanOrEqual } else { IntCC::SignedLessThan }; + + let entry_check_block = self.builder.create_block(); + let body_block = self.builder.create_block(); + let continue_block = self.builder.create_block(); + let exit_block = self.builder.create_block(); + self.builder.ins().jump(entry_check_block, &[]); + + self.builder.switch_to_block(entry_check_block); + let current = self.builder.use_var(loop_var); let cond = self.builder.ins().icmp(cc, current, to_value); self.builder .ins() .brif(cond, body_block, &[], exit_block, &[]); self.builder.switch_to_block(body_block); - let shadowed = self.vars.insert(var_name.clone(), (loop_var, ty)); self.lower_stmt(body)?; - match shadowed { - Some(shadowed) => { - self.vars.insert(var_name, shadowed); - } - None => { - self.vars.remove(&var_name); - } - } let current = self.builder.use_var(loop_var); let next = self.builder.ins().iadd(current, step_value); + let cond = self.builder.ins().icmp(cc, next, to_value); + self.builder + .ins() + .brif(cond, continue_block, &[], exit_block, &[]); + + self.builder.switch_to_block(continue_block); self.builder.def_var(loop_var, next); - self.builder.ins().jump(header_block, &[]); + self.builder.ins().jump(body_block, &[]); self.builder.switch_to_block(exit_block); Ok(()) diff --git a/src/backend/cranelift/convert/definition.rs b/src/backend/cranelift/convert/definition.rs index 5062b1cc9..624592f3c 100644 --- a/src/backend/cranelift/convert/definition.rs +++ b/src/backend/cranelift/convert/definition.rs @@ -213,6 +213,16 @@ impl<'a> FnLower<'a> { let name = fun_name(var)?; let ty = cranelift_type(expr)?; let value = self.lower_expr(expr)?; + // `ty` (from the checker) and `value`'s own Cranelift type can disagree for an + // Int-shaped literal initializing a declared-`Float` variable (e.g. `def x: + // Float := 2`) -- `lower_expr` always builds an Int-shaped literal as `Int` + // (see its own doc comment), so convert here if the declared type says + // otherwise. + let value = if ty == types::F64 && self.builder.func.dfg.value_type(value) != ty { + self.builder.ins().fcvt_from_sint(ty, value) + } else { + value + }; let var = self.new_var(ty); self.builder.def_var(var, value); self.vars.insert(name, (var, ty)); diff --git a/src/backend/cranelift/convert/mod.rs b/src/backend/cranelift/convert/mod.rs index 962390dc7..a0478ba29 100644 --- a/src/backend/cranelift/convert/mod.rs +++ b/src/backend/cranelift/convert/mod.rs @@ -134,7 +134,7 @@ impl<'a> FnLower<'a> { NodeTy::FunctionCall { name, .. } if name.name == PRINT => { self.lower_print(ast).map(|_| ()) } - NodeTy::FunctionCall { .. } => self.lower_expr(ast).map(|_| ()), + NodeTy::FunctionCall { .. } => self.lower_call_stmt(ast), other => Err(BackendErr::unimplemented( ast, &format!("{other:?} statement"), @@ -183,7 +183,14 @@ impl<'a> FnLower<'a> { // literal argument to `print`, whose parameter accepts several printable types // unifies to that broader union rather than staying just `Int`) -- but the node // variant itself already tells us the literal's true type, so there's no need to - // consult `ast.ty` at all here. + // consult `ast.ty` at all here. An Int-shaped literal that Mamba's numeric-literal + // adaptation means is really meant as a `Float` (e.g. `def x: Float := 2`, or `x > + // 0.0`) is *not* handled here -- it's handled contextually, by whichever caller + // ends up comparing this value's actual Cranelift type against a sibling value's + // (see `operation.rs`'s docs) -- since `ast.ty` turns out to be an unreliable signal + // for this even when it looks unambiguous (it can resolve to `Float` from unifying + // against an operator's own polymorphic parameter type, even when the concrete + // value everything else around it expects is `Int`). NodeTy::Int { lit } => { let value: i64 = lit.parse().map_err(|_| { BackendErr::new(ast.pos, &format!("Invalid int literal '{lit}'")) @@ -212,8 +219,10 @@ impl<'a> FnLower<'a> { | NodeTy::Ge { .. } | NodeTy::Geq { .. } | NodeTy::Eq { .. } - | NodeTy::Neq { .. } => self.lower_operation(ast), - NodeTy::FunctionCall { .. } => self.lower_call(ast), + | NodeTy::Neq { .. } + | NodeTy::AddU { .. } + | NodeTy::SubU { .. } => self.lower_operation(ast), + NodeTy::FunctionCall { .. } => self.lower_call_expr(ast), other => Err(BackendErr::unimplemented( ast, &format!("{other:?} expression"), diff --git a/src/backend/cranelift/convert/operation.rs b/src/backend/cranelift/convert/operation.rs index 962eb1c48..415e87763 100644 --- a/src/backend/cranelift/convert/operation.rs +++ b/src/backend/cranelift/convert/operation.rs @@ -3,12 +3,11 @@ use cranelift_codegen::ir::{types, InstBuilder, Value}; use cranelift_frontend::FunctionBuilder; use crate::backend::cranelift::convert::FnLower; -use crate::backend::cranelift::primitive::cranelift_type; use crate::backend::cranelift::result::{BackendErr, BackendResult}; use crate::check::ast::{ASTTy, NodeTy}; impl<'a> FnLower<'a> { - /// Lower a binary arithmetic or comparison operation. + /// Lower a binary arithmetic operation, a comparison, or a unary `+`/`-`. pub(super) fn lower_operation(&mut self, ast: &ASTTy) -> BackendResult { match &ast.node { NodeTy::Add { left, right } => self.lower_arith( @@ -29,12 +28,7 @@ impl<'a> FnLower<'a> { |b, a, c| b.ins().imul(a, c), |b, a, c| b.ins().fmul(a, c), ), - NodeTy::Div { left, right } => self.lower_arith( - left, - right, - |b, a, c| b.ins().sdiv(a, c), - |b, a, c| b.ins().fdiv(a, c), - ), + NodeTy::Div { left, right } => self.lower_div(left, right), NodeTy::Le { left, right } => { self.lower_cmp(left, right, IntCC::SignedLessThan, FloatCC::LessThan) } @@ -57,6 +51,8 @@ impl<'a> FnLower<'a> { NodeTy::Neq { left, right } => { self.lower_cmp(left, right, IntCC::NotEqual, FloatCC::NotEqual) } + NodeTy::AddU { expr } => self.lower_expr(expr), + NodeTy::SubU { expr } => self.lower_negate(expr), other => Err(BackendErr::unimplemented( ast, &format!("{other:?} operation"), @@ -64,9 +60,11 @@ impl<'a> FnLower<'a> { } } - /// Lower a binary arithmetic operation, picking `int_op` or `float_op` by `left`'s own - /// Cranelift type -- `Int` and `Float` need different opcodes entirely (`iadd` vs `fadd` and - /// so on), unlike comparisons where only the condition code differs. + /// Lower a binary arithmetic operation, picking `int_op` or `float_op` depending on whether + /// either operand turns out to be `Float` -- `Int` and `Float` need different opcodes + /// entirely (`iadd` vs `fadd` and so on), unlike comparisons where only the condition code + /// differs. See [`Self::float_pair`]'s doc comment for how "either operand is `Float`" is + /// decided. fn lower_arith( &mut self, left: &ASTTy, @@ -74,22 +72,44 @@ impl<'a> FnLower<'a> { int_op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, float_op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, ) -> BackendResult { - // Reject non-primitive-typed arithmetic early, with a clear error. Checked against - // `left`'s own type rather than the whole expression's -- e.g. as a `print(...)` argument, - // the *expression*'s resolved type widens to whatever union `print` accepts, even though - // the operands (and thus the actual machine type produced) stay concretely typed. - let ty = cranelift_type(left)?; let l = self.lower_expr(left)?; let r = self.lower_expr(right)?; - if ty == types::F64 { - Ok(float_op(&mut self.builder, l, r)) + match self.float_pair(l, r) { + Some((l, r)) => Ok(float_op(&mut self.builder, l, r)), + None => Ok(int_op(&mut self.builder, l, r)), + } + } + + /// Lower Mamba's `/`, which -- like Python's `/` -- is always true (float) division: unlike + /// `+`/`-`/`*`, which preserve the operand type, `Int / Int` still produces a `Float` (see + /// `int.__truediv__`'s signature in `check/resource/primitive/int.py`). `Int` operands are + /// converted to `Float` first; `//` (floor division, preserving `Int`) isn't implemented. + fn lower_div(&mut self, left: &ASTTy, right: &ASTTy) -> BackendResult { + let l = self.lower_expr(left)?; + let r = self.lower_expr(right)?; + // Unlike the other arithmetic ops, `/` always produces a `Float` even for two `Int` + // operands, so unconditionally treat this as the float pair (converting both if needed) + // rather than only converting when one side already happens to be `Float`. + let (l, r) = match self.float_pair(l, r) { + Some(pair) => pair, + None => (self.int_to_float(l), self.int_to_float(r)), + }; + Ok(self.builder.ins().fdiv(l, r)) + } + + /// Lower unary negation (`-x`), picking `ineg`/`fneg` by `expr`'s actual Cranelift value + /// type (no promotion to consider -- there's only the one operand). + fn lower_negate(&mut self, expr: &ASTTy) -> BackendResult { + let value = self.lower_expr(expr)?; + if self.builder.func.dfg.value_type(value) == types::F64 { + Ok(self.builder.ins().fneg(value)) } else { - Ok(int_op(&mut self.builder, l, r)) + Ok(self.builder.ins().ineg(value)) } } - /// Lower a comparison, picking `icmp`/`fcmp` (with the matching condition code) by `left`'s - /// own Cranelift type. + /// Lower a comparison, picking `icmp`/`fcmp` (with the matching condition code) depending on + /// whether either operand turns out to be `Float`. See [`Self::float_pair`]'s doc comment. fn lower_cmp( &mut self, left: &ASTTy, @@ -97,13 +117,38 @@ impl<'a> FnLower<'a> { int_cc: IntCC, float_cc: FloatCC, ) -> BackendResult { - let ty = cranelift_type(left)?; let l = self.lower_expr(left)?; let r = self.lower_expr(right)?; - if ty == types::F64 { - Ok(self.builder.ins().fcmp(float_cc, l, r)) - } else { - Ok(self.builder.ins().icmp(int_cc, l, r)) + match self.float_pair(l, r) { + Some((l, r)) => Ok(self.builder.ins().fcmp(float_cc, l, r)), + None => Ok(self.builder.ins().icmp(int_cc, l, r)), + } + } + + /// If either `l` or `r` is actually `Float`, return both as `Float` (converting whichever + /// isn't); otherwise `None` (both are `Int`/`Bool`, handle as integer). + /// + /// Decided from the *lowered values'* actual Cranelift types, not `cranelift_type(...)` on + /// the AST: a checked operand's resolved `ty` can come back reflecting the operator's own + /// (possibly generic) parameter type rather than the operand's own concrete type -- e.g. + /// `int.__gt__`'s parameter is typed `Union[int, float]`, and unifying against it can leave + /// an `Int` literal's resolved type looking like `Float` even when every value actually + /// involved is `Int`. A value `lower_expr` already produced has no such ambiguity: it's + /// exactly the machine type that was built, by construction -- so promotion is decided by + /// asking "did the *other* side turn out to be a real `Float` value", not by asking either + /// side what the checker inferred for it in isolation. + fn float_pair(&mut self, l: Value, r: Value) -> Option<(Value, Value)> { + let l_is_float = self.builder.func.dfg.value_type(l) == types::F64; + let r_is_float = self.builder.func.dfg.value_type(r) == types::F64; + if !l_is_float && !r_is_float { + return None; } + let l = if l_is_float { l } else { self.int_to_float(l) }; + let r = if r_is_float { r } else { self.int_to_float(r) }; + Some((l, r)) + } + + fn int_to_float(&mut self, value: Value) -> Value { + self.builder.ins().fcvt_from_sint(types::F64, value) } } diff --git a/tests/execution.rs b/tests/execution.rs index 3aafbec45..45163e36d 100644 --- a/tests/execution.rs +++ b/tests/execution.rs @@ -7,29 +7,106 @@ use std::error::Error; use std::path::Path; use test_case::test_case; -use tests_util::{resource_path, run_cli, run_via_bin, run_via_python, Runner}; +use tests_util::{resource_path, run_cli, run_via_asm, run_via_bin, run_via_python, Runner}; +/// Fixtures whose output is identical across both backends -- the bulk of correctness coverage. #[test_case(run_via_python, &["function"], "hello_world.mamba" => "hello world\n")] #[test_case(run_via_bin, &["function"], "hello_world.mamba" => "hello world\n")] #[test_case(run_via_python, &["function"], "arithmetic_sum.mamba" => "14\n")] #[test_case(run_via_bin, &["function"], "arithmetic_sum.mamba" => "14\n")] -#[test_case(run_via_python, &["function"], "for_loop_sum.mamba" => "10\n")] -#[test_case(run_via_bin, &["function"], "for_loop_sum.mamba" => "10\n")] +#[test_case(run_via_python, &["function"], "arithmetic_ops.mamba" => "6\n40\n1\n")] +#[test_case(run_via_bin, &["function"], "arithmetic_ops.mamba" => "6\n40\n1\n")] #[test_case(run_via_python, &["function"], "float_arithmetic.mamba" => "1\n")] #[test_case(run_via_bin, &["function"], "float_arithmetic.mamba" => "1\n")] +#[test_case(run_via_python, &["function"], "comparison_int.mamba" => "0\n0\n1\n1\n0\n")] +#[test_case(run_via_bin, &["function"], "comparison_int.mamba" => "0\n0\n1\n1\n0\n")] +#[test_case(run_via_python, &["function"], "comparison_bool.mamba" => "0\n1\n")] +#[test_case(run_via_bin, &["function"], "comparison_bool.mamba" => "0\n1\n")] +#[test_case(run_via_python, &["function"], "if_no_else_stmt.mamba" => "5\n")] +#[test_case(run_via_bin, &["function"], "if_no_else_stmt.mamba" => "5\n")] +#[test_case(run_via_python, &["function"], "void_function_call_stmt.mamba" => "42\n")] +#[test_case(run_via_bin, &["function"], "void_function_call_stmt.mamba" => "42\n")] +#[test_case(run_via_python, &["function"], "for_loop_sum.mamba" => "10\n")] +#[test_case(run_via_bin, &["function"], "for_loop_sum.mamba" => "10\n")] +#[test_case(run_via_python, &["function"], "for_loop_exclusive_range.mamba" => "10\n")] +#[test_case(run_via_bin, &["function"], "for_loop_exclusive_range.mamba" => "10\n")] +#[test_case(run_via_python, &["function"], "for_loop_shadow.mamba" => "6\n3\n")] +#[test_case(run_via_bin, &["function"], "for_loop_shadow.mamba" => "6\n3\n")] fn execution(run: Runner, dirs: &[&str], file: &str) -> String { run(dirs, file).unwrap() } -#[test] -fn bin_backend_rejects_float_print_cleanly() -> Result<(), Box> { - let err = run_via_bin(&["function"], "float_print_unsupported.mamba") - .expect_err("printing a Float isn't supported by the Cranelift backend yet"); +/// Fixtures that only run through one specific backend: either the two backends' output +/// legitimately diverges (e.g. the Cranelift backend prints a `Bool` as `1`/`0` via `printf`, +/// where the Python backend prints `True`/`False`), or -- for `if_else_tail.mamba` and +/// `implicit_last_expr_return.mamba` -- the Python backend has a real, pre-existing bug +/// unrelated to the Cranelift backend under test here: without `--annotate` (off by default), +/// it fails to emit a `return` for a function whose body is an implicit last-expression (no +/// `return` keyword), so the function silently returns `None` instead. `run_via_python` uses +/// `Arguments::default()` (`annotate: false`), so pairing these against it would just be +/// asserting on that separate, known-bad behavior. +#[test_case(run_via_bin, &["function"], "print_bool.mamba" => "1\n0\n")] +#[test_case(run_via_bin, &["function"], "if_else_tail.mamba" => "1\n-1\n")] +#[test_case(run_via_bin, &["function"], "implicit_last_expr_return.mamba" => "36\n")] +#[test_case(run_via_bin, &["function"], "float_var_decl.mamba" => "1\n")] +fn bin_only_execution(run: Runner, dirs: &[&str], file: &str) -> String { + run(dirs, file).unwrap() +} + +/// Mamba constructs that are valid (the Python backend handles all of these) but fall outside +/// this backend's supported subset -- each should fail with a clear, specific error rather than +/// panicking or producing silently wrong output. +#[test_case("float_print_unsupported.mamba", "print of a Float value")] +#[test_case("floordiv_unsupported.mamba", "FDiv")] +#[test_case("compound_reassign_unsupported.mamba", "compound reassignment")] +#[test_case("for_over_list_unsupported.mamba", "for-loop collection")] +#[test_case( + "nullable_arg_unsupported.mamba", + "not supported by the machine-code backend" +)] +#[test_case( + "generic_arg_unsupported.mamba", + "not supported by the machine-code backend" +)] +#[test_case( + "str_arg_unsupported.mamba", + "not supported by the machine-code backend" +)] +#[test_case("print_zero_args_unsupported.mamba", "!= 1 argument")] +#[test_case("print_two_args_unsupported.mamba", "!= 1 argument")] +#[test_case("print_interpolated_unsupported.mamba", "interpolated string")] +#[test_case("no_initializer_unsupported.mamba", "variable definition")] +fn bin_backend_rejects_gracefully(file: &str, expected_substring: &str) { + let err = run_via_bin(&["function"], file) + .expect_err("this fixture is deliberately outside the Cranelift backend's support"); assert!( - err.to_string().contains("print of a Float value"), - "expected a graceful 'not supported' error, got: {err}" + err.to_string().contains(expected_substring), + "expected an error containing {expected_substring:?}, got: {err}" + ); +} + +/// Drives `--asm`'s whole pipeline (`print_asm` -> `mamba_to_asm` -> `disassemble` -> +/// `build_isa`) in-process, for both the default (host) and an explicit target triple -- see +/// [run_via_asm]'s doc comment for why this needs to be in-process rather than via [run_cli]. +#[test_case(None)] +#[test_case(Some("x86_64-unknown-linux-gnu"))] +fn asm_backend_disassembles_in_process(triple: Option<&str>) { + run_via_asm(&["function"], "hello_world.mamba", triple) + .unwrap_or_else(|err| panic!("expected disassembly to succeed, got: {err}")); +} + +/// `not-a-real-triple` isn't parseable as a target triple at all; `sparc-unknown-none-elf` +/// parses fine but isn't an ISA Cranelift implements -- two different rejection points in +/// `build_isa`, both exercised here. +#[test_case("not-a-real-triple", "Invalid target")] +#[test_case("sparc-unknown-none-elf", "Unsupported target")] +fn asm_backend_rejects_bad_target_triple(triple: &str, expected_substring: &str) { + let err = run_via_asm(&["function"], "hello_world.mamba", Some(triple)) + .expect_err("not a target Cranelift can compile for"); + assert!( + err.to_string().contains(expected_substring), + "expected an error containing {expected_substring:?}, got: {err}" ); - Ok(()) } #[test] diff --git a/tests/resource/valid/function/arithmetic_ops.mamba b/tests/resource/valid/function/arithmetic_ops.mamba new file mode 100644 index 000000000..58ef2deb5 --- /dev/null +++ b/tests/resource/valid/function/arithmetic_ops.mamba @@ -0,0 +1,5 @@ +def a := 10 +def b := 4 +print(a - b) +print(a * b) +if a / b = 2.5 then print(1) else print(0) diff --git a/tests/resource/valid/function/comparison_bool.mamba b/tests/resource/valid/function/comparison_bool.mamba new file mode 100644 index 000000000..a7246f26c --- /dev/null +++ b/tests/resource/valid/function/comparison_bool.mamba @@ -0,0 +1,4 @@ +def a := True +def b := False +if a = b then print(1) else print(0) +if a != b then print(1) else print(0) diff --git a/tests/resource/valid/function/comparison_int.mamba b/tests/resource/valid/function/comparison_int.mamba new file mode 100644 index 000000000..664e22bd7 --- /dev/null +++ b/tests/resource/valid/function/comparison_int.mamba @@ -0,0 +1,7 @@ +def a := 10 +def b := 4 +if a < b then print(1) else print(0) +if a <= b then print(1) else print(0) +if a > b then print(1) else print(0) +if a >= b then print(1) else print(0) +if a = b then print(1) else print(0) diff --git a/tests/resource/valid/function/compound_reassign_unsupported.mamba b/tests/resource/valid/function/compound_reassign_unsupported.mamba new file mode 100644 index 000000000..f3c59cabe --- /dev/null +++ b/tests/resource/valid/function/compound_reassign_unsupported.mamba @@ -0,0 +1,3 @@ +def total := 0 +total += 5 +print(total) diff --git a/tests/resource/valid/function/float_var_decl.mamba b/tests/resource/valid/function/float_var_decl.mamba new file mode 100644 index 000000000..bd125b1fb --- /dev/null +++ b/tests/resource/valid/function/float_var_decl.mamba @@ -0,0 +1,2 @@ +def x: Float := 2 +print(x = 2.0) diff --git a/tests/resource/valid/function/floordiv_unsupported.mamba b/tests/resource/valid/function/floordiv_unsupported.mamba new file mode 100644 index 000000000..4907b437d --- /dev/null +++ b/tests/resource/valid/function/floordiv_unsupported.mamba @@ -0,0 +1 @@ +print(10 // 4) diff --git a/tests/resource/valid/function/for_loop_exclusive_range.mamba b/tests/resource/valid/function/for_loop_exclusive_range.mamba new file mode 100644 index 000000000..f762847f5 --- /dev/null +++ b/tests/resource/valid/function/for_loop_exclusive_range.mamba @@ -0,0 +1,3 @@ +def total := 0 +for i in 0 .. 5 do total := total + i end +print(total) diff --git a/tests/resource/valid/function/for_loop_shadow.mamba b/tests/resource/valid/function/for_loop_shadow.mamba new file mode 100644 index 000000000..85cd82c24 --- /dev/null +++ b/tests/resource/valid/function/for_loop_shadow.mamba @@ -0,0 +1,5 @@ +def i := 100 +def total := 0 +for i in 0 ..= 3 do total := total + i end +print(total) +print(i) diff --git a/tests/resource/valid/function/for_over_list_unsupported.mamba b/tests/resource/valid/function/for_over_list_unsupported.mamba new file mode 100644 index 000000000..d235050eb --- /dev/null +++ b/tests/resource/valid/function/for_over_list_unsupported.mamba @@ -0,0 +1 @@ +for x in [1, 2, 3] do print(x) end diff --git a/tests/resource/valid/function/generic_arg_unsupported.mamba b/tests/resource/valid/function/generic_arg_unsupported.mamba new file mode 100644 index 000000000..88deb7782 --- /dev/null +++ b/tests/resource/valid/function/generic_arg_unsupported.mamba @@ -0,0 +1,5 @@ +def show(x: List[Int]) := do + print(1) +end + +show([1, 2]) diff --git a/tests/resource/valid/function/if_else_tail.mamba b/tests/resource/valid/function/if_else_tail.mamba new file mode 100644 index 000000000..289d38bd5 --- /dev/null +++ b/tests/resource/valid/function/if_else_tail.mamba @@ -0,0 +1,4 @@ +def sign(x: Int) -> Int := if x > 0 then 1 else -1 + +print(sign(5)) +print(sign(-5)) diff --git a/tests/resource/valid/function/if_no_else_stmt.mamba b/tests/resource/valid/function/if_no_else_stmt.mamba new file mode 100644 index 000000000..2e10ed87a --- /dev/null +++ b/tests/resource/valid/function/if_no_else_stmt.mamba @@ -0,0 +1,3 @@ +def total := 0 +if total = 0 then total := total + 5 +print(total) diff --git a/tests/resource/valid/function/implicit_last_expr_return.mamba b/tests/resource/valid/function/implicit_last_expr_return.mamba new file mode 100644 index 000000000..68eee61be --- /dev/null +++ b/tests/resource/valid/function/implicit_last_expr_return.mamba @@ -0,0 +1,3 @@ +def square(x: Int) -> Int := x * x + +print(square(6)) diff --git a/tests/resource/valid/function/no_initializer_unsupported.mamba b/tests/resource/valid/function/no_initializer_unsupported.mamba new file mode 100644 index 000000000..3746dc24d --- /dev/null +++ b/tests/resource/valid/function/no_initializer_unsupported.mamba @@ -0,0 +1,3 @@ +def x: Int +x := 5 +print(x) diff --git a/tests/resource/valid/function/nullable_arg_unsupported.mamba b/tests/resource/valid/function/nullable_arg_unsupported.mamba new file mode 100644 index 000000000..1c29a133b --- /dev/null +++ b/tests/resource/valid/function/nullable_arg_unsupported.mamba @@ -0,0 +1,5 @@ +def show(x: Int?) := do + print(1) +end + +show(5) diff --git a/tests/resource/valid/function/print_bool.mamba b/tests/resource/valid/function/print_bool.mamba new file mode 100644 index 000000000..54b7f1f44 --- /dev/null +++ b/tests/resource/valid/function/print_bool.mamba @@ -0,0 +1,2 @@ +print(True) +print(False) diff --git a/tests/resource/valid/function/print_interpolated_unsupported.mamba b/tests/resource/valid/function/print_interpolated_unsupported.mamba new file mode 100644 index 000000000..9fe4a8f78 --- /dev/null +++ b/tests/resource/valid/function/print_interpolated_unsupported.mamba @@ -0,0 +1,2 @@ +def x := 5 +print("value: {x}") diff --git a/tests/resource/valid/function/print_two_args_unsupported.mamba b/tests/resource/valid/function/print_two_args_unsupported.mamba new file mode 100644 index 000000000..5f6afae8f --- /dev/null +++ b/tests/resource/valid/function/print_two_args_unsupported.mamba @@ -0,0 +1 @@ +print(1, 2) diff --git a/tests/resource/valid/function/print_zero_args_unsupported.mamba b/tests/resource/valid/function/print_zero_args_unsupported.mamba new file mode 100644 index 000000000..aec929174 --- /dev/null +++ b/tests/resource/valid/function/print_zero_args_unsupported.mamba @@ -0,0 +1 @@ +print() diff --git a/tests/resource/valid/function/str_arg_unsupported.mamba b/tests/resource/valid/function/str_arg_unsupported.mamba new file mode 100644 index 000000000..8ebda9701 --- /dev/null +++ b/tests/resource/valid/function/str_arg_unsupported.mamba @@ -0,0 +1,3 @@ +def show(x: Str) := print(x) + +show("hi") diff --git a/tests/resource/valid/function/void_function_call_stmt.mamba b/tests/resource/valid/function/void_function_call_stmt.mamba new file mode 100644 index 000000000..ea4c6823d --- /dev/null +++ b/tests/resource/valid/function/void_function_call_stmt.mamba @@ -0,0 +1,6 @@ +def show(x: Int) := do + print(x) + return +end + +show(42) diff --git a/tests_util/src/lib.rs b/tests_util/src/lib.rs index ddcf77191..08753f979 100644 --- a/tests_util/src/lib.rs +++ b/tests_util/src/lib.rs @@ -72,10 +72,14 @@ pub fn run_via_python(subdirs: &[&str], file: &str) -> Result Result> { let src_dir = resource_path(true, subdirs, ""); let out_dir = tempfile::tempdir()?; - let bin_path = out_dir.path().join("bin_out"); + let bin_path = out_dir.path().join("nested").join("bin_out"); let arguments = Arguments { annotate: false, @@ -100,6 +104,30 @@ pub fn run_via_bin(subdirs: &[&str], file: &str) -> Result, +) -> Result<(), Box> { + let src_dir = resource_path(true, subdirs, ""); + let arguments = Arguments { + annotate: false, + backend: Backend::Asm { + target: triple.map(String::from), + }, + }; + transpile_dir(Path::new(&src_dir), Some(file), None, &arguments) + .map_err(|errs| format!("{errs:?}"))?; + Ok(()) +} + /// Run the `mamba` CLI binary itself with `args`, from within `cwd`, returning its captured /// stdout. Fails if the process exits with an error. pub fn run_cli(cwd: &Path, args: &[&str]) -> Result> { From e289c40563021594730443eddf47430c1a44ef0a Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 12:55:54 +0200 Subject: [PATCH 12/16] fix: scoping rules strict for loops The body should never leak out. We do not override outer variable. This is stricter than Python, if we define a new variable then it is only scoped there. We are, however, allowed te reassign to outer variables, if mutable, of course. --- src/backend/cranelift/convert/control_flow.rs | 80 +++---- src/backend/python/ast/mod.rs | 1 + src/backend/python/ast/node.rs | 6 + src/backend/python/convert/control_flow.rs | 196 ++++++++++++++++-- src/backend/python/convert/mod.rs | 101 +++++++-- tests/execution.rs | 40 ++-- tests/resource/valid/call/input.py | 6 + .../control_flow/for_over_range_from_func.py | 9 +- .../valid/control_flow/for_statements.py | 51 ++++- tests/resource/valid/control_flow/if.py | 6 + .../valid/control_flow/shadow_in_if_arms.py | 12 ++ .../control_flow/shadow_in_if_arms_else.py | 6 + .../control_flow/shadow_in_if_arms_then.py | 6 + tests/resource/valid/error/with.py | 12 ++ .../valid/function/def_in_if_shadow.mamba | 6 + .../valid/function/def_in_loop_shadow.mamba | 6 + 16 files changed, 446 insertions(+), 98 deletions(-) create mode 100644 tests/resource/valid/function/def_in_if_shadow.mamba create mode 100644 tests/resource/valid/function/def_in_loop_shadow.mamba diff --git a/src/backend/cranelift/convert/control_flow.rs b/src/backend/cranelift/convert/control_flow.rs index ce1cbc3ef..841da3e09 100644 --- a/src/backend/cranelift/convert/control_flow.rs +++ b/src/backend/cranelift/convert/control_flow.rs @@ -8,6 +8,23 @@ use crate::backend::cranelift::result::{BackendErr, BackendResult}; use crate::check::ast::{ASTTy, NodeTy}; impl<'a> FnLower<'a> { + /// Lower `ast` as a statement within its own scope: any variable binding it introduces -- + /// most directly a `def` that shadows an outer variable, but also a for-loop's own control + /// variable (`lower_for` relies on this for that) -- is undone once `ast` is done, so it + /// never persists past the block it belongs to. + /// + /// This is the whole mechanism behind Mamba having real block scoping for `def`, unlike + /// Python (which this backend must still *behave* like Python for everything else -- e.g. + /// reassigning an outer variable with `:=`, which isn't a new binding, still works exactly as + /// expected; only fresh bindings are undone here, since a `:=` never touches `self.vars`, only + /// the value already tracked by whichever `Variable` the name already resolves to). + fn lower_scoped_stmt(&mut self, ast: &ASTTy) -> BackendResult<()> { + let snapshot = self.vars.clone(); + let result = self.lower_stmt(ast); + self.vars = snapshot; + result + } + /// Lower an `IfElse` in statement position: both arms are lowered as statements, and control /// re-joins in a shared `merge_block` afterwards (or falls straight through to it when there /// is no `else`). @@ -31,12 +48,12 @@ impl<'a> FnLower<'a> { .brif(cond_value, then_block, &[], else_block, &[]); self.builder.switch_to_block(then_block); - self.lower_stmt(then)?; + self.lower_scoped_stmt(then)?; self.builder.ins().jump(merge_block, &[]); if let Some(el) = el { self.builder.switch_to_block(else_block); - self.lower_stmt(el)?; + self.lower_scoped_stmt(el)?; self.builder.ins().jump(merge_block, &[]); } @@ -50,25 +67,17 @@ impl<'a> FnLower<'a> { /// supported -- iterating a collection would need this backend to support collections at all, /// which is out of scope (see the module-level docs). /// - /// Rotated loop, entered through a pre-check: `entry_check` handles the (possibly empty - /// up-front) range and falls through to `exit` or into `body`; `body` runs the loop body, - /// computes the next value, and -- checking *that* -- either commits it and loops back into - /// `body` directly (skipping `entry_check` on every subsequent iteration) or falls through to - /// `exit` without committing it. + /// Classic three-block loop: `header` checks the bound and either enters `body` or falls + /// through to `exit`; `body` runs the loop body and increments before jumping back to + /// `header`. /// - /// Mamba (like Python, which this must match) has no block scoping, so the loop variable is - /// just an ordinary binding of its name -- if that name already existed, this loop - /// permanently overwrites it. Committing the next value only on the branch that's actually - /// going to use it (rather than unconditionally at the bottom of `body`, then discovering at - /// `header` that it was one too many and exiting anyway) is what makes the loop variable keep - /// whatever value it was last actually *used* with once the loop exits, instead of one past - /// it -- matching what Python's own `for` leaves its loop variable holding. - /// - /// One known gap: for a range that's empty from the start (e.g. `for i in 5 .. 5`), Python's - /// `i` never gets touched at all -- it keeps whatever it held *before* the loop -- whereas - /// this still binds it to `from` before the (never-taken) entry check. Shadowing an outer - /// variable with a loop that may run zero times is the only way this is observable, and isn't - /// worth the extra restructuring to close. + /// The loop variable is bound inside `body`'s own scope (see [`Self::lower_scoped_stmt`]), so + /// it's its own fresh Cranelift `Variable` (never the same one as any outer variable of the + /// same name) and never persists past the loop. So a `for` never touches an outer variable it + /// happens to shadow -- during the loop *or* after it -- unlike Python's own `for`, which has + /// no block scoping and would happily clobber it. (The Python backend has to work to emulate + /// this same guarantee, since generated Python doesn't get it for free -- see + /// `backend::python::convert::control_flow`.) pub(super) fn lower_for( &mut self, expr: &ASTTy, @@ -108,39 +117,34 @@ impl<'a> FnLower<'a> { let loop_var = self.new_var(ty); self.builder.def_var(loop_var, from_value); - self.vars.insert(var_name, (loop_var, ty)); + let header_block = self.builder.create_block(); + let body_block = self.builder.create_block(); + let exit_block = self.builder.create_block(); + self.builder.ins().jump(header_block, &[]); + + self.builder.switch_to_block(header_block); + let current = self.builder.use_var(loop_var); let cc = if inclusive { IntCC::SignedLessThanOrEqual } else { IntCC::SignedLessThan }; - - let entry_check_block = self.builder.create_block(); - let body_block = self.builder.create_block(); - let continue_block = self.builder.create_block(); - let exit_block = self.builder.create_block(); - self.builder.ins().jump(entry_check_block, &[]); - - self.builder.switch_to_block(entry_check_block); - let current = self.builder.use_var(loop_var); let cond = self.builder.ins().icmp(cc, current, to_value); self.builder .ins() .brif(cond, body_block, &[], exit_block, &[]); self.builder.switch_to_block(body_block); - self.lower_stmt(body)?; + let snapshot = self.vars.clone(); + self.vars.insert(var_name, (loop_var, ty)); + let result = self.lower_stmt(body); + self.vars = snapshot; + result?; let current = self.builder.use_var(loop_var); let next = self.builder.ins().iadd(current, step_value); - let cond = self.builder.ins().icmp(cc, next, to_value); - self.builder - .ins() - .brif(cond, continue_block, &[], exit_block, &[]); - - self.builder.switch_to_block(continue_block); self.builder.def_var(loop_var, next); - self.builder.ins().jump(body_block, &[]); + self.builder.ins().jump(header_block, &[]); self.builder.switch_to_block(exit_block); Ok(()) diff --git a/src/backend/python/ast/mod.rs b/src/backend/python/ast/mod.rs index 068456359..0818bebe3 100644 --- a/src/backend/python/ast/mod.rs +++ b/src/backend/python/ast/mod.rs @@ -409,6 +409,7 @@ fn to_py(core: &PythonCore, ind: usize) -> String { } PythonCore::Continue => String::from("continue"), PythonCore::Break => String::from("break"), + PythonCore::Del { name } => format!("del {name}"), PythonCore::ClassDef { name, diff --git a/src/backend/python/ast/node.rs b/src/backend/python/ast/node.rs index 079ae0dd2..bbf27588b 100644 --- a/src/backend/python/ast/node.rs +++ b/src/backend/python/ast/node.rs @@ -241,6 +241,12 @@ pub enum PythonCore { Return { expr: Box, }, + /// Python's `del `, removing a name binding entirely. + /// Used to keep a generated `for` loop's variable from leaking into the enclosing scope when Mamba's own scoping says it shouldn't. + /// Generally, Mamba's scoping rules are stricter than Python's. + Del { + name: String, + }, UnderScore, Pass, None, diff --git a/src/backend/python/convert/control_flow.rs b/src/backend/python/convert/control_flow.rs index 35ce98f78..dc5b3ddc6 100644 --- a/src/backend/python/convert/control_flow.rs +++ b/src/backend/python/convert/control_flow.rs @@ -1,4 +1,4 @@ -use crate::backend::python::ast::node::PythonCore; +use crate::backend::python::ast::node::{CoreOp, PythonCore}; use crate::backend::python::convert::convert_node; use crate::backend::python::convert::state::{Imports, State}; use crate::backend::python::result::{GenResult, UnimplementedErr}; @@ -31,23 +31,31 @@ pub fn convert_cntrl_flow( .remove_ret(true) .must_assign_to(None, None); + // A ternary's arms are expressions, not statements -- `is_valid_in_ternary` + // already rules out anything (a `Block`, in particular) that could contain + // a `def`, so there's nothing here that needs scope-guarding. PythonCore::Ternary { cond, then: Box::from(convert_node(then, imp, &state, ctx)?), el: Box::from(convert_node(el, imp, &state, ctx)?), } } else { + let then_core = convert_node(then, imp, state, ctx)?; + let el_core = convert_node(el, imp, state, ctx)?; PythonCore::IfElse { cond, - then: Box::from(convert_node(then, imp, state, ctx)?), - el: Box::from(convert_node(el, imp, state, ctx)?), + then: Box::from(scope_guarded(then, then_core)), + el: Box::from(scope_guarded(el, el_core)), } } } - None => PythonCore::If { - cond, - then: Box::from(convert_node(then, imp, state, ctx)?), - }, + None => { + let then_core = convert_node(then, imp, state, ctx)?; + PythonCore::If { + cond, + then: Box::from(scope_guarded(then, then_core)), + } + } } } NodeTy::Match { @@ -65,6 +73,7 @@ pub fn convert_cntrl_flow( for case in match_cases { if let NodeTy::Case { cond, body } = &case.node { if let NodeTy::ExpressionType { expr, .. } = &cond.node { + let body_core = convert_node(body.as_ref(), imp, state, ctx)?; cases.push(PythonCore::Case { expr: Box::from(convert_node( expr.as_ref(), @@ -72,7 +81,7 @@ pub fn convert_cntrl_flow( &state.is_last_must_be_ret(false).must_assign_to(None, None), ctx, )?), - body: Box::from(convert_node(body.as_ref(), imp, state, ctx)?), + body: Box::from(scope_guarded(body.as_ref(), body_core)), }) } } @@ -80,15 +89,36 @@ pub fn convert_cntrl_flow( PythonCore::Match { expr, cases } } - NodeTy::While { cond, body } => PythonCore::While { - cond: Box::from(convert_node(cond, imp, state, ctx)?), - body: Box::from(convert_node(body, imp, state, ctx)?), - }, - NodeTy::For { expr, col, body } => PythonCore::For { - expr: Box::from(convert_node(expr, imp, state, ctx)?), - col: Box::from(convert_node(col, imp, state, ctx)?), - body: Box::from(convert_node(body, imp, state, ctx)?), - }, + NodeTy::While { cond, body } => { + let while_core = PythonCore::While { + cond: Box::from(convert_node(cond, imp, state, ctx)?), + body: Box::from(convert_node(body, imp, state, ctx)?), + }; + // Unlike an `if`/`case` branch, `body` runs every iteration -- the guard has to + // wrap the *whole loop* (set up once before it, torn down once after), not `body` + // itself, or a second iteration would "save" the value the first iteration's own + // shadowing def already left behind, not the real outer one. + wrap_scoped(&direct_def_names(body), while_core) + } + NodeTy::For { expr, col, body } => { + let for_core = PythonCore::For { + expr: Box::from(convert_node(expr, imp, state, ctx)?), + col: Box::from(convert_node(col, imp, state, ctx)?), + body: Box::from(convert_node(body, imp, state, ctx)?), + }; + // The loop variable itself is always a fresh binding to guard, same reasoning as + // `While` above for why the whole loop (not just `body`) gets wrapped. + let mut names = vec![]; + if let NodeTy::Id { lit } = &expr.node { + names.push(lit.clone()); + } + for name in direct_def_names(body) { + if !names.contains(&name) { + names.push(name); + } + } + wrap_scoped(&names, for_core) + } NodeTy::Break => PythonCore::Break, NodeTy::Continue => PythonCore::Continue, other => { @@ -98,6 +128,129 @@ pub fn convert_cntrl_flow( }) } +/// Wrap `core` (the already-converted form of `body`) so any `def` directly inside `body` (not +/// nested deeper -- a `def` inside a further-nested block is that block's own responsibility, +/// handled when *it* is converted) doesn't leak or overwrite an outer variable of the same name +/// once this block exits. `body` runs at most once here (an `if`/`case` branch), so the guard can +/// wrap `core` itself directly -- contrast [`wrap_scoped`], used where the body is a loop. +pub(super) fn scope_guarded(body: &ASTTy, core: PythonCore) -> PythonCore { + wrap_scoped(&direct_def_names(body), core) +} + +/// The names `body` directly `def`s at its own top level -- i.e. not inside a further-nested +/// block. `body` is either a single statement or a `NodeTy::Block` of statements, matching how +/// Mamba represents a block/branch body. +fn direct_def_names(body: &ASTTy) -> Vec { + let statements: Vec<&ASTTy> = match &body.node { + NodeTy::Block { statements } => statements.iter().collect(), + _ => vec![body], + }; + + let mut names = vec![]; + for statement in statements { + if let NodeTy::VariableDef { var: id_ast_ty, .. } = &statement.node { + if let NodeTy::Id { lit } = &id_ast_ty.node { + if !names.contains(lit) { + names.push(lit.clone()); + } + } + } + } + names +} + +/// Sandwich `core` between a runtime existed/saved setup and a restore-or-delete teardown, one +/// pair per name in `names` -- so each of those names' bindings, as `core` leaves them, never +/// escape to whatever called this. A no-op (`core` unchanged) when `names` is empty. +/// +/// This is the whole mechanism behind Mamba having real block scoping for `def`, unlike Python +/// (which this backend must still *behave* like Python for everything else -- e.g. reassigning +/// an outer variable with `:=`, which isn't a new binding, is untouched by this and keeps working +/// exactly as before; only fresh bindings introduced by `def` are undone here). +/// +/// Whether a name was already bound has to be decided at runtime (via `locals()`), since nothing +/// has kept scope information around by the time code generation runs: +/// +/// ```python +/// __mamba_i_existed = 'i' in locals() +/// __mamba_i_saved = i if __mamba_i_existed else None +/// +/// if __mamba_i_existed: +/// i = __mamba_i_saved +/// else: +/// del i +/// ``` +pub(super) fn wrap_scoped(names: &[String], core: PythonCore) -> PythonCore { + if names.is_empty() { + return core; + } + + let mut statements: Vec = names.iter().flat_map(|n| setup(n)).collect(); + // Flatten rather than nest `core` as a single list item: `to_py`'s renderer indents each of + // a `Block`'s *items* uniformly, but a `Block` item that is itself a `Block` also indents its + // own first line, doubling up on just that one line. Splicing its statements in directly + // keeps every item here a plain (non-`Block`) statement, so they all render at the same, + // correct indent. + match core { + PythonCore::Block { statements: inner } => statements.extend(inner), + other => statements.push(other), + } + statements.extend(names.iter().map(|n| restore_or_delete(n))); + + PythonCore::Block { statements } +} + +fn id(lit: &str) -> PythonCore { + PythonCore::Id { + lit: String::from(lit), + } +} + +fn setup(name: &str) -> [PythonCore; 2] { + let existed = format!("__mamba_{name}_existed"); + let saved = format!("__mamba_{name}_saved"); + [ + PythonCore::VarDef { + var: Box::from(id(&existed)), + ty: None, + expr: Some(Box::from(PythonCore::In { + left: Box::from(PythonCore::Str { + string: String::from(name), + }), + right: Box::from(PythonCore::FunctionCall { + function: Box::from(id("locals")), + args: vec![], + }), + })), + }, + PythonCore::VarDef { + var: Box::from(id(&saved)), + ty: None, + expr: Some(Box::from(PythonCore::Ternary { + cond: Box::from(id(&existed)), + then: Box::from(id(name)), + el: Box::from(PythonCore::None), + })), + }, + ] +} + +fn restore_or_delete(name: &str) -> PythonCore { + let existed = format!("__mamba_{name}_existed"); + let saved = format!("__mamba_{name}_saved"); + PythonCore::IfElse { + cond: Box::from(id(&existed)), + then: Box::from(PythonCore::Assign { + left: Box::from(id(name)), + right: Box::from(id(&saved)), + op: CoreOp::Assign, + }), + el: Box::from(PythonCore::Del { + name: String::from(name), + }), + } +} + fn is_valid_in_ternary(then: &ASTTy, el: &ASTTy) -> bool { is_expr_valid_in_ternary(then) && is_expr_valid_in_ternary(el) } @@ -264,8 +417,15 @@ mod tests { }); let for_stmt = to_pos!(Node::For { expr, col, body }); + // Wrapped in a scope-guarding `Block` (see `scope_guarded_for`) -- the `For` itself is + // the third statement, sandwiched between the setup and the restore-or-delete. let (core_expr, core_col, core_body) = match gen(&ASTTy::from(&for_stmt)) { - Ok(PythonCore::For { expr, col, body }) => (expr, col, body), + Ok(PythonCore::Block { statements }) => match statements.as_slice() { + [_, _, PythonCore::For { expr, col, body }, _] => { + (expr.clone(), col.clone(), body.clone()) + } + other => panic!("Expected a 4-statement scope-guarded for, was {other:?}"), + }, other => panic!("Expected for but was {other:?}"), }; diff --git a/src/backend/python/convert/mod.rs b/src/backend/python/convert/mod.rs index bcf988d97..f60d5293b 100644 --- a/src/backend/python/convert/mod.rs +++ b/src/backend/python/convert/mod.rs @@ -5,7 +5,9 @@ use crate::backend::python::convert::builder::convert_builder; use crate::backend::python::convert::call::convert_call; use crate::backend::python::convert::class::convert_class; use crate::backend::python::convert::common::convert_vec; -use crate::backend::python::convert::control_flow::convert_cntrl_flow; +use crate::backend::python::convert::control_flow::{ + convert_cntrl_flow, scope_guarded, wrap_scoped, +}; use crate::backend::python::convert::definition::convert_def; use crate::backend::python::convert::handle::convert_handle; use crate::backend::python::convert::range_slice::convert_range_slice; @@ -251,15 +253,28 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context resource, alias: Some((alias, ..)), expr, - } => PythonCore::WithAs { - resource: Box::from(convert_node(resource, imp, state, ctx)?), - alias: Box::from(convert_node(alias, imp, &state.expand_ty(false), ctx)?), - expr: Box::from(convert_node(expr, imp, state, ctx)?), - }, - NodeTy::With { resource, expr, .. } => PythonCore::With { - resource: Box::from(convert_node(resource, imp, state, ctx)?), - expr: Box::from(convert_node(expr, imp, state, ctx)?), - }, + } => { + let expr_core = convert_node(expr, imp, state, ctx)?; + // The `as alias` binding is a fresh name too (like a for-loop's own control + // variable), not just whatever `expr` itself directly `def`s -- guard it the same + // way. + let expr_core = match &alias.node { + NodeTy::Id { lit } => wrap_scoped(std::slice::from_ref(lit), expr_core), + _ => expr_core, + }; + PythonCore::WithAs { + resource: Box::from(convert_node(resource, imp, state, ctx)?), + alias: Box::from(convert_node(alias, imp, &state.expand_ty(false), ctx)?), + expr: Box::from(scope_guarded(expr, expr_core)), + } + } + NodeTy::With { resource, expr, .. } => { + let expr_core = convert_node(expr, imp, state, ctx)?; + PythonCore::With { + resource: Box::from(convert_node(resource, imp, state, ctx)?), + expr: Box::from(scope_guarded(expr, expr_core)), + } + } NodeTy::Raise { .. } | NodeTy::Handle { .. } => convert_handle(ast, imp, state, ctx)?, @@ -409,7 +424,7 @@ fn skip_return(core: &PythonCore) -> bool { #[cfg(test)] mod tests { - use crate::backend::python::ast::node::PythonCore; + use crate::backend::python::ast::node::{CoreOp, PythonCore}; use crate::backend::python::gen; use crate::common::position::Position; use crate::parse::ast::Node; @@ -831,11 +846,67 @@ mod tests { lit: String::from("other") } ); + // `alias` (`other`) is a fresh binding for the `with` block's own scope, so `expr` is + // wrapped the same way a shadowing `def` would be -- see `scope_guarded`/`wrap_scoped`. + let PythonCore::Block { statements } = *expr else { + panic!("Expected a scope-guarded with-as expr, was {expr:?}"); + }; assert_eq!( - *expr, - PythonCore::Int { - int: String::from("9") - } + statements.as_slice(), + &[ + PythonCore::VarDef { + var: Box::from(PythonCore::Id { + lit: String::from("__mamba_other_existed") + }), + ty: None, + expr: Some(Box::from(PythonCore::In { + left: Box::from(PythonCore::Str { + string: String::from("other") + }), + right: Box::from(PythonCore::FunctionCall { + function: Box::from(PythonCore::Id { + lit: String::from("locals") + }), + args: vec![], + }), + })), + }, + PythonCore::VarDef { + var: Box::from(PythonCore::Id { + lit: String::from("__mamba_other_saved") + }), + ty: None, + expr: Some(Box::from(PythonCore::Ternary { + cond: Box::from(PythonCore::Id { + lit: String::from("__mamba_other_existed") + }), + then: Box::from(PythonCore::Id { + lit: String::from("other") + }), + el: Box::from(PythonCore::None), + })), + }, + PythonCore::Int { + int: String::from("9") + }, + PythonCore::IfElse { + cond: Box::from(PythonCore::Id { + lit: String::from("__mamba_other_existed") + }), + then: Box::from(PythonCore::Assign { + left: Box::from(PythonCore::Id { + lit: String::from("other") + }), + right: Box::from(PythonCore::Id { + lit: String::from("__mamba_other_saved") + }), + op: CoreOp::Assign, + }), + el: Box::from(PythonCore::Del { + name: String::from("other") + }), + }, + ] ); } diff --git a/tests/execution.rs b/tests/execution.rs index 45163e36d..cd935c2c7 100644 --- a/tests/execution.rs +++ b/tests/execution.rs @@ -6,32 +6,24 @@ use std::error::Error; use std::path::Path; -use test_case::test_case; +use test_case::{test_case, test_matrix}; use tests_util::{resource_path, run_cli, run_via_asm, run_via_bin, run_via_python, Runner}; /// Fixtures whose output is identical across both backends -- the bulk of correctness coverage. -#[test_case(run_via_python, &["function"], "hello_world.mamba" => "hello world\n")] -#[test_case(run_via_bin, &["function"], "hello_world.mamba" => "hello world\n")] -#[test_case(run_via_python, &["function"], "arithmetic_sum.mamba" => "14\n")] -#[test_case(run_via_bin, &["function"], "arithmetic_sum.mamba" => "14\n")] -#[test_case(run_via_python, &["function"], "arithmetic_ops.mamba" => "6\n40\n1\n")] -#[test_case(run_via_bin, &["function"], "arithmetic_ops.mamba" => "6\n40\n1\n")] -#[test_case(run_via_python, &["function"], "float_arithmetic.mamba" => "1\n")] -#[test_case(run_via_bin, &["function"], "float_arithmetic.mamba" => "1\n")] -#[test_case(run_via_python, &["function"], "comparison_int.mamba" => "0\n0\n1\n1\n0\n")] -#[test_case(run_via_bin, &["function"], "comparison_int.mamba" => "0\n0\n1\n1\n0\n")] -#[test_case(run_via_python, &["function"], "comparison_bool.mamba" => "0\n1\n")] -#[test_case(run_via_bin, &["function"], "comparison_bool.mamba" => "0\n1\n")] -#[test_case(run_via_python, &["function"], "if_no_else_stmt.mamba" => "5\n")] -#[test_case(run_via_bin, &["function"], "if_no_else_stmt.mamba" => "5\n")] -#[test_case(run_via_python, &["function"], "void_function_call_stmt.mamba" => "42\n")] -#[test_case(run_via_bin, &["function"], "void_function_call_stmt.mamba" => "42\n")] -#[test_case(run_via_python, &["function"], "for_loop_sum.mamba" => "10\n")] -#[test_case(run_via_bin, &["function"], "for_loop_sum.mamba" => "10\n")] -#[test_case(run_via_python, &["function"], "for_loop_exclusive_range.mamba" => "10\n")] -#[test_case(run_via_bin, &["function"], "for_loop_exclusive_range.mamba" => "10\n")] -#[test_case(run_via_python, &["function"], "for_loop_shadow.mamba" => "6\n3\n")] -#[test_case(run_via_bin, &["function"], "for_loop_shadow.mamba" => "6\n3\n")] +/// We use test matrix to emphaize that output should be the same for python and binary. +#[test_matrix([run_via_python, run_via_bin], &["function"], "hello_world.mamba" => "hello world\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "arithmetic_sum.mamba" => "14\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "arithmetic_ops.mamba" => "6\n40\n1\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "float_arithmetic.mamba" => "1\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "comparison_int.mamba" => "0\n0\n1\n1\n0\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "comparison_bool.mamba" => "0\n1\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "if_no_else_stmt.mamba" => "5\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "void_function_call_stmt.mamba" => "42\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "for_loop_sum.mamba" => "10\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "for_loop_exclusive_range.mamba" => "10\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "for_loop_shadow.mamba" => "6\n100\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "def_in_if_shadow.mamba" => "999\n100\n")] +#[test_matrix([run_via_python, run_via_bin], &["function"], "def_in_loop_shadow.mamba" => "999\n999\n999\n100\n")] fn execution(run: Runner, dirs: &[&str], file: &str) -> String { run(dirs, file).unwrap() } @@ -42,7 +34,7 @@ fn execution(run: Runner, dirs: &[&str], file: &str) -> String { /// `implicit_last_expr_return.mamba` -- the Python backend has a real, pre-existing bug /// unrelated to the Cranelift backend under test here: without `--annotate` (off by default), /// it fails to emit a `return` for a function whose body is an implicit last-expression (no -/// `return` keyword), so the function silently returns `None` instead. `run_via_python` uses +/// `return` keyword), so the function silently returns `None` instead. `[run_via_python, run_via_bin]` uses /// `Arguments::default()` (`annotate: false`), so pairing these against it would just be /// asserting on that separate, known-bad behavior. #[test_case(run_via_bin, &["function"], "print_bool.mamba" => "1\n0\n")] diff --git a/tests/resource/valid/call/input.py b/tests/resource/valid/call/input.py index 3eff08ea7..c9d928b47 100644 --- a/tests/resource/valid/call/input.py +++ b/tests/resource/valid/call/input.py @@ -1,7 +1,13 @@ num: str = input("Compute factorial: ") if num.is_digit(): + __mamba_result_existed = "result" in locals() + __mamba_result_saved = result if __mamba_result_existed else None result: int = int(num) print(f"Factorial {num} is: {result}.") + if __mamba_result_existed: + result = __mamba_result_saved + else: + del result else: print("Input was not an integer.") diff --git a/tests/resource/valid/control_flow/for_over_range_from_func.py b/tests/resource/valid/control_flow/for_over_range_from_func.py index 15a15e892..3cb363629 100644 --- a/tests/resource/valid/control_flow/for_over_range_from_func.py +++ b/tests/resource/valid/control_flow/for_over_range_from_func.py @@ -1,5 +1,12 @@ def f() -> range: - return range(0,2,1) + return range(0, 2, 1) +__mamba_x_existed = "x" in locals() +__mamba_x_saved = x if __mamba_x_existed else None for x in f(): print(x) + +if __mamba_x_existed: + x = __mamba_x_saved +else: + del x diff --git a/tests/resource/valid/control_flow/for_statements.py b/tests/resource/valid/control_flow/for_statements.py index 14592faa4..0df665e41 100644 --- a/tests/resource/valid/control_flow/for_statements.py +++ b/tests/resource/valid/control_flow/for_statements.py @@ -1,28 +1,75 @@ -b: set[int] = {1,2} +b: set[int] = {1, 2} +__mamba_b_existed = "b" in locals() +__mamba_b_saved = b if __mamba_b_existed else None +__mamba_new_existed = "new" in locals() +__mamba_new_saved = new if __mamba_new_existed else None for b in b: print(b + 5) new: int = b + 1 new = 30 print(new) +if __mamba_b_existed: + b = __mamba_b_saved +else: + del b +if __mamba_new_existed: + new = __mamba_new_saved +else: + del new + e: set[int] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} +__mamba_d_existed = "d" in locals() +__mamba_d_saved = d if __mamba_d_existed else None for d in e: print(d) print(d - 1) - print(d + 1) +if __mamba_d_existed: + d = __mamba_d_saved +else: + del d + +__mamba_i_existed = "i" in locals() +__mamba_i_saved = i if __mamba_i_existed else None for i in range(0, 34, 1): print(i) +if __mamba_i_existed: + i = __mamba_i_saved +else: + del i + +__mamba_i_existed = "i" in locals() +__mamba_i_saved = i if __mamba_i_existed else None for i in range(0, 345 + 1, 1): print(i) +if __mamba_i_existed: + i = __mamba_i_saved +else: + del i + a: int = 1 b: int = 112 +__mamba_i_existed = "i" in locals() +__mamba_i_saved = i if __mamba_i_existed else None for i in range(a, b, 1): print("hello") +if __mamba_i_existed: + i = __mamba_i_saved +else: + del i + c: int = 2451 +__mamba_i_existed = "i" in locals() +__mamba_i_saved = i if __mamba_i_existed else None for i in range(a, c + 1, 20): print("world") + +if __mamba_i_existed: + i = __mamba_i_saved +else: + del i diff --git a/tests/resource/valid/control_flow/if.py b/tests/resource/valid/control_flow/if.py index 6ebce0554..91347e730 100644 --- a/tests/resource/valid/control_flow/if.py +++ b/tests/resource/valid/control_flow/if.py @@ -11,6 +11,8 @@ cond = True or False if cond: + __mamba_iii_existed = "iii" in locals() + __mamba_iii_saved = iii if __mamba_iii_existed else None "asdf" print("hello \"world\"") @@ -24,6 +26,10 @@ "hhh" else: iii + if __mamba_iii_existed: + iii = __mamba_iii_saved + else: + del iii else: "other" print("hello \"world\"") diff --git a/tests/resource/valid/control_flow/shadow_in_if_arms.py b/tests/resource/valid/control_flow/shadow_in_if_arms.py index 0011293ba..7c6aa232a 100644 --- a/tests/resource/valid/control_flow/shadow_in_if_arms.py +++ b/tests/resource/valid/control_flow/shadow_in_if_arms.py @@ -7,8 +7,20 @@ def f2(self): print("2") if True: + __mamba_x_existed = "x" in locals() + __mamba_x_saved = x if __mamba_x_existed else None x: MyClass1 = MyClass1() x.f1() + if __mamba_x_existed: + x = __mamba_x_saved + else: + del x else: + __mamba_x_existed = "x" in locals() + __mamba_x_saved = x if __mamba_x_existed else None x: MyClass2 = MyClass2() x.f2() + if __mamba_x_existed: + x = __mamba_x_saved + else: + del x diff --git a/tests/resource/valid/control_flow/shadow_in_if_arms_else.py b/tests/resource/valid/control_flow/shadow_in_if_arms_else.py index 73f6bba64..6876c1276 100644 --- a/tests/resource/valid/control_flow/shadow_in_if_arms_else.py +++ b/tests/resource/valid/control_flow/shadow_in_if_arms_else.py @@ -10,5 +10,11 @@ def f2(self): if True: x.f2() else: + __mamba_x_existed = "x" in locals() + __mamba_x_saved = x if __mamba_x_existed else None x: MyClass1 = MyClass1() x.f1() + if __mamba_x_existed: + x = __mamba_x_saved + else: + del x diff --git a/tests/resource/valid/control_flow/shadow_in_if_arms_then.py b/tests/resource/valid/control_flow/shadow_in_if_arms_then.py index 20d9a45e4..94de1ebb9 100644 --- a/tests/resource/valid/control_flow/shadow_in_if_arms_then.py +++ b/tests/resource/valid/control_flow/shadow_in_if_arms_then.py @@ -8,7 +8,13 @@ def f2(self): x: MyClass2 = MyClass2() if True: + __mamba_x_existed = "x" in locals() + __mamba_x_saved = x if __mamba_x_existed else None x: MyClass1 = MyClass1() x.f1() + if __mamba_x_existed: + x = __mamba_x_saved + else: + del x else: x.f2() diff --git a/tests/resource/valid/error/with.py b/tests/resource/valid/error/with.py index b365eee0d..8a43ea0df 100644 --- a/tests/resource/valid/error/with.py +++ b/tests/resource/valid/error/with.py @@ -4,10 +4,22 @@ def do_something(x: int): my_resource: int = 10 with my_resource as other: + __mamba_other_existed = "other" in locals() + __mamba_other_saved = other if __mamba_other_existed else None do_something(other) + if __mamba_other_existed: + other = __mamba_other_saved + else: + del other with my_resource as yet_another: + __mamba_yet_another_existed = "yet_another" in locals() + __mamba_yet_another_saved = yet_another if __mamba_yet_another_existed else None do_something(yet_another) + if __mamba_yet_another_existed: + yet_another = __mamba_yet_another_saved + else: + del yet_another with my_resource: do_something(my_resource) diff --git a/tests/resource/valid/function/def_in_if_shadow.mamba b/tests/resource/valid/function/def_in_if_shadow.mamba new file mode 100644 index 000000000..2444cf589 --- /dev/null +++ b/tests/resource/valid/function/def_in_if_shadow.mamba @@ -0,0 +1,6 @@ +def x := 100 +if True then do + def x := 999 + print(x) +end +print(x) diff --git a/tests/resource/valid/function/def_in_loop_shadow.mamba b/tests/resource/valid/function/def_in_loop_shadow.mamba new file mode 100644 index 000000000..9b6f66040 --- /dev/null +++ b/tests/resource/valid/function/def_in_loop_shadow.mamba @@ -0,0 +1,6 @@ +def x := 100 +for i in 0 ..= 2 do + def x := 999 + print(x) +end +print(x) From 9a729ee5702785478f16f28251afbebd4597d17b Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 13:05:10 +0200 Subject: [PATCH 13/16] fix: Position-Independent Code machine code This is required for MacOS. --- src/backend/cranelift/mod.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/backend/cranelift/mod.rs b/src/backend/cranelift/mod.rs index 16a2e7308..361dc559e 100644 --- a/src/backend/cranelift/mod.rs +++ b/src/backend/cranelift/mod.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use cranelift_codegen::isa::{self, OwnedTargetIsa}; -use cranelift_codegen::settings::{self}; +use cranelift_codegen::settings::{self, Configurable}; use cranelift_module::default_libcall_names; use cranelift_object::{ObjectBuilder, ObjectModule}; use log::{info, trace}; @@ -191,8 +191,17 @@ pub fn disassemble(ast_ty: &ASTTy, target: Option<&str>, ctx: &Context) -> Backe /// Create target which is understood by cranelift. /// If None, then default to host architecture as target. +/// +/// Cranelift defaults to non-PIC (Position-Independent Code), which is direct/absolute addressing for calls and data. +/// This is fine for `cc`-driven linking on Linux. +/// However, it fails on macOS: +/// modern `ld` rejects those as "illegal text-relocations" in a regular (non-`-static`) executable. +/// PIC (RIP-relative/GOT addressing instead) links cleanly on both, so just always ask for it rather than branching on target OS. fn build_isa(target: Option<&str>) -> BackendResult { - let flag_builder = settings::builder(); + let mut flag_builder = settings::builder(); + flag_builder + .set("is_pic", "true") + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string()))?; let flags = settings::Flags::new(flag_builder); match target { From 5d71aaab21046092336a4517d2a9fcf736bd6d24 Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 13:13:08 +0200 Subject: [PATCH 14/16] fix: line endings in test comparison --- tests_util/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests_util/src/lib.rs b/tests_util/src/lib.rs index 08753f979..d8f9775ad 100644 --- a/tests_util/src/lib.rs +++ b/tests_util/src/lib.rs @@ -31,6 +31,8 @@ pub type Runner = fn(&[&str], &str) -> Result>; /// Unlike [test_directory]/[fallable], which only diff the generated Python's *AST* against a /// reference, this actually executes the file -- for asserting on runtime behavior (e.g. what a /// program actually prints), not just structural equivalence to a reference. +/// +/// Line endings always '\n', not the antiquated '\r\n` from Windows. pub fn run_python(path: &Path) -> Result { let output = Command::new(PYTHON) .arg(path) @@ -38,7 +40,7 @@ pub fn run_python(path: &Path) -> Result { .map_err(|e| format!("Could not run '{PYTHON} {}': {e}", path.display()))?; if output.status.success() { - Ok(String::from_utf8_lossy(&output.stdout).into_owned()) + Ok(String::from_utf8_lossy(&output.stdout).replace("\r\n", "\n")) } else { Err(format!( "'{PYTHON} {}' exited with an error:\n{}", @@ -101,7 +103,7 @@ pub fn run_via_bin(subdirs: &[&str], file: &str) -> Result Date: Wed, 26 Aug 2026 13:40:55 +0200 Subject: [PATCH 15/16] fix: compilation of floats in MacOS Deal with some complex edge cases for min and max. Relied heavily on Claude here in this case. --- Cargo.toml | 2 +- src/backend/cranelift/convert/call.rs | 137 +++++++++++++++----- src/backend/cranelift/convert/definition.rs | 36 +++-- src/backend/cranelift/convert/mod.rs | 21 ++- 4 files changed, 129 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8992047f0..84e7bab50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ categories = ["parsing"] [dependencies] ansi_term = "0.12.1" # Colors in the terminal clap = { version = "4.5.40", features = ["derive"] } # Compiler frontend -cranelift-codegen = "0.114.0" # Machine code IR, instruction selection and register allocation +cranelift-codegen = { version = "0.114.0", features = ["all-native-arch"] } # Machine code IR, instruction selection and register allocation cranelift-frontend = "0.114.0" # Build Cranelift IR function bodies (SSA construction) cranelift-module = "0.114.0" # Function/data declaration shared across Cranelift backends cranelift-native = "0.114.0" # Detect the host target when --target is not given diff --git a/src/backend/cranelift/convert/call.rs b/src/backend/cranelift/convert/call.rs index a8972680e..1f95167f5 100644 --- a/src/backend/cranelift/convert/call.rs +++ b/src/backend/cranelift/convert/call.rs @@ -1,4 +1,5 @@ -use cranelift_codegen::ir::{types, InstBuilder, Value}; +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::{types, InstBuilder, MemFlags, StackSlotData, StackSlotKind, Value}; use cranelift_module::{DataDescription, Module}; use crate::backend::cranelift::convert::FnLower; @@ -48,15 +49,12 @@ impl<'a> FnLower<'a> { Ok(self.builder.inst_results(call).first().copied()) } - /// `print(...)`: a string literal goes through `puts` (which appends its own newline, like - /// Mamba/Python's `print`); an `Int`/`Bool` value goes through `printf` with a fixed - /// `%lld\n` format. Anything else (interpolated strings, a `Float` value, non-primitive - /// values, multiple arguments) is out of scope for this backend. + /// `print(...)`: a string literal goes through `puts` (which appends its own newline, like Mamba/Python's `print`). + /// An `Int`/`Bool` value is formatted to a decimal string ourselves (see [`Self::lower_print_int`]) and then also printed via `puts`. + /// Anything else (interpolated strings, a `Float` value, non-primitive values, multiple arguments) is out of scope for this backend. /// - /// A `Float` is deliberately rejected rather than attempted: `%lld` would read raw float bits - /// as an integer (garbage, not a crash), and doing this properly means both a `%f`-style - /// format string and setting `%al` to the SysV-mandated vector-register count for a variadic - /// call passing a float -- printf-only ABI plumbing this backend doesn't have yet. + /// A `Float` is deliberately rejected rather than attempted: formatting one correctly. + /// E.g. shortest round-tripping decimal output, the way Python's own `print` does, is a meaningfully harder problem than an integer. pub(super) fn lower_print(&mut self, ast: &ASTTy) -> BackendResult> { let args = match &ast.node { NodeTy::FunctionCall { args, .. } => args, @@ -98,32 +96,105 @@ impl<'a> FnLower<'a> { if self.builder.func.dfg.value_type(value) == types::F64 { return Err(BackendErr::unimplemented(ast, "print of a Float value")); } - let fmt = format!("{}\0", "%lld\n").into_bytes().into_boxed_slice(); - let data_id = self - .module - .declare_anonymous_data(false, false) - .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; - let mut desc = DataDescription::new(); - desc.define(fmt); - self.module - .define_data(data_id, &desc) - .map_err(|e| BackendErr::new(ast.pos, &e.to_string()))?; - - let gv = self.module.declare_data_in_func(data_id, self.builder.func); - let pointer_type = self.module.isa().pointer_type(); - let ptr = self.builder.ins().global_value(pointer_type, gv); - // Widen a narrower-than-i64 value (e.g. Bool, stored as i8) to match `%lld`. - let value = if self.builder.func.dfg.value_type(value) != types::I64 { - self.builder.ins().sextend(types::I64, value) - } else { - value - }; - let callee = self - .module - .declare_func_in_func(self.printf_id, self.builder.func); - self.builder.ins().call(callee, &[ptr, value]); + self.lower_print_int(value)?; Ok(None) } } } + + /// Format an `Int`/`Bool` value (`value`, an `I64` or `I8`) to a NUL-terminated decimal ASCII string on the stack then print it via `puts`. + /// See [`Self::lower_print`]'s doc comment for why this hand-rolled formatting exists instead of a `printf` call. + /// + /// Classic `itoa: write digits` into a stack buffer back-to-front (least-significant first, at the end, working backward). + /// There, no separate reversal pass is needed. + /// Then `puts` the resulting suffix of the buffer. + /// `value`'s sign is handled by working with its absolute value throughout and writing a leading `-` afterwards if negative. + /// + /// The buffer is deliberately generous. + /// 24 bytes: at most 19 digits for any `i64` magnitude, one sign byte, one NUL, with room to spare so indices never need bounds-checking. + /// + /// `i64::MIN`'s magnitude doesn't fit in a positive `i64` -- naively negating it would overflow. + /// `ineg`'s raw two's-complement bit pattern is exactly the right *unsigned* magnitude even in that case. + /// E.g. `i64::MIN`'s bit pattern negates right back to itself, which reinterpreted as `u64` is precisely `-i64::MIN`. + /// So as long as everything from there on (`udiv_imm`/`urem_imm`) treats the value as unsigned rather than signed the `i64::MIN` case falls out correctly with no special-casing. + fn lower_print_int(&mut self, value: Value) -> BackendResult<()> { + let value = if self.builder.func.dfg.value_type(value) != types::I64 { + self.builder.ins().sextend(types::I64, value) + } else { + value + }; + + const BUF_SIZE: u32 = 24; + let slot = self.builder.create_sized_stack_slot(StackSlotData::new( + StackSlotKind::ExplicitSlot, + BUF_SIZE, + 0, + )); + let pointer_type = self.module.isa().pointer_type(); + let base = self.builder.ins().stack_addr(pointer_type, slot, 0); + + let last = i64::from(BUF_SIZE) - 1; + let zero_byte = self.builder.ins().iconst(types::I8, 0); + let nul_addr = self.builder.ins().iadd_imm(base, last); + self.builder + .ins() + .store(MemFlags::new(), zero_byte, nul_addr, 0); + + let is_neg = self.builder.ins().icmp_imm(IntCC::SignedLessThan, value, 0); + let negated = self.builder.ins().ineg(value); + let magnitude = self.builder.ins().select(is_neg, negated, value); + + let idx_var = self.new_var(types::I64); + let first_digit_idx = self.builder.ins().iconst(types::I64, last - 1); + self.builder.def_var(idx_var, first_digit_idx); + let mag_var = self.new_var(types::I64); + self.builder.def_var(mag_var, magnitude); + + let loop_block = self.builder.create_block(); + let exit_block = self.builder.create_block(); + self.builder.ins().jump(loop_block, &[]); + + self.builder.switch_to_block(loop_block); + let mag = self.builder.use_var(mag_var); + let digit = self.builder.ins().urem_imm(mag, 10); + let quotient = self.builder.ins().udiv_imm(mag, 10); + let digit_char = self.builder.ins().iadd_imm(digit, i64::from(b'0')); + let digit_byte = self.builder.ins().ireduce(types::I8, digit_char); + let idx = self.builder.use_var(idx_var); + let digit_addr = self.builder.ins().iadd(base, idx); + self.builder + .ins() + .store(MemFlags::new(), digit_byte, digit_addr, 0); + + let next_idx = self.builder.ins().iadd_imm(idx, -1); + self.builder.def_var(idx_var, next_idx); + self.builder.def_var(mag_var, quotient); + + let more_digits = self.builder.ins().icmp_imm(IntCC::NotEqual, quotient, 0); + self.builder + .ins() + .brif(more_digits, loop_block, &[], exit_block, &[]); + + self.builder.switch_to_block(exit_block); + // `idx_var` now holds one index before the first (most significant) digit written -- + // exactly where a '-' belongs, or where the digits themselves start if there isn't one. + let sign_idx = self.builder.use_var(idx_var); + let unsigned_start_idx = self.builder.ins().iadd_imm(sign_idx, 1); + let minus_byte = self.builder.ins().iconst(types::I8, i64::from(b'-')); + let sign_addr = self.builder.ins().iadd(base, sign_idx); + self.builder + .ins() + .store(MemFlags::new(), minus_byte, sign_addr, 0); + let start_idx = self + .builder + .ins() + .select(is_neg, sign_idx, unsigned_start_idx); + + let str_ptr = self.builder.ins().iadd(base, start_idx); + let callee = self + .module + .declare_func_in_func(self.puts_id, self.builder.func); + self.builder.ins().call(callee, &[str_ptr]); + Ok(()) + } } diff --git a/src/backend/cranelift/convert/definition.rs b/src/backend/cranelift/convert/definition.rs index 624592f3c..911b85c2f 100644 --- a/src/backend/cranelift/convert/definition.rs +++ b/src/backend/cranelift/convert/definition.rs @@ -57,13 +57,22 @@ pub(super) fn arg_type(arg: &ASTTy) -> BackendResult BackendResult<(FuncId, FuncId)> { +) -> BackendResult { let pointer_type = module.isa().pointer_type(); let puts_sig = Signature { @@ -71,20 +80,9 @@ pub(super) fn declare_libc( returns: vec![AbiParam::new(types::I32)], call_conv, }; - let puts_id = module + module .declare_function("puts", Linkage::Import, &puts_sig) - .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string()))?; - - let printf_sig = Signature { - params: vec![AbiParam::new(pointer_type), AbiParam::new(types::I64)], - returns: vec![AbiParam::new(types::I32)], - call_conv, - }; - let printf_id = module - .declare_function("printf", Linkage::Import, &printf_sig) - .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string()))?; - - Ok((puts_id, printf_id)) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string())) } /// Define a top-level `FunDef`'s body as a real Cranelift function. @@ -111,7 +109,7 @@ pub(super) fn define_function( builder.append_block_params_for_function_params(entry); builder.switch_to_block(entry); - let (puts_id, printf_id) = declare_libc(module, builder.func.signature.call_conv)?; + let puts_id = declare_libc(module, builder.func.signature.call_conv)?; let mut lower = FnLower { builder, module, @@ -119,7 +117,6 @@ pub(super) fn define_function( var_seq: 0, funcs, puts_id, - printf_id, }; let block_params = lower.builder.block_params(entry).to_vec(); @@ -165,7 +162,7 @@ pub(super) fn define_main( let entry = builder.create_block(); builder.switch_to_block(entry); - let (puts_id, printf_id) = declare_libc(module, builder.func.signature.call_conv)?; + let puts_id = declare_libc(module, builder.func.signature.call_conv)?; let mut lower = FnLower { builder, module, @@ -173,7 +170,6 @@ pub(super) fn define_main( var_seq: 0, funcs, puts_id, - printf_id, }; for statement in statements { diff --git a/src/backend/cranelift/convert/mod.rs b/src/backend/cranelift/convert/mod.rs index a0478ba29..707038e04 100644 --- a/src/backend/cranelift/convert/mod.rs +++ b/src/backend/cranelift/convert/mod.rs @@ -114,7 +114,6 @@ struct FnLower<'a> { var_seq: u32, funcs: &'a Funcs, puts_id: FuncId, - printf_id: FuncId, } impl<'a> FnLower<'a> { @@ -179,18 +178,14 @@ impl<'a> FnLower<'a> { /// Lower `ast` as a value-producing expression. fn lower_expr(&mut self, ast: &ASTTy) -> BackendResult { match &ast.node { - // Int/Bool/Real literals' own resolved `ty` can come back widened to a union (e.g. a - // literal argument to `print`, whose parameter accepts several printable types - // unifies to that broader union rather than staying just `Int`) -- but the node - // variant itself already tells us the literal's true type, so there's no need to - // consult `ast.ty` at all here. An Int-shaped literal that Mamba's numeric-literal - // adaptation means is really meant as a `Float` (e.g. `def x: Float := 2`, or `x > - // 0.0`) is *not* handled here -- it's handled contextually, by whichever caller - // ends up comparing this value's actual Cranelift type against a sibling value's - // (see `operation.rs`'s docs) -- since `ast.ty` turns out to be an unreliable signal - // for this even when it looks unambiguous (it can resolve to `Float` from unifying - // against an operator's own polymorphic parameter type, even when the concrete - // value everything else around it expects is `Int`). + // Int/Bool/Real literals' own resolved `ty` can come back widened to a union. + // E.g. a literal argument to `print`, whose parameter accepts several printable types unifies to that broader union rather than staying just `Int`). + // However, the node variant itself already tells us the literal's true type, so there's no need to consult `ast.ty` at all here. + // An Int-shaped literal that Mamba's numeric-literal daptation means is really meant as a `Float` (e.g. `def x: Float := 2`, or `x > 0.0`) is *not* handled here. + // Instead, it's handled contextually, by whichever caller ends up comparing this value's actual Cranelift type against a sibling value's (see `operation.rs`'s docs). + // Since `ast.ty` turns out to be an unreliable signal for this even when it looks unambiguous, + // it can resolve to `Float` from unifying against an operator's own polymorphic parameter type, + // even when the concrete value everything else around it expects is `Int`. NodeTy::Int { lit } => { let value: i64 = lit.parse().map_err(|_| { BackendErr::new(ast.pos, &format!("Invalid int literal '{lit}'")) From 9696562defe2fe53ea643c4eb72244bce550fb54 Mon Sep 17 00:00:00 2001 From: Joel Abrahams Date: Wed, 26 Aug 2026 13:48:32 +0200 Subject: [PATCH 16/16] doc: some explanation of compilation in README --- README.md | 24 +++++++++++++++++--- src/backend/cranelift/convert/definition.rs | 25 ++++++++++++--------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 95e79acfd..05d1b6419 100644 --- a/README.md +++ b/README.md @@ -750,6 +750,24 @@ end recover do end ``` +## 💽 Machine Output + +There is an experimental feature where we output a very small subset of the language to machine code. +This is more of a 'fun' feature meant to explore a bit how compiler backends work to an extent. +We mostly limit this to simple arithmetic for now. + +To use, us either the `--bin` flag to produce a binary, or `--asm` to print AT&T style assembly to stdout. +We aim to make sure that: + +1. The output is identical to running and checking the output of the resulting Python (see `./tests/execution.rs`). +2. That compilation works as is identical on the latest Windows, Linux, and Mac OS. + We verify this by making use of GitHub agents which run the test suite on each, see `./github/workflows/test.yml`. + +In general, we aim to stay within the Rust ecosystem as much as possible. +We prefer writing our own boilerplate, or depending on rust crates, over depending on native C. +The reasoning is that we want to reduce external dependencies, and more importantly, that this arguably improves the educational value this crate provides (for the author). +Having to (re)-implement difficult compilation problems which have been solved in the past (and there are _many_, including edge cases) increases our exposure to them. + ## 💻 The Command Line Interface ``` @@ -759,9 +777,9 @@ Usage: mamba [OPTIONS] Options: -i, --input Input file or directory. If file, file taken as input. If directory, recursively search all sub-directories for *.mamba files. If no input given, current directory used as input directory - -o, --output Output location. With `--python` (the default): output directory to store Python files, structured to reflect the input directory; if not given, a 'target' directory is created in the current directory. With `--bin`: path of the linked executable to produce; if not given, 'a.out' is created in the current directory. Ignored with`--asm`, which always prints to stdout instead of writing a file + -o, --output Output location. With `--python` (the default): output directory to store Python files, structured to reflect the input directory; if not given, a 'target' directory is created in the current directory. With `--bin`: path of the linked executable to produce; if not given, 'a.out' is created in the current directory. Ignored with `--asm`, which always prints to stdout instead of writing a file --python Output Python source (the default) - --bin Compile and link a native executable via the Cranelift backend, instead of outputting Python source. Only a small subset of the language is currently supported: literals, arithmetic and comparison operators, if/else, top-level function definitions and calls, and `print` + --bin Compile and link a native executable via the Cranelift backend, instead of outputting Python source. Only a small subset of the language is currently supported:literals, arithmetic and comparison operators, if/else, top-level function definitions and calls, and `print` --asm Compile via the Cranelift backend and print the resulting disassembly to stdout, instead of outputting Python source or linking an executable. No file is written -- pipe stdout (e.g. `> out.s`) if you want to save it. Same language subset as `--bin` (see its help). Printed in AT&T syntax (`movq %rsp, %rbp`, source before destination) -- Cranelift's own disassembler doesn't support switching to Intel syntax --target Target triple to pass to Cranelift, e.g. `x86_64-unknown-linux-gnu` (only meaningful with `--bin`/`--asm`; defaults to the host triple) -v... Set level of verbosity: - `-v` : info, error, warning printed to stderr (default) - `-vv` : debug messages are printed - `-vvv` : trace messages are printed @@ -770,7 +788,7 @@ Options: --no-color Disable colorized output -l, --level Print log level -a, --annotate Enable type annotation of the output source. Currently still buggy feature - -h, --help Print help + -h, --help Print help (see more with '--help') ``` You can type `mamba -help` for a message containing roughly the above information. diff --git a/src/backend/cranelift/convert/definition.rs b/src/backend/cranelift/convert/definition.rs index 911b85c2f..018ce5ea1 100644 --- a/src/backend/cranelift/convert/definition.rs +++ b/src/backend/cranelift/convert/definition.rs @@ -57,18 +57,21 @@ pub(super) fn arg_type(arg: &ASTTy) -> BackendResult