diff --git a/Cargo.lock b/Cargo.lock index c48dbc2fb..7f87132dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,18 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +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" @@ -61,6 +73,18 @@ 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 = "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" @@ -96,6 +120,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "bytecount" version = "0.3.2" @@ -154,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" @@ -166,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" @@ -186,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" @@ -204,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" @@ -225,6 +444,16 @@ dependencies = [ "libc", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -282,13 +511,19 @@ dependencies = [ "ansi_term", "assert_cmd", "clap", + "cranelift-codegen", + "cranelift-frontend", + "cranelift-module", + "cranelift-native", + "cranelift-object", "glob", "itertools", "log", "loggerv", "pathdiff", - "permutate", "python-parser", + "target-lexicon", + "tempfile", "test-case", "tests_util", ] @@ -306,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]] @@ -349,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" @@ -367,12 +614,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 +679,25 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "regalloc2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" version = "1.0.7" @@ -489,6 +749,24 @@ dependencies = [ "serde", ] +[[package]] +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 = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -506,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" @@ -562,6 +846,7 @@ dependencies = [ name = "tests_util" version = "0.0.0" dependencies = [ + "assert_cmd", "itertools", "mamba", "python-parser", @@ -598,6 +883,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" @@ -789,3 +1080,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 f250973dd..84e7bab50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,18 +13,23 @@ keywords = ["python", "transpiler", "compiler"] categories = ["parsing"] [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 +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 +cranelift-object = "0.114.0" # Emit a native object file from compiled Cranelift IR +glob = "0.3.0" # Make traversing directories (Mamba projects) easier +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] -test-case = "3.3.1" -# dependencies: test -> test util -> mamba +assert_cmd = "0.10" # Assert commands work +test-case = "3.3.1" # Parameterize tests tests_util = { path = "./tests_util" } diff --git a/README.md b/README.md index d828975c6..05d1b6419 100644 --- a/README.md +++ b/README.md @@ -750,34 +750,45 @@ 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 ``` -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, 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. 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` + --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 + --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 (see more with '--help') ``` You can type `mamba -help` for a message containing roughly the above information. 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/src/backend/cranelift/README.md b/src/backend/cranelift/README.md new file mode 100644 index 000000000..5c8b5a5cd --- /dev/null +++ b/src/backend/cranelift/README.md @@ -0,0 +1,66 @@ +

+ 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). + +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: + +- `Int`, `Bool`, `Float` primitives -- no collections, strings (beyond a `print` argument), classes, or traits. +- 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. +- 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` (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. +- `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`) 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 new file mode 100644 index 000000000..1f95167f5 --- /dev/null +++ b/src/backend/cranelift/convert/call.rs @@ -0,0 +1,200 @@ +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; +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, 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 => { + 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); + 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 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: 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, + _ => 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)?; + if self.builder.func.dfg.value_type(value) == types::F64 { + return Err(BackendErr::unimplemented(ast, "print of a Float 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/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..841da3e09 --- /dev/null +++ b/src/backend/cranelift/convert/control_flow.rs @@ -0,0 +1,181 @@ +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::primitive::cranelift_type; +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`). + 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_scoped_stmt(then)?; + self.builder.ins().jump(merge_block, &[]); + + if let Some(el) = el { + self.builder.switch_to_block(else_block); + self.lower_scoped_stmt(el)?; + self.builder.ins().jump(merge_block, &[]); + } + + self.builder.switch_to_block(merge_block); + 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 and increments before jumping back to + /// `header`. + /// + /// 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, + 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 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); + 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( + &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..018ce5ea1 --- /dev/null +++ b/src/backend/cranelift/convert/definition.rs @@ -0,0 +1,283 @@ +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; +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( + 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 one external `libc` function `print` needs. +/// Declared lazily and re-declared per function via `Module::declare_function`. +/// That is idempotent, meaning that it merges with the existing declaration of the same name, rather than threading a single shared declaration through. +/// +/// Deliberately just `puts` (a single fixed pointer argument). +/// An `Int`/`Bool` value is formatted to a decimal string ourselves, in Cranelift IR, rather than going through `printf`. +/// `printf` is variadic, and Cranelift has no notion of a variadic call at all. +/// On standard SysV (Linux/Windows) that happened to not matter for plain integers (the first 6 integer args are register-passed either way). +/// +/// However, Apple's aarch64 ABI has no register-based `va_list` at all: +/// *every* variadic argument must be stack-passed, which a call built from an ordinary fixed-arg `Signature` never does. +/// Therefore, the callee's `va_arg` reads garbage. +/// Doing our own formatting sidesteps this whole class of problem on every target. +/// +/// See also `lower_print`'s doc comment. +pub(super) fn declare_libc( + module: &mut ObjectModule, + call_conv: CallConv, +) -> BackendResult { + 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, + }; + module + .declare_function("puts", Linkage::Import, &puts_sig) + .map_err(|e| BackendErr::new(Position::invisible(), &e.to_string())) +} + +/// 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, + sig: Signature, + args: &[ASTTy], + body: Option<&ASTTy>, + funcs: &Funcs, + 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(); + + { + 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 = declare_libc(module, builder.func.signature.call_conv)?; + let mut lower = FnLower { + builder, + module, + vars: HashMap::new(), + var_seq: 0, + funcs, + puts_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()))?; + 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, + 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(); + + { + let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx); + let entry = builder.create_block(); + builder.switch_to_block(entry); + + let puts_id = declare_libc(module, builder.func.signature.call_conv)?; + let mut lower = FnLower { + builder, + module, + vars: HashMap::new(), + var_seq: 0, + funcs, + puts_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()))?; + 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> { + /// 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)?; + // `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)); + Ok(()) + } + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} variable definition"), + )), + } + } + + /// 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)?, + _ => 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..707038e04 --- /dev/null +++ b/src/backend/cranelift/convert/mod.rs @@ -0,0 +1,227 @@ +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. +/// +/// 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, + want_asm: bool, +) -> 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![]; + let mut asm = 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)?; + 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()), + } + } + + 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()))?; + 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(asm) +} + +/// 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, +} + +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::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)?; + } + Ok(()) + } + NodeTy::FunctionCall { name, .. } if name.name == PRINT => { + self.lower_print(ast).map(|_| ()) + } + NodeTy::FunctionCall { .. } => self.lower_call_stmt(ast), + 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/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}'")) + })?; + 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}'")) + })?; + 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 { .. } + | 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 new file mode 100644 index 000000000..415e87763 --- /dev/null +++ b/src/backend/cranelift/convert/operation.rs @@ -0,0 +1,154 @@ +use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; +use cranelift_codegen::ir::{types, InstBuilder, Value}; +use cranelift_frontend::FunctionBuilder; + +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 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( + 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_div(left, right), + NodeTy::Le { left, right } => { + self.lower_cmp(left, right, IntCC::SignedLessThan, FloatCC::LessThan) + } + 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::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::AddU { expr } => self.lower_expr(expr), + NodeTy::SubU { expr } => self.lower_negate(expr), + other => Err(BackendErr::unimplemented( + ast, + &format!("{other:?} operation"), + )), + } + } + + /// 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, + right: &ASTTy, + int_op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, + float_op: impl Fn(&mut FunctionBuilder, Value, Value) -> Value, + ) -> BackendResult { + let l = self.lower_expr(left)?; + let r = self.lower_expr(right)?; + 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(self.builder.ins().ineg(value)) + } + } + + /// 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, + right: &ASTTy, + int_cc: IntCC, + float_cc: FloatCC, + ) -> BackendResult { + let l = self.lower_expr(left)?; + let r = self.lower_expr(right)?; + 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/src/backend/cranelift/link.rs b/src/backend/cranelift/link.rs new file mode 100644 index 000000000..323cf385d --- /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/mod.rs b/src/backend/cranelift/mod.rs new file mode 100644 index 000000000..361dc559e --- /dev/null +++ b/src/backend/cranelift/mod.rs @@ -0,0 +1,235 @@ +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, Configurable}; +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::common::result::WithSource; +use crate::{check_sources, strip_source_paths, Context}; + +mod convert; +mod link; +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`. +/// `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(TARGET)); + 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, 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!("Compiled {} files to object code", objects.len()); + 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`). +/// If `None`, the host triple is used. +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); + + convert::lower_program(ast_ty, ctx, &mut module, false)?; + + module + .finish() + .emit() + .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. +/// +/// 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 { + 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. +/// +/// 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 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 { + 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/primitive.rs b/src/backend/cranelift/primitive.rs new file mode 100644 index 000000000..bc02fa73e --- /dev/null +++ b/src/backend/cranelift/primitive.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 `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 { + 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/backend/cranelift/result.rs b/src/backend/cranelift/result.rs new file mode 100644 index 000000000..dd8ff08a7 --- /dev/null +++ b/src/backend/cranelift/result.rs @@ -0,0 +1,68 @@ +use std::fmt::{self, 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/mod.rs b/src/backend/mod.rs new file mode 100644 index 000000000..91bdc6468 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,27 @@ +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 }, + /// 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. + /// + /// 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/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..0818bebe3 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,130 @@ 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"), + PythonCore::Del { name } => format!("del {name}"), - Core::ClassDef { + PythonCore::ClassDef { name, parent_names, body, @@ -421,18 +426,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 +448,7 @@ fn to_py(core: &Core, ind: usize) -> String { newline_if_body(expr, ind) ), - Core::TryExcept { + PythonCore::TryExcept { setup, attempt, except, @@ -457,18 +462,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 +481,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 +496,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 55% rename from src/generate/ast/node.rs rename to src/backend/python/ast/node.rs index 503511a90..bbf27588b 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,186 @@ 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, + }, + /// 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, 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 51% rename from src/generate/convert/control_flow.rs rename to src/backend/python/convert/control_flow.rs index 6e5e4688c..dc5b3ddc6 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::{CoreOp, 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,23 +31,31 @@ pub fn convert_cntrl_flow( .remove_ret(true) .must_assign_to(None, None); - Core::Ternary { + // 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 { - Core::IfElse { + 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 => Core::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,32 +73,54 @@ 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 { + let body_core = convert_node(body.as_ref(), imp, state, ctx)?; + cases.push(PythonCore::Case { expr: Box::from(convert_node( expr.as_ref(), imp, &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)), }) } } } - Core::Match { expr, cases } + PythonCore::Match { expr, cases } } - NodeTy::While { cond, body } => Core::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 { - 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::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 => { let msg = format!("Expected control flow but was: {other:?}."); return Err(Box::from(UnimplementedErr::new(ast, &msg))); @@ -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) } @@ -123,9 +276,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 +313,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 +349,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 +385,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") } @@ -264,26 +417,33 @@ 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(Core::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:?}"), }; 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 +465,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 +479,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 +513,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 +527,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 +569,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 +583,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 67% rename from src/generate/convert/mod.rs rename to src/backend/python/convert/mod.rs index e54ff2032..f60d5293b 100644 --- a/src/generate/convert/mod.rs +++ b/src/backend/python/convert/mod.rs @@ -1,20 +1,22 @@ 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, 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; +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 +43,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 +56,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 +76,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 +111,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 +127,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 +143,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 +224,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 +253,33 @@ pub fn convert_node(ast: &ASTTy, imp: &mut Imports, state: &State, ctx: &Context resource, alias: Some((alias, ..)), expr, - } => Core::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 { - 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)?, - 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 +297,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 +342,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 +360,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::{CoreOp, 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 +449,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 +474,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 +487,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 +507,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 +533,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 +560,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 +621,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 +637,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 +700,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 +733,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 +761,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 +825,7 @@ mod tests { expr }); - let Ok(Core::WithAs { + let Ok(PythonCore::WithAs { resource, alias, expr, @@ -816,21 +836,77 @@ 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") } ); + // `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, - Core::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") + }), + }, + ] ); } @@ -849,19 +925,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 +954,7 @@ mod tests { }); let (setup, _try, except) = match gen(&ASTTy::from(&handle)) { - Ok(Core::TryExcept { + Ok(PythonCore::TryExcept { setup, attempt, except, @@ -889,7 +965,7 @@ mod tests { assert_eq!(setup, None); assert_eq!( *_try, - Core::Id { + PythonCore::Id { lit: String::from("my_fun") } ); @@ -922,7 +998,7 @@ mod tests { cases: vec![case] }); - let Ok(Core::TryExcept { + let Ok(PythonCore::TryExcept { setup, attempt, except, @@ -937,31 +1013,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/backend/python/mod.rs b/src/backend/python/mod.rs new file mode 100644 index 000000000..ef4dd7f33 --- /dev/null +++ b/src/backend/python/mod.rs @@ -0,0 +1,166 @@ +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::common::result::WithSource; +use crate::{check_sources, io, strip_source_paths, Context, PipelineArguments}; + +mod convert; + +pub mod ast; +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, +} + +impl From<&PipelineArguments> for GenArguments { + fn from(pipeline_args: &PipelineArguments) -> Self { + GenArguments { + annotate: pipeline_args.annotate, + } + } +} + +/// 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::parse::ast::AST) must be +/// correctly formed. Therefore, malformed +/// [AST](mamba::parse::ast::AST)'s should be caught by either +/// the parser or the type checker. +/// +/// # Examples +/// +/// ``` +/// # use mamba::check::ast::ASTTy; +/// # use mamba::parse::ast::Node; +/// # use mamba::parse::ast::AST; +/// # use mamba::backend::python::ast::node::PythonCore; +/// # use mamba::common::position::{CaretPos, Position}; +/// # 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, PythonCore::Return { expr: Box::from(PythonCore::None) }); +/// ``` +/// +/// # Failures +/// +/// Fails if converting a construct which has not been implemented yet. +/// +/// ```rust +/// # use mamba::check::ast::ASTTy; +/// # use mamba::parse::ast::Node; +/// # use mamba::parse::ast::AST; +/// # use mamba::backend::python::ast::node::PythonCore; +/// # use mamba::common::position::{CaretPos, Position}; +/// # 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 }; +/// let ast = AST::new(Position::new(CaretPos::new(0, 0), CaretPos::new(0, 5)), node); +/// let ast_ty = ASTTy::from(&ast); +/// let core_result = gen(&ast_ty); +/// +/// assert!(core_result.is_err()); +/// ``` +/// +/// # Panics +/// +/// A malformed [AST](crate::parser::ast::AST) causes this stage +/// to panic. +pub fn gen_arguments(ast_ty: &ASTTy, gen_args: &GenArguments, ctx: &Context) -> GenResult { + let state = State::from(gen_args); + + let import = &mut Imports::new(); + match convert_node(ast_ty, import, &state, ctx)? { + PythonCore::Block { statements } => Ok(PythonCore::Block { + statements: import.imports().into_iter().chain(statements).collect(), + }), + other if !import.is_empty() => Ok(PythonCore::Block { + statements: import.imports().into_iter().chain(vec![other]).collect(), + }), + other => Ok(other), + } +} + +pub fn gen(ast_ty: &ASTTy) -> GenResult { + gen_arguments(ast_ty, &GenArguments::default(), &Context::default()) +} 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/cli.rs b/src/cli.rs index cc367c35e..53ad3491a 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, 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." + about = "Transpile Mamba to Python code, compile it to a native binary, or print its assembly." )] pub struct Cli { /// Input file or directory. @@ -15,12 +15,45 @@ 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. + /// 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_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_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. + /// + /// 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, + + /// Target triple to pass to Cranelift, e.g. `x86_64-unknown-linux-gnu` (only meaningful with + /// `--bin`/`--asm`; defaults to the host triple). + #[arg(long = "target", value_name = "TARGET")] + pub target: Option, + /// Set level of verbosity: /// - `-v` : info, error, warning printed to stderr (default) /// - `-vv` : debug messages are printed diff --git a/src/generate/mod.rs b/src/generate/mod.rs deleted file mode 100644 index 1d2ff36dc..000000000 --- a/src/generate/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -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; - -pub mod ast; -pub mod name; - -pub mod result; - -#[derive(Default)] -pub struct GenArguments { - pub annotate: bool, -} - -impl From<&PipelineArguments> for GenArguments { - fn from(pipeline_args: &PipelineArguments) -> Self { - GenArguments { - annotate: pipeline_args.annotate, - } - } -} - -/// Consumes the given [AST](mamba::parser::ast::AST) and produces -/// a [Core](mamba::generate.ast::construct::Core) node. -/// -/// Note that the given [AST](mamba::parser::ast::AST) must be -/// correctly formed. Therefore, malformed -/// [AST](mamba::parser::ast::AST)'s should be caught by either -/// the parser or the type checker. -/// -/// # Examples -/// -/// ``` -/// # use mamba::check::ast::ASTTy; -/// # use mamba::parse::ast::Node; -/// # use mamba::parse::ast::AST; -/// # use mamba::generate::ast::node::Core; -/// # use mamba::common::position::{CaretPos, Position}; -/// # use mamba::generate::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) }); -/// ``` -/// -/// # Failures -/// -/// Fails if converting a construct which has not been implemented yet. -/// -/// ```rust -/// # use mamba::check::ast::ASTTy; -/// # use mamba::parse::ast::Node; -/// # use mamba::parse::ast::AST; -/// # use mamba::generate::ast::node::Core; -/// # use mamba::common::position::{CaretPos, Position}; -/// # use mamba::generate::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 }; -/// let ast = AST::new(Position::new(CaretPos::new(0, 0), CaretPos::new(0, 5)), node); -/// let ast_ty = ASTTy::from(&ast); -/// let core_result = gen(&ast_ty); -/// -/// assert!(core_result.is_err()); -/// ``` -/// -/// # Panics -/// -/// A malformed [AST](crate::parser::ast::AST) causes this stage -/// to panic. -pub fn gen_arguments(ast_ty: &ASTTy, gen_args: &GenArguments, ctx: &Context) -> GenResult { - let state = State::from(gen_args); - - let import = &mut Imports::new(); - match convert_node(ast_ty, import, &state, ctx)? { - Core::Block { statements } => Ok(Core::Block { - statements: import.imports().into_iter().chain(statements).collect(), - }), - other if !import.is_empty() => Ok(Core::Block { - statements: import.imports().into_iter().chain(vec![other]).collect(), - }), - other => Ok(other), - } -} - -pub fn gen(ast_ty: &ASTTy) -> GenResult { - gen_arguments(ast_ty, &GenArguments::default(), &Context::default()) -} diff --git a/src/lib.rs b/src/lib.rs index 70e7e7ed7..e7975fd2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,32 +1,31 @@ use std::convert::TryFrom; -use std::fs::create_dir; use std::path::{Path, PathBuf}; use log::{info, trace}; +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; pub mod io; -const TARGET: &str = "target"; const SOURCE: &str = "src"; #[derive(Default)] pub struct Arguments { pub annotate: bool, + pub backend: Backend, } /// Convert `*.mamba` files to `*.py`. @@ -62,15 +61,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)) @@ -78,19 +70,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![]; @@ -104,15 +88,31 @@ 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)?; - - 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])?; + match &arguments.backend { + 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(), + ), + Backend::Asm { target: triple } => { + backend::cranelift::print_asm( + source_option_pairs.as_slice(), + &src_path, + triple.as_deref(), + )?; + Ok(dir.to_path_buf()) + } } - - Ok(out_dir) } pub struct PipelineArguments { @@ -127,16 +127,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. +pub(crate) 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| { @@ -144,11 +139,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. +pub(crate) fn check_sources( + source: &[(String, Option)], +) -> Result<(Context, Vec), Vec> { let (asts, parse_errs): (Vec<_>, Vec<_>) = source .iter() .map(|(src, path)| { @@ -169,7 +170,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() @@ -193,25 +194,5 @@ pub fn mamba_to_python( .collect::>(); trace!("Checked {} files", typed_ast.len()); - - 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) + Ok((ctx, typed_ast)) } diff --git a/src/main.rs b/src/main.rs index a407c143f..45e5825fc 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,21 @@ pub fn main() -> Result<(), String> { .init() .unwrap(); + let backend = if cli_input.bin { + Backend::Bin { + target: cli_input.target.clone(), + } + } else if cli_input.asm { + Backend::Asm { + 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..cd935c2c7 --- /dev/null +++ b/tests/execution.rs @@ -0,0 +1,118 @@ +//! 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::error::Error; +use std::path::Path; + +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. +/// 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() +} + +/// 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, 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")] +#[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(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}" + ); +} + +#[test] +fn asm_prints_disassembly_to_stdout() -> Result<(), Box> { + let src_dir = resource_path(true, &["function"], ""); + + 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}" + ); + + // 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(()) +} 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/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/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/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/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) 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) 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_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/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/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/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/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 99ef9eab3..d8f9775ad 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,130 @@ 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 +/// 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) + .output() + .map_err(|e| format!("Could not run '{PYTHON} {}': {e}", path.display()))?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).replace("\r\n", "\n")) + } else { + Err(format!( + "'{PYTHON} {}' exited with an error:\n{}", + path.display(), + String::from_utf8_lossy(&output.stderr) + )) + } +} + +/// 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. +/// +/// The executable is placed a directory level below the temp dir (rather than directly in it) so +/// that this also exercises `write_output`'s own `create_dir` of that not-yet-existing parent, +/// the way a real `-o some/nested/path` invocation would. +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("nested").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)?.replace("\r\n", "\n")) +} + +/// Compile `file` (relative to `subdirs`, under `tests/resource/valid`) to disassembly text via +/// the Cranelift backend, targeting `triple` (`None` for the host), discarding the printed +/// output but propagating any error. +/// +/// Unlike [run_cli]'s black-box invocation of the `mamba` binary, this drives `transpile_dir` (and +/// so `backend::cranelift::print_asm`/`disassemble`/`build_isa`) in-process -- needed for these to +/// show up in coverage at all, since a separately-built subprocess isn't instrumented. +pub fn run_via_asm( + subdirs: &[&str], + file: &str, + triple: Option<&str>, +) -> 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> { + 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; @@ -37,7 +164,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) }