Public API

The crate root (src/lib.rs) has these entry points. Each one reads the source as a &str. The library reads no file, except through a ModuleResolver when the source imports a module.

FunctionReturnsUse case
compile_to_ir(src)Result<IrModule, Vec<CompilerError>>A backend (canonical)
compile_to_ir_with_path(src, path)Result<IrModule, Vec<CompilerError>>A backend, with real source paths in the spans
compile_to_ir_with_resolver(src, resolver)Result<IrModule, Vec<CompilerError>>A program of several modules, then MonomorphisePass
compile_to_ir_with_path_and_resolver(src, path, resolver)Result<IrModule, Vec<CompilerError>>A program of several modules, with the source path, then MonomorphisePass
compile_with_analyzer(src)Result<(File, SemanticAnalyzer<FileSystemResolver>), Vec<CompilerError>>LSP, AST-level tooling
compile_with_analyzer_and_resolver(src, resolver)Result<(File, SemanticAnalyzer<R>), Vec<CompilerError>>The same, with a custom resolver
parse_only(src)Result<File, Vec<CompilerError>>Parse with no semantic analysis
compile_and_report(src, filename)Result<IrModule, String>CLI: compile_to_ir with the errors as a report

compile_to_ir, compile_to_ir_with_path and compile_with_analyzer resolve an import through a FileSystemResolver that starts at the current directory. Give a resolver of your own for any other root: a FileSystemResolver with another root, a MemoryResolver for modules in memory, or a custom ModuleResolver. A ModuleResolver must be Send and Sync, because the compile runs on a thread of its own and takes the resolver with it.

The stack of the caller does not matter. Each entry point runs its work on a compile thread with a stack sized from the program, so a caller on a small thread can compile the deepest program that the parser accepts. An IrPass must be Send for the same reason: Pipeline::run runs the passes on such a thread. See The compile thread. On wasm32 this is not true; see WebAssembly.

Each entry point that returns an IrModule gives one self-contained module. The IR of each imported module links into it. See Obtaining the IR.

compile_to_ir_with_resolver and compile_to_ir_with_path_and_resolver run MonomorphisePass on the result. The other entry points do not: their result can hold generic definitions. Add the pass through a Pipeline when you need it.

WebAssembly

The library builds for wasm32-unknown-unknown and wasm32-wasip1. The binary fvc does not run on WebAssembly.

A browser has no file system. Give the modules to a MemoryResolver, each with a path relative to the root .. It finds a module the same way as a FileSystemResolver with the root ., and it gives the same errors.

wasm32 has no threads, so the compile runs on the stack of the caller, not on a compile thread. Two stacks limit the depth of nesting that a compile can take:

  • The stack of the module, in its linear memory. It is 1 MiB by default, and the linker flag -C link-arg=-zstack-size=<bytes> sets another size. An overflow traps with "out of bounds memory access".
  • The native stack of the engine, which runs the frames of the compiled code. The browser, or the runtime, sets it, and the linker flag does not change it. wasmtime allows 512 KiB by default. An overflow traps with "call stack exhausted" in wasmtime, and throws a RangeError in a browser.

A stack overflow stops the whole module, so the compiler has lower limits on wasm32:

Limitwasm32Other targets
The nesting score that the parser accepts961024
The depth of an expression that the semantic pass accepts48500

A deeper program is a compile error. These are the deepest programs that compile on wasm32-wasip1, in a release build, and then run the passes of a backend (DefunctionalisePass, ConstantFoldingPass and DeadCodeEliminationPass):

ShapeThe deepest programThe depth that traps with no limit
A call in a call, f(v: f(v: 1))47229
A call of a block, f(v: { f(v: { 1 }) })47105
Parentheses46236
Blocks92195
if in a branch46173
match in an arm31243
Arrays46246

The last column is for the default stacks of wasmtime. Each program at the limit compiles with a native stack of 256 KiB, half the default of wasmtime, and with the default stack of the module. Each engine has its own frame sizes, so measure the deepest program in the browser that you use. The numbers change with each release of the compiler.

scripts/wasm_stack.sh measures the depths with the probe in tests/wasm-probe/. It needs wasmtime. Give it a size to measure another stack of the module: scripts/wasm_stack.sh 2097152.

Cargo features

The crate has one feature, serde. It is off by default. It derives serde::Serialize and serde::Deserialize on IrModule and on every type in it. It also derives them on the AST types that the IR holds, for example Literal, PrimitiveType and Span.

[dependencies]
formalang = { version = "0.0.16-beta", features = ["serde"] }

The JSON form of the IR is not a stable format. It can change in each release. After you read a module, call IrModule::rebuild_indices before a lookup by name.

The AST has no serialized form. File has no format_version, and there is no File::to_json or File::from_json.

The symbol table

SemanticAnalyzer::symbols() returns the SymbolTable. IR lowering and LSP tools read it. Two parts of its public surface changed:

  • SymbolTable::define_trait_impl(trait_name, struct_name, generics, trait_args, span) takes trait_args: Vec<Type>. These are the type arguments of a generic trait: [I32] for impl Container<I32> for Box. Give an empty vector for a trait with no type parameters.
  • TraitImplInfo has the field trait_args: Vec<Type> with the same meaning.

A type has one impl of each trait instance. A second impl of the same trait with the same trait_args for the same type is a duplicate. Two impls with other trait_args, such as Container<I32> and Container<String>, are two instances, and both are allowed.