Architecture
Sere is split so the compiler and the editor share one analysis pipeline. Codegen, linking, and the language server are thin clients of that pipeline.
Process shape
tools/sere/main.cpp only parses argv and calls Compiler::run.
Compiler::run
├── --lsp → runLanguageServer()
├── init/init-lib/build/pack/run/clean
└── compileInput()
├── Frontend::analyze() lex, parse, import, prelude, macros, sema
├── IRGenerator::emit() typed AST → llvm::Module
│ └── or SeremGenerator typed AST → Serem IR → SeremTransform
│ → SeremLLVMBackend → llvm::Module
├── runOptPipeline() Sere IR rewrites + the composed LLVM pipeline
└── clang/lld + sere_rt unless --emit-llvm, --emit-asm, or --emit-serem--analyze runs Frontend and prints JSON diagnostics. It never touches LLVM.
That is the same path the LSP uses for textDocument/publishDiagnostics.
Both backends produce the same llvm::Module, so the optimizer, the emit modes,
and the linker are shared. backend.md covers lowering and linking,
optimization.md the switches, and serem.md the
Serem IR.
Frontend pipeline
Frontend::analyze(path, text, stdlibDir) in lib/driver/Frontend.cpp:
- Reset diagnostics, AST, types, imports.
- Wrap
textin aSourceManager. - Lex the whole file (
Lexer::tokenizeAll). - Parse a
Module(Parser::parseModule). - Load imports by walking
import/from … importstatements. Search order is the origin directory, thenstdlib/. SeeImportPath.h. - Load prelude (
stdlib/prelude.sere) into the user module, markedfromPrelude()so it is not emitted as user code. - Collect macro use sites for the LSP (
collectMacroUses). - Expand macros on imported modules, then on the user module.
- Build a
TypeContextand typecheck imports (export bindings become module fields). - Typecheck the user module.
TypeCheckerfillsresolvedType()on nodes and aSemanticSymboltable for the LSP.
If parse fails, Frontend still keeps an empty module so the LSP can report
diagnostics instead of crashing.
Types
Types are interned. Pointer identity is equality (TypeContext). Kinds:
TypeKind | Examples |
|---|---|
Primitive | i32, bool, str, void, never |
Generic | Unique[T], Ptr[i32], list[str], dict[K, V] |
Record | class, struct, enum |
Function | (i32, i32) -> i32 |
Alias | type Meters = i32 |
TypeParam | T on a generic def / class |
Module | imported package object |
Union | T | U |
Unique, Shared, and Ptr are pointer-like (Type::isPointerLike,
pointeeType()). Unary * loads the pointee; unary & produces Ptr[T].
Intrinsics vs stdlib vs extern
Three different ways a name becomes callable:
| Kind | How it exists | Example |
|---|---|---|
| Intrinsic | IntrinsicKind + TypeChecker::registerBuiltins + IRGenerator::emitIntrinsic | unique, alloc, len, print |
| Prelude / stdlib Sere | Parsed from stdlib/*.sere, often extern "C" "symbol" | io.read_line, gc.use |
| User / native | def in the program, or extern "C" + --link | examples/native_add.sere |
Intrinsics are always in scope. They are not “magic syntax”; they are names the
type checker and codegen special-case. Prefer a stdlib extern "C" wrapper
when the operation is just a runtime call.
Diagnostics
The compiler does not use C++ exceptions for user errors.
DiagnosticEngine collects error / warn / note with a DiagnosticCode
(NameError, TypeError, …). Messages print as error[NameError]: ….
# type: ignore and # type[NameError]: ignore are parsed by
IgnoreDirective from source comments. Unknown names in those comments are
ValueError and list the catalog.
Codegen contract
IRGenerator lowers a typed AST. It should not re-run language rules.
If a construct is legal, sema has already set resolvedType(). If codegen
needs a new fact, put it on the AST or the Type, not in an LLVM pass.
Reachable functions are emitted; main is wrapped as a C main that converts
argv into list[str] when the user main takes it.
Linking always includes sere_rt. Extra native libs come from --link.
Importing qt6 also pulls sere_qt6 when that library was built.
Invariants
- No circular CMake library deps.
- UI/editor code stays in
lib/lspandeditors/vscode. Language rules stay in parse / sema / codegen. - Heavy work stays off the UI thread in the editor by running in
sere --lspas a child process. - New public API goes in
include/sere/…with a file docstring.