SPEC.md — Magpie Language + Toolchain v0.1 (Authoritative Implementation Specification)
Defines the complete specification for the Magpie programming language and toolchain, optimized for LLM code generation and agent workflows.
What this file does
Defines the complete specification for the Magpie programming language and toolchain, optimized for LLM code generation and agent workflows.
When to use it
- Building a compiler or language toolchain targeting LLM consumption
- Designing a language with deterministic formatting and token budgeting
- Implementing an MCP server for compiler tooling
- Creating a specification for a new programming language
Assumes this stack
SPEC.md — Magpie Language + Toolchain v0.1 (Authoritative Implementation Specification)
Primary constraint: Magpie optimizes only for LLMs (Transformer/Attention architectures) and automated agent workflows. Human ergonomics are explicitly out of scope. Normative keywords: MUST/SHOULD/MAY are used in the RFC 2119 sense. Scope: This is a buildable spec: concrete syntax, invariants, IR schemas, algorithms, CLI/MCP contracts, packaging, runtime ABI, web frameworks (frontend SSR + backend services), GPU compute, and compiler-integrated memory/retrieval.
1. Document control
1.1 Versioning
- This spec defines Magpie v0.1.
- v0.1 is allowed to break compatibility at any time; once v1.0 ships, breaking changes require a migration tool (
magpie migrate) and a compatibility policy.
1.2 Conformance levels
Implementations may claim conformance to:
- Magpie Core v0.1 (compiler, IR, safety, ARC, CPU targets)
- Magpie Web v0.1 (SSR + backend framework)
- Magpie GPU v0.1 (kernel subset + at least one backend)
- Magpie Pkg v0.1 (package manager + registry protocol)
- Magpie MCP v0.1 (MCP server + tool schemas)
1.3 Edition system
The edition field in Magpie.toml controls LLM default settings:
- Edition
"2026"is the v0.1 baseline with these defaults:token_budget = 12000budget_policy = "balanced"auto_fmt = truemeta_block_required = false(SHOULD, not MUST)
- Future editions MAY change these defaults without breaking old code.
- Packages with different editions interoperate at the MPIR level.
1.4 Build priority (implementation ordering)
- Tier 1 (ship first): lexer, parser, CSNF formatter, type checker, ownership checker, ARC insertion, LLVM codegen, runtime, CLI (
build/run/fmt/test) - Tier 2 (ship second): MMS, diagnostics engine, MCP server, REPL, package manager, linter, doc generator
- Tier 3 (ship third): web framework (backend + SSR), GPU compute, WASM targets
All tiers are v0.1 scope with clear sequencing.
1.5 Glossary
- Surface Magpie (
.mp): authoring format written by LLMs; LLVM-close; explicit SSA blocks. - MPIR (
.mpir): Magpie IR, canonical SSA+CFG+typed IR, the compiler's main internal representation and test output. - HIR: High-level IR, the first fully-resolved representation after parsing and name resolution.
- LLVM IR: target-independent IR produced by Magpie for LLVM backends.
- CSNF: Canonical Source Normal Form (Magpie canonical formatting for stable tokenization).
- ARC: automatic reference counting inserted by compiler.
- MMS: Magpie Memory Store (compiler-integrated retrieval store).
- RAG: retrieval-augmented generation (here: retrieval-augmented compilation outputs for LLMs).
- Token budget (TB): maximum "LLM token" cost allowed for compiler/tool outputs.
- SID: Stable ID for progressive disclosure and linking.
- Unique handle: an owned reference with exclusive aliasing guarantees (Rust-like), ARC-managed.
- Shared handle: explicitly shared ARC reference, clonable and thread-safe.
- Borrow: temporary non-owning reference that cannot escape its scope.
- TCallable: ARC-managed heap type wrapping a function pointer + optional captured environment.
2. LLM-first design principles (hard requirements)
These are language + toolchain requirements motivated by Transformer limitations: finite context window, attention dilution over long sequences, and token-level ambiguity.
2.1 Canonicalization and determinism (CSNF)
- Every Magpie artifact MUST have a canonical form:
- source canonicalization (
magpie fmt) - canonical symbol ordering
- canonical numeric formatting
- canonical type printing
- source canonicalization (
magpie fmtMUST rewrite.mpinto CSNF — a canonical, deterministic form.- Canonical output MUST be stable given the same compiler version/config and input semantics.
- Canonical JSON: When emitting JSON (
--output json), Magpie MUST serialize canonical JSON:- no whitespace outside string literals
- object keys lexicographically sorted at every level
- floats printed with shortest-roundtrip canonical format
- Stable IDs (SIDs): Compiler MUST emit SIDs for modules/symbols to allow retrieval and compact referencing (§18).
Why (LLM): canonicalization prevents "format drift" that wastes tokens and causes divergent edits. Stable IDs enable progressive disclosure.
2.2 Progressive disclosure built into the language
Magpie MUST support "compressed summaries" that can be loaded without reading full code:
.mpdModule Definition Digest filesmagpie graph symbols --jsonSymbol Graphmagpie graph deps --jsonDependency Graphmagpie graph ownership --jsonOwnership Graph (per function)magpie graph cfg --jsonCFG graph (per function)
All outputs MUST support token budgeting (§3).
Why (LLM): agents can retrieve minimal context to resolve unknowns without reloading everything.
2.3 Locality constraints (anti-attention-fade rules)
Surface Magpie MUST enforce:
- No implicit imports (lang items are compiler-recognized built-ins, not imports; see §2.4)
- No wildcard imports (except none — v0.1 has no wildcard imports)
- No operator overloading
- No precedence-sensitive operators in v0.1 (all operations are spelled opcodes)
- No hidden control flow (no exceptions; no implicit destructors beyond ARC codegen; explicit
suspend.callfor async) - No method syntax (
obj.method()is forbidden; all calls arecall @function { args=[...] }) - No closures as a language primitive (TCallable is an explicit heap type with visible captures)
- SSA locals MUST be explicitly typed
- Functions SHOULD be small; compiler MUST provide lint
MPL2001 FN_TOO_LARGEif exceeding config
Optional but recommended:
- A function SHOULD declare
meta { uses {…} effects {…} cost {…} }blocks; the compiler can auto-generate and verify them (§7.7).
2.4 Lang item auto-availability
The following types are compiler-recognized built-ins available in all modules without import:
TOption<T>(fromstd.core)TResult<T,E>(fromstd.core)bool(alias ofi1)unit(empty value)Str(heap string)Array<T>(heap array)Map<K,V>(heap map)
These are NOT imports. They are built-in types recognized by the compiler. The imports block only lists package-level imports.
Why (LLM): These types appear in virtually every file. Requiring explicit imports for them adds tokens that are always present and never informative.
3. LLM token budget system (TB) — global, enforced (MUST)
Magpie MUST treat token consumption of outputs as a first-class bounded resource.
3.1 Token budget parameter: sources and precedence (MUST)
Token budget configuration MUST be available via:
- CLI:
--llm-token-budget <u32> - MCP per-tool request:
llm.token_budget - Env:
MAGPIE_LLM_TOKEN_BUDGET=<u32> - Manifest:
Magpie.toml[llm].token_budget
Precedence: CLI > MCP > Env > Manifest > edition default.
Defaults (edition "2026"):
- In
--llmmode:12000 - Otherwise: unlimited (unless configured)
3.2 Tokenizer selection (MUST)
Token counting MUST be performed with a configured tokenizer:
approx:utf8_4charsMUST be supported with zero dependencies:tokens = ceil(utf8_bytes / 4)
- Optional tokenizers MAY be supported (plugins):
openai:cl100k_base,openai:o200k_base,anthropic:claude, etc.
Compiler MUST expose:
- CLI:
--llm-tokenizer <id> - Manifest:
[llm].tokenizer - MCP:
llm.tokenizer
If tokenizer is unavailable, fallback to approx:utf8_4chars and emit warning MPL0802 TOKENIZER_FALLBACK_USED.
3.3 Budget enforcement: deterministic dropping/truncation (MUST)
If output would exceed budget, Magpie MUST reduce payload deterministically in this priority order:
Tier 0 (MUST keep)
success- tool/command metadata
- diagnostic
code,severity,title primary_span- short
message(≤ 200 chars; truncated if necessary)
Tier 1 (SHOULD keep)
- top 1 suggested fix patch (if available)
- minimal
why.traceevents
Tier 2 (MAY drop next)
- secondary spans
- graphs reduced to summaries (node counts + root IDs)
Tier 3 (drop after Tier 4)
- long code excerpts, full slices, large retrieved items
Tier 4 (drop first)
- long explanations
- doc excerpts beyond top snippet
- verbose graphs
Drop order is: 4→3→2→1.
If Tier 0 alone exceeds budget:
- output MUST contain only:
success=false- a single diagnostic
MPL0801 TOKEN_BUDGET_TOO_SMALL - recommended budget value (estimated Tier0 tokens × 2)
3.4 Budget report (MUST)
Every budgeted output MUST include:
{"llm_budget":{"token_budget":12000,"tokenizer":"approx:utf8_4chars","estimated_tokens":11840,"policy":"balanced","dropped":[{"field":"diagnostics[0].explanation_md","reason":"budget"}]}}
4. Toolchain requirements
4.1 Delivered binaries
Magpie distribution MUST ship:
magpie(CLI: compiler + package manager + web tooling)magpie-mcp(ormagpie mcp servesubcommand)magpie-rt(runtime library: linked into Magpie programs)magpie-std(standard packages compiled/installed)
4.2 LLVM baseline and licensing
- Magpie v0.1 targets an LLVM release line selected by the project (recommend pinning a specific major/minor for reproducibility).
- Redistribution must respect LLVM's Apache-2.0 WITH LLVM-exception license text.
4.3 GPU baseline facts
- NVPTX backend conventions are defined by LLVM documentation.
- SPIR-V is an official LLVM backend since LLVM 20.
- Metal shader converter converts LLVM IR bytecode into Metal-loadable bytecode.
- WebGPU shaders are WGSL source strings; WGSL is the shader language for WebGPU.
5. CLI specification (magpie)
5.1 Global flags (MUST)
--output <text|json|jsonl>json= single JSON documentjsonl= JSON lines for streaming build logs
--color <auto|always|never>(ignored in JSON modes)--log-level <error|warn|info|debug|trace>--profile <dev|release|custom>--target <llvm-triple>--emit <artifact-list>where artifacts include:llvm-ir(.ll),llvm-bc(.bc),object(.o/.obj),asm,spv(.spv),exe,shared-lib(.so/.dylib),mpir,mpd,symgraph
--cache-dir <path>--jobs <n>--features <list>--no-default-features--offline(package manager and web tooling must not use network)--llm(forces LLM-optimized behavior; see §5.3)--llm-token-budget <u32>--llm-tokenizer <id>--llm-budget-policy balanced|diagnostics_first|slices_first|minimal--max-errors <n>(default 20; max errors collected per compiler pass)--shared-generics(use vtable-based approach for generics to reduce binary size)
5.2 Commands
5.2.1 magpie new <name>
Creates:
Magpie.tomlsrc/main.mpMagpie.lock(empty).magpie/(local cache + generated digests)tests/(empty test directory)
5.2.2 magpie build
Behavior:
- Resolves deps via
magpie pkgengine - Produces requested artifacts via
--emit - In
--profile dev, defaults: fast incremental, minimal optimization, full diagnostics, DWARF debug info,.mpdbgstructured debug - In
--profile release, defaults: full optimization, LTO optionally, deterministic build outputs - Each pass collects up to
--max-errorserrors; dependent passes are skipped if earlier passes fail
5.2.3 magpie run
- In dev: MAY use JIT (ORC) or compile+run based on
Magpie.tomlpolicy - In release: MUST compile native and run executable
- Supports passing program args after
--
5.2.4 magpie repl
- Persistent session with incremental compilation
- Must expose:
:type <expr>:ir <fn>:llvm <fn>:diag last
5.2.5 magpie fmt
- MUST rewrite
.mpinto CSNF - MUST update file digests
- MUST ensure every instruction uses explicit key-value argument form
- MUST ensure block labels are canonical
bb0..bbN(renumbered if necessary) --fix-meta: auto-generate missing meta blocks in--llmmode
5.2.6 magpie lint
- MUST provide structured, fixable diagnostics (same format as compiler)
- Full suite: style + complexity + safety + LLM-specific (see §33)
- Configurable severity per lint code in
Magpie.toml
5.2.7 magpie test
- Discovers functions prefixed with
@test_intests/*.mpand source modules - Runs each test function, reports pass/fail
- Supports
--filter <pattern>for selective test execution - Output format follows §26 diagnostics schema
5.2.8 magpie doc
- Generates:
.mpddigests- HTML/JSON docs of public API
- "LLM doc pack" (compact symbol index, budget-aware)
5.2.9 magpie web ...
See §30 (Web frameworks). Includes both frontend SSR and backend services.
5.2.10 magpie pkg ...
See §28 (Package manager).
5.2.11 magpie mcp serve
See §29.
5.2.12 magpie memory ...
magpie memory build(incremental MMS index update)magpie memory query --q "<query>" --k <n> [--kinds ...]
5.2.13 magpie ctx pack ...
Generates prompt-ready context pack bounded by token budget. See §25.
5.2.14 magpie explain <CODE>
- MUST output: explanation, minimal examples, canonical remediation templates
5.2.15 magpie ffi import --header <path> --out <file.mp>
- Auto-generates extern module declarations from C headers
- Output MUST include TODO markers for unknown ownership
5.2.16 magpie mpir verify
- Verifies MPIR correctness: SSA, type IDs, SIDs, call arity, arc ops
5.3 --llm mode behavior (MUST)
When --llm is set OR environment variable MAGPIE_LLM=1:
- Default output MUST be JSON (unless overridden).
- Diagnostics MUST include suggested patches whenever possible.
- Compiler MUST emit additional graphs on failure:
- minimal symbol graph for the file(s) involved
- ownership trace graph for the failing function(s)
magpie fmtMUST run automatically before build unless--no-auto-fmt.- Token budget is enforced on all outputs.
- MMS retrieval is enabled for diagnostic augmentation.
6. Source format and module system
6.1 File types
.mp: Magpie source (Surface).mpir: Core Magpie IR (debug/test).mpd: Module definition digest (public API summary).mpdbg: Structured debug metadata (JSON, LLM-friendly)Magpie.lock: lockfile (canonical JSON)
6.2 Module-to-file mapping (MUST)
Every .mp file is exactly one module. Module path maps to filesystem path:
module pkg.sub.module→src/sub/module.mp- No directory modules, no multiple modules per file.
- This mapping is strict, unambiguous, and LLM-friendly.
6.3 Mandatory module header (Surface .mp)
Every file MUST begin with a header block:
module <module_path>
exports { <export_list> }
imports { <import_list> }
digest "<hex>"
Rules:
module_pathis a dotted path:pkg.sub.module.exportsMUST list all exported symbols (no implicit exports).importsMUST list all imported symbols using grouped syntax:imports { std.io::{@println, @readln}, pkg.other::{@foo, TBar} }
digestMUST equal the compiler-defined digest of the canonicalized file contents (excluding the digest line itself).magpie fmtis the authority that inserts/updatesdigest.- Lang items (TOption, TResult, bool, unit, Str, Array, Map) do NOT need to appear in imports.
6.4 Digest algorithm (MUST)
- Digest MUST be computed as:
BLAKE3(canonical_source_without_digest_line) - Encoded as lowercase hex.
- File digest MUST change if and only if canonical source changes.
6.5 Name resolution model
- Every symbol has a fully qualified name (FQN):
pkg.module.@functionpkg.module.TTypepkg.module.@global
- Within a module, local references MUST be either:
- fully qualified, or
- imported via
importsheader
- No implicit prelude (lang items are built-in, not prelude).
6.6 No overloads (MUST)
- A module MUST NOT define two symbols with the same unqualified name in the same namespace.
- Namespaces:
- functions/globals:
@ - types:
T - SSA locals:
% - signatures:
sig
- functions/globals:
6.7 Documentation comments
;;;doc comments attach to the next declaration.- Doc comments are included in
.mpdoutput.
7. Surface language spec (syntax + semantics)
7.1 Lexical tokens
7.1.1 Whitespace
- Space, tab, newline are whitespace.
- Newlines are significant only for diagnostics and canonical formatting; grammar is whitespace-insensitive.
7.1.2 Comments
- Line comment:
; ...to end of line - Doc comment:
;;; ...to end of line - Block comments are NOT supported in v0.1.
7.1.3 Identifiers
- Global function/global:
@+[A-Za-z_][A-Za-z0-9_]* - SSA value:
%+ same pattern - Type name:
T+[A-Za-z_][A-Za-z0-9_]* - Basic block label:
bb+ digits (canonical), e.g.bb0,bb1
7.1.4 Literals
- Integers: decimal
123, hex0x7f - Floats:
1.0,1.0f32(canonical form prints viaconst.f32 1.0) - Strings:
"..."UTF-8 with escapes\n \t \\ \" \u{...} - Booleans:
true,false
7.1.5 Keywords
module exports imports digest fn async meta uses effects cost heap value struct enum extern global unsafe gpu target sig impl
7.1.6 Op keywords
const.* i.add i.sub i.mul i.sdiv i.udiv i.srem i.urem i.add.wrap i.sub.wrap i.mul.wrap i.add.checked i.sub.checked i.mul.checked i.and i.or i.xor i.shl i.lshr i.ashr f.add f.sub f.mul f.div f.rem f.add.fast f.sub.fast f.mul.fast f.div.fast icmp.* fcmp.* call call_void call.indirect call_void.indirect try suspend.call suspend.await new getfield setfield enum.new enum.tag enum.payload enum.is share clone.shared clone.weak weak.downgrade weak.upgrade cast borrow.shared borrow.mut ptr.null ptr.addr ptr.from_addr ptr.add ptr.load ptr.store arr.new arr.len arr.get arr.set arr.push arr.pop arr.slice arr.contains arr.sort arr.map arr.filter arr.reduce arr.foreach map.new map.len map.get map.get_ref map.set map.delete map.delete_void map.contains_key map.keys map.values str.concat str.len str.eq str.slice str.bytes str.parse_i64 str.parse_u64 str.parse_f64 str.parse_bool str.builder.new str.builder.append_str str.builder.append_i64 str.builder.append_i32 str.builder.append_f64 str.builder.append_bool str.builder.build json.encode json.decode callable.capture arc.retain arc.release arc.retain_weak arc.release_weak panic phi gpu.thread_id gpu.workgroup_id gpu.workgroup_size gpu.global_id gpu.barrier gpu.shared gpu.buffer_load gpu.buffer_store gpu.buffer_len gpu.launch gpu.launch_async
7.2 Grammar (EBNF, Surface .mp)
This grammar is intentionally verbose and unambiguous.
File := Header Decl* EOF ;
Header := "module" ModulePath
"exports" "{" ExportList? "}"
"imports" "{" ImportList? "}"
"digest" StringLit ;
ModulePath := Ident ("." Ident)* ;
ExportList := ExportItem ("," ExportItem)* ;
ExportItem := "@" Ident | "T" Ident ;
ImportList := ImportGroup ("," ImportGroup)* ;
ImportGroup := ModulePath "::" "{" ImportItem ("," ImportItem)* "}" ;
ImportItem := "@" Ident | "T" Ident ;
Decl := FnDecl
| AsyncFnDecl
| UnsafeFnDecl
| GpuFnDecl
| TypeDecl
| ExternModuleDecl
| GlobalDecl
| ImplDecl
| SigDecl ;
FnDecl := Doc? "fn" FnName "(" Params? ")" "->" Type FnMeta? "{" Block+ "}" ;
AsyncFnDecl := Doc? "async" "fn" FnName "(" Params? ")" "->" Type FnMeta? "{" Block+ "}" ;
UnsafeFnDecl := Doc? "unsafe" "fn" FnName "(" Params? ")" "->" Type FnMeta? "{" Block+ "}" ;
GpuFnDecl := Doc? "gpu" "fn" FnName "(" Params? ")" "->" Type "target" "(" Ident ")" FnMeta? "{" Block+ "}" ;
FnName := "@" Ident ;
FnMeta := "meta" "{" UsesBlock? EffectsBlock? CostBlock? "}" ;
UsesBlock := "uses" "{" FqnRef ("," FqnRef)* "}" ;
EffectsBlock := "effects" "{" Ident ("," Ident)* "}" ;
CostBlock := "cost" "{" CostItem ("," CostItem)* "}" ;
CostItem := Ident "=" IntLit ;
Params := Param ("," Param)* ;
Param := SSAName ":" Type ;
SSAName := "%" Ident ;
TypeDecl := Doc? ("heap" | "value") ("struct" | "enum") TypeName TypeParams? "{" TypeBody "}" ;
TypeName := "T" Ident ;
TypeParams := "<" TypeParam ("," TypeParam)* ">" ;
TypeParam := Ident ":" TypeConstraint ;
TypeConstraint:= "type" | "send" | "sync" | "hash" | "eq" | "ord" ;
ImplDecl := "impl" Ident "for" Type "=" FnRef ;
SigDecl := "sig" "T" Ident "(" TypeList? ")" "->" Type ;
TypeList := Type ("," Type)* ;
ExternModuleDecl := Doc? "extern" StringLit "module" Ident "{" ExternItem* "}" ;
ExternItem := "fn" FnName "(" ExternParams? ")" "->" Type ExternAttrs? ;
ExternParams := ExternParam ("," ExternParam)* ;
ExternParam := SSAName ":" Type ;
ExternAttrs := "attrs" "{" ExternAttr* "}" ;
GlobalDecl := Doc? "global" "@" Ident ":" Type "=" ConstExpr ;
Block := BlockLabel ":" Instr* Terminator ;
BlockLabel := "bb" Digits ;
Digits := [0-9]+ ;
Instr := SSAName ":" Type "=" Op
| OpVoid
| UnsafeBlock ;
UnsafeBlock := "unsafe" "{" (SSAName ":" Type "=" Op | OpVoid)+ "}" ;
Terminator := "ret" (ValueRef)?
| "br" BlockLabel
| "cbr" ValueRef BlockLabel BlockLabel
| "switch" ValueRef "{" SwitchArm+ "}" "else" BlockLabel
| "unreachable" ;
SwitchArm := "case" ConstLit "->" BlockLabel ;
Op := ConstExpr
/* Integer arithmetic (checked by default) */
| "i.add" BinArgs | "i.sub" BinArgs | "i.mul" BinArgs
| "i.sdiv" BinArgs | "i.udiv" BinArgs
| "i.srem" BinArgs | "i.urem" BinArgs
/* Integer arithmetic (wrapping, safe) */
| "i.add.wrap" BinArgs | "i.sub.wrap" BinArgs | "i.mul.wrap" BinArgs
/* Integer arithmetic (checked, returns TOption) */
| "i.add.checked" BinArgs | "i.sub.checked" BinArgs | "i.mul.checked" BinArgs
/* Bitwise */
| "i.and" BinArgs | "i.or" BinArgs | "i.xor" BinArgs
| "i.shl" BinArgs | "i.lshr" BinArgs | "i.ashr" BinArgs
/* Float (strict IEEE 754) */
| "f.add" BinArgs | "f.sub" BinArgs | "f.mul" BinArgs | "f.div" BinArgs | "f.rem" BinArgs
/* Float (fast-math opt-in) */
| "f.add.fast" BinArgs | "f.sub.fast" BinArgs | "f.mul.fast" BinArgs | "f.div.fast" BinArgs
/* Compare (dotted form: icmp.eq, fcmp.oeq, etc.) */
| "icmp." IcmpPred CmpArgs
| "fcmp." FcmpPred CmpArgs
/* Calls */
| "call" FnRef TypeArgs? "{" CallArgs? "}"
| "call.indirect" ValueRef "{" CallArgs? "}"
| "try" FnRef TypeArgs? "{" CallArgs? "}"
| "suspend.call" FnRef TypeArgs? "{" CallArgs? "}"
| "suspend.await" "{" "fut" "=" ValueRef "}"
/* Heap and fields */
| "new" TypeCtor
| "getfield" "{" "obj" "=" ValueRef "," "field" "=" FieldName "}"
| "phi" PhiArgs
/* Enum operations */
| "enum.new" "<" Ident ">" "{" CallArgs? "}"
| "enum.tag" "{" "v" "=" ValueRef "}"
| "enum.payload" "<" Ident ">" "{" "v" "=" ValueRef "}"
| "enum.is" "<" Ident ">" "{" "v" "=" ValueRef "}"
/* Ownership conversions */
| "share" "{" "v" "=" ValueRef "}"
| "clone.shared" "{" "v" "=" ValueRef "}"
| "clone.weak" "{" "v" "=" ValueRef "}"
| "weak.downgrade" "{" "v" "=" ValueRef "}"
| "weak.upgrade" "{" "v" "=" ValueRef "}"
| "cast" "<" PrimType "," PrimType ">" "{" "v" "=" ValueRef "}"
/* Borrow creation (surface opcodes; lowered to BorrowShared/BorrowMut in HIR) */
| "borrow.shared" "{" "v" "=" ValueRef "}"
| "borrow.mut" "{" "v" "=" ValueRef "}"
/* Raw pointer ops (unsafe-only; MUST appear inside `unsafe {}` blocks) */
| "ptr.null" "<" Type ">"
| "ptr.addr" "<" Type ">" "{" "p" "=" ValueRef "}"
| "ptr.from_addr" "<" Type ">" "{" "addr" "=" ValueRef "}"
| "ptr.add" "<" Type ">" "{" "p" "=" ValueRef "," "count" "=" ValueRef "}"
| "ptr.load" "<" Type ">" "{" "p" "=" ValueRef "}"
/* TCallable */
| "callable.capture" FnRef "{" CaptureList "}"
/* Array intrinsics (value-producing) */
| "arr.new" "<" Type ">" "{" "cap" "=" ValueRef "}"
| "arr.len" "{" "arr" "=" ValueRef "}"
| "arr.get" "{" "arr" "=" ValueRef "," "idx" "=" ValueRef "}"
| "arr.pop" "{" "arr" "=" ValueRef "}"
| "arr.slice" "{" "arr" "=" ValueRef "," "start" "=" ValueRef "," "end" "=" ValueRef "}"
| "arr.contains" "{" "arr" "=" ValueRef "," "val" "=" ValueRef "}"
| "arr.map" "{" "arr" "=" ValueRef "," "fn" "=" ValueRef "}"
| "arr.filter" "{" "arr" "=" ValueRef "," "fn" "=" ValueRef "}"
| "arr.reduce" "{" "arr" "=" ValueRef "," "init" "=" ValueRef "," "fn" "=" ValueRef "}"
/* Map intrinsics (value-producing) */
| "map.new" "<" Type "," Type ">" "{" "}"
| "map.len" "{" "map" "=" ValueRef "}"
| "map.get" "{" "map" "=" ValueRef "," "key" "=" ValueRef "}"
| "map.get_ref" "{" "map" "=" ValueRef "," "key" "=" ValueRef "}"
| "map.delete" "{" "map" "=" ValueRef "," "key" "=" ValueRef "}"
| "map.contains_key" "{" "map" "=" ValueRef "," "key" "=" ValueRef "}"
| "map.keys" "{" "map" "=" ValueRef "}"
| "map.values" "{" "map" "=" ValueRef "}"
/* String intrinsics (value-producing) */
| "str.concat" "{" "a" "=" ValueRef "," "b" "=" ValueRef "}"
| "str.len" "{" "s" "=" ValueRef "}"
| "str.eq" "{" "a" "=" ValueRef "," "b" "=" ValueRef "}"
| "str.slice" "{" "s" "=" ValueRef "," "start" "=" ValueRef "," "end" "=" ValueRef "}"
| "str.bytes" "{" "s" "=" ValueRef "}"
| "str.builder.new" "{" "}"
| "str.builder.build" "{" "b" "=" ValueRef "}"
/* String parse intrinsics (value-producing, return TResult) */
| "str.parse_i64" "{" "s" "=" ValueRef "}"
| "str.parse_u64" "{" "s" "=" ValueRef "}"
| "str.parse_f64" "{" "s" "=" ValueRef "}"
| "str.parse_bool" "{" "s" "=" ValueRef "}"
/* JSON intrinsics (value-producing, return TResult) */
| "json.encode" "<" Type ">" "{" "v" "=" ValueRef "}"
| "json.decode" "<" Type ">" "{" "s" "=" ValueRef "}"
/* GPU device ops (value-producing; only valid inside gpu fn) */
| "gpu.thread_id" "{" "dim" "=" ValueRef "}"
| "gpu.workgroup_id" "{" "dim" "=" ValueRef "}"
| "gpu.workgroup_size" "{" "dim" "=" ValueRef "}"
| "gpu.global_id" "{" "dim" "=" ValueRef "}"
| "gpu.buffer_load" "<" Type ">" "{" "buf" "=" ValueRef "," "idx" "=" ValueRef "}"
| "gpu.buffer_len" "<" Type ">" "{" "buf" "=" ValueRef "}"
| "gpu.shared" "<" IntLit "," Type ">"
/* GPU host ops (value-producing; only valid outside gpu fn) */
| "gpu.launch" "{" "device" "=" ValueRef "," "kernel" "=" FnRef "," "grid" "=" ArgValue "," "block" "=" ArgValue "," "args" "=" ArgValue "}"
| "gpu.launch_async" "{" "device" "=" ValueRef "," "kernel" "=" FnRef "," "grid" "=" ArgValue "," "block" "=" ArgValue "," "args" "=" ArgValue "}" ;
OpVoid := "call_void" FnRef TypeArgs? "{" CallArgs? "}"
| "call_void.indirect" ValueRef "{" CallArgs? "}"
| "setfield" "{" "obj" "=" ValueRef "," "field" "=" FieldName "," "val" "=" ValueRef "}"
| "panic" "{" "msg" "=" ValueRef "}"
| "ptr.store" "<" Type ">" "{" "p" "=" ValueRef "," "v" "=" ValueRef "}"
/* Array (void) */
| "arr.set" "{" "arr" "=" ValueRef "," "idx" "=" ValueRef "," "val" "=" ValueRef "}"
| "arr.push" "{" "arr" "=" ValueRef "," "val" "=" ValueRef "}"
| "arr.sort" "{" "arr" "=" ValueRef "}"
| "arr.foreach" "{" "arr" "=" ValueRef "," "fn" "=" ValueRef "}"
/* Map (void) */
| "map.set" "{" "map" "=" ValueRef "," "key" "=" ValueRef "," "val" "=" ValueRef "}"
| "map.delete_void" "{" "map" "=" ValueRef "," "key" "=" ValueRef "}"
/* StringBuilder (void) */
| "str.builder.append_str" "{" "b" "=" ValueRef "," "s" "=" ValueRef "}"
| "str.builder.append_i64" "{" "b" "=" ValueRef "," "v" "=" ValueRef "}"
| "str.builder.append_i32" "{" "b" "=" ValueRef "," "v" "=" ValueRef "}"
| "str.builder.append_f64" "{" "b" "=" ValueRef "," "v" "=" ValueRef "}"
| "str.builder.append_bool" "{" "b" "=" ValueRef "," "v" "=" ValueRef "}"
/* GPU device ops (void; only valid inside gpu fn) */
| "gpu.barrier"
| "gpu.buffer_store" "<" Type ">" "{" "buf" "=" ValueRef "," "idx" "=" ValueRef "," "v" "=" ValueRef "}" ;
CallArgs := CallArg ("," CallArg)* ;
CallArg := Ident "=" ArgValue ;
ArgValue := ValueRef | "[" (ArgListElem ("," ArgListElem)*)? "]" | FnRef ;
ArgListElem := ValueRef | FnRef ;
BinArgs := "{" "lhs" "=" ValueRef "," "rhs" "=" ValueRef "}" ;
CmpArgs := "{" "lhs" "=" ValueRef "," "rhs" "=" ValueRef "}" ;
CaptureList := CaptureItem ("," CaptureItem)* ;
CaptureItem := Ident "=" ValueRef ;
TypeCtor := NamedType "{" FieldInit ("," FieldInit)* "}" ;
FieldInit := FieldName "=" ValueRef ;
IcmpPred := "eq" | "ne" | "slt" | "sgt" | "sle" | "sge"
| "ult" | "ugt" | "ule" | "uge" ;
FcmpPred := "oeq" | "one" | "olt" | "ogt" | "ole" | "oge" ;
ExternAttr := Ident "=" StringLit ;
FqnRef := ModulePath "." ("@" Ident | "T" Ident)
| "@" Ident
| "T" Ident ;
ConstExpr := "const." Type ConstLit ;
ConstLit := IntLit | FloatLit | StringLit | "true" | "false" | "unit" ;
ValueRef := SSAName | ConstExpr ;
FnRef := FnName | QualifiedFnName ;
QualifiedFnName := ModulePath "." FnName ;
TypeRef := TypeName | ModulePath "." TypeName ;
PhiArgs := Type "{" PhiIncoming ("," PhiIncoming)* "}" ;
PhiIncoming := "[" BlockLabel ":" ValueRef "]" ;
FieldName := Ident ;
TypeArgs := "<" Type ("," Type)* ">" ;
Type := OwnershipMod? BaseType ;
OwnershipMod := "shared" | "borrow" | "mutborrow" | "weak" ;
BaseType := PrimType | NamedType | BuiltinType | CallableType | RawPtrType ;
RawPtrType := "rawptr" "<" Type ">" ;
PrimType := "i1" | "i8" | "i16" | "i32" | "i64" | "i128"
| "u1" | "u8" | "u16" | "u32" | "u64" | "u128"
| "f16" | "f32" | "f64" | "bool" | "unit" ;
BuiltinType := "Str" | "Array" "<" Type ">" | "Map" "<" Type "," Type ">"
| "TOption" "<" Type ">" | "TResult" "<" Type "," Type ">"
| "TStrBuilder"
| "TMutex" "<" Type ">" | "TRwLock" "<" Type ">" | "TCell" "<" Type ">"
| "TFuture" "<" Type ">"
| "TChannelSend" "<" Type ">" | "TChannelRecv" "<" Type ">" ;
NamedType := TypeRef TypeArgs? ;
CallableType := "TCallable" "<" TypeRef ">" ;
Doc := (";;;" .* newline)+ ;
Canonical style constraint: operations MUST use the { key=value } argument form, even when redundant, to prevent ambiguous tokenization.
new example:
%person: TPerson = new TPerson { name=%name_str, age=%age_val }
7.3 Semantics: evaluation order
- Instructions execute in program order within a basic block.
- SSA values are immutable once defined.
setfieldis a side-effecting instruction requiring mutable access.
7.4 Integer overflow (MUST)
- All default integer arithmetic (
i.add,i.sub,i.mul) is checked: overflow panics with a diagnostic message including the values and operation. i.add.wrap,i.sub.wrap,i.mul.wrapperform two's complement wrapping (defined behavior, no UB).i.add.checked,i.sub.checked,i.mul.checkedreturnTOption<T>—Someon success,Noneon overflow.- Division by zero always panics (for both
i.sdiv/i.udiv).
Why: Magpie guarantees no UB in safe code. Checked arithmetic is the default safety mechanism.
7.5 Float semantics (MUST)
- All default float operations (
f.add,f.sub, etc.) follow strict IEEE 754: NaN propagates, signed zeros preserved, denormals supported. Every float operation has defined behavior. f.add.fast,f.sub.fast, etc. opt into LLVM fast-math flags (loses NaN/inf determinism).- No UB — every float operation has defined behavior in both modes.
7.6 No implicit casts
- Any conversion must use an explicit opcode:
cast<i32, i64> { v=%x }, etc. Cast is restricted to primitive types in v0.1. - Canonical form always prints casts explicitly.
7.7 Function meta blocks (meta {}) (LLM locality feature)
A function MAY include:
meta {
uses { pkg.mod.@foo, std.io.@println, pkg.mod.TThing }
effects { io.write, alloc.heap, net.tcp, fs.read }
cost { approx_instructions=120, approx_allocs=2 }
}
Rules:
- In
--llmmode, the compiler SHOULD auto-generate and insert missing meta blocks (magpie fmt --fix-meta). - If present,
usesMUST be a complete set of referenced external symbols (excluding SSA locals). - If present,
effectsMUST match inferred effects; mismatch is a warning in v0.1, error in v1. - If present,
costis user-authored and verified with tolerance: if actual cost exceeds declared by >2x, emit warningMPL2002 COST_UNDERESTIMATE.
7.8 try opcode (error propagation)
The try opcode provides compact error propagation:
%user: TUser = try @get_user { id=%id }
The compiler desugars try to:
%result: TResult<TUser, TErr> = call @get_user { id=%id }%tag: i32 = enum.tag { v=%result }%is_err: bool = icmp.eq { lhs=%tag, rhs=const.i32 1 }cbr %is_err bb_err bb_ok- In
bb_ok:%user: TUser = enum.payload<Ok> { v=%result } - In
bb_err:ret %result(propagate the fullTResult<_,E>value unchanged)
The desugared form is what appears in MPIR. The function's return type MUST be TResult<T,E> to use try.
7.9 Hello world example
module hello.main
exports { @main }
imports { std.io::{@println} }
digest "0000000000000000"
fn @main() -> i32 {
bb0:
%msg: Str = const.Str "Hello, world!"
call_void std.io.@println { args=[%msg] }
ret const.i32 0
}
8. Type system (strong + explicit)
8.1 Type categories
8.1.1 Primitive value types
- Signed integers:
i1, i8, i16, i32, i64, i128 - Unsigned:
u1, u8, u16, u32, u64, u128 - Floats:
f16, f32, f64 boolis alias ofi1unitis the empty value
8.1.2 Aggregates (planned for v0.2)
vec<N, T>fixed-size SIMD vector (value type) — deferred to v0.2arr<N, T>fixed-size array (value type) — deferred to v0.2tuple<T0,T1,...>(value type) — deferred to v0.2
v0.1 restriction: These types exist in
TypeKindas internal-only representations (the compiler may use them for ABI lowering), but MUST NOT appear in surface.mpsyntax. The parser MUST reject them withMPT1021 AGGREGATE_TYPE_DEFERRED. HeapArray<T>(§8.1.3) is the v0.1 dynamic array type.
8.1.3 Heap-managed types (ARC)
StrArray<T>Map<K,V>TStrBuilder- user-defined
heap struct TName { ... } - user-defined
heap enum TName { ... } TCallable<TSig>(callable with captures)
8.1.4 Lang items (compiler-known, auto-available)
TOption<T>— builtin value enum with variants:None { }Some { field v: T }
TResult<T,E>— builtin value enum with variants:Ok { field v: T }Err { field e: E }
These are the only value enums supported in v0.1. User-defined value enum remains deferred to v0.2.
Layout rules for lang items (codegen-visible; semantic model is value enum):
TOption<T>whenTis a heap handle type: niche optimization (NULL = None, non-NULL = Some)TOption<T>whenTis a non-handle value type: tagged{ i1 tag; T payload }(0=None, 1=Some)TResult<T,E>: tagged{ i1 tag; union { T ok; E err } }(0=Ok, 1=Err)
Ownership + ARC notes:
TOption/TResultvalues are not ARC-managed heap objects themselves, but they may contain heap handles.- If the payload contains heap handles, the compiler MUST apply move/borrow rules + ARC insertion/drop elaboration to those payload handles.
enum.tag,enum.is,enum.payload, andenum.newapply to both heap enums and these builtin value enums.
8.2 Ownership modifiers (compile-time)
Ownership is part of the type:
T(unique owned handle — default for heap-managed types)borrow T(scoped shared borrow; v0.1 borrows are non-escaping)mutborrow T(scoped exclusive borrow; v0.1 borrows are non-escaping)shared T(explicit shareable ARC handle; clonable; thread-safe; immutable by default) — heap-managed onlyweak T(non-owning weak reference; may be null-like) — heap-managed onlyrawptr<T>(unsafe-only raw pointer type; only usable insideunsafe {})
Rules:
sharedandweakare only valid whenTis heap-managed.borrow/mutborroware valid for any typeTand represent a scoped reference to a value of typeT.- For heap-managed
T,borrow T/mutborrow Tare non-owning references to the heap object. - For value types (including builtin value enums like
TOption/TResult),borrow T/mutborrow Trefer to an in-memory slot (stack/local spill, heap field, or container element).
- For heap-managed
8.3 Type declarations
8.3.1 Heap struct
heap struct TPerson {
field name: Str
field age: i32
}
Rules:
- Heap structs are ARC-managed.
- Fields may be value types or heap types.
8.3.2 Value struct
value struct TVec2 {
field x: f32
field y: f32
}
Rules:
- Value structs copy by value.
- No ARC actions for value types.
- In v0.1, value struct fields MUST NOT include heap handles. Violation:
MPT1005 VALUE_TYPE_CONTAINS_HEAP.
8.3.3 Enums
heap enum TShape {
variant Circle { field radius: f64 }
variant Rect { field w: f64, field h: f64 }
}
Rules:
- Heap enums are ARC-managed (payload may be heap/value).
- Enum access/construction is via dedicated opcodes:
enum.new<VariantName>,enum.tag,enum.payload<VariantName>,enum.is<VariantName>. - User-defined value enums are deferred to v0.2. The grammar accepts
value enumbut the compiler MUST reject it withMPT1020 VALUE_ENUM_DEFERRED. The only value enums available in v0.1 are the builtin lang itemsTOptionandTResult(§8.1.4).
enum.new construction rules (MUST):
- Surface form:
%v: TEnum = enum.new<VariantName> { field1=%x, field2=%y } - The result type annotation determines the enum type (
TEnum). The compiler MUST rejectenum.newif the result type is not an enum type. VariantNameMUST exist for that enum type.- The provided
key=valuepairs MUST match the variant's fields exactly (same names, same count). Order is irrelevant; canonical formatting sorts by field name. - For empty variants, the arg block MUST be present but MAY be empty:
enum.new<None> { }.
Semantics:
- For heap enums: allocate a new enum object, set the tag, write payload fields, and return a unique handle to the new enum object.
- For builtin value enums (
TOption,TResult): construct the value directly according to §8.1.4 layout rules (no heap allocation).
8.3.4 Recursive types
- Heap struct/enum types can reference themselves or each other, but self-referential fields MUST use explicit indirection:
TOption<T>orweak T. - Direct
field next: TNodewithinTNodeis forbidden — usefield next: TOption<TNode>instead. - Value structs cannot be recursive (compiler error
MPT1010 RECURSIVE_VALUE_TYPE).
8.4 Generics (v0.1 target)
Magpie MUST support monomorphization generics for:
Array<T>,Map<K,V>- user-defined types and functions
Constraints:
type(any type)send,sync(concurrency markers)hash,eq,ord(behavioral traits with impl blocks; see §9)
8.5 Type inference policy
- Surface Magpie MUST NOT infer types of SSA values; they MUST be annotated.
- The compiler MAY infer some generic parameters only when the call site provides all needed information; canonical form SHOULD print all instantiated types explicitly.
8.6 Monomorphization controls
- Default is full monomorphization (fast, no vtables).
--shared-genericsflag switches to vtable-based approach for specific functions to reduce binary size.- ABI compatibility rule: all packages in a binary MUST use the same generics mode. Mixed compilation (some packages with
--shared-generics, others without) is forbidden. The linker MUST reject mixed-mode object files withMPL2021 MIXED_GENERICS_MODE. Detection mechanism: the compiler emits a global symbolmp$0$ABI$generics_modewith value 0 (monomorphized) or 1 (shared). The linker checks all object files for this symbol and rejects if values differ. [build].max_mono_instancesinMagpie.tomlsets a budget. Compiler errorsMPL2020 EXCESSIVE_MONOif exceeded. This budget is global across all packages in the build — each package contributes to the shared count.- In release profile, LTO deduplicates identical instantiations.
9. Trait system (minimal, explicit)
9.1 Overview
Magpie v0.1 has a minimal trait system supporting:
- Marker traits:
send,sync - Behavioral traits:
hash,eq,ord
9.2 Trait declarations (built-in only in v0.1)
Traits are compiler-known in v0.1. The built-in traits are:
hash— requiresfn @hash_<Type>(%self: borrow <Type>) -> u64eq— requiresfn @eq_<Type>(%a: borrow <Type>, %b: borrow <Type>) -> boolord— requiresfn @ord_<Type>(%a: borrow <Type>, %b: borrow <Type>) -> i32(returns -1, 0, 1)send— marker, no function requiredsync— marker, no function required
9.3 Impl declarations
Standalone binding syntax — functions defined separately:
;;; Bind trait implementations
impl hash for TPerson = @hash_TPerson
impl eq for TPerson = @eq_TPerson
;;; Implementation function (normal global fn)
fn @hash_TPerson(%self: borrow TPerson) -> u64 {
bb0:
%name_hash: u64 = call std.hash.@hash_Str { args=[%self_name] }
; ...
ret %h
}
Rules:
impl Trait for Type = @functionbinds a trait to a type with a specific function.- The function MUST match the trait's required signature.
- The function is a normal global function accessible by its FQN.
9.4 Dispatch model
- All trait dispatch is monomorphized (static).
- At monomorphization time, the compiler resolves
call @hash { k=%k }to the concretecall @hash_TMyKey { k=%k }based on the impl binding. - No vtables in the default mode.
- LLMs see the exact function being called in MPIR output.
9.5 Coherence (strict orphan rules)
- An
impldeclaration is only valid if the current package owns either the trait or the type (or both). - No implementing foreign traits for foreign types.
- Violation:
MPT1200 ORPHAN_IMPL.
9.5 Runtime callback ABI for hash / eq / ord (v0.1)
Some runtime collection operations (Map hashing/equality, Array contains, Array sort) call back into compiler-generated functions.
For each concrete type T that is used with these operations, the compiler MUST generate C-ABI wrappers with the following signatures:
// The pointers point to an in-memory value with layout identical to Magpie type `T`.
uint64_t mp_cb_hash_T(const void* x_bytes);
int32_t mp_cb_eq_T(const void* a_bytes, const void* b_bytes); // 0=false, 1=true
int32_t mp_cb_cmp_T(const void* a_bytes, const void* b_bytes); // negative / 0 / positive
Lowering contracts (normative):
map.new<K,V>passeshash_fn = &mp_cb_hash_Kandeq_fn = &mp_cb_eq_Ktomp_rt_map_new.arr.contains<T>passeseq_fn = &mp_cb_eq_Ttomp_rt_arr_contains.arr.sort<T>passescmp = &mp_cb_cmp_Ttomp_rt_arr_sort.
Determinism requirements:
- These wrappers MUST be pure (no I/O, no mutation, no nondeterminism) and MUST return the same result for the same input bytes.
- For heap handle types (e.g.,
Str), the wrapper reads the handle and hashes/compares the underlying object content.
10. Ownership and borrow checking (Rust-like guarantees)
10.1 Core invariants
Magpie enforces Rust-like aliasing for move-only values, and allows free duplication for copy values.
Definitions (v0.1):
- Copy types: primitives,
rawptr<T>, andvalue structtypes whose fields are all Copy. - Move-only types: all heap handles (
T,shared T,weak T) and any value type that contains a move-only field (including many instantiations ofTOption/TResult).
Invariants for move-only values:
- A unique handle
Thas exclusive ownership. - A
borrow Tmay coexist with otherborrow Tborrows, but not withmutborrow T. - A
mutborrow Tis exclusive and cannot coexist with any other borrow. - No borrow may outlive the scope in which it was created (non-escaping).
- A move-only value cannot be used after it is moved.
- Borrows are block-local: MUST NOT appear in
phi, MUST NOT cross basic block boundaries.
Invariants for Copy values:
- Copy values may be used multiple times with no move tracking.
- Creating borrows of Copy values is allowed, but still subject to the borrow lifetime restrictions in §10.5.
10.2 Move semantics
- Assignment/consumption of any move-only value is a move (transfers ownership), not an implicit clone.
- After move, the source becomes invalid (use-after-move is an error).
- Copy types are exempt.
10.3 Explicit cloning/sharing
- Convert unique → shared:
%s: shared TPerson = share { v=%p }(consumes %p) - Clone shared:
%s2: shared TPerson = clone.shared { v=%s }(does NOT consume %s) - Clone weak:
%w2: weak TPerson = clone.weak { v=%w }(does NOT consume %w) - No implicit aliasing.
10.4 Mutation rules
setfieldrequires operand to bemutborrow Tor uniqueTwith no active borrows.- Mutating a
shared Tis forbidden in v0.1 (useTMutex<T>for interior mutability).
10.4.1 Storable types and projection reads (unique-in-storage)
Magpie v0.1 allows heap handles (including unique handles) to be stored inside heap fields and collections. Containers/fields own their stored values.
Storable types (MUST)
- A type is storable if it may appear in:
- heap struct/enum fields
Array<T>elementsMap<K,V>keys/values- globals
- In v0.1, all types are storable except
borrow Tandmutborrow T.- Attempting to store a borrow MUST error:
MPO0003 BORROW_ESCAPES_SCOPE.
- Attempting to store a borrow MUST error:
Store/write semantics (MUST)
- Writing a value into a field/array/map consumes the input value (move-in). There are no implicit clones.
- Overwriting an existing stored value MUST drop the old value first (releasing any contained heap handles), then move-in the new value.
- To store multiple references to the same heap object, code MUST explicitly use
share,clone.shared,weak.downgrade, and/orclone.weak.
Read/projection semantics (MUST)
Reading from storage MUST NOT transfer ownership. Therefore, read operations return:
- A value copy for Copy types.
- A borrow for move-only types (including heap handles and non-Copy value enums/structs).
getfield rules
- Operand type requirement:
objMUST beborrow TStructormutborrow TStruct.
- Result type rule, based on the field type
F:- If
Fis Copy:getfieldreturnsFby value. - If
Fis a strong heap handle:- If
Fisshared T:getfieldreturnsborrow T(shared borrow of the underlying heap object). - If
FisT(a unique strong handle):- If
objisborrow TStruct: result isborrow T. - If
objismutborrow TStruct: result ismutborrow T(exclusive borrow of the underlying heap object).
- If
- If
- If
Fis a weak heap handle (weak T):getfieldreturnsweak Tby cloning the weak handle (compiler insertsarc.retain_weak). - Otherwise (
Fis move-only value type, e.g.TOption<T>with move-only payload):getfieldreturnsborrow F(borrow of the field slot).
- If
arr.get rules
- Operand type requirement:
arrMUST beborrow Array<T>ormutborrow Array<T>.
- Result type rule is the same as
getfield, withF := T(the element type).
map.get rules (v0.1)
- Operand type requirement:
mapMUST beborrow Map<K,V>ormutborrow Map<K,V>.
- Return type (v0.1):
- If
Vis Copy:TOption<V>. - If
Visweak T:TOption<weak T>(weak handle is cloned on success). - If
Visshared T:TOption<shared T>(strong handle is cloned on success; compiler insertsarc.retain_strong). - Otherwise (including unique strong handles
Tand move-only value types): forbidden in safe v0.1, because it would require returning borrows insideTOption.- Violation:
MPO0103 MAP_GET_REQUIRES_DUPABLE_V. - Use
map.contains_key+map.get_ref(below), ormap.deleteto move the value out, or unsafe code.
- Violation:
- If
map.get_ref rules (v0.1)
- Operand type requirement:
mapMUST beborrow Map<K,V>ormutborrow Map<K,V>.
- Semantics:
- Panics if the key is not present (mirrors
arr.getpanicking on out-of-bounds). - Otherwise returns a projection of the stored value without transferring ownership.
- Panics if the key is not present (mirrors
- Result type rule:
- Same as
arr.get, withT := V.
- Same as
Duplication-producing collection intrinsics (v0.1 constraints)
The following intrinsics allocate a new collection and therefore duplicate elements/keys/values:
arr.slice,arr.filter,map.keys,map.values.
They require the relevant element/key/value types to be Dupable.
Dupable(v0.1):Copytypes,shared Tstrong handles, andweak Thandles.- Unique strong handles
Tare not Dupable. - Types that contain borrows (directly or indirectly) are never Dupable.
Violation: MPT1022 COLLECTION_DUPLICATION_REQUIRES_DUPABLE.
Trait requirements for collection algorithms (v0.1)
arr.containsrequiresimpl eq for T.arr.sortrequiresimpl ord for T.map.newrequiresimpl hash for Kandimpl eq for K.
Violation: MPT1023 MISSING_REQUIRED_TRAIT_IMPL.
Move-out operations
arr.popmoves an element out of the array (returns an ownedTOption<T>).map.deletemoves the removed value out of the map (returns an ownedTOption<V>).map.delete_voiddeletes the entry and drops the removed value (returnsunit).- These are the only stable, v0.1-supported ways to extract owned move-only values from collections without unsafe code.
10.5 Lifetime model (v0.1)
- Lifetimes are lexical scopes only (no named lifetimes).
- Borrows cannot be returned from functions in v0.1, except compiler-known interior-mutability intrinsics (
@mutex_lock,@rwlock_read,@rwlock_write,@rwlock_unlock) which return scoped borrows with automatic scope-end release semantics (§13.3.1). These are not considered function-return borrows for the purposes of this rule — the compiler inserts the corresponding unlock/release at the borrow's scope end. - Borrows cannot be stored into heap fields, arrays, or globals. Violation:
MPO0003 BORROW_ESCAPES_SCOPE. - Borrows cannot cross basic block boundaries. Violation:
MPO0101 BORROW_CROSSES_BLOCK. - Borrows cannot appear in phi nodes. Violation:
MPO0102 BORROW_IN_PHI.
10.6 Formal ownership dataflow (moved-set analysis)
The complete formal specification follows:
- Domain:
Moved ⊆ OwhereO= set of all locals whose types are move-only (heap handles and non-Copy value types). Copy-typed locals are excluded fromO. - Transfer rules: Rule U (use-after-move check), Rule C (consume and add to moved set)
- Join rule at CFG merge:
Moved_in[B] = ⋃ Moved_out[Pi](union = "maybe moved") - Phi incoming use semantics: phi is treated as consuming the incoming value on each edge
10.7 Formal borrow checking (block-local, linear scan)
For each owned local v ∈ O, maintain within each block:
SharedCount[v]: u32MutActive[v]: bool
Borrow creation:
BorrowShared: requiresMutActive[v] == false, incrementsSharedCount[v]BorrowMut: requiresSharedCount[v] == 0andMutActive[v] == false, setsMutActive[v] = true
Borrow end (at last use):
- Shared:
SharedCount[v] -= 1 - Mut:
MutActive[v] = false
Moves while borrowed: any Consume(v) requires SharedCount[v] == 0 and MutActive[v] == false. Violation: MPO0011 MOVE_WHILE_BORROWED.
10.8 Call argument mode
- By-value parameter whose type is move-only (heap handles and non-Copy value types): argument is consumed.
- By-value parameter whose type is Copy: argument is copied (not consumed).
borrowparameter: temporary shared borrow lasting only for the call.mutborrowparameter: temporary mutable borrow lasting only for the call.
10.9 Collection intrinsic ownership requirements
Collection-mutating intrinsics follow the same ownership rules as setfield:
| Ownership required | Intrinsics |
|---|---|
| unique or mutborrow | arr.set, arr.push, arr.pop, arr.sort, map.set, map.delete, map.delete_void, str.builder.append_* |
| borrow (read-only) | arr.len, arr.get, arr.slice, arr.contains, arr.map, arr.filter, arr.reduce, arr.foreach, map.len, map.get, map.get_ref, map.contains_key, map.keys, map.values, str.len, str.eq, str.slice, str.bytes |
| consumes (moves) | arr.new, map.new, str.concat, str.builder.new, str.builder.build |
- Mutating intrinsics on
sharedreferences are forbidden — the ownership checker MUST reject them withMPO0004 SHARED_MUTATION. arr.map/arr.filter/arr.reduce/arr.foreachonly borrow the array; thefunc(TCallable) argument is borrowed for the call duration.
11. ARC memory model (Swift-like insertion, with Magpie ownership)
11.1 Key idea
ARC manages lifetime, while ownership checking manages aliasing safety.
- Unique handles still use refcounted allocation under the hood.
- Most refcounts are 1 unless explicitly shared.
11.2 Runtime representation
Every heap object has:
- header: strong refcount (atomic), weak refcount, type id, flags, reserved
- payload bytes
11.3 ARC operations
arc.retain/arc.release(strong)arc.retain_weak/arc.release_weak
11.4 ARC insertion rules
newinitializes strong=1, no retain needed.shareconsumes Unique, produces Shared WITHOUT changing count (type reclassification).clone.sharedemitsarc.retain.clone.weakemitsarc.retain_weak.weak.downgradeemitsarc.retain_weak.- End of scope for strong handle: emit
arc.release. - Field/element overwrite: drop old, then move in new. There is no implicit retain of the new value; retains only occur when the IR explicitly clones (
clone.shared,clone.weak,weak.downgrade) or when a spec-defined read clones a weak handle (see §10.4.1).
11.5 Drop elaboration
- Drop functions are compiler-generated only — no user-defined destructors.
- Drop recursively releases all contained heap handles (including handles nested inside value enums like
TOption/TResultand inside nested value structs). - Types needing custom cleanup (files, connections) require explicit
call @close { handle=%h }before scope exit. - For values (including
TOption/TResult) that die without being moved: insert the appropriate releases for any contained handles (arc.releasefor strong,arc.release_weakfor weak), using the compiler-generated drop logic. - For values that are moved: no release inserted (ownership transferred).
11.6 ARC optimization passes
MUST implement:
- retain/release pairing elimination in straight-line code
- CFG-aware ARC: sink releases, hoist retains, eliminate redundant retains on same value
- Optional: no-escape stack promotion
- Drop function deduplication: LLVM ICF (Identical Code Folding) during LTO SHOULD deduplicate structurally identical drop functions. The compiler MAY also detect identical drop bodies at the MPIR level and emit a single shared drop function with multiple type_id aliases.
11.7 Cycle handling
weak Ttype modifier for cycle breaking.- Weak references do not keep object alive.
- Weak can be upgraded to
TOption<shared T>viaweak.upgrade.
11.8 Threading + ARC
shared Tstrong refcount operations MUST be atomic.- Unique
Tstrong refcount operations MAY be non-atomic. - Converting
T -> shared TMUST upgrade to atomic refcounting.
12. Async model (stackless coroutines)
12.1 Overview
Magpie uses stackless coroutines with explicit suspend.call and suspend.await opcodes. Every suspension point is visible in the source code. The compiler generates state machines.
12.2 Async function declaration
async fn @fetch_user(%id: u64) -> TUser {
bb0:
%conn: TDbConn = suspend.call @db.@connect {}
%row: TRow = suspend.call @db.@query { conn=%conn, id=%id }
%user: TUser = call @parse_user { row=%row }
ret %user
}
Rules:
async fndeclares an async function.suspend.callis a suspension opcode — the function yields to the executor at this point.suspend.callandsuspend.awaitMUST appear only insideasync fnbodies. Violation:MPAS0001 SUSPEND_IN_NON_ASYNC.suspend.awaitis a suspension opcode that awaits an existingTFuture<T>value (see §12.4).- The compiler generates a state machine that saves/restores state across suspend points.
- Every suspension point is a visible
suspend.callinstruction — no hidden suspend.
12.3 Async + ARC interaction
- The compiler generates a heap struct per async function containing all SSA values that are live across suspend points.
- On suspend, live values are moved into the state struct (ARC-managed).
- On resume, values are moved back out.
- The state struct IS the future object.
- Drop of the future releases all captured values (compiler-generated drop).
- The runtime provides the executor that manages scheduling and polling.
12.4 Async return type
async fn @f(...) -> Tactually returns a future type internally.- Callers use
suspend.callto await an async function call. - Callers use
suspend.await { fut=... }to await an existingTFuture<T>value. - Non-async callers cannot call async functions directly — use
call std.async.@block_on { fn=@f, args=[...] }.
12.4.1 suspend.await (MUST)
suspend.await awaits an existing future value.
- Surface form:
%v: T = suspend.await { fut=%f } - Requirements:
- MUST appear only inside an
async fn. %fMUST have typeTFuture<T>(orshared TFuture<T>), and is consumed by the await.
- MUST appear only inside an
- Semantics:
- If the future is not ready, the current async state machine stores
%fin its state and yields control to the executor. - When resumed, it re-polls
%funtil ready, then produces%vand drops the awaited future.
- If the future is not ready, the current async state machine stores
13. Concurrency
13.1 Threading model
Magpie exposes OS threads + channels:
std.thread.@spawn— spawns an OS thread, takes aTCallableor function pointerstd.sync.TChannel<T>— typed channel for message passingstd.sync.TMutex<T>— mutex for shared mutable statestd.sync.TRwLock<T>— reader-writer lockstd.sync.TCell<T>— single-threaded interior mutability
13.2 Send/Sync enforcement
sendtrait: type can be transferred across thread boundaries.synctrait: type can be shared (viashared) across threads.- The type checker enforces
send/syncat thread API boundaries:std.thread.@spawnrequiresTCallable<TSig>where captured values aresend.shared TrequiresT: syncfor cross-thread sharing.
13.3 Interior mutability types
TMutex<T>,TRwLock<T>,TCell<T>are compiler-known types with special runtime support.- They allow controlled mutation of
sharedreferences. TMutexandTRwLockaresync(safe for cross-thread sharing).TCellis NOTsync(single-threaded only).
13.3.1 Interior mutability intrinsic signatures
| Intrinsic | Signature | Semantics |
|---|---|---|
@mutex_new<T> | (T) -> TMutex<T> | Create mutex wrapping initial value |
@mutex_lock<T> | (borrow TMutex<T>) -> mutborrow T | Acquire lock; returns scoped mutable borrow |
@mutex_unlock<T> | (borrow TMutex<T>) -> unit | Release lock (compiler-inserted at borrow scope end) |
@rwlock_new<T> | (T) -> TRwLock<T> | Create reader-writer lock |
@rwlock_read<T> | (borrow TRwLock<T>) -> borrow T | Acquire read lock |
@rwlock_write<T> | (borrow TRwLock<T>) -> mutborrow T | Acquire write lock |
@rwlock_unlock<T> | (borrow TRwLock<T>) -> unit | Releases the held lock (read or write). The compiler MUST also insert an implicit unlock when the borrow/mutborrow returned from @rwlock_read/@rwlock_write ends (v0.1: end of basic block). |
@cell_get<T> | (borrow TCell<T>) -> T | Copy value out (T must be Copy-like value type) |
@cell_set<T> | (borrow TCell<T>, T) -> unit | Replace interior value |
@mutex_lock returns a mutborrow T with a scoped guard pattern: the compiler inserts @mutex_unlock at the end of the borrow's scope. This mirrors Rust's MutexGuard but is explicit in the IR.
13.3.2 Runtime ABI for interior mutability
MpRtHeader* mp_rt_mutex_new(uint32_t type_id, void* initial_value, uint64_t size);
void* mp_rt_mutex_lock(MpRtHeader* mutex); // returns pointer to payload
void mp_rt_mutex_unlock(MpRtHeader* mutex);
MpRtHeader* mp_rt_rwlock_new(uint32_t type_id, void* initial_value, uint64_t size);
const void* mp_rt_rwlock_read(MpRtHeader* rwlock);
void* mp_rt_rwlock_write(MpRtHeader* rwlock);
void mp_rt_rwlock_unlock(MpRtHeader* rwlock);
void mp_rt_cell_get(MpRtHeader* cell, void* out, uint64_t size);
void mp_rt_cell_set(MpRtHeader* cell, const void* val, uint64_t size);
13.4 Typed channels (TChannel<T>)
Channels provide typed message passing between threads.
%pair: TChannelPair<i32> = call std.sync.@channel_new<i32> {}
%pair_b: borrow TChannelPair<i32> = borrow.shared { v=%pair }
%sender: borrow TChannelSend<i32> = getfield { obj=%pair_b, field=send }
%receiver: borrow TChannelRecv<i32> = getfield { obj=%pair_b, field=recv }
TChannelPair<T> is a heap struct: heap struct TChannelPair<T: type> { field send: TChannelSend<T>, field recv: TChannelRecv<T> }
| Intrinsic | Signature | Semantics |
|---|---|---|
@channel_new<T> | () -> TChannelPair<T> | Create an unbounded MPSC channel pair |
@channel_send<T> | (borrow TChannelSend<T>, T) -> unit | Send a value (moves T into channel) |
@channel_recv<T> | (borrow TChannelRecv<T>) -> TOption<T> | Receive a value (blocks until available; returns None if sender dropped) |
TChannelSend<T>issend + sync(clonable for multiple producers).TChannelRecv<T>issendbut NOTsync(single consumer).Tmust besend.
Runtime ABI:
MpRtHeader* mp_rt_channel_new(uint32_t elem_type_id, uint64_t elem_size); // returns pair struct
void mp_rt_channel_send(MpRtHeader* sender, const void* val, uint64_t elem_size);
int32_t mp_rt_channel_recv(MpRtHeader* receiver, void* out, uint64_t elem_size); // 0=closed, 1=ok
14. TCallable and signature types
14.1 Signature declarations
sig THandlerSig(TRequest, TContext) -> TResponse
- Defines a named function signature type.
- Referenced as
TCallable<THandlerSig>. - Signatures appear in the
signamespace.
14.2 TCallable type
TCallable<TSig>is an ARC-managed heap type wrapping a function pointer + optional captured environment.- Supports vtable-dispatched calls.
14.2.1 TCallable memory layout
TCallable<TSig> runtime layout:
┌─────────────────────────┐
│ MpRtHeader (32 bytes) │ ← standard ARC header
├─────────────────────────┤
│ vtable_ptr: *const TCallableVtable │
│ data_ptr: *mut u8 │ ← pointer to captured environment (or null)
└─────────────────────────┘
TCallableVtable:
┌─────────────────────────┐
│ call_fn: fn_ptr │ ← matches TSig; first implicit arg is data_ptr
│ drop_fn: fn_ptr │ ← drops captured environment
│ size: u64 │ ← size of captured data (for debug/introspection)
└─────────────────────────┘
call_fnsignature: the TSig parameter types prepended with*mut u8(the data_ptr for captures).drop_fnsignature:fn(*mut u8) -> void— releases any ARC-managed captures.- When there are no captures,
data_ptris NULL anddrop_fnis a no-op.
14.2.2 TCallable + async restriction (v0.1)
suspend.call on TCallable is forbidden in v0.1. The compiler MUST reject suspend.call %callable { ... } where the callee is a TCallable with error MPT1030 TCALLABLE_SUSPEND_FORBIDDEN.
Rationale: TCallable uses vtable dispatch, and the async state machine generator cannot determine the coroutine frame layout through a vtable indirection. Async middleware MUST use a regular async function reference, not TCallable. This restriction may be lifted in v0.2 with runtime-typed coroutine frames.
14.3 Creating TCallable
User code can create TCallable via callable.capture:
%mul_fn: TCallable<TMulSig> = callable.capture @multiply_by { n=%n }
- Captured values are moved into the TCallable (consuming them).
- To keep using a value after capture, clone it first.
- The runtime can also create TCallable instances with opaque captures (e.g., for middleware chains).
14.4 Calling TCallable
%result: TReturn = call.indirect %my_callable { args=[%arg1, %arg2] }
TCallable values are callable via call.indirect (or call_void.indirect).
14.5 Functional intrinsics with TCallable
Array/Map functional operations accept TCallable:
%mul_fn: TCallable<TMapI32Sig> = callable.capture @multiply_by { factor=%n }
%result: Array<i32> = arr.map { arr=%a, fn=%mul_fn }
15. Core Magpie IR (MPIR) spec
15.1 Overview
MPIR is the compiler's canonical mid-level IR:
- SSA values
- Explicit basic blocks and terminators
- Ownership states attached to values
- ARC operations explicit after insertion pass
- Resolved SIDs and TypeIds
15.2 MPIR file format
- Extension:
.mpir - Encoding: UTF-8
- Newlines:
\nonly (CSNF normalized)
15.3 MPIR file header (MUST)
mpir.version 0.1
module pkg.sub.module
module_sid "M:XXXXXXXXXX"
module_digest "<blake3hex>"
target "<llvm-triple>" ; optional
15.4 Sections and order (MUST)
- header
types { ... }externs { ... }globals { ... }fns { ... }
Omitted sections MUST still appear empty.
15.5 MPIR type table
types {
type_id 4 = prim i32
type_id 20 = heap_builtin Str
type_id 1000 = heap_struct TPerson {
field name : type_id 20
field age : type_id 4
} layout { size=16 align=8 fields { name=0 age=8 } }
}
Type table entry forms (MUST):
- Primitives:
type_id N = prim i32
- Heap builtins (no type parameters):
type_id N = heap_builtin Strtype_id N = heap_builtin StrBuilder
- Heap builtins (with type parameters):
- Arrays:
type_id N = heap_builtin Array { elem=type_id X } - Maps:
type_id N = heap_builtin Map { key=type_id K, val=type_id V } - Futures:
type_id N = heap_builtin TFuture { result=type_id X } - Channels:
type_id N = heap_builtin ChannelSend { elem=type_id X },type_id N = heap_builtin ChannelRecv { elem=type_id X } - Interior mutability:
type_id N = heap_builtin Mutex { inner=type_id X },RwLock { inner=type_id X },Cell { inner=type_id X } - Callables:
type_id N = heap_builtin TCallable { sig=@SigName, caps=[type_id A, ...] }
- Arrays:
- Builtin value enums (lang items):
type_id N = builtin TOption { inner=type_id X }type_id N = builtin TResult { ok=type_id A, err=type_id B }
- Raw pointers:
type_id N = rawptr { to=type_id X }
- Handle qualifiers (for heap handle types):
type_id N = shared { inner=type_id X }type_id N = weak { inner=type_id X }
- Borrow qualifiers:
type_id N = borrow { inner=type_id X }type_id N = mutborrow { inner=type_id X }
- User types:
heap_struct/heap_enumentries as shown above. Generic instantiations MAY includetargs=[...]for debugging.
Notes:
@SigNameis the fully-qualified signature name (a Sid) declared viasig.
Type IDs MUST be assigned deterministically across the build graph: fixed IDs for primitives/builtins, then user types by fully-qualified name (FQN) lexicographic order, then monomorphized instances by canonical type string.
15.6 MPIR functions
fns {
fn @main sid "F:XXXXXXXXXX" sigdigest "<blake3hex>" inst_id "I:base" ( ) -> type_id 1
meta { uses { ... } effects { ... } cost { ... } }
{
bb0:
%msg : type_id 20 = const.type_id 20 "hello"
call_void sid "F:YYYYYYYYYY" @std.io.@println { targs=[], args=[%msg] }
ret const.type_id 4 0
}
}
15.7 Required opcodes in MPIR v0.1
const.*- Integer ops:
i.add i.sub i.mul i.sdiv i.udiv i.srem i.urem i.add.wrap i.sub.wrap i.mul.wrap i.add.checked i.sub.checked i.mul.checked i.and i.or i.xor i.shl i.lshr i.ashr - Float ops:
f.add f.sub f.mul f.div f.rem f.add.fast f.sub.fast f.mul.fast f.div.fast - Compares:
icmp.* fcmp.* - Control flow:
br cbr switch ret unreachable phi - Calls:
call call_void call.indirect call_void.indirect try suspend.call suspend.await - Heap:
new getfield setfield - Enum:
enum.new enum.tag enum.payload enum.is - Ownership:
share clone.shared clone.weak weak.downgrade weak.upgrade - ARC:
arc.retain arc.release arc.retain_weak arc.release_weak - Unsafe pointers:
ptr.null ptr.addr ptr.from_addr ptr.add ptr.load ptr.store - Collections:
arr.* map.* str.* - JSON:
json.encode json.decode - GPU:
gpu.*(required when a module usesgpuor when--emit=spvis requested) - Callable:
callable.capture - Error:
panic
15.8 MPIR verifier (MUST)
magpie mpir verify MUST check:
- SSA correctness (each name defined once, all uses dominated by defs)
- Type IDs exist in type table
- SIDs are valid format
- Call arity matches signature in symbol table
- ARC ops appear only after ARC insertion stage
- Phi nodes only for value types and Unique/Shared/Weak handles (not Borrow/MutBorrow)
- Block labels canonical ascending
- Each block ends with exactly one terminator
16. HIR specification (Rust data model)
16.1 Core ID types
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct PackageId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct ModuleId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct DefId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct TypeId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct InstId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct FnId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct GlobalId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct LocalId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct BlockId(pub u32);
16.2 Primitive types and stable IDs
/// Primitive type enum — covers all surface-level primitive types.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum PrimType {
I1, I8, I16, I32, I64, I128,
U1, U8, U16, U32, U64, U128,
F16, F32, F64,
Bool, // alias for I1
Unit,
}
/// Stable ID — a content-addressed identifier for modules, functions, types, globals.
/// Format: `<Kind>:<10 chars>` where Kind ∈ {M, F, T, G, E}.
/// The suffix is `base32_crockford(blake3(input))[0..10]`.
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct Sid(pub String);
impl Sid {
/// Regex for validation: `^[MFTGE]:[0-9A-Z]{10}$`
pub fn is_valid(&self) -> bool {
// runtime check against the regex
self.0.len() == 12
&& matches!(self.0.as_bytes()[0], b'M' | b'F' | b'T' | b'G' | b'E')
&& self.0.as_bytes()[1] == b':'
}
}
/// A reference to an SSA value — either a local (SSA name) or an inline constant.
#[derive(Clone, Debug)]
pub enum HirValue {
Local(LocalId),
Const(HirConst),
}
/// A compile-time constant value.
#[derive(Clone, Debug)]
pub struct HirConst {
pub ty: TypeId,
pub lit: HirConstLit,
}
#[derive(Clone, Debug)]
pub enum HirConstLit {
IntLit(i128),
FloatLit(f64),
BoolLit(bool),
StringLit(String),
Unit,
}
16.3 Type structure
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum HandleKind {
Unique, Shared, Borrow, MutBorrow, Weak,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum TypeKind {
Prim(PrimType),
// Heap-managed handles
HeapHandle { hk: HandleKind, base: HeapBase },
// Builtin value enums (lang items)
BuiltinOption { inner: TypeId },
BuiltinResult { ok: TypeId, err: TypeId },
// Unsafe raw pointer type
RawPtr { to: TypeId },
// Aggregate value types (internal-only in v0.1 surface syntax)
Arr { n: u32, elem: TypeId },
Vec { n: u32, elem: TypeId },
Tuple { elems: Vec<TypeId> },
ValueStruct { sid: Sid },
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum HeapBase {
BuiltinStr,
BuiltinArray { elem: TypeId },
BuiltinMap { key: TypeId, val: TypeId },
BuiltinStrBuilder,
BuiltinMutex { inner: TypeId },
BuiltinRwLock { inner: TypeId },
BuiltinCell { inner: TypeId },
BuiltinFuture { result: TypeId },
BuiltinChannelSend { elem: TypeId },
BuiltinChannelRecv { elem: TypeId },
Callable { sig_sid: Sid },
UserType { type_sid: Sid, targs: Vec<TypeId> },
}
16.4 HIR operations (v0.1 set)
Note on
Move,BorrowShared,BorrowMut: These are compiler-inserted during HIR lowering from the ownership checker, not surface opcodes. Surface code usesborrow.sharedandborrow.mutexplicitly to create borrows; the compiler then verifies and lowers them to the HIR variants below.
pub enum HirOp {
Const(HirConst),
// Ownership (compiler-inserted during HIR lowering)
Move { v: HirValue },
BorrowShared { v: HirValue },
BorrowMut { v: HirValue },
// Heap and fields
New { ty: TypeId, fields: Vec<(String, HirValue)> },
GetField { obj: HirValue, field: String },
SetField { obj: HirValue, field: String, value: HirValue },
// Integer arithmetic (checked — default, traps on overflow)
IAdd { lhs: HirValue, rhs: HirValue },
ISub { lhs: HirValue, rhs: HirValue },
IMul { lhs: HirValue, rhs: HirValue },
ISDiv { lhs: HirValue, rhs: HirValue },
IUDiv { lhs: HirValue, rhs: HirValue },
ISRem { lhs: HirValue, rhs: HirValue },
IURem { lhs: HirValue, rhs: HirValue },
// Integer arithmetic (wrapping — safe, no trap)
IAddWrap { lhs: HirValue, rhs: HirValue },
ISubWrap { lhs: HirValue, rhs: HirValue },
IMulWrap { lhs: HirValue, rhs: HirValue },
// Integer arithmetic (checked → TOption)
IAddChecked { lhs: HirValue, rhs: HirValue },
ISubChecked { lhs: HirValue, rhs: HirValue },
IMulChecked { lhs: HirValue, rhs: HirValue },
// Bitwise
IAnd { lhs: HirValue, rhs: HirValue },
IOr { lhs: HirValue, rhs: HirValue },
IXor { lhs: HirValue, rhs: HirValue },
IShl { lhs: HirValue, rhs: HirValue },
ILshr { lhs: HirValue, rhs: HirValue },
IAshr { lhs: HirValue, rhs: HirValue },
// Compare
ICmp { pred: String, lhs: HirValue, rhs: HirValue },
FCmp { pred: String, lhs: HirValue, rhs: HirValue },
// Float (strict IEEE 754)
FAdd { lhs: HirValue, rhs: HirValue },
FSub { lhs: HirValue, rhs: HirValue },
FMul { lhs: HirValue, rhs: HirValue },
FDiv { lhs: HirValue, rhs: HirValue },
FRem { lhs: HirValue, rhs: HirValue },
// Float (fast-math opt-in)
FAddFast { lhs: HirValue, rhs: HirValue },
FSubFast { lhs: HirValue, rhs: HirValue },
FMulFast { lhs: HirValue, rhs: HirValue },
FDivFast { lhs: HirValue, rhs: HirValue },
// Cast
Cast { to: TypeId, v: HirValue },
// Unsafe raw pointer ops (surface: `ptr.*`, restricted to `unsafe {}`)
PtrNull { to: TypeId },
PtrAddr { p: HirValue },
PtrFromAddr { to: TypeId, addr: HirValue },
PtrAdd { p: HirValue, count: HirValue },
PtrLoad { to: TypeId, p: HirValue },
PtrStore { to: TypeId, p: HirValue, v: HirValue },
// Calls
Call { callee_sid: Sid, inst: Vec<TypeId>, args: Vec<HirValue> },
CallIndirect { callee: HirValue, args: Vec<HirValue> },
CallVoidIndirect { callee: HirValue, args: Vec<HirValue> },
SuspendCall { callee_sid: Sid, inst: Vec<TypeId>, args: Vec<HirValue> },
SuspendAwait { fut: HirValue },
// Control flow
Phi { ty: TypeId, incomings: Vec<(BlockId, HirValue)> },
// Ownership conversions
Share { v: HirValue },
CloneShared { v: HirValue },
CloneWeak { v: HirValue },
WeakDowngrade { v: HirValue },
WeakUpgrade { v: HirValue }, // returns TOption<shared T>
// Enum operations
EnumNew { variant: String, args: Vec<(String, HirValue)> },
EnumTag { v: HirValue }, // always returns i32 (§16.5)
EnumPayload { variant: String, v: HirValue },
EnumIs { variant: String, v: HirValue },
// TCallable
CallableCapture { fn_ref: Sid, captures: Vec<(String, HirValue)> },
// Array intrinsics
ArrNew { elem_ty: TypeId, cap: HirValue },
ArrLen { arr: HirValue },
ArrGet { arr: HirValue, idx: HirValue },
ArrSet { arr: HirValue, idx: HirValue, val: HirValue },
ArrPush { arr: HirValue, val: HirValue },
ArrPop { arr: HirValue },
ArrSlice { arr: HirValue, start: HirValue, end: HirValue },
ArrContains { arr: HirValue, val: HirValue },
ArrSort { arr: HirValue },
ArrMap { arr: HirValue, func: HirValue },
ArrFilter { arr: HirValue, func: HirValue },
ArrReduce { arr: HirValue, init: HirValue, func: HirValue },
ArrForeach { arr: HirValue, func: HirValue },
// Map intrinsics
MapNew { key_ty: TypeId, val_ty: TypeId },
MapLen { map: HirValue },
MapGet { map: HirValue, key: HirValue },
MapGetRef { map: HirValue, key: HirValue },
MapGetRef { map: HirValue, key: HirValue },
MapSet { map: HirValue, key: HirValue, val: HirValue },
MapDelete { map: HirValue, key: HirValue },
MapContainsKey { map: HirValue, key: HirValue },
MapDeleteVoid { map: HirValue, key: HirValue },
MapKeys { map: HirValue },
MapValues { map: HirValue },
// String intrinsics
StrConcat { a: HirValue, b: HirValue },
StrLen { s: HirValue },
StrEq { a: HirValue, b: HirValue },
StrSlice { s: HirValue, start: HirValue, end: HirValue },
StrBytes { s: HirValue },
StrBuilderNew,
StrBuilderAppendStr { b: HirValue, s: HirValue },
StrBuilderAppendI64 { b: HirValue, v: HirValue },
StrBuilderAppendI32 { b: HirValue, v: HirValue },
StrBuilderAppendF64 { b: HirValue, v: HirValue },
StrBuilderAppendBool { b: HirValue, v: HirValue },
StrBuilderBuild { b: HirValue },
// Error
Panic { msg: HirValue },
}
Surface borrow opcodes (parsed from .mp, lowered to BorrowShared/BorrowMut above):
%ref: borrow TPerson = borrow.shared { v=%person }
%mref: mutborrow TPerson = borrow.mut { v=%person }
16.5 enum.tag return type (P1-10)
EnumTag/enum.tagALWAYS returnsi32, regardless of the number of variants.- Tag values are assigned sequentially starting from 0 in declaration order.
- For
TOption<T>with heap handles: niche optimization applies at the representation level (NULL = None), butenum.tagstill returnsi32(0=None, 1=Some). The niche representation is an LLVM codegen optimization, not visible at HIR/MPIR level. - For
TResult<T,E>: tag 0=Ok, tag 1=Err.
16.6 HIR invariants (MUST)
- SSA well-formed: each LocalId defined exactly once, all uses dominated by defs, each block ends with exactly one terminator.
- Borrow locality: borrows MUST NOT appear in phi, MUST NOT cross blocks, last use before terminator.
- Field access requires explicit borrow: GetField requires borrow/mutborrow, SetField requires mutborrow.
- No returning/storing borrows: return type MUST NOT be borrow/mutborrow, borrows MUST NOT be stored into heap.
17. Typed MPIR (Rust data model)
MPIR adds to HIR:
- Explicit ARC ops after ARC pass
- Verified type tables and symbol table snapshots
- Field overwrite expansions
- Resolved SIDs for all symbols
17.1 MPIR Rust data structures
/// A single MPIR value reference — extends HirValue with resolved SIDs.
#[derive(Clone, Debug)]
pub enum MpirValue {
Local(LocalId),
Const(HirConst),
}
/// MPIR instruction — an SSA assignment.
#[derive(Clone, Debug)]
pub struct MpirInstr {
pub dst: LocalId,
pub ty: TypeId,
pub op: MpirOp,
}
/// MPIR void operation (no result value).
#[derive(Clone, Debug)]
pub enum MpirOpVoid {
CallVoid { callee_sid: Sid, inst: Vec<TypeId>, args: Vec<MpirValue> },
CallVoidIndirect { callee: MpirValue, args: Vec<MpirValue> },
SetField { obj: MpirValue, field: String, val: MpirValue },
ArrPush { arr: MpirValue, val: MpirValue },
ArrForeach { arr: MpirValue, func: MpirValue },
MapDeleteVoid { map: MpirValue, key: MpirValue },
PtrStore { to: TypeId, p: MpirValue, v: MpirValue },
Panic { msg: MpirValue },
// ARC operations (inserted by ARC pass)
ArcRetain { v: MpirValue },
ArcRelease { v: MpirValue },
ArcRetainWeak { v: MpirValue },
ArcReleaseWeak { v: MpirValue },
}
/// MPIR operation — extends HirOp with ARC ops and resolved SIDs.
#[derive(Clone, Debug)]
pub enum MpirOp {
// All HirOp variants carry over with MpirValue instead of HirValue.
// Only MPIR-specific additions listed here:
// ARC operations (not present in HIR, inserted by ARC insertion pass)
ArcRetain { v: MpirValue },
ArcRelease { v: MpirValue },
ArcRetainWeak { v: MpirValue },
ArcReleaseWeak { v: MpirValue },
WeakUpgrade { v: MpirValue }, // returns TOption<shared T>
// All other ops mirror HirOp with MpirValue substituted for HirValue
// Every HirOp variant (§16.4) has a 1:1 MpirOp counterpart with
// HirValue replaced by MpirValue. The canonical list is:
// Const, Move, BorrowShared, BorrowMut, New, GetField, SetField,
// IAdd, ISub, IMul, ISDiv, IUDiv, ISRem, IURem,
// IAddWrap, ISubWrap, IMulWrap, IAddChecked, ISubChecked, IMulChecked,
// IAnd, IOr, IXor, IShl, ILshr, IAshr, ICmp, FCmp,
// FAdd, FSub, FMul, FDiv, FRem, FAddFast, FSubFast, FMulFast, FDivFast,
// Cast, PtrNull, PtrAddr, PtrFromAddr, PtrAdd, PtrLoad, PtrStore,
// Call, CallIndirect, SuspendCall, SuspendAwait, Phi,
// Share, CloneShared, CloneWeak, WeakDowngrade, WeakUpgrade,
// EnumNew, EnumTag, EnumPayload, EnumIs, CallableCapture,
// ArrNew, ArrLen, ArrGet, ArrSet, ArrPush, ArrPop, ArrSlice,
// ArrContains, ArrSort, ArrMap, ArrFilter, ArrReduce, ArrForeach,
// MapNew, MapLen, MapGet, MapSet, MapDelete, MapContainsKey, MapDeleteVoid,
// MapKeys, MapValues,
// StrConcat, StrLen, StrEq, StrSlice, StrBytes,
// StrBuilderNew, StrBuilderAppendStr, StrBuilderAppendI64,
// StrBuilderAppendI32, StrBuilderAppendF64, StrBuilderAppendBool,
// StrBuilderBuild, Panic
}
/// MPIR block terminator.
#[derive(Clone, Debug)]
pub enum MpirTerminator {
Ret(Option<MpirValue>),
Br(BlockId),
Cbr { cond: MpirValue, then_bb: BlockId, else_bb: BlockId },
Switch { val: MpirValue, arms: Vec<(HirConst, BlockId)>, default: BlockId },
Unreachable,
}
/// MPIR basic block.
#[derive(Clone, Debug)]
pub struct MpirBlock {
pub id: BlockId,
pub instrs: Vec<MpirInstr>,
pub void_ops: Vec<MpirOpVoid>,
pub terminator: MpirTerminator,
}
/// MPIR function.
#[derive(Clone, Debug)]
pub struct MpirFn {
pub sid: Sid,
pub name: String,
pub params: Vec<(LocalId, TypeId)>,
pub ret_ty: TypeId,
pub blocks: Vec<MpirBlock>,
pub locals: Vec<MpirLocalDecl>,
pub is_async: bool,
}
/// MPIR local declaration.
#[derive(Clone, Debug)]
pub struct MpirLocalDecl {
pub id: LocalId,
pub ty: TypeId,
pub name: String, // debug name from SSA (e.g., "msg")
}
/// MPIR module — the unit of compilation output.
#[derive(Clone, Debug)]
pub struct MpirModule {
pub sid: Sid,
pub path: String,
pub type_table: MpirTypeTable,
pub functions: Vec<MpirFn>,
pub globals: Vec<(GlobalId, TypeId, HirConst)>,
}
/// MPIR type table snapshot.
#[derive(Clone, Debug)]
pub struct MpirTypeTable {
pub types: Vec<(TypeId, TypeKind)>,
}
17.2 MPIR invariants
MPIR invariants include all HIR invariants (§16.6) plus:
- ARC ops only after ARC insertion pass
- TypeRef correctness — all TypeIds resolve in the type table
- Phi only for value types and Unique/Shared/Weak handles
18. Canonical strings, stable IDs, and digests
18.1 Canonical module path string
ModulePathStr = "pkg.sub.mod"(dotted, no whitespace)
18.2 Canonical FQN strings
- Function:
ModulePathStr + ".@" + Ident - Type:
ModulePathStr + ".T" + Ident - Global:
ModulePathStr + ".@" + Ident
18.3 Canonical type strings (TypeStr)
Complete grammar and key rules:
- Ownership modifiers followed by exactly one space
- No extra whitespace
- Type args without spaces after commas
18.4 Stable IDs (SIDs)
Format: <Kind>:<10 chars> where Kind is M/F/T/G/E.
Suffix: base32_crockford(blake3(input))[0..10]
Input strings (all prefixed with "magpie:sid:v0.1|"):
- Module:
"magpie:sid:v0.1|module|" + ModulePathStr - Function:
"magpie:sid:v0.1|fn|" + FqnFnStr - Type:
"magpie:sid:v0.1|type|" + FqnTypeStr - Global:
"magpie:sid:v0.1|global|" + FqnGlobalStr
18.5 Signature core string (SigCoreStr)
SigCoreStr(fn) is the canonical string representation of a function signature used for digest computation:
SigCoreStr(fn) = "fn " + FqnFnStr + "(" + Join(",", ParamTypeStrs) + ") -> " + RetTypeStr
Where:
FqnFnStris the fully qualified function name (e.g.,mymod.@my_func)ParamTypeStrsis the ordered list of canonical type strings (§18.3) for each parameterRetTypeStris the canonical type string for the return typeJoin(",", list)concatenates elements with,separator (no spaces)
Example: fn mymod.@add(i32,i32) -> i32
18.6 Signature digests
SigDigestHex = blake3_hex("magpie:sigdigest:v0.1|" + SigCoreStr(fn))
18.7 Monomorphized instance IDs
Format: I:<16 chars>
Input: "magpie:inst:v0.1|" + SymbolSID + "|" + Join("|", TypeArgTypeStrs)
19. LLVM symbol mangling (exact, deterministic)
19.1 Global conventions
- All Magpie-generated LLVM symbols start with:
mp$0$(ABI version 0) $is the delimiter
19.2 Function symbols
- Non-generic:
mp$0$FN$<F_sid_suffix> - Monomorphized:
mp$0$FN$<F_sid_suffix>$I$<inst_suffix16>
19.3 Other symbols
- Globals:
mp$0$GL$<G_sid_suffix> - Type info:
mp$0$TI$<T_sid_suffix> - Drop function:
mp$0$DROP$<T_sid_suffix> - Type init:
mp$0$INIT_TYPES$<M_sid_suffix>
19.4 Program entrypoint
C main MUST: call mp_rt_init(), call all INIT_TYPES in deterministic order, call Magpie @main, return exit code.
20. Runtime ABI
20.1 64-bit ABI (primary, LP64)
20.1.1 Object header (C layout)
typedef struct MpRtHeader {
_Atomic uint64_t strong;
_Atomic uint64_t weak;
uint32_t type_id;
uint32_t flags;
uint64_t reserved0;
} MpRtHeader;
// sizeof == 32, alignof >= 8
// Payload at byte offset 32
20.1.2 Required runtime functions
void mp_rt_init(void);
void mp_rt_register_types(const MpRtTypeInfo* infos, uint32_t count);
const MpRtTypeInfo* mp_rt_type_info(uint32_t type_id);
MpRtHeader* mp_rt_alloc(uint32_t type_id, uint64_t payload_size, uint64_t payload_align, uint32_t flags);
void mp_rt_retain_strong(MpRtHeader* obj);
void mp_rt_release_strong(MpRtHeader* obj);
void mp_rt_retain_weak(MpRtHeader* obj);
void mp_rt_release_weak(MpRtHeader* obj);
MpRtHeader* mp_rt_weak_upgrade(MpRtHeader* obj);
MpRtHeader* mp_rt_str_from_utf8(const uint8_t* bytes, uint64_t len);
const uint8_t* mp_rt_str_bytes(MpRtHeader* str, uint64_t* out_len);
void mp_rt_panic(MpRtHeader* str_msg) __attribute__((noreturn));
For parse/json boundary hardening, the runtime MUST also expose fallible status-returning APIs:
#define MP_RT_OK 0
#define MP_RT_ERR_INVALID_UTF8 1
#define MP_RT_ERR_INVALID_FORMAT 2
#define MP_RT_ERR_UNSUPPORTED_TYPE 3
#define MP_RT_ERR_NULL_OUT_PTR 4
#define MP_RT_ERR_NULL_INPUT 5
int32_t mp_rt_str_try_parse_i64(MpRtHeader* s, int64_t* out, MpRtHeader** out_errmsg);
int32_t mp_rt_str_try_parse_u64(MpRtHeader* s, uint64_t* out, MpRtHeader** out_errmsg);
int32_t mp_rt_str_try_parse_f64(MpRtHeader* s, double* out, MpRtHeader** out_errmsg);
int32_t mp_rt_str_try_parse_bool(MpRtHeader* s, int32_t* out, MpRtHeader** out_errmsg);
int32_t mp_rt_json_try_encode(uint8_t* obj, uint32_t type_id, MpRtHeader** out_str, MpRtHeader** out_errmsg);
int32_t mp_rt_json_try_decode(MpRtHeader* json_str, uint32_t type_id, uint8_t** out_val, MpRtHeader** out_errmsg);
int32_t mp_rt_json_decoded_free(uint8_t* decoded, uint32_t type_id);
Error ownership contract:
- On success:
status == MP_RT_OKand*out_errmsg == NULL(whenout_errmsgis non-NULL). - On error: runtime MAY allocate an error
Strand store it in*out_errmsg; caller owns it and MUST release viamp_rt_release_strong. - If
out_errmsg == NULL, runtime MUST still return status and drop any temporary error string internally. - On
mp_rt_json_try_decodesuccess: caller owns*out_valand MUST release it viamp_rt_json_decoded_free(*out_val, type_id). mp_rt_json_decoded_free(NULL, type_id)is a no-op and MUST returnMP_RT_OK.- Legacy panic-oriented entry points (
mp_rt_str_parse_*,mp_rt_json_encode,mp_rt_json_decode) remain as compatibility wrappers over*_try_*.
20.1.3 MpRtTypeInfo struct
typedef void (*MpRtDropFn)(MpRtHeader* obj);
typedef struct MpRtTypeInfo {
uint32_t type_id;
uint32_t flags; // bitfield: 0x1=heap, 0x2=has_drop, 0x4=send, 0x8=sync
uint64_t payload_size;
uint64_t payload_align;
MpRtDropFn drop_fn; // NULL if no custom drop (see rules below)
const char* debug_fqn; // e.g. "mymod.TPerson", for diagnostics only
} MpRtTypeInfo;
drop_fn rules:
drop_fnis NULL for: primitive types, value structs with no heap fields, and builtin types whose drop is handled entirely by the runtime (Str, Array, Map, TStrBuilder).drop_fnis non-NULL for: user-defined heap structs/enums whose fields include other heap handles (the compiler generates a drop function that releases each heap field).- Value structs do NOT get
MpRtTypeInfoentries — they have no runtime header and no ARC lifecycle. - Drop function deduplication (§11.6): if two types have structurally identical drop functions, LLVM ICF or compiler-level dedup may merge them. The
type_idin the header still distinguishes the types; only the function pointer is shared.
20.1.4 Fixed type_id table for primitives and builtins
Primitive and builtin type_ids are deterministic and fixed across all compilations:
| type_id | Type | Notes |
|---|---|---|
| 0 | unit | zero-size |
| 1 | bool / i1 | |
| 2 | i8 | |
| 3 | i16 | |
| 4 | i32 | |
| 5 | i64 | |
| 6 | i128 | |
| 7 | u8 | |
| 8 | u16 | |
| 9 | u32 | |
| 10 | u64 | |
| 11 | u128 | |
| 12 | u1 | |
| 13 | f16 | |
| 14 | f32 | |
| 15 | f64 | |
| 20 | Str | heap builtin |
| 21 | TStrBuilder | heap builtin |
| 22 | Array<?> | base; instantiations get unique IDs ≥ 1000 |
| 23 | Map<?,?> | base; instantiations get unique IDs ≥ 1000 |
| 24 | TOption<?> | base; instantiations get unique IDs ≥ 1000 |
| 25 | TResult<?,?> | base; instantiations get unique IDs ≥ 1000 |
| 26 | TCallable<?> | base; instantiations get unique IDs ≥ 1000 |
| 30 | gpu.TDevice | heap builtin |
| 31 | gpu.TBuffer<?> | base; instantiations get unique IDs ≥ 1000 |
| 32 | gpu.TFence | heap builtin |
All non-fixed types (user-defined heap types, generic instantiations, and ownership-qualified derived types) receive type_ids starting at 1000, assigned deterministically across the entire build graph by lexicographic order of their canonical type key (see §15.5).
20.1.5 Collection runtime ABI
All arr.*, map.*, str.* intrinsics lower to these C functions:
typedef uint64_t (*MpRtHashFn)(const void* key_bytes);
typedef int32_t (*MpRtEqFn)(const void* a_bytes, const void* b_bytes);
typedef int32_t (*MpRtCmpFn)(const void* a_bytes, const void* b_bytes);
// Array operations
MpRtHeader* mp_rt_arr_new(uint32_t elem_type_id, uint64_t elem_size, uint64_t capacity);
uint64_t mp_rt_arr_len(MpRtHeader* arr);
void* mp_rt_arr_get(MpRtHeader* arr, uint64_t idx); // returns ptr to element; panics on OOB
void mp_rt_arr_set(MpRtHeader* arr, uint64_t idx, const void* val, uint64_t elem_size);
void mp_rt_arr_push(MpRtHeader* arr, const void* val, uint64_t elem_size);
int32_t mp_rt_arr_pop(MpRtHeader* arr, void* out, uint64_t elem_size); // returns 0=empty, 1=ok
MpRtHeader* mp_rt_arr_slice(MpRtHeader* arr, uint64_t start, uint64_t end);
int32_t mp_rt_arr_contains(MpRtHeader* arr, const void* val, uint64_t elem_size, MpRtEqFn eq_fn); // 0=no, 1=yes
void mp_rt_arr_sort(MpRtHeader* arr, MpRtCmpFn cmp);
void mp_rt_arr_foreach(MpRtHeader* arr, MpRtHeader* callable);
MpRtHeader* mp_rt_arr_map(MpRtHeader* arr, MpRtHeader* callable, uint32_t result_elem_type_id, uint64_t result_elem_size);
MpRtHeader* mp_rt_arr_filter(MpRtHeader* arr, MpRtHeader* callable);
void mp_rt_arr_reduce(MpRtHeader* arr, void* acc_inout, uint64_t acc_size, MpRtHeader* callable);
// Map operations
MpRtHeader* mp_rt_map_new(
uint32_t key_type_id,
uint32_t val_type_id,
uint64_t key_size,
uint64_t val_size,
uint64_t capacity,
MpRtHashFn hash_fn,
MpRtEqFn eq_fn
);
uint64_t mp_rt_map_len(MpRtHeader* map);
void* mp_rt_map_get(MpRtHeader* map, const void* key, uint64_t key_size); // returns ptr or NULL
void mp_rt_map_set(MpRtHeader* map, const void* key, uint64_t key_size, const void* val, uint64_t val_size);
int32_t mp_rt_map_take(MpRtHeader* map, const void* key, uint64_t key_size, void* out_val, uint64_t val_size); // 0=not_found, 1=ok (moves out; does NOT drop)
int32_t mp_rt_map_delete(MpRtHeader* map, const void* key, uint64_t key_size); // 0=not_found, 1=deleted (drops removed value)
int32_t mp_rt_map_contains_key(MpRtHeader* map, const void* key, uint64_t key_size);
MpRtHeader* mp_rt_map_keys(MpRtHeader* map); // returns Array of keys
MpRtHeader* mp_rt_map_values(MpRtHeader* map); // returns Array of values
// String operations
MpRtHeader* mp_rt_str_concat(MpRtHeader* a, MpRtHeader* b);
uint64_t mp_rt_str_len(MpRtHeader* s);
int32_t mp_rt_str_eq(MpRtHeader* a, MpRtHeader* b);
MpRtHeader* mp_rt_str_slice(MpRtHeader* s, uint64_t start, uint64_t end);
// mp_rt_str_bytes and mp_rt_str_from_utf8 defined in §20.1.2
// StringBuilder operations
MpRtHeader* mp_rt_strbuilder_new(void);
void mp_rt_strbuilder_append_str(MpRtHeader* b, MpRtHeader* s);
void mp_rt_strbuilder_append_i64(MpRtHeader* b, int64_t v);
void mp_rt_strbuilder_append_i32(MpRtHeader* b, int32_t v);
void mp_rt_strbuilder_append_f64(MpRtHeader* b, double v);
void mp_rt_strbuilder_append_bool(MpRtHeader* b, int32_t v);
MpRtHeader* mp_rt_strbuilder_build(MpRtHeader* b); // consumes builder, returns Str
// Async executor
int32_t mp_rt_future_poll(MpRtHeader* state); // 0=Pending, 1=Ready
void mp_rt_future_take(MpRtHeader* state, void* out_result);
Notes (async executor):
mp_rt_future_pollis called by the executor to drive aTFuture<T>state machine.- When
mp_rt_future_pollreports Ready, the executor (orstd.async.block_on) MAY callmp_rt_future_taketo move the completed result value intoout_result(a caller-allocated buffer). Callingmp_rt_future_takebefore Ready is undefined behavior.
20.1.6 Web runtime ABI
The web runtime provides an HTTP/1.1 server loop and bridges requests/responses to Magpie via fixed callback symbols (see §30.1.7).
// Starts a blocking HTTP server.
//
// Returns 0 on clean shutdown.
// Returns non-zero on startup/fatal error and sets *out_errmsg to an owned Str.
//
// The runtime MUST invoke the callback symbols:
// __magpie_web_handle_request
// __magpie_web_stream_next
//
// Ownership:
// On error, out_errmsg is an owned Str and must be released by the caller.
// On success, *out_errmsg MUST be NULL.
int32_t mp_rt_web_serve(
MpRtHeader* svc, // web.router.TService
MpRtHeader* addr, // Str (bind address, e.g. "127.0.0.1")
uint16_t port, // bind port
uint8_t keep_alive, // 0/1
uint32_t threads, // worker thread count (>=1)
uint64_t max_body_bytes, // request body limit
uint64_t read_timeout_ms,
uint64_t write_timeout_ms,
uint8_t log_requests, // 0/1
MpRtHeader** out_errmsg // Str* (nullable)
);
Request parsing rules (MUST):
- The runtime MUST parse:
- method (uppercase)
- path (no query string)
- query string into
Map<Str,Str>(first value wins) - headers into
Map<Str,Str>with lowercase names (first value wins) - body into
Array<u8>(buffered; may be empty)
- The runtime MUST generate a
request_id: Strthat is unique within the process lifetime. - The runtime MUST call
__magpie_web_handle_request(...)once per request. - The runtime MUST write all chunks returned by
__magpie_web_stream_nextin order. - The runtime MAY close the connection early on I/O error.
Static assets (MUST in Magpie Web v0.1):
magpie web devMUST serve/assets/*fromapp/assets(no caching).magpie web serveMUST serve/assets/*fromdist/assets(caching enabled). The asset handler lives in the runtime and does not require Magpie file I/O in v0.1.
20.1.7 GPU runtime ABI
The GPU runtime is responsible for device discovery, buffer management, kernel registry, and dispatch.
// --- Kernel registry ---
typedef enum MpRtGpuBackend {
MP_GPU_BACKEND_SPV = 1, // Vulkan SPIR-V (required for Magpie GPU v0.1)
} MpRtGpuBackend;
typedef enum MpRtGpuParamKind {
MP_GPU_PARAM_BUFFER = 1, // gpu.TBuffer<T>
MP_GPU_PARAM_SCALAR = 2, // primitive scalar packed into push constants
} MpRtGpuParamKind;
typedef struct MpRtGpuParam {
uint8_t kind; // MpRtGpuParamKind
uint8_t _reserved0;
uint16_t _reserved1;
uint32_t type_id; // primitive TypeId for scalars; 0 for buffers
uint32_t offset_or_binding; // scalar: byte offset in push constants; buffer: binding index
uint32_t size; // scalar: size in bytes; buffer: 0
} MpRtGpuParam;
typedef struct MpRtGpuKernelEntry {
uint64_t sid_hash; // hash of kernel SID string
uint32_t backend; // MpRtGpuBackend
const uint8_t* blob;
uint64_t blob_len;
uint32_t num_params;
const MpRtGpuParam* params;
uint32_t num_buffers; // convenience: count of buffer params
uint32_t push_const_size; // size of scalar block (bytes; multiple of 16)
} MpRtGpuKernelEntry;
void mp_rt_gpu_register_kernels(const MpRtGpuKernelEntry* entries, uint32_t count);
// --- Device discovery ---
uint32_t mp_rt_gpu_device_count(void);
int32_t mp_rt_gpu_device_default(MpRtHeader** out_dev, MpRtHeader** out_errmsg); // Str errmsg
int32_t mp_rt_gpu_device_by_index(uint32_t idx, MpRtHeader** out_dev, MpRtHeader** out_errmsg);
MpRtHeader* mp_rt_gpu_device_name(MpRtHeader* dev); // returns owned Str
// --- Buffers ---
int32_t mp_rt_gpu_buffer_new(
MpRtHeader* dev,
uint32_t elem_type_id,
uint64_t elem_size,
uint64_t len,
uint32_t usage_flags,
MpRtHeader** out_buf,
MpRtHeader** out_errmsg
);
int32_t mp_rt_gpu_buffer_from_array(
MpRtHeader* dev,
MpRtHeader* host_arr, // Array<T>
uint32_t usage_flags,
MpRtHeader** out_buf,
MpRtHeader** out_errmsg
);
int32_t mp_rt_gpu_buffer_to_array(
MpRtHeader* buf,
MpRtHeader** out_arr, // Array<T>
MpRtHeader** out_errmsg
);
uint64_t mp_rt_gpu_buffer_len(MpRtHeader* buf);
int32_t mp_rt_gpu_buffer_copy(MpRtHeader* src, MpRtHeader* dst, MpRtHeader** out_errmsg);
int32_t mp_rt_gpu_device_sync(MpRtHeader* dev, MpRtHeader** out_errmsg);
// --- Launch ---
// args_blob layout (MUST):
// [8 * num_buffers bytes] buffer pointers (MpRtHeader*) in binding order
// [push_const_size bytes] scalar push-constant bytes
//
// The compiler MUST pass args_len = 8*num_buffers + push_const_size.
int32_t mp_rt_gpu_launch_sync(
MpRtHeader* dev,
uint64_t kernel_sid_hash,
uint32_t grid_x, uint32_t grid_y, uint32_t grid_z,
uint32_t block_x, uint32_t block_y, uint32_t block_z,
const uint8_t* args_blob,
uint64_t args_len,
MpRtHeader** out_errmsg
);
// Optional async launch/fence.
int32_t mp_rt_gpu_launch_async(
MpRtHeader* dev,
uint64_t kernel_sid_hash,
uint32_t grid_x, uint32_t grid_y, uint32_t grid_z,
uint32_t block_x, uint32_t block_y, uint32_t block_z,
const uint8_t* args_blob,
uint64_t args_len,
MpRtHeader** out_fence,
MpRtHeader** out_errmsg
);
int32_t mp_rt_gpu_fence_wait(
MpRtHeader* fence,
uint64_t timeout_ms,
uint8_t* out_done, // 0/1
MpRtHeader** out_errmsg
);
The compiler lowers gpu.host.* intrinsics (§31.2) and gpu.launch* (§31.6) to these runtime calls.
20.2 wasm32 ABI variant
For WASM targets (wasm32-unknown-unknown):
typedef struct MpRtHeader32 {
_Atomic uint32_t strong;
_Atomic uint32_t weak;
uint32_t type_id;
uint32_t flags;
} MpRtHeader32;
// sizeof == 16, alignof >= 4
// Payload at byte offset 16
- Pointers are 4 bytes.
- All runtime functions have the same signatures but with 32-bit pointer arguments.
- The compiler selects the ABI variant based on target triple.
20.3 Refcount semantics
- Strong retain: increment
strongby 1. - Strong release: decrement
strong; if 0, call drop, then release weak once (implicit weak). - Weak retain: increment
weak. - Weak release: decrement
weak; if 0, free memory. - Weak upgrade: atomically check strong > 0, increment, return ptr or NULL.
- Memory ordering: increments may use relaxed; decrements that hit zero use release + acquire.
21. LLVM IR lowering
21.1 Type mapping
- Value types → LLVM integer/float types
- Heap handles →
ptr(opaque pointer) - Value structs → LLVM struct types
21.2 Calls ABI
- Parameters: value types by value, heap handles as
ptr - Returns: value types by value, heap handles as
ptr
21.3 ARC lowering
arc.retain/release lower to runtime calls: @mp_rt_retain_strong(ptr), @mp_rt_release_strong(ptr)
21.4 Checked arithmetic lowering
i.add lowers to LLVM intrinsic llvm.sadd.with.overflow.i32 (or equivalent), with branch to panic on overflow.
21.5 Collection intrinsic lowering
All arr.*, map.*, str.* intrinsics lower to runtime function calls (e.g., @mp_rt_arr_push, @mp_rt_map_get, etc.).
22. Compilation pipeline
22.1 Stages
- Parse + CSNF: raw
.mp→ CSNF source +AstFile+ file digest - Resolve: AST modules + import/export headers →
HirPackage - Typecheck: HIR → typed HIR (TypeIds, layouts) 3.5. Async lowering: async functions → coroutine state machines (see below)
- Verify HIR: SSA and borrow-locality invariants
- Ownership check: typed HIR → ownership proof traces or errors
- Lower to MPIR: typed HIR → MPIR (no ARC ops yet)
- MPIR verify: SSA + type refs + phi restrictions
- ARC insertion: MPIR → MPIR with ARC ops + field-overwrite expansions
- ARC optimization: eliminate redundant retain/release
- LLVM codegen: MPIR + type layouts → LLVM module
- Link: output final exe/shared-lib
- MMS update: update capsules, signatures, repair episodes
Stage 3.5: Async lowering (detail)
For each async fn, the compiler:
-
Generates a coroutine state struct (heap-allocated, ARC-managed):
- Contains all SSA values live across any
suspend.callpoint. - Contains a
state_index: i32field tracking which resume point to jump to. - Has a compiler-generated
type_id(registered viamp_rt_register_types).
- Contains all SSA values live across any
-
Rewrites the function body into a resume function:
- Entry dispatches on
state_indexto the correct resume point. - Each
suspend.callsaves live values into the state struct and returnsPending. - On final completion, the result is written into the state struct and returns
Ready.
- Entry dispatches on
-
Resume function ABI:
// Returns: 0 = Pending (suspended), 1 = Ready (completed) int32_t mp_rt_future_poll(MpRtHeader* state);The result value is read from the state struct payload after
Readyis returned. -
Caller-side:
suspend.calllowers to creating the state struct, then yielding to the executor which polls viamp_rt_future_poll.
Async lowering MPIR example — given:
async fn @fetch_user(%id: u64) -> TUser {
bb0:
%conn: TDbConn = suspend.call @db.@connect {}
%user: TUser = call @parse_user { row=%conn }
ret %user
}
The compiler generates (conceptual MPIR):
;; Compiler-generated state struct (heap, ARC-managed)
heap struct T__fetch_user_state {
field state_index: i32
field id: u64 ;; captured parameter
field conn: TDbConn ;; live across suspend point 0
field result: TUser ;; final result
}
;; Resume function — called by executor via mp_rt_future_poll
fn @__fetch_user_resume(%state: mutborrow T__fetch_user_state) -> i32 {
bb0:
%idx: i32 = getfield { obj=%state, field=state_index }
switch %idx {
case 0 -> bb_start
case 1 -> bb_resume_0
} else bb_unreachable
bb_start:
;; Start async call to @db.@connect, save state, return Pending
%future: TFuture<TDbConn> = call @db.@connect {}
setfield { obj=%state, field=state_index, val=const.i32 1 }
ret const.i32 0 ;; 0 = Pending
bb_resume_0:
;; Resumed: read result from completed sub-future
%conn: borrow TDbConn = getfield { obj=%state, field=conn }
%user: TUser = call @parse_user { row=%conn }
setfield { obj=%state, field=result, val=%user }
ret const.i32 1 ;; 1 = Ready
bb_unreachable:
unreachable
}
22.2 Error recovery
- Each pass collects up to
--max-errors(default 20) errors. - If a pass produces errors, dependent passes are skipped (e.g., no ownership check if type check failed).
- All collected diagnostics are emitted together.
- MMS query timing: MMS queries for diagnostic augmentation (§24) run after stage 5 (ownership check) and before diagnostic output, using the existing MMS index. This ensures repair suggestions can reference ownership errors without blocking the critical compilation path. Staleness handling: the MMS index may reference code from a previous compilation. The query layer MUST compare the module digest (BLAKE3) of each MMS capsule against the current compilation's digests. Capsules with stale digests are excluded from repair suggestions and marked for re-indexing at stage 12 (MMS update).
22.3 Incremental compilation
Cache keys MUST include: compiler version, toolchain hash, module digests (BLAKE3), dependency .mpd digests, feature set + target triple.
Cache layers: parsed AST, resolved HIR, MPIR, LLVM bitcode.
23. Interpreter/JIT and REPL
23.1 JIT engine
- Use LLVM ORC JIT.
- Maintain per-session symbol table.
- JIT and AOT must produce semantically equivalent results.
23.2 REPL cell model
Each cell compiles into a hidden module repl.cell.N. Expressions compile into @__repl_eval_N.
23.3 REPL over MCP (stateful sessions)
MCP exposes:
magpie.repl.create→ returnssession_idmagpie.repl.eval→ takessession_id+ code, returns result (budget-aware)magpie.repl.inspect→ type/ir/llvm queries on session state
Sessions persist in memory. Session state is serializable for checkpointing.
23.4 Hot reload (magpie web dev)
- JIT-based hot swap via LLVM ORC.
- File watcher detects changes.
- Changed functions are recompiled and swapped in-place.
- Sub-second iteration. Request state preserved across reloads.
- Zero downtime.
23.4.1 Hot reload mechanism (concrete)
- File watcher: an OS-level watcher (inotify/kqueue/FSEvents) monitors
.mpsource files. - Incremental recompile: on change, only the affected module(s) are re-parsed, type-checked, and lowered to LLVM IR. The incremental cache (§22.3) is used to skip unchanged dependencies.
- ORC JIT replacement: the new LLVM IR module is compiled via LLVM ORC JIT. The runtime calls
LLVMOrcReplaceObjectFiles(or equivalent ORC API) to atomically replace function bodies in the running process. Old function bodies are freed after all in-flight calls complete. - State preservation: request-scoped state (in-flight HTTP contexts, channel buffers) is not affected because only function code is replaced — heap data and ARC objects remain valid.
- Constraints: hot reload does NOT support changes to type layouts (struct field additions/removals). Such changes require a full restart. The compiler detects layout-breaking changes and logs a warning instead of attempting hot swap.
24. Compiler-integrated memory and RAG (MMS)
24.1 MMS storage layout
.magpie/
memory/
mms_meta.json
items/
<item_id>.json
index_lex/
vocab.bin
postings.bin
doclens.bin
itemmap.bin
bm25_meta.json
episodes/
<episode_id>.json
24.2 MMS item schema
{
"schema": 1,
"item_id": "I:xxxxxxxxxxxxxxxx",
"kind": "symbol_capsule|mpd_signature|doc_excerpt|spec_excerpt|diag_template|repair_episode|test_case",
"sid": "F:XXXXXXXXXX",
"fqn": "pkg.module.@fn",
"module_sid": "M:XXXXXXXXXX",
"source_digest": "<blake3hex>",
"body_digest": "<blake3hex>",
"text": "<canonical text>",
"tags": ["MPO0007", "ownership", "web.http"],
"priority": 50,
"token_cost": {"approx:utf8_4chars": 350}
}
24.3 MMS retrieval (budget-aware, BM25)
BM25 with k1=1.2, b=0.75. Field boosts and deterministic tie-breaking. See Appendix E for exact lexical tokenizer spec and Appendix F for BM25 scoring formula, IDF definition, and boost values.
24.4 MMS integration
In --llm mode, magpie build MUST:
- Augment diagnostics with retrieved items
- Provide minimal symbol graph + ownership trace for failing functions
- Emit
rag_bundleper diagnostic (budgeted)
24.5 MMS commands
magpie memory build(incremental index update)magpie memory query --q "<query>" --k <n> [--kinds ...]magpie ctx pack ...(prompt-ready context pack builder)
25. Context pack builder (magpie ctx pack)
Generates prompt-ready context pack bounded by token budget. See Appendix G for complete specification including:
- Chunk types (structural, problem-focused, code capsules, retrieved)
- Scoring formula (base_priority + relevance + proximity + retrieval_score - size_penalty)
- Budget partitioning policies (balanced, diagnostics_first, slices_first, minimal)
- Multi-variant compression ladder (v3 full → v0 one-line identity)
- Deterministic selection algorithm
26. Diagnostics specification (LLM-grade)
26.1 JSON root schema
{
"magpie_version": "0.1.0",
"command": "build",
"target": "x86_64-unknown-linux-gnu",
"success": false,
"artifacts": [],
"diagnostics": [],
"graphs": { "symbols": {}, "deps": {}, "ownership": {}, "cfg": {} },
"timing_ms": { "parse": 12, "typecheck": 31, "owncheck": 20, "arc": 5, "codegen": 40, "link": 60 },
"llm_budget": {}
}
26.2 Per-diagnostic schema
{
"code": "MPO0007",
"severity": "error",
"title": "use of moved value",
"primary_span": { "file": "src/main.mp", "start": 120, "end": 135 },
"secondary_spans": [],
"message": "...",
"explanation_md": "...",
"why": { "kind": "ownership_conflict", "trace": [] },
"suggested_fixes": [{ "title": "clone into shared handle", "patch_format": "unified-diff", "patch": "...", "confidence": 0.82 }],
"rag_bundle": [],
"related_docs": []
}
26.3 Diagnostic code namespaces
MPPparse/lexMPTtypesMPOownershipMPAARCMPFFFIMPGGPUMPWWebMPKPackage managerMPLLint / LLM featuresMPSSSA verification
26.4 Debug information
- Dev profile: Emit DWARF debug info. SSA values map to debug variables. Basic blocks map to scope ranges.
- All profiles: Emit
.mpdbgstructured JSON — maps SIDs + block labels to source spans. Budget-aware for MMS retrieval.
27. Unified diff patch contract
27.1 Format
- MUST be Git-style unified diff
- Paths relative to workspace root
- No absolute paths, no out-of-root modifications
27.2 Patch JSON envelope
{
"title": "clone into shared handle",
"patch_format": "unified-diff",
"patch": "diff --git a/src/main.mp b/src/main.mp\n...",
"applies_to": {"src/main.mp": "<pre_digest>"},
"produces": {"src/main.mp": "<post_digest>"},
"requires_fmt": true,
"confidence": 0.82
}
28. Package manager (magpie pkg)
28.1 Manifest (Magpie.toml)
[package]
name = "my_pkg"
version = "0.1.0"
edition = "2026"
[build]
entry = "src/main.mp"
profile_default = "dev"
max_mono_instances = 10000
[dependencies]
std = { version = "^0.1" }
[features]
gui = { modules = ["src/gui/*.mp"] }
[toolchain.aarch64-unknown-linux-gnu]
sysroot = "/path/to/sysroot"
linker = "aarch64-linux-gnu-ld"
[llm]
mode_default = true
token_budget = 12000
tokenizer = "approx:utf8_4chars"
budget_policy = "balanced"
max_module_lines = 800
max_fn_lines = 80
[llm.rag]
enabled = true
backend = "lexical"
top_k = 12
[web]
addr = "127.0.0.1"
port = 3000
open_browser = false
max_body_bytes = 10000000
threads = 0 # 0 => auto
[gpu]
enabled = true
backend = "spv" # currently: "spv" (Vulkan SPIR-V)
device_index = -1 # -1 => default device
Tool-specific tables (v0.1):
[web]configuresmagpie web dev/build/serve(defaults are used when omitted).addr(Str),port(u16),open_browser(bool)max_body_bytes(u64),threads(u32;0means auto)
[gpu]configures GPU defaults.enabled(bool),backend(Str;"spv"in v0.1),device_index(i32;-1means default)
Unknown keys and unknown tables MUST be ignored to preserve forwards compatibility.
28.2 Registry model
- Git-based, no central hosted registry in v0.1.
- Packages referenced by git URL or local path.
- Curated index repository for discovery.
- Registry HTTP protocol defined for future self-hosted registries.
magpie pkg why <pkg>outputs dependency reason tree.
28.3 Lockfile (Magpie.lock)
Canonical JSON. See Appendix C and Appendix D for JSON Schema definitions.
28.4 Feature flags
- Features gate entire modules via
[features]section. gui = { modules = ["src/gui/*.mp"] }— whenguifeature is inactive, those modules are excluded.- No function-level or type-level conditionals in v0.1.
29. MCP server (magpie mcp serve)
29.1 Tools exposed
magpie.build,magpie.run,magpie.test,magpie.fmt,magpie.lint,magpie.explainmagpie.pkg.resolve,magpie.pkg.add,magpie.pkg.remove,magpie.pkg.planmagpie.memory.build,magpie.memory.querymagpie.ctx.packmagpie.repl.create,magpie.repl.eval,magpie.repl.inspectmagpie.graph.symbols,magpie.graph.deps,magpie.graph.ownership,magpie.graph.cfg
Each tool request MUST accept an optional llm object controlling budget/tokenizer/policy.
29.2 Security model
- Config file controlling: allowed filesystem roots, allowed network access (default deny), allowed subprocesses (default deny except linker/llvm tools).
- MUST never execute arbitrary scripts unless explicitly enabled.
30. Web frameworks
Magpie Web v0.1 is a batteries-included server-side web stack optimized for automated agents: deterministic routing, explicit types, explicit serialization, and minimal ambient magic.
30.0 Scope and guarantees (v0.1)
Included (MUST):
- HTTP/1.1 server (cleartext) with keep-alive (configurable)
- Routing with compile-time validated route patterns (const-str only)
- Typed path parameters via compiler-generated wrappers (no runtime reflection)
- Middleware chain with explicit call to
next - Request/response bodies as bytes (buffered requests) and streaming responses
- JSON encode/decode intrinsics for heap structs (already defined in §30.1.8 / §7 ops)
- Testing harness that invokes router without opening sockets
Out of scope (MAY in future):
- TLS termination (use a reverse proxy in v0.1)
- HTTP/2, WebSockets, SSE, multipart/form-data
- Zero-copy request bodies (v0.1 buffers the body into memory)
30.1 Backend: Magpie Web Service Framework (MWSF)
30.1.1 Core web types (authoritative)
All web framework types live under the web.* package namespace and are part of Magpie Web v0.1.
; -------- web.stream --------
sig web.stream.TNextSig() -> TOption<Array<u8>>
heap struct web.stream.TByteStream {
field next: TCallable<web.stream.TNextSig> ;; yields next chunk; None => end-of-stream
}
; -------- web.http --------
heap struct web.http.TRequest {
field method: Str ;; "GET", "POST", etc. (uppercase)
field path: Str ;; URL path only (e.g., "/users/42")
field query: Map<Str, Str> ;; parsed query params (first value wins)
field headers: Map<Str, Str> ;; lowercase header names; first value wins
field body: Array<u8> ;; fully-buffered request body (may be empty)
field path_params: Map<Str, Str> ;; extracted path params (strings, even if typed)
field remote_addr: Str ;; e.g. "203.0.113.10:54321"
}
heap struct web.http.TResponse {
field status: i32 ;; HTTP status code (e.g., 200, 404)
field headers: Map<Str, Str> ;; lowercase header names
field body_kind: i32 ;; 0=bytes, 1=stream
field body_bytes: Array<u8> ;; used when body_kind==0
field body_stream: web.stream.TByteStream ;; used when body_kind==1
}
heap struct web.http.TContext {
field state: Map<Str, Str> ;; per-request scratch state (middleware may write)
field request_id: Str ;; unique request identifier (opaque string)
}
Normalization rules (MUST):
TRequest.methodMUST be uppercase.TRequest.headerskeys MUST be lowercase ASCII.TResponse.headerskeys MUST be lowercase ASCII.- The server MUST set/overwrite header
x-request-idon every response toctx.request_id.
30.1.2 Router/service types
sig web.router.THandlerSig(web.http.TRequest, web.http.TContext) -> web.http.TResponse
sig web.router.TMiddlewareSig(
web.http.TRequest,
web.http.TContext,
TCallable<web.router.THandlerSig> ;; next
) -> web.http.TResponse
heap struct web.router.TRoute {
field method: Str ;; uppercase
field pattern: Str ;; canonical route pattern string
field handler: TCallable<web.router.THandlerSig> ;; normalized to base signature via wrapper
}
heap struct web.router.TService {
field prefix: Str ;; base path prefix (e.g., "/api")
field routes: Array<web.router.TRoute>
field middleware: Array<TCallable<web.router.TMiddlewareSig>>
}
30.1.3 Route pattern syntax (const-only, compile-time validated)
A route pattern is a const.Str with this grammar:
- Literal segments:
/users - Typed parameters:
/{name:type}name=[A-Za-z_][A-Za-z0-9_]*type∈{ i32, i64, u32, u64, bool, Str }
- Wildcard tail (optional, at end only):
/*{name}(captures the remainder, unescaped)
Examples:
"/""/users/{id:u64}""/assets/*{path}"
v0.1 restrictions (MUST):
methodandpatternarguments toweb.router.@route_addMUST beconst.Str.- If
patterncontains typed params, the handler function MUST be compatible (see below). - Duplicate route registrations (same method + canonical pattern) are a hard error:
MPW1001 DUPLICATE_ROUTE.
30.1.4 Handler signature matching + compiler-generated wrappers (typed params)
User handlers MAY include typed parameters after (TRequest, TContext).
Given:
- method =
"GET" - pattern =
"/users/{id:u64}/posts/{slug:Str}"
Then a compatible handler is:
fn @get_posts(%req: web.http.TRequest, %ctx: web.http.TContext, %id: u64, %slug: Str) -> web.http.TResponse { ... }
Matching rule (MUST):
- The handler's first two parameters MUST be
(web.http.TRequest, web.http.TContext). - The remaining parameters MUST match the typed params in the pattern left-to-right.
Wrapper generation (MUST):
For every @route_add(..., handler=@H) where %H has typed params, the compiler generates a wrapper:
fn @__web_route_wrap_<SID>(%req: TRequest, %ctx: TContext) -> TResponse
Wrapper semantics:
- Match the route pattern against
req.path(after strippingsvc.prefix). - Extract param substrings.
- Store string params into
req.path_params(always). - Parse each typed param:
u64/u32/i64/i32/boolviastr.parse_*Struses the raw substring (no decoding in v0.1)
- On parse failure: return a 400 response with JSON body:
{"error":"bad_request","param":"<name>","request_id":"..."} - Call the user handler with typed values and return its response.
The route table stores the wrapper as TCallable<web.router.THandlerSig>.
30.1.5 Middleware model (synchronous, production-safe)
Middleware is synchronous in v0.1 (no suspend.call), but may still perform blocking I/O.
Rules (MUST):
- Middleware MUST call
nextat most once. - Middleware MAY short-circuit by returning a response without calling
next. - Middleware MAY read/write
ctx.state. - Middleware MUST NOT mutate
req.pathorreq.method(compiler lintMPW1102 REQUEST_MUTATION_FORBIDDEN).
Example:
fn @auth_mw(
%req: web.http.TRequest,
%ctx: web.http.TContext,
%next: TCallable<web.router.THandlerSig>
) -> web.http.TResponse {
bb0:
; auth check omitted (may short-circuit here)
%resp: web.http.TResponse = call.indirect %next { args=[%req, %ctx] }
ret %resp
}
30.1.6 Required library functions (web.http / web.stream / web.router / web.server)
These functions are part of the web.* standard packages and MUST exist.
web.stream helpers:
web.stream.@from_bytes(bytes: Array<u8>) -> web.stream.TByteStreamweb.stream.@empty() -> web.stream.TByteStreamweb.stream.@concat(a: web.stream.TByteStream, b: web.stream.TByteStream) -> web.stream.TByteStream
web.http helpers:
web.http.@response_bytes(status: i32, headers: Map<Str,Str>, body: Array<u8>) -> web.http.TResponseweb.http.@response_stream(status: i32, headers: Map<Str,Str>, body: web.stream.TByteStream) -> web.http.TResponseweb.http.@text(status: i32, body: Str) -> web.http.TResponse(setscontent-type: text/plain; charset=utf-8)web.http.@json<T>(status: i32, v: borrow T) -> web.http.TResponse(setscontent-type: application/json)web.http.@bad_request(msg: Str, request_id: Str) -> web.http.TResponseweb.http.@not_found(request_id: Str) -> web.http.TResponseweb.http.@internal_error(request_id: Str) -> web.http.TResponseweb.http.@serialize_http1(resp: web.http.TResponse, request_id: Str) -> web.stream.TByteStream- Produces the full HTTP/1.1 response bytes (status line + headers + body). Uses
Content-Lengthforbody_kind==0andTransfer-Encoding: chunkedforbody_kind==1, and injects/overwritesx-request-id.
- Produces the full HTTP/1.1 response bytes (status line + headers + body). Uses
web.router entry points:
web.router.@service_new(prefix: Str) -> web.router.TServiceweb.router.@route_add(svc: web.router.TService, method: Str, pattern: Str, handler: TCallable<web.router.THandlerSig>) -> web.router.TService-
In surface code, the final argument MAY be a
FnRef(e.g.@get_user). The compiler MUST coerce aFnRefto a capturelessTCallable<web.router.THandlerSig>. -
If the
FnRefhas typed params matching the route pattern (§30.1.4), the compiler MUST generate a wrapper first, then coerce the wrapper toTCallable<web.router.THandlerSig>. -
MUST be called with
methodandpatternasconst.Str(compile-time validated).
-
web.router.@middleware_add(svc: web.router.TService, mw: TCallable<web.router.TMiddlewareSig>) -> web.router.TServiceweb.router.@dispatch(svc: web.router.TService, req: web.http.TRequest, ctx: web.http.TContext) -> web.http.TResponse- Matches route + runs middleware chain + handler wrapper.
30.1.7 HTTP server entry point (web.server.@serve) and runtime contract
web.server.@serve is the only socket-facing API in v0.1.
heap struct web.server.TServeOpts {
field keep_alive: bool ;; default true
field threads: u32 ;; default = number of logical CPUs (min 1)
field max_body_bytes: u64 ;; default 10_000_000
field read_timeout_ms: u64 ;; default 30_000
field write_timeout_ms: u64 ;; default 30_000
field log_requests: bool ;; default false
}
fn web.server.@serve(
%svc: web.router.TService,
%addr: Str,
%port: u16,
%opts: web.server.TServeOpts
) -> TResult<unit, Str> ;; Err contains a human-opaque error string
Lowering rule (MUST):
The compiler lowers web.server.@serve to the runtime function mp_rt_web_serve (defined in §20.1.6).
Ok(()) is returned only when the server exits cleanly. Err(msg) indicates startup failure.
Runtime → Magpie callbacks (MUST): The runtime MUST call these exported symbols provided by the web standard package:
// For each request: returns an owned TByteStream producing the full HTTP response bytes.
MpRtHeader* __magpie_web_handle_request(
MpRtHeader* svc, // web.router.TService
MpRtHeader* method, // Str
MpRtHeader* path, // Str (path only, no query)
MpRtHeader* query, // Map<Str,Str>
MpRtHeader* headers, // Map<Str,Str>
MpRtHeader* body_bytes, // Array<u8>
MpRtHeader* remote_addr, // Str
MpRtHeader* request_id // Str
);
// Pull next chunk. Returns 1 and sets *out_chunk on success, 0 when finished.
int32_t __magpie_web_stream_next(MpRtHeader* stream, MpRtHeader** out_chunk);
Ownership contract (MUST):
- Runtime transfers ownership of all request objects passed to
__magpie_web_handle_request. - The callback transfers ownership of returned
streamto the runtime. - Each
out_chunkreturned by__magpie_web_stream_nextis owned by the runtime, which MUST release it after write. - Runtime MUST release the stream after completion (and on error).
This design keeps the runtime oblivious to Magpie struct field layouts.
30.1.8 JSON encode/decode intrinsics (backend)
The json.encode<T> / json.decode<T> intrinsics described in §7 are the canonical JSON mechanism.
Additional web JSON rules (MUST):
- JSON for
web.httperrors MUST use UTF-8 and setcontent-type: application/json; charset=utf-8. - Encoding/decoding MUST be deterministic (stable key order for maps).
30.1.9 Testing harness (no sockets)
web.test.@request MUST exist:
fn web.test.@request(
%svc: web.router.TService,
%method: Str,
%path: Str,
%query: Map<Str,Str>,
%headers: Map<Str,Str>,
%body: Array<u8>
) -> web.http.TResponse
Semantics:
- Constructs a
TRequest/TContextwith a deterministicrequest_id(e.g."test-<counter>"). - Calls
web.router.@dispatch. - Returns the response.
30.2 Frontend: Magpie Web App Framework (MWAF) (SSR)
MWAF is a code generator + conventions that builds on MWSF. It is required for Magpie Web v0.1 conformance.
30.2.1 Project layout (MUST)
app/
routes/
index.mp
about.mp
users/
[id:u64].mp
_layout.mp ;; optional root layout
assets/
... static files ...
30.2.2 File-based routing rules (MUST)
app/routes/index.mpmaps to path/.app/routes/<name>.mpmaps to path/<name>.- Nested directories map to nested paths.
- Dynamic segment file name:
[param:type].mpmaps to/{param:type}.typeis the same set as §30.1.3.
30.2.3 Route module contract (MUST)
A route file MUST export:
fn @render(%req: web.http.TRequest, %ctx: web.http.TContext) -> web.ui.TNode
Optional:
fn @data(%req: web.http.TRequest, %ctx: web.http.TContext) -> TResult<TProps, Str>TPropsMUST be a JSON-serializable heap struct.- If present, the generator calls
@data, and passes the resulting props to@rendervia an overload:fn @render(%req, %ctx, %props: TProps) -> web.ui.TNode
Optional root layout:
app/routes/_layout.mpMAY export:fn @wrap(%req: web.http.TRequest, %ctx: web.http.TContext, %child: web.ui.TNode) -> web.ui.TNode
30.2.4 UI representation (web.ui.TNode) (authoritative)
heap struct web.ui.TAttr {
field key: Str
field value: Str
}
heap struct web.ui.TNode {
field tag: Str ;; "div", "span", "#text", "#raw"
field text: Str ;; used when tag == "#text" or "#raw"
field attrs: Array<web.ui.TAttr>
field children: Array<web.ui.TNode>
}
Rules (MUST):
tag == "#text":textis HTML-escaped.tag == "#raw":textis inserted verbatim (unsafe; only allowed inunsafecontext in v0.1).- Otherwise:
tagis a lower-case HTML tag name;textMUST be empty; children render recursively.
30.2.5 SSR rendering (bytes and streaming)
Required functions:
web.ui.@render_bytes(node: web.ui.TNode) -> Array<u8>web.ui.@render_stream(node: web.ui.TNode) -> web.stream.TByteStream
Streaming rules (MUST):
@render_streamMUST produce valid incremental HTML. It MAY chunk at arbitrary boundaries.- The default chunk size SHOULD be 16KiB (configurable).
30.2.6 MWAF code generation (magpie web build / magpie web dev)
The magpie web tool scans app/routes/** and generates a module:
.magpie/gen/webapp_routes.mpcontaining:fn web.app.@service(prefix: Str) -> web.router.TServicewhich registers:- page routes (GET)
Static assets are served by the web runtime (not by generated Magpie routes):
-
In
magpie web dev,/assets/*is served fromapp/assetswith caching disabled. -
In
magpie web serve,/assets/*is served fromdist/assetswith caching enabled.- wrappers for typed path params (via §30.1.4)
- wrappers for
@data/@render/_layout.@wrap - SSR responses using
web.ui.@render_stream
The generated service is deterministic and stable under CSNF.
30.2.7 Islands (WASM) (optional in v0.1)
Islands are optional in Magpie Web v0.1. If implemented, they MUST follow this contract:
- A function annotated with
meta { effects { web.client } }and returningweb.ui.TNodeis a client island entry. magpie web buildcompiles islands to wasm32 and emits:dist/islands/<sid>.wasmdist/islands/<sid>.jsloader
- SSR rendering inserts placeholders:
<div data-magpie-island="<sid>" data-magpie-props="<json>"></div> - The JS loader MUST:
- find placeholders
- fetch wasm
- call an exported function
magpie_island_mount(ptr, len)where(ptr,len)is UTF-8 JSON props in WASM memory - mount/hydrate into the DOM element
30.3 magpie web commands and artifacts (v0.1)
30.3.1 magpie web dev
- Runs the web server on
addr/portfrom[web]manifest config (defaults:127.0.0.1:3000). - Enables hot reload (§23.4).
- Serves assets from
app/assetswith no caching. - Rebuilds generated MWAF module on file changes under
app/routesandapp/assets.
30.3.2 magpie web build
Produces dist/:
dist/server/<name>— native executabledist/assets/**— copied static assets (hash in filename optional)dist/openapi.json— OpenAPI (MWSF routes only; pages excluded)dist/routes.json— route manifest (for tooling)
30.3.3 magpie web serve
Runs dist/server/<name> and serves dist/assets/** with caching headers.
31. GPU specification
Magpie GPU v0.1 defines a compute-only kernel subset and a host dispatch API designed for LLM-written code: explicit buffers, explicit launches, and deterministic binding rules.
31.0 Conformance and backends (v0.1)
A toolchain claiming Magpie GPU v0.1 conformance MUST implement:
- Vulkan Compute backend via SPIR-V (
target(spv))
The following backends are OPTIONAL (MAY be implemented later):
- CUDA via LLVM NVPTX (
target(ptx)) - Metal (
target(msl)) - WebGPU/WGSL (
target(wgsl))
If a backend is not available at runtime, GPU operations MUST return Err(<message>) (not panic).
31.1 Host-visible GPU types (builtins)
GPU handles are builtin heap types implemented by the runtime (opaque payloads):
gpu.TDevice— a GPU device handlegpu.TBuffer<T>— a typed device buffergpu.TFence— completion handle for async launches (optional; may be a no-op in sync mode)
Errors are represented as Str in v0.1 (opaque, for machine consumption).
31.2 Host API surface (compiler-known intrinsics)
All host GPU functions live in the gpu.host namespace and are compiler-known intrinsics lowered to runtime ABI calls (§20.1.7).
; Device discovery
fn gpu.host.@device_default() -> TResult<gpu.TDevice, Str>
fn gpu.host.@device_count() -> u32
fn gpu.host.@device_by_index(%idx: u32) -> TResult<gpu.TDevice, Str>
fn gpu.host.@device_name(%dev: borrow gpu.TDevice) -> Str
; Buffers (device-local by default)
fn gpu.host.@buffer_new<T: type>(
%dev: borrow gpu.TDevice,
%len: u64,
%usage_flags: u32
) -> TResult<gpu.TBuffer<T>, Str>
fn gpu.host.@buffer_from_array<T: type>(
%dev: borrow gpu.TDevice,
%src: borrow Array<T>,
%usage_flags: u32
) -> TResult<gpu.TBuffer<T>, Str>
fn gpu.host.@buffer_to_array<T: type>(
%buf: borrow gpu.TBuffer<T>
) -> TResult<Array<T>, Str>
fn gpu.host.@buffer_copy<T: type>(
%src: borrow gpu.TBuffer<T>,
%dst: borrow gpu.TBuffer<T>
) -> TResult<unit, Str>
fn gpu.host.@buffer_len<T: type>(%buf: borrow gpu.TBuffer<T>) -> u64
; Synchronization
fn gpu.host.@fence_wait(%f: borrow gpu.TFence, %timeout_ms: u64) -> TResult<bool, Str> ;; Ok(true)=done, Ok(false)=timeout
fn gpu.host.@device_sync(%dev: borrow gpu.TDevice) -> TResult<unit, Str>
Usage flags (v0.1):
- Bit 0 (
1<<0) =STORAGE(read/write in kernels) — default - Bit 1 (
1<<1) =UNIFORM(read-only uniform) — optional - Bit 2 (
1<<2) =TRANSFER_SRC - Bit 3 (
1<<3) =TRANSFER_DST
The compiler MAY infer missing transfer bits when needed for copies.
31.3 Kernel declaration (gpu fn) and restrictions
A kernel is declared with gpu fn and a required backend target:
gpu fn @kernel_add(
%in: gpu.TBuffer<f32>,
%out: gpu.TBuffer<f32>,
%n: u32
) -> unit target(spv) {
bb0:
%gid: u32 = gpu.global_id { dim=const.u32 0 }
%in_bounds: bool = icmp.ult { lhs=%gid, rhs=%n }
cbr %in_bounds bb1 bb2
bb1:
%x: f32 = gpu.buffer_load<f32> { buf=%in, idx=%gid }
%y: f32 = f.add { a=%x, b=const.f32 1.0 }
gpu.buffer_store<f32> { buf=%out, idx=%gid, v=%y }
br bb2
bb2:
ret
}
Kernel restrictions (MUST):
- No heap allocation, no ARC operations, no Str/Array/Map, no TCallable.
- No recursion.
- No dynamic dispatch (no TCallable calls; no
call.indirect). - Allowed local types:
- primitives (
i*,u*,f*,bool) - value structs containing only primitives
gpu.TBuffer<T>handles
- primitives (
- All out-of-bounds checks MUST be explicit unless the kernel body is inside
unsafe {}.
31.4 Device-side buffer access ops (MUST)
These ops are valid only inside gpu fn:
gpu.buffer_load<T> { buf=<ValueRef>, idx=<ValueRef> } -> T
gpu.buffer_store<T> { buf=<ValueRef>, idx=<ValueRef>, v=<ValueRef> } -> unit
gpu.buffer_len<T> { buf=<ValueRef> } -> u32
Semantics:
idxis an element index (not bytes).gpu.buffer_loadreadsbuf[idx].gpu.buffer_storewritesbuf[idx].- Bounds are NOT implicit: out-of-bounds behavior is undefined unless compiler inserts checks.
31.5 GPU builtins (thread/workgroup) (MUST)
Available inside gpu fn:
gpu.thread_id { dim=<0|1|2> } -> u32gpu.workgroup_id { dim=<0|1|2> } -> u32gpu.workgroup_size { dim=<0|1|2> } -> u32gpu.global_id { dim=<0|1|2> } -> u32gpu.barrier(workgroup barrier)gpu.shared<N,T>— shared memory allocation (optional in v0.1; MAY be unimplemented, in which case compiler errors withMPG1201 SHARED_UNSUPPORTED)
31.6 Kernel launch (gpu.launch / gpu.launch_async)
Host code dispatches kernels via explicit launch ops:
%dev: gpu.TDevice = try gpu.host.@device_default { args=[] }
%buf_in: gpu.TBuffer<f32> = try gpu.host.@buffer_from_array<f32> { args=[%dev, %a, const.u32 0] }
%buf_out: gpu.TBuffer<f32> = try gpu.host.@buffer_new<f32> { args=[%dev, %n, const.u32 0] }
%launch: TResult<unit, Str> = gpu.launch {
device=%dev,
kernel=@kernel_add,
grid=[%gx, %gy, %gz],
block=[%bx, %by, %bz],
args=[%buf_in, %buf_out, %n]
}
Launch forms:
- Synchronous (MUST):
gpu.launch { ... } -> TResult<unit, Str>- The runtime MUST not return
Ok(())until the kernel has completed and all writes are visible to subsequent host reads/copies on the same device.
- The runtime MUST not return
- Asynchronous (OPTIONAL):
gpu.launch_async { ... } -> TResult<gpu.TFence, Str>- Completion is tested via
gpu.host.@fence_wait.
- Completion is tested via
Deterministic binding rules (MUST):
- Buffer parameters are bound in parameter order to set=0, binding=b where b increments for each buffer param.
- Scalar parameters are packed into a single push-constant block in parameter order using std430-like alignment:
- scalars aligned to their size, capped at 16 bytes
- the block size is rounded up to 16
- The compiler MUST generate the same layout across builds given the same kernel signature.
31.7 Kernel compilation and embedding (MUST)
During magpie build:
- Each
gpu fn ... target(spv)is lowered to a GPU LLVM module. - The module is compiled to a SPIR-V binary blob.
- The blob is embedded in the host object as a
constbyte array. - The compiler emits a kernel registry section:
typedef struct MpRtGpuKernelEntry {
uint64_t sid_hash; // hash of the kernel's SID string
uint32_t backend; // 1=SPV
const uint8_t* blob;
uint64_t blob_len;
uint32_t num_params;
const MpRtGpuParam* params;
} MpRtGpuKernelEntry;
At program startup, the runtime is called to register all kernels:
mp_rt_gpu_register_kernels(entries, count) (§20.1.7).
31.8 CLI integration (v0.1)
magpie buildMUST compile and embed GPU kernels when they are reachable from the build graph.--emit spvMUST emit per-kernel SPIR-V blobs totarget/<profile>/gpu/<sid>.spv.--features gpuMAY be used to gate GPU-only modules.
32. Unsafe and C FFI
32.1 Unsafe blocks and functions
unsafe {
%p: rawptr<u8> = call @malloc { size=%sz }
}
unsafe fn @dangerous_op(%p: rawptr<u8>) -> i32 {
bb0:
; entire body is unsafe context
...
}
Rules:
rawptr<T>operations outsideunsafeis a hard error.unsafe fnmakes the entire function body an unsafe context.- Callers of
unsafe fnmust be in an unsafe context.
32.1.1 Raw pointer opcodes (ptr.*)
rawptr<T> is an unsafe, non-owning pointer to a value of type T. rawptr<T> is a Copy type and never participates in ARC.
All ptr.* opcodes MUST appear in an unsafe context (inside unsafe {} or inside an unsafe fn). Violations are a hard error.
Semantics (LLVM-like; UB is allowed):
%p: rawptr<T> = ptr.null<T>produces a null pointer.%addr: u64 = ptr.addr<T> { p=%p }converts a raw pointer to an address.%p: rawptr<T> = ptr.from_addr<T> { addr=%addr }converts an address to a raw pointer.%q: rawptr<T> = ptr.add<T> { p=%p, count=%n }performs element-wise pointer arithmetic (q = p + n*sizeof(T)).%v: T = ptr.load<T> { p=%p }loads aTfrom memory.ptr.store<T> { p=%p, v=%v }stores aTto memory.
Undefined behavior (UB): ptr.load/ptr.store on an invalid address, misaligned address for T, or violating aliasing rules is UB.
32.2 Extern modules
extern "c" module libc {
fn @puts(%s: borrow Str) -> i32 attrs { returns="borrowed" }
}
Every extern function returning a pointer MUST specify ownership in attrs. Missing: MPF0001 FFI_RETURN_OWNERSHIP_REQUIRED.
33. Testing and linting
33.1 Test discovery
- Functions prefixed with
@test_are test cases. - Test modules live in
tests/*.mp. magpie testdiscovers and runs all test functions.- Assertions via
std.test.@assert(panics with message) andstd.test.@assert_eq(panics with expected/actual).
33.2 Lint categories
Style + complexity:
MPL2001 FN_TOO_LARGE— function exceedsmax_fn_linesMPL2003 MODULE_TOO_LARGE— module exceedsmax_module_linesMPL2004 UNUSED_IMPORT— import not referencedMPL2005 UNUSED_LOCAL— SSA value defined but never usedMPL2006 MISSING_META— function lacks meta block in--llmmodeMPL2007 NAMING_CONVENTION— identifier doesn't match convention
Safety:
MPL2010 POTENTIAL_CYCLE— type can form ownership cycles without weakMPL2011 UNCHECKED_RESULT— Result value used without checking errorMPL2012 DEAD_CODE— unreachable code detectedMPL2013 UNBOUNDED_RECURSION— recursive call with no base case detectedMPL2014 PANIC_REACHABLE— panic instruction reachable in production code
LLM-specific:
MPL2002 COST_UNDERESTIMATE— actual cost >2x declaredMPL2020 EXCESSIVE_MONO— monomorphized instances exceed budgetMPL0801 TOKEN_BUDGET_TOO_SMALL— budget insufficient for minimal output
All lints configurable in Magpie.toml:
[lint]
MPL2001 = "error" # promote to error
MPL2010 = "allow" # silence
MPL2006 = "warn" # default
34. Standard library surface
Minimal std (Go-like):
| Package | Contents |
|---|---|
std.core | TOption, TResult (lang items, auto-available) |
std.io | @println, @readln, @stdin, @stdout, @stderr, file read/write |
std.str | String operations (intrinsics: concat, len, eq, slice, bytes) + TStrBuilder |
std.os | @env_var, @args, @exit, @cwd |
std.math | Basic numeric functions (abs, min, max, pow, sqrt, floor, ceil) |
std.test | @assert, @assert_eq, @assert_ne, @fail |
std.sync | TMutex, TRwLock, TChannel, TCell |
std.thread | @spawn, @sleep, @yield_now |
std.hash | @hash_Str, @hash_i32, @hash_i64, etc. |
std.async | @block_on, @spawn_task, TFuture<T> — async executor entry points |
std.parse | Intrinsic opcodes: str.parse_i64, str.parse_u64, str.parse_f64, str.parse_bool (§34.2) |
Extended packages (v0.1):
| Package | Summary |
|---|---|
gpu.host | Host-side GPU API (device discovery, buffers, dispatch) (§31) |
web.http | HTTP request/response types and helpers (§30) |
web.router | Routing and middleware (§30) |
web.server | HTTP server entry point (@serve) (§30) |
web.stream | Byte streams for streaming responses (§30) |
web.ui | SSR UI nodes and HTML renderer (§30) |
web.test | Socketless request testing harness (§30) |
web.app | MWAF generated glue module (not authored by hand) (§30) |
All core type operations (Str, Array, Map) are compiler intrinsics lowered to runtime ABI calls. No Magpie source needed for these.
34.1 std.async package
| Intrinsic | Call form | Semantics |
|---|---|---|
@block_on<T> | call std.async.@block_on<T> { fn=@async_fn, args=[...] } | Runs an async function to completion on the default executor (blocking the current thread). |
@spawn_task<T> | call std.async.@spawn_task<T> { fn=@async_fn, args=[...] } | Spawns an async task on the default executor; returns TFuture<T>. |
Notes:
- These are compiler intrinsics. The
fn/argscall form is validated by the compiler (arity and types).TFuture<T>is the handle returned by@spawn_task. It is ARC-managed andsend + sync. @block_onis the entry point for calling async code from non-async contexts.
34.2 str.parse_* intrinsics
| Intrinsic | Signature | Semantics |
|---|---|---|
str.parse_i64 | (Str) -> TResult<i64, TParseError> | Parse string to i64 |
str.parse_u64 | (Str) -> TResult<u64, TParseError> | Parse string to u64 |
str.parse_f64 | (Str) -> TResult<f64, TParseError> | Parse string to f64 |
str.parse_bool | (Str) -> TResult<bool, TParseError> | Parse "true"/"false" to bool |
TParseError is a heap struct: heap struct TParseError { field message: Str }
Transitional compatibility note (v0.1 implementation status):
- The language target model remains fallible (
TResult<_, TParseError>). - Current lowering keeps legacy success-only parse/json op shapes for compatibility, but codegen MUST call fallible runtime ABI (
mp_rt_*_try_*) and branch on status. - On non-OK status in this compatibility path, codegen currently calls
mp_rt_panicwith the runtime-provided error string. - Direct Rust
panic!/expect!behavior at parse/json FFI boundaries is not allowed for recoverable input failures.
35. Distribution and agent packaging
35.1 Codex skill pack
SKILL.mdwith YAML front matteragents/openai.yaml- scripts:
bin/magpie-build,bin/magpie-run
35.2 Claude Code plugin pack
.claude-plugin/plugin.jsonoptional manifest
36. Compiler implementation requirements (Rust)
36.1 Workspace crates
magpie_cli— CLI entry pointmagpie_driver— compilation orchestrationmagpie_lex— lexermagpie_parse— parser (hand-written recursive descent)magpie_csnf— formatter/canonicalizermagpie_ast— AST typesmagpie_hir— HIR types and loweringmagpie_sema— name resolution, symbol tablesmagpie_types— type interning/layoutmagpie_mono— monomorphizationmagpie_own— ownership checker (dataflow)magpie_mpir— MPIR builder + verifiermagpie_arc— ARC insertion + optimizationmagpie_codegen_llvm— LLVM codegen (llvm-sys bindings)magpie_codegen_wasm— WASM-specific codegen adjustmentsmagpie_jit— ORC JITmagpie_diag— diagnostics + patchesmagpie_pkg— manifest/lock/registrymagpie_memory— MMS indexing + retrievalmagpie_ctx— ctx pack buildermagpie_web— backend + SSR frameworksmagpie_gpu— GPU compilationmagpie_rt— runtime library
36.2 SourceMap and spans
All parsed nodes MUST carry byte spans for precise diagnostics.
pub struct FileId(pub u32);
pub struct Span {
pub file: FileId,
pub start: u32,
pub end: u32,
}
pub struct Spanned<T> {
pub node: T,
pub span: Span,
}
36.3 Parsing strategy
- Hand-written recursive descent with recovery by synchronization tokens at block/decl boundaries.
- All parse errors produce diagnostics with precise span, expected tokens set, and recovery action.
36.4 Linking
- Default: static linking, all
.ointo one binary. - Optional:
--emit shared-libproduces.so/.dylibwith C ABI exports. magpie_rtis always statically linked.- Prefer
lldif available; otherwise system linker. - Release mode: deterministic builds.
36.5 Cross-compilation
- Toolchain bundles defined in
Magpie.tomlunder[toolchain.<triple>]. - Specifies sysroot, linker, and flags.
magpie_rtmust be pre-compiled for each target and bundled.
37. Appendices
Appendix A — Complete instruction set table (v0.1)
Constants: const.*
Integer (checked): i.add i.sub i.mul i.sdiv i.udiv i.srem i.urem
Integer (wrapping): i.add.wrap i.sub.wrap i.mul.wrap
Integer (checked → TOption): i.add.checked i.sub.checked i.mul.checked
Bitwise: i.and i.or i.xor i.shl i.lshr i.ashr
Float (strict IEEE 754): f.add f.sub f.mul f.div f.rem
Float (fast-math): f.add.fast f.sub.fast f.mul.fast f.div.fast
Compare: icmp.eq icmp.ne icmp.slt icmp.sgt icmp.sle icmp.sge icmp.ult icmp.ugt icmp.ule icmp.uge / fcmp.oeq fcmp.one fcmp.olt fcmp.ogt fcmp.ole fcmp.oge
Control: br cbr switch ret unreachable phi
Calls: call call_void try suspend.call
Heap: new getfield setfield
Enum: enum.tag enum.payload<V> enum.is<V>
Ownership: share clone.shared weak.downgrade weak.upgrade
ARC: arc.retain arc.release arc.retain_weak arc.release_weak
Callable: callable.capture
Array: arr.new arr.len arr.get arr.set arr.push arr.pop arr.slice arr.contains arr.sort arr.map arr.filter arr.reduce arr.foreach
Map: map.new map.len map.get map.set map.delete map.contains_key map.keys map.values
String: str.concat str.len str.eq str.slice str.bytes str.builder.new str.builder.append_str str.builder.append_i64 str.builder.append_i32 str.builder.append_f64 str.builder.append_bool str.builder.build
GPU: gpu.thread_id gpu.workgroup_id gpu.workgroup_size gpu.global_id gpu.barrier gpu.shared
Error: panic
Cast: cast<From,To> (primitive types only in v0.1)
Appendix B — Backend framework MVP checklist
web.httprequest/response typesweb.routerroute matcher + typed param extraction (compiler-generated parsers)web.serverminimal async server (Rust runtime)- JSON encode/decode for heap structs
- Route registration builder function pattern
magpie web devwiring: build + JIT hot-swap- OpenAPI JSON generation from route table + types
Appendix C — Manifest JSON Schema (magpie-manifest.schema.json, Draft 2020-12)
{
"$schema":"https://json-schema.org/draft/2020-12/schema",
"$id":"magpie-manifest.schema.json",
"type":"object",
"required":["package","build","dependencies"],
"properties":{
"package":{
"type":"object",
"required":["name","version","edition"],
"properties":{
"name":{"type":"string","pattern":"^[a-z][a-z0-9_\\-]{1,63}$"},
"version":{"type":"string","pattern":"^[0-9]+\\.[0-9]+\\.[0-9]+.*$"},
"edition":{"type":"string"}
},
"additionalProperties":true
},
"build":{
"type":"object",
"required":["entry","profile_default"],
"properties":{
"entry":{"type":"string"},
"profile_default":{"type":"string","enum":["dev","release","custom"]}
},
"additionalProperties":true
},
"dependencies":{
"type":"object",
"additionalProperties":{
"type":"object",
"required":["version"],
"properties":{
"version":{"type":"string"},
"registry":{"type":"string"},
"path":{"type":"string"},
"git":{"type":"string"},
"rev":{"type":"string"},
"features":{"type":"array","items":{"type":"string"}},
"optional":{"type":"boolean"}
},
"additionalProperties":true,
"allOf":[{"not":{"anyOf":[{"required":["path","git"]},{"required":["path","registry"]},{"required":["git","registry"]}]}}]
}
},
"llm":{
"type":"object",
"properties":{
"mode_default":{"type":"boolean"},
"token_budget":{"type":"integer","minimum":256,"maximum":1000000},
"tokenizer":{"type":"string"},
"budget_policy":{"type":"string","enum":["balanced","diagnostics_first","slices_first","minimal"]},
"max_module_lines":{"type":"integer","minimum":50,"maximum":200000},
"max_fn_lines":{"type":"integer","minimum":10,"maximum":200000},
"auto_split_on_budget_violation":{"type":"boolean"},
"rag":{
"type":"object",
"properties":{
"enabled":{"type":"boolean"},
"backend":{"type":"string","enum":["lexical","vector","hybrid"]},
"top_k":{"type":"integer","minimum":1,"maximum":200},
"max_items_per_diag":{"type":"integer","minimum":0,"maximum":50},
"include_repair_episodes":{"type":"boolean"}
},
"additionalProperties":true
}
},
"additionalProperties":true
}
},
"additionalProperties":true
}
Appendix D — Lockfile JSON Schema (magpie-lock.schema.json, Draft 2020-12)
{
"$schema":"https://json-schema.org/draft/2020-12/schema",
"$id":"magpie-lock.schema.json",
"type":"object",
"required":["lock_version","generated_by","packages"],
"properties":{
"lock_version":{"type":"integer","enum":[1]},
"generated_by":{
"type":"object",
"required":["magpie_version","toolchain_hash"],
"properties":{
"magpie_version":{"type":"string"},
"toolchain_hash":{"type":"string"}
},
"additionalProperties":true
},
"packages":{
"type":"array",
"items":{
"type":"object",
"required":["name","version","source","content_hash","deps"],
"properties":{
"name":{"type":"string"},
"version":{"type":"string"},
"source":{
"type":"object",
"required":["kind"],
"properties":{
"kind":{"type":"string","enum":["registry","path","git"]},
"registry":{"type":"string"},
"url":{"type":"string"},
"path":{"type":"string"},
"rev":{"type":"string"}
},
"additionalProperties":true
},
"content_hash":{"type":"string"},
"deps":{
"type":"array",
"items":{
"type":"object",
"required":["name","req"],
"properties":{
"name":{"type":"string"},
"req":{"type":"string"},
"features":{"type":"array","items":{"type":"string"}}
},
"additionalProperties":true
}
},
"resolved_features":{"type":"array","items":{"type":"string"}},
"targets":{"type":"array","items":{"type":"string"}}
},
"additionalProperties":true
}
}
},
"additionalProperties":true
}
Appendix E — MMS Lexical Tokenizer (exact spec)
This tokenizer is used for MMS lexical indexing and BM25 scoring. It MUST be deterministic and platform-independent.
Input normalization (MUST): Given UTF-8 input S: (1) Convert to Unicode NFKC normalization. (2) Apply Unicode case folding (full). (3) Replace \r\n and \r with \n. (4) Collapse 2+ whitespace to single space. (5) Trim leading/trailing whitespace.
Token categories: Each term has term_text, term_kind (word|symbol|number|code|diag|path), position (0-based).
Character classes: ALNUM (Unicode letters/digits), UNDERSCORE (_), DOT (.), COLON (:), SLASH (/), AT (@), PERCENT (%), HASH (#), DASH (-), PLUS (+), OTHER.
Joiner set: @ % . : / _ - (inside certain tokens).
Primary scan (left-to-right, longest match):
- Pattern A — Diagnostic code (
diag):MP[A-Z][0-9]{4}(e.g.,MPO0007) - Pattern B — Symbol (
symbol): Starts with@or%or contains.@/.%. Allowed: ALNUM + joiners. Must contain@or%. - Pattern C — Path (
path): Contains/. Allowed: ALNUM +. : / _ -. - Pattern D — Number (
number):0x[0-9a-f]+or[0-9]+. - Pattern E — Word (
word): Longest run of ALNUM or_. - All other characters are delimiters.
Identifier decomposition (MUST): For symbol and word terms, emit sub-terms by splitting on _, camelCase transitions (using pre-casefold string), and digit boundaries. Parts with length >= 2 emitted as code. Also emit compressed form (remove _ and -).
Stopword filtering (MUST): Remove for word and code terms only. Set: a an and are as at be by for from has have if in is it its of on or that the to was were with true false unit ret br cbr switch bb fn module imports exports digest const call call_void new
Term length limits: Drop terms < 2 chars (except diag). Truncate > 64 chars.
Query tokenization: Same algorithm; stopword removal MAY be disabled if query contains only stopwords.
Appendix F — BM25 Defaults (exact spec)
Document model: Each MMS item is a document. Document length |D| = number of retained terms.
Scoring formula:
score(D,Q) = Σ_{t in Q} IDF(t) * ((f(t,D) * (k1 + 1)) / (f(t,D) + k1 * (1 - b + b * |D|/avgdl)))
Where f(t,D) = term frequency, avgdl = average doc length.
IDF (Robertson/Sparck Jones with +1 smoothing):
IDF(t) = ln(1 + (N - df(t) + 0.5)/(df(t) + 0.5))
Default parameters: k1 = 1.2, b = 0.75
Field boosts (multiplicative, applied after BM25):
boost_kind: diag_template=1.40, spec_excerpt=1.25, mpd_signature=1.20, symbol_capsule=1.15, test_case=1.10, repair_episode=1.05, default=1.0boost_tags: query contains exact diag code matching doc tag: x1.30; query contains module terms matching doc module: x1.15boost_priority:x (0.5 + priority/100)clamped to[0.75, 1.50]
Deterministic tie-break: (1) smaller token_cost, (2) lexicographically smaller item_id.
Index persistence: index_lex/ stores: vocab.bin, postings.bin, doclens.bin, itemmap.bin, bm25_meta.json (N, avgdl, k1, b, tokenizer id, schema version).
Appendix G — Context Pack Scoring and Selection (exact spec)
Chunk types: Core structural (module_header, mpd_public_api, symgraph_summary, deps_summary), Problem-focused (diagnostics, ownership_trace, cfg_summary), Code capsules (symbol_capsule, snippet), Retrieved (rag_item).
Chunk IDs: chunk_id = "C:" + base32(blake3(kind + "|" + subject_id + "|" + variant + "|" + body_digest))[:16]
Scoring formula:
score = base_priority(kind) + relevance(kind, scope) + proximity(kind, failing_sid) + retrieval_score(kind, MMS) - size_penalty(token_cost)
base_priority: module_header=100, mpd_public_api=90, symgraph_summary=85, diagnostics=80, ownership_trace=78, cfg_summary=72, symbol_capsule=70, snippet=60, rag_item=55, deps_summary=50.
relevance: symbol scope=+30, module=+20, files=+15, pkg=+10.
proximity: subject==failing_sid=+25, same module=+10, direct dep=+5, else=+0.
retrieval_score: rag_item only: +min(25, floor(mms_score)).
size_penalty: token_cost / 200 (integer division).
Tie-break: (1) higher score, (2) lower token_cost, (3) smaller chunk_id.
Budget partitioning policies:
balanced: 25% structural, 45% problem+capsules, 30% retrieveddiagnostics_first: 30% structural, 60% problem+capsules, 10% retrievedslices_first: 35% structural, 55% capsules/snippets, 10% retrievedminimal: 60% structural, 40% problem, 0% retrieved
Spillover (default in balanced): structural -> problem -> retrieved.
Multi-variant compression ladder: v3 (full text) -> v2 (trimmed, signatures+key lines) -> v1 (signatures+bullets, <=20 lines) -> v0 (one-line identity: SID+name+type). Selection picks highest variant that fits budget.
Selection algorithm: Per bucket: build candidates, sort by score desc/token_cost asc/chunk_id asc, greedily select highest fitting variant. Merge bucket results in order: structural -> problem -> retrieved.
End of specification.
What's inside
33 sections covering syntax, IR, CLI, package manager, web framework, GPU compute, and memory store
Change this for your project
- Replace
magpiewith your own toolchain name throughout - Replace
Magpie.tomlwith your own manifest filename - Replace
magpie-lang/magpiewith your own repository path - Replace edition
"2026"with your own version identifier
Where it goes
Keep in docs/ or alongside the feature. Agents read it to implement against a defined contract.
Worth borrowing
- Token budget system with deterministic dropping tiers for LLM outputs
- Canonical source normal form (CSNF) to prevent format drift
- Progressive disclosure via module digests and symbol graphs
Related Documents
GPU Selection Guide for Large Language Models (LLMs)
Guides GPU selection for LLM inference, fine-tuning, and training by mapping model sizes, precision levels, and budgets to VRAM requirements.
Community AI Agent Skills Discovery Sources
Catalogs 50+ platforms, repositories, directories, and communities for discovering and sharing AI agent skills across multiple coding tools.
ReleaseKit - Technical Requirements Document
Specifies a Go library and CLI for release automation with conventional commit parsing, validation checks, and workflow orchestration.
api_llm Specification
Defines a workspace of thin HTTP API clients for major LLM providers with no abstraction layer and explicit developer control.