You write .fv files; the library parses and validates them, and you get a fully type-resolved IR back. What you do with that IR (generate code, drive a UI framework, configure a system) is up to your backend. The first such backend is formawasm, which lowers the IR to WebAssembly.
.fv source → formalang library → IrModule → your Backend → output
Contents
- Why FormaLang?
- Quick Start
- Language Tour
- Rust API
- File extension
- What is not built in
- Further reading
- License
Why FormaLang?
You're building a Rust application that needs to accept user-authored logic: UI definitions, configuration with computation, state machines, scripted rules. The usual options each have a sharp edge:
- Ship Rust as the user-facing language. Rust is a great host but a poor guest: it's AOT-compiled, lifetimes and the borrow checker land on whoever writes the file, and you can't load
.rssnippets at runtime without dragging in a full toolchain. - Embed Lua, Rhai, or JavaScript. These are dynamically typed. Errors that should have been caught when the file was loaded surface only when the offending branch runs, usually in production.
- Use JSON, YAML, or TOML. No expressions, no functions, no real types. The moment your config grows a conditional, you reinvent half a language inside string templates.
FormaLang fills that gap:
- Statically typed and fully resolved. The library hands back an
IrModulewhere every type, name, and overload is already settled. A broken.fvfails at load, not when the user clicks the button that runs the bad branch. - Embeddable by design. A pure compiler frontend with no runtime, no I/O, no globals, no sandbox to maintain. The output is data: walk it, transform it, emit whatever you want.
- Small surface for users. Structs, enums, traits, closures, generics, modules. No lifetimes, no async, no unsafe, no macros. Someone fluent in Swift or Rust can read it on day one.
- Backend-agnostic. Drive a UI framework, generate code for any target, configure a runtime, layer custom IR passes. The compiler stops at the IR; you decide what comes next.
Quick Start
Add to Cargo.toml:
[]
= "0.0.5-beta"
Compile a source string:
use compile_to_ir;
let source = r#"
pub struct User {
name: String,
age: I32
}
"#;
let module = compile_to_ir.unwrap;
println!; // User
Language Tour
Primitives
let text: String = "hello"
let count: I32 = 42
let big: I64 = 9_223_372_036_854_775_807
let ratio: F64 = 3.14
let small: F32 = 0.5F32 // type-suffix pins literal precision
let flag: Boolean = true
let logo: Path = /assets/logo.svg
let pattern: Regex = r/+/i
let nothing: String? = nil // optional; any type can be made optional with ?
Numeric primitives are width-tagged: I32, I64, F32, F64. Unsuffixed
integer literals default to I32; unsuffixed float literals default to
F64. Suffix syntax is uppercase and adjacent to the digits (42I64,
3.14F32).
Structs
// Instantiate with named arguments
let p = Point
let u = User
// Mutability is a property of the binding, not the field; to mutate any
// field of `u` you bind it with `let mut u = User(...)`.
Methods (impl blocks)
Parameter Conventions
Every function parameter has a convention controlling how the argument is received. The call site always looks the same as f(x); only the function declaration changes.
// default: immutable; the callee reads the value
// mut: callee may mutate; argument binding must be let mut
// sink: ownership transfer; caller cannot use the binding after the call
// Self conventions work the same way
Traits
Traits declare field and method requirements. Any struct that satisfies all of them can declare conformance.
// Declare conformance
// fields checked against struct definition
// Trait composition
// A trait can also stand in as a value type. The IR lowers method
// calls on a trait-typed binding through the trait's vtable, so two
// branches that produce different concrete types implementing the
// same trait unify cleanly.
Enums
// Instantiate with leading dot
let s: Status = .active
let m: Message = .text
Let bindings
let x = 42
let name: String = "Alice"
pub let MAX: I32 = 100
let mut counter: I32 = 0 // mutable binding
Arrays, Dictionaries, Tuples
// Arrays
let tags: =
let matrix: =
// Dictionaries
let config: =
let empty: =
// Tuples (all fields must be named)
let point =
let name = point.x
// Indexing returns an Optional. The bound may be out of range or the
// key absent, so `xs[i]` and `d[k]` yield `T?` / `V?`. Use `if let` to
// consume the inner value.
let timeout: I32? = config
let first: String? = tags
Control Flow
// if: branches on a Boolean.
if user.score > 0 else
// if let: Rust-style optional unwrap. Both branches required.
if let nickname = user.nickname else
// for: iterates arrays, returns array of results
for item in items
// match: exhaustive, on enums (and on Optional, treated as .some / .none)
match message
Closures
Closure types describe a callable shape; closure expressions construct one. Both wrap their parameter list in parentheses so every -> in the language is preceded by ).
Closure expressions wrap their parameter list in parentheses — even
for a single parameter — so every -> in the language is preceded by
):
// Untyped — parameter types come from the binding annotation or call context
let onPress = .pressed
let onChange = .textChanged
let onResize = .resized
// Typed parameters — annotate inline with `name: Type`
let increment = + 1
let combine = + y
Closures capture values from their surrounding scope. The ClosureConversionPass lifts each closure into a top-level function plus a synthetic env struct, so backends only ever consume named functions.
let add5 = make_adder
Closure parameters carry the same conventions as regular function parameters (mut, sink). The convention constrains the caller of the closure:
Closures are pure and single-expression: no statements, no side effects in the language itself. Effects live in the host runtime, reached through extern declarations.
Generics
let b =
let r: = .ok
// Type-argument inference: when every generic parameter shows up in a
// field position, the type args can be omitted at the call site.
let inferred = Box // Box<I32>
let pair = Pair // Pair<I32, Boolean>
Destructuring
// Arrays
let = items
let = items // skip with _
// Structs (by field name)
let = user
let = user // rename
// Enums (extract associated data)
let = some_text_message
Modules
// Inline module
let p: Point = Point
// Import from other .fv files
use Point
use
use User
Files map to module paths: use geometry::shapes::Circle resolves to geometry/shapes.fv.
Only pub items can be imported. Circular imports are a compile error.
Extern declarations
Describe functions and method surfaces provided by the host runtime; they have no FormaLang body. There is no extern type; host-provided types are declared as regular structs and given an extern impl so their methods are resolved by the host.
extern
Function overloading
The compiler resolves overloads by the named-argument label set. Ambiguous or unresolvable calls are compile errors.
Rust API
Entry points
| Function | Returns | Use case |
|---|---|---|
compile_to_ir(src) |
Result<IrModule, Vec<CompilerError>> |
Code generation (canonical) |
compile_with_analyzer(src) |
Result<(File, SemanticAnalyzer), …> |
LSP hover / completion |
compile_and_report(src, filename) |
Result<IrModule, String> |
CLI: compile + human-readable errors |
parse_only(src) |
Result<File, …> |
Syntax check only |
Custom module resolver (to load .fv files from anywhere):
use ;
use PathBuf;
let resolver = new;
let module = compile_to_ir_with_resolver?;
The IrModule
let module = compile_to_ir?;
module.structs // Vec<IrStruct>
module.traits // Vec<IrTrait>
module.enums // Vec<IrEnum>
module.functions // Vec<IrFunction> (extern fns: extern_abi = Some(_), body = None)
module.impls // Vec<IrImpl>
module.lets // Vec<IrLet>
module.imports // Vec<IrImport>
module.modules // Vec<IrModuleNode> (preserves source `mod foo { ... }` hierarchy)
// ID-based lookup
let id = module.struct_id.unwrap;
let s = module.get_struct.unwrap;
All types in the IR are fully resolved; no unresolved references remain.
Pipeline (passes + backends)
use ;
use ;
let module = compile_to_ir?;
let output = new
.pass
.pass
.emit?;
Implement IrPass to write your own transforms, and Backend to emit code:
use ;
use IrModule;
;
;
Error reporting
use ;
match compile_to_ir
File extension
FormaLang source files use the .fv extension.
What is not built in
FormaLang is a pure compiler frontend. It does not include:
- A runtime or interpreter
- Code generation for any specific target
- A standard library (bring your own via
externdeclarations) - A package manager
These are responsibilities of the embedding application and its backends.
Further reading
- Language Reference: user-facing syntax and feature reference
- Architecture: compiler internals
- IR Reference: IrModule structure for backend authors
- AST Reference: AST structure for tooling authors
License
Dual-licensed under either of:
at your option.