Documentation
¶
Overview ¶
Package guest describes the WebAssembly feature surface FkLua can compile, and reads back what the guest toolchains actually emit.
The point is to make a fragile assumption fail loudly. Which wasm proposals we need to support is not a property of the spec -- it is a property of whatever TinyGo and Rust happen to enable by default, and that moves between releases. The corpus in scripts/fetch-spec.sh hardcodes one such feature string in a comment, which is documentation and can rot silently.
This has already bitten once: i32/i64.trunc_sat_* went unimplemented for three milestones because "TinyGo emits nontrapping-fptoint unconditionally" was recorded in prose and never checked.
Index ¶
- Constants
- Variables
- func Available() (bool, string)
- func Build(dir, pkg, out string) error
- func BuildCollected(dir, pkg, out string) error
- func BuildRust(workspace, pkg, outDir string) (string, error)
- func BuildRustCollected(workspace, pkg, outDir string) (string, error)
- func BuildRustLib(workspace, pkg, outDir string) (string, error)
- func BuildWith(dir, pkg, out string, flags []string) error
- func Root() (string, error)
- func RustAvailable() (bool, string)
- func SourceKey() (sum uint64, files int, err error)
- func ToolchainDeclaredAbsent() bool
- type Feature
- type Gap
- type Target
Constants ¶
const NoToolchainEnv = "FKLUA_NO_GUEST_TOOLCHAIN"
NoToolchainEnv is how an environment DECLARES that it carries no guest toolchain on purpose, which is the one thing that stops the availability guards being a failure.
It exists because those guards were written for a fresh worktree -- where a missing TinyGo means fifteen tests skip and the package still says `ok` -- and CI is the other case: .github/workflows/ci.yml does not install TinyGo, says so, and gives its reasons. Absence there is declared, reviewed and in version control, not the silent gap the guards exist to expose. Until this existed the two collided and took the `go` job down on every push, which took `spectest` with it as a SKIP, because it declares `needs: [go, lua52f]`.
Deliberately a declaration rather than a sniff at `CI`: a future job that does install TinyGo must still be guarded, and inferring the answer from the environment would quietly exempt it. And deliberately not `-short`, which is the guards' own opt-out but is too blunt here -- it would also skip TestTheRustToolchainIsAvailable, and CI installs the Rust target, so CI is exactly where that one is worth asserting. It was a false positive on every runner until 2026-07-31 and nothing noticed.
It needs no stale-declaration check. Setting it on a machine that HAS a toolchain changes nothing -- Available reports true and the guard passes on its own -- so the variable can only ever speak for an absence that is real.
const RustCollectorFeature = "fk/fkgc"
RustCollectorFeature is the one flag that turns the collector on, and it is the Rust analogue of swapping -gc=leaking for -gc=custom.
It is `fk`'s feature and not an example's, which is a decision with a reason on it. Cargo's v2 resolver unifies features across every package built in one invocation, so an example that DECLARED this in its own Cargo.toml would turn the collector on for every other example in a workspace-wide `cargo build` -- silently, and only for that invocation, which is exactly the shape of build-dependent non-determinism CLAUDE.md rules out. Passed on the command line against a single -p there is nothing to unify. TestNoRustExampleDeclaresTheCollectorFeature holds the other half.
const RustFlags = "-C target-feature=-multivalue,-reference-types"
RustFlags is a defensive target-feature string, no longer a requirement.
bulk-memory is compiled natively now, so it is not listed. multivalue and reference-types are: neither reaches a module in practice -- multivalue needs a multi-return signature, which the C ABI a guest exports does not have, and reference-types needs an externref, which no FkLua binding produces -- but turning them off costs nothing and removes the way they could.
const RustLoweringPass = "--llvm-memory-copy-fill-lowering"
RustLoweringPass is binaryen's byte-loop lowering for memory.copy and memory.fill.
KEPT FOR THE TEST THAT MEASURES WHAT IT COSTS, and for nothing else. A guest does not need it: 173 ns/byte against 3.5 for the native path is why the instructions were implemented instead.
const RustTarget = "wasm32-unknown-unknown"
RustTarget is the wasm target the Rust guest commits to.
Variables ¶
var BuildFlags = []string{
"-target=wasm-unknown",
"-scheduler=none",
"-gc=leaking",
"-opt=2",
}
BuildFlags are the TinyGo flags a guest must be built with, and the reasons are not stylistic:
- -target=wasm-unknown is the only target whose feature set FkLua can compile. TestTinyGoEmitsNothingWeCannotCompile checks that claim against TinyGo's own target JSON rather than against this comment.
- -scheduler=none because a Factorio tick cannot block. With a scheduler, a parked goroutine becomes a busy spin inside the game loop.
- -gc=leaking because a collector's pauses land in a lockstep game loop, where one client stalling desyncs everyone.
- -opt=2 because TinyGo's default is -opt=z, which optimises for SIZE, and size is the one cost this target does not have: the day-0 probe measured Factorio parsing 4 MB of Lua in 106 ms and a generated chunk never appears in a save. Measured against -opt=z through the real compiler: real_names 0.577x, real_grid 0.771x, pure_sum 0.770x, pure_dot 0.847x, real_entities 0.958x. pure_prng is ~2% slower and is the only kernel that does not gain. -opt=0 and -opt=1 are NOT substitutes: -opt=0 fails to build under -scheduler=none, and -opt=1 leaves most of the win.
Kept in step with guest/go/fk.BuildFlags, which is what a guest author reads.
var CollectedBuildFlags = []string{
"-target=wasm-unknown",
"-scheduler=none",
"-gc=custom",
"-opt=2",
}
CollectedBuildFlags are BuildFlags with the collector turned on: the same four flags with -gc=custom in place of -gc=leaking, and nothing else.
-gc=custom is the supported seam for plugging an external collector into TinyGo (src/runtime/gc_custom.go). It requires the application to provide seven functions by //go:linkname, which is what guest/go/fkgc does -- so a guest built with these flags and WITHOUT that import does not link, with `missing core function "runtime.free"` from deep inside the builder. That is the trap here, and it is the same shape as wasip1's -buildmode=c-shared: the flag alone is not the feature.
Everything else is deliberately identical, because the stage-B allocation measurement is only meaningful if the two arms differ in one flag. Kept in step with guest/go/fk.CollectedBuildFlags.
var GuestSourceRoots = []string{ filepath.Join("guest", "go"), filepath.Join("guest", "rust"), }
GuestSourceRoots are the trees whose contents a guest-dependent test result really depends on, relative to the repo root.
var Planned = map[Feature]string{
"bulk-memory": "partial: memory.copy/fill compiled, segment-indexed ops unscheduled",
"bulk-memory-opt": "partial: memory.copy/fill compiled, segment-indexed ops unscheduled",
}
Planned maps a feature we do NOT support onto a note saying what its status actually is. Anything here is known rather than a surprise.
bulk-memory is PARTIAL and saying so is the point. memory.copy and memory.fill -- the two a guest toolchain actually emits -- are compiled, and natively, at 3.5 and 2.2 ns/byte against 173 for the byte loop binaryen would lower them into. The segment-indexed half is not: memory.init, data.drop, table.copy, table.init and elem.drop need the data and elem sections kept live past instantiation, which is a different change and is not scheduled.
It read "M10" until the audit, two milestones after M10 shipped, which turns a decision into a roadmap item nobody is working on. If a guest is ever observed emitting memory.init, THAT is when to schedule the rest.
var RustMitigated = map[Feature]string{ "bulk-memory": "memory.copy/fill are compiled; memory.init is not", "bulk-memory-opt": "compiled natively", "multivalue": RustFlags, "reference-types": RustFlags, "call-indirect-overlong": "absorbed by the decoder", }
RustMitigated is the set of features FkLua does not support but which the documented build recipe removes from the emitted module.
Being here is a claim with a test behind it: TestRustBuildRecipeRemovesWhatItClaims builds a guest that uses the feature and checks the module afterwards. A feature listed here without that evidence would be a guard that lies.
var Supported = map[Feature]bool{ "sign-ext": true, "nontrapping-fptoint": true, "mutable-globals": true, "call-indirect-overlong": true, }
Supported lists every feature FkLua can compile today.
A guest toolchain enabling anything outside this set produces modules we will refuse, so the set is the real contract -- not the exact feature string, which can gain neutral entries or change order without meaning anything.
var TinyGoTargets = []Target{ {Name: "wasm-unknown", MustBeFullySupported: true, Why: "the M4 flagship guest target"}, {Name: "wasip1", MustBeFullySupported: false, Why: "the M10 target; bulk-memory is a known, scheduled gap"}, }
TinyGoTargets are the configurations the roadmap commits to.
var WASIBuildFlags = []string{
"-target=wasip1",
"-buildmode=c-shared",
}
WASIBuildFlags are the flags for a wasip1 guest, which is what buys goroutines. Each is load-bearing for a different reason:
- -target=wasip1 brings the asyncify scheduler, which rewrites the module into a resumable state machine INSIDE the wasm. That is what makes goroutines work with no host coroutines, which Lua 5.2 does not have.
- -buildmode=c-shared is NOT optional and is the trap. wasip1 defaults to building a COMMAND, exporting `_start`: it runs main and terminates, and calling an export afterwards is out of contract. The symptom is "//go:wasmexport function called before runtime initialization" from the guest's own runtime, which reads like an ordering bug in the host and is not. A mod needs a REACTOR, which exports `_initialize`.
The gc is left at TinyGo's wasip1 default (precise) rather than forced to leaking: asyncify already costs what it costs, and a guest reaching for goroutines is not the guest optimising for a tick budget.
Functions ¶
func Available ¶
Available reports whether a guest can be built here, and why not when it cannot.
wasm-opt is checked separately from TinyGo because it is a separate install (binaryen) that TinyGo shells out to and hard-requires for wasm targets. Its absence produces "could not find wasm-opt" from deep inside a build, which does not tell an unlucky reader to `brew install binaryen`.
func Build ¶
Build compiles a guest package with TinyGo and writes a wasm module to out.
dir is the guest module's root -- the directory holding its go.mod -- and pkg is the package to build relative to it.
func BuildCollected ¶
BuildCollected compiles a guest with the collector enabled. The package must import guest/go/fkgc, which supplies the -gc=custom hooks; without it the link fails rather than producing a guest that quietly does not collect.
func BuildRust ¶
BuildRust compiles a Rust guest crate to wasm and returns the module path.
One command and no post-processing, which is the whole recipe. Release mode is not optional: the workspace's release profile carries panic=abort, and a debug build would try to unwind across a boundary that cannot unwind.
func BuildRustCollected ¶
BuildRustCollected compiles a Rust guest with the collector enabled: the same crate, the same command, one --features flag.
The Go pair it mirrors is Build/BuildCollected, and the two differ in exactly the same way -- one build knob, nothing else -- for the same reason: an A/B between the two arms is only a measurement if that is all that changed.
THERE IS NO IMPORT TO ADD, which is where the two toolchains genuinely diverge. A Go guest must `import _ ".../fkgc"` or -gc=custom fails to link with `missing core function "runtime.free"`; a Rust guest needs nothing, because `fk` owns the single #[global_allocator] site and the feature chooses what backs it. So this flag alone IS the feature here, and every example in the corpus builds both ways with no source change at all.
func BuildRustLib ¶
BuildRustLib compiles a Rust library crate and returns its rlib path.
Separate from BuildRust because a cdylib and an rlib land under different names, and because a library is the right unit for a compile gate: rustc type-checks every item in one, where a cdylib guest only pulls in what it calls.
func BuildWith ¶
BuildWith compiles a guest with an explicit flag set. Callers should use BuildFlags or CollectedBuildFlags rather than assembling their own -- the flags are load-bearing and the reasons are on the variables.
func RustAvailable ¶
RustAvailable reports whether a Rust wasm toolchain is installed.
func SourceKey ¶
SourceKey opens every source file under GuestSourceRoots and returns a hash of their contents together with the number of files read.
The hash is not the point and nothing compares it across runs; OPENING the files is the point, because that is what the `go test` cache records. The value is returned anyway so a caller can log it and so the read cannot be optimised away.
The file COUNT is returned because a corpus walk that matched nothing passes forever -- the habit `agents/testing.md` states as "count what you audited and fail on zero". A caller that does not check it has a cache key of the empty set, which is the state this whole file exists to rule out.
func ToolchainDeclaredAbsent ¶
func ToolchainDeclaredAbsent() bool
ToolchainDeclaredAbsent reports whether this environment has declared that it carries no guest toolchain. The name lives here, next to Available, so the two guards that read it cannot drift apart on the spelling.
Types ¶
type Feature ¶
type Feature string
Feature is a WebAssembly proposal as named in an LLVM/TinyGo feature string.
func Features ¶
Features reads a TinyGo target's enabled and disabled features, following `inherits` until a target declares its own feature string.
func RustFeatures ¶
RustFeatures reads the features rustc enables by default for the guest target.
This is rustc's own answer rather than a table in this file, for the reason the package exists: the set has moved between releases and will move again.
type Gap ¶
type Gap struct {
Feature Feature
// Milestone is when it is scheduled, or "" if it is not on the roadmap at
// all -- which means a toolchain has started emitting something new.
Milestone string
}
Gap is a feature a guest emits that FkLua cannot compile.
type Target ¶
type Target struct {
// Name is the toolchain's own target name.
Name string
// MustBeFullySupported is true for targets a milestone already claims to
// compile. A planned-but-missing feature is a failure for those.
MustBeFullySupported bool
// Why records what the target is for, so a failure explains itself.
Why string
}
Target is one guest toolchain configuration we care about.