Native Backend

Choose between the Rust and Cranelift backends when compiling to native.

When compiling Tish to a native binary (tish build --target native), you can select the native backend with --native-backend.

Backends

BackendFlagNative importsWhat it actually produces
rust--native-backend rust (default)✅ SupportedRust source (via tish_compile) linked against tishlang_runtime — cargo builds the final binary
cranelift--native-backend cranelift❌ Not supportedSerialized bytecode embedded in the binary; executable runs tishlang_vm at startup (not CLIF lowering of opcodes)
llvm--native-backend llvm❌ Not supportedSame embedded-bytecode + tishlang_vm pattern as cranelift, linked via clang instead of the Cranelift object builder

When to use each

Rust backend (default)

The Rust backend transpiles your Tish program to Rust source calling tishlang_runtime — dynamic operations on Value — then invokes cargo build --release. This gives:

  • Full native-module support (import { Egui } from 'tish:egui', import { x } from 'cargo:my_crate' with tish.rustDependencies, @scope/pkg)
  • Access to the full Rust ecosystem
  • Destructuring in function parameters
tish build main.tish -o app
# or explicitly:
tish build main.tish -o app --native-backend rust

Primitive lowering (in progress): Where types are annotated or can be inferred, the Rust backend emits native Rust primitives (f64, Vec<f64>, direct arithmetic) instead of Value. See Type Annotations for the current status.

Native typing flags (opt-in)

The deeper typed-native optimizations are dark-shipped behind environment variables — they are off by default, so the default build stays byte-identical, and you opt in per build. They apply only to --native-backend rust.

FlagWhat it does
TISH_PARAM_NATIVE=1Annotated scalar params (a: number/boolean/string) get a native shadow (f64/bool/String) so the body lowers without boxing.
TISH_PARAM_INFER=1Unannotated params used purely numerically are inferred number (conservative, sound).
TISH_NATIVE_FN=1Numeric-only top-level functions are emitted as native fn(f64, …) -> f64; direct calls bypass the boxed call ABI.
TISH_STRUCT_INFER=1Block-local let o = {…} / let xs = […] are inferred to a native struct / T[] when every later use is safe.
TISH_NATIVE_HOF=1reduce/map/filter/some/every over a native number[] lower to a direct Rust iterator chain.
TISH_AGGREGATE_INFER=1Interprocedural monomorphic struct inference: when a whole-program candidacy predicate holds (a monomorphic all-f64 object shape that never hits ===/escape/reshape), an array-of-objects is unboxed into a native Vec<Struct> threaded by reference and its factory/operator functions are de-virtualized into typed Rust free functions — removing the boxed Value + RefCell + property-map hot path. All-or-nothing per group: any unlowerable use disables it and falls back to the byte-identical boxed path.
# Example: build with the full typed-native flag set
TISH_PARAM_NATIVE=1 TISH_PARAM_INFER=1 TISH_NATIVE_FN=1 \
TISH_STRUCT_INFER=1 TISH_NATIVE_HOF=1 TISH_AGGREGATE_INFER=1 \
  tish build main.tish -o app --native-backend rust

These flags are correctness-preserving (each bails to the boxed path rather than miscompile), but they are still being hardened — treat them as opt-in performance experiments.

Value-representation flags (opt-in)

Distinct from the native-typing flags above, this one changes the in-memory Value representation on every backend (interpreter, bytecode VM, and native), not just the Rust codegen.

FlagWhat it does
TISH_PACKED_ARRAYS=1Store all-numeric arrays as an unboxed Vec<f64> (NumberArray) instead of a vector of individually-boxed values. A non-numeric store deopts the backing in place (Packed → Boxed), so every alias of the array stays sound.

It is sound — a full parity sweep runs the entire test corpus flag-on vs flag-off across interp / vm / native, plus GAUNTLET checksums against Node — but it is kept off by default on purpose, because it is a performance wash. The VM JIT does not consume NumberArray, so packed arrays never reach the tier that actually speeds numeric loops; flag-on and flag-off measure within ~1% of each other on large numeric workloads. The representation exists so a future JIT path can turn it into a real win — until then there is no reason to enable it.

Cranelift backend

The Cranelift backend produces a standalone binary without a cargo invocation — build is fast. However, the binary runs the same tishlang_vm on embedded bytecode; it is not AOT machine-code compilation of Tish opcodes. Throughput is VM-class, similar to tish run --backend vm.

Use when your program:

  • Has no external native imports (cargo:…, packaged tish:* modules such as tish:egui, or @scope/pkg). Built-in tish:fs, tish:http, and tish:process (when enabled) are still allowed on this path.
  • Benefits from a fast build (no cargo + rustc overhead)
tish build main.tish -o app --native-backend cranelift

If your program uses external native imports (including cargo:… crates), the Cranelift backend will error with a message like:

Cranelift backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported. Use --native-backend rust for external modules.

Known limitations (Cranelift / LLVM)

Because the binary runs tishlang_vm, any construct the VM supports is supported; constructs not yet implemented in the VM (e.g. certain destructuring patterns) are equally unsupported here. Destructuring parameters are not supported in bytecode — use the Rust backend or destructure inside the function body.

Build environment variables

tish build --target native generates a Rust crate and drives cargo build --release for it. These variables shape that nested build. All are optional; the default is a shipping build.

Build profile

VariableEffect
(unset)Shipping profile: lto = "fat", codegen-units = 1, stripped, panic = "abort". Smallest, fastest binary; slowest to produce — the final LTO link is single-threaded and dominates a warm build of a large app.
TISH_FAST_NATIVE_BUILD=1Iteration profile: no LTO, opt-level = 1, 16 codegen units, incremental. Use for CI verification builds and local iteration; do not ship the result. Also selects the fast profile on the GBA path.
TISH_NATIVE_THIN_LTO=1Thin LTO over 8 codegen units, still stripped: most of fat's size/speed win with a parallel link. The CI-friendly shipping profile. TISH_FAST_NATIVE_BUILD takes precedence when both are set. (tish ≥ 3.11)

Where the build happens

VariableEffect
TISH_NATIVE_TARGET_DIR=<dir>Persistent cargo target dir for native builds. By default the generated crate is built in a per-process temp dir that is deleted on success, so nothing survives between two builds and CI recompiles every dependency each run. Point this at a directory that persists (a CI cache) and cargo reuses the dependency artifacts; only the generated crate is recompiled. Created on demand; an unusable path warns and falls back to the temp dir. Ignored inside the tish workspace itself, which already shares its target/. (tish ≥ 3.11)
TISH_KEEP_BUILD_DIR=1Keep the generated crate's source directory after a successful build (it is otherwise removed). Useful to read the generated main.rs / Cargo.toml. Failed builds always keep it.
TISH_GBA_TARGET_DIR=<dir>The GBA-target equivalent of TISH_NATIVE_TARGET_DIR (ROM builds otherwise each carry a private dependency tree).

CARGO_TARGET_DIR, RUSTC_WRAPPER, RUSTC_WORKSPACE_WRAPPER and CARGO_BUILD_RUSTC_WRAPPER are removed from the environment before cargo runs — an inherited target dir made cargo write where the compiler never looked for the output. Use the two *_TARGET_DIR variables above to share a target dir, and cargo's own $CARGO_HOME/config.toml ([build] rustc-wrapper) to wrap rustc, e.g. with sccache.

Code generation

VariableEffect
RUSTFLAGSPassed through. If it does not name a target-cpu, tish appends -C target-cpu=native for host builds (never for --cargo-target cross builds, and only when native is at least as capable as the target default). Pin one (-C target-cpu=x86-64-v2) when the binary ships to other machines.
TISH_NATIVE_OPT=0Turn off the whole typed-native optimization stack (the flags in "Native typing flags" above are gated behind it). For A/B baselines and bisecting a suspected miscompile.
TISH_NO_OPTIMIZE=1Skip the compiler's own optimizer passes before codegen (1, true or yes).
TISH_NATIVE_FAST_ALLOC=0Do not link mimalloc as the global allocator (default on: ~20% on object/array-heavy code). Off automatically for static libraries and cross builds.

GBA target

VariableEffect
TISH_GBA_THIN_LTO=1Thin LTO / 8 codegen units for the ROM instead of the default fat LTO (fat keeps stack frames smallest; see the GBA guide).
TISH_GBA_DEBUG=1Keep debuginfo in the release ROM (slower link; for mGBA backtraces).
TISH_GBA_FAT_LTO=1Legacy no-op: fat is already the default.

Target and feature flags

--native-backend applies only when --target native (the default). Other targets (js, wasm, wasi, bytecode) ignore it.

For WebAssembly targets, see WASM Targets.

Feature flags (--feature http, etc.) apply to the Rust backend when compiling to native; the Cranelift/LLVM paths support only pure Tish.

Improve this documentation