Extending Sere
This is the recipe book. Each section is a complete checklist. Skip a row and the compiler, tests, or editor will disagree with each other.
Work in this order unless a recipe says otherwise: lex → parse → AST → sema → codegen → runtime/stdlib → LSP/grammar → tests → docs.
1. Add a keyword
Example: a new statement keyword unless.
include/sere/lex/TokenKind.h— addKeywordUnlessinside the blockKeywordFalse…KeywordWith(beforeUnknown). Semantic tokens treat that whole range as keywords.lib/lex/Token.cpp—kKeywords[]entry{"unless", TokenKind::KeywordUnless}and atokenKindNamecase.include/sere/ast/Syntax.h— newNodeKindand class if it is a new statement or expression; or reuseIfStmtif it is sugar.lib/ast/Syntax.cpp— construct / accessors.lib/parse/Parser.cpp—parseStatement(or expression) branch.lib/ast/Query.cpp—searchStmt/searchExprso hover hits the node.lib/sema/TypeChecker.cpp—checkStatement/checkExpr.lib/codegen/IRGenerator.cpp— emit IR (or desugar entirely in the parser and skip this).- Editor:
editors/vscode/syntaxes/sere.tmLanguage.jsonkeyword list;lib/lsp/LanguageServer.cppaddKeywordCompletions. - Tests: parse + sema + an
examples/file. - Mention it in
README.mdlanguage table if users should see it.
2. Add an operator
Example: prefix * / & (already implemented — follow the same path).
- Lexer: new
TokenKindonly if the spelling is not already a token.*and&reuseStar/Amp. - Parser: prefix in
parseUnary(prefixOp) or infix in the right precedence function (parseMul,parseBitAnd, …). Binary*/&must stay multiplication / bitwise AND. - AST:
UnaryOporBinaryOpenumerator. - Sema:
checkUnary/checkBinary. Pointer ops:checkDeref,checkAddrOf. - Codegen:
emitUnary/emitBinary/emitAddress. - LSP:
isOperatorTokeninSemanticTokens.cpp; TextMate#operator. - Tests in
parse_lang.cppandsema_types.cpp.
3. Add a builtin type constructor
Example: Unique[T], Ptr[T], list[T].
TypeContext— factory (uniqueType,ptrType, …) and intern key.Typehelpers —isPointerLike,pointeeType,isList, …TypeChecker::resolveNamedType— recognize the name and argument count.- Codegen
lower— LLVM representation. - LSP builtin-type lists (
SemanticTokens.cpp, completions, tmLanguage#type). - Docs in
stdlib/memory.sereor the README language table.
Do not special-case the name only in codegen. Sema must produce the interned
Type* first.
4. Add an intrinsic
Example: alloc, len, typeof.
include/sere/types/Intrinsic.h— newIntrinsicKind.lib/types/Intrinsic.cpp—intrinsicNameandintrinsicByName.TypeChecker::registerBuiltins— include the kind.TypeChecker::checkIntrinsicCall— arity, type args, return type.IRGenerator::emitIntrinsic— runtime calls or LLVM.- LSP: builtin function list + optional snippet
(
unique[${1:i32}](${2:value})). - tmLanguage
#functionif it should highlight before semantic tokens. - Sema test + example.
If the operation is a thin C call with a stable type, prefer
extern "C" in stdlib instead of a new intrinsic.
5. Add a diagnostic code
include/sere/diag/DiagnosticCode.h— enumerator.lib/diag/DiagnosticCode.cpp— name, description, catalog, inference heuristics (inferDiagnosticCode).- Emit via
DiagnosticEngine::erroras today; codes are inferred from the message unless you thread a code through explicitly. - README catalog table.
tests/type_ignore.cppif# type[YourError]: ignoreshould hide it.
Keep names Python-shaped (TypeError, NameError) so ignore comments stay
familiar.
6. Add a pure stdlib module
No compiler change.
stdlib/foo.sere— file docstring at the top.import foofrom a program. The last path segment is the module name.examples/…that uses it.add_test(NAME sere.example.… COMMAND sere --emit-llvm …)intests/CMakeLists.txt.
7. Add a stdlib module that needs C
- Declare and implement the C function (
runtime/sere_rt.h+.c). extern "C" "sere_foo_bar"instdlib/foo.sere.- Rebuild
seresosere_rtupdates. - Example + emit test.
See stdlib/io.sere and stdlib/gc.sere.
8. Add a native extension library
Two options:
Typed extern (preferred for simple functions)
extern "C" "native_add"
def add(left: i32, right: i32) -> i32sere main.sere --link native.libBoxed module API — implement sere_mod_init and Sere_DefineFunction
(include/sere/api/sere_mod.h).
9. Add a garbage collector
- Implement
SereGcVTable(include/sere/api/sere_gc.h). sere_gc_install(&vtable)fromsere_mod_init.- Link with
--link. - Call
gc.use("your_name")only if you also registered a builtin name insere_gc.c; otherwise installing fromsere_mod_initis enough. - Install before the program allocates.
Builtin names today: none, mark_sweep, arena.
10. Add an LSP feature
- Implement on the typed AST /
TypeCheckersymbols when possible so--analyzeand tests can see it. LanguageServer.cpp— handler +initializecapability bit.Query.cppif you need hit-testing.- Client:
editors/vscode/package.json/extension.jsonly if the client must opt in. - Unit test with
Frontend, not a live editor.
11. Add a project command
ProjectCommandinOptions.h.- Parse in
Options.cpp(parseProjectCommand+ usage text). - Branch in
Compiler::run. - Implementation under
lib/driver/Project*.cpp. tests/project_cli.cpp.
Library projects: sere init-lib / --init-lib / init --lib write
kind = "lib" and src/lib.sere. sere pack (also --pack, build-lib,
and sere build on a lib) writes a .slib via Library.h. Pack keeps only
reachable local modules and compiled native objects. Import resolution
accepts .slib and folder libraries (name/lib.sere, name/name.sere) in
ImportPath.cpp.
12. Add a test / example
See testing.md. Minimum for a language change:
- one unit test that fails without your patch
- one
examples/*.sereemit test if users should write that syntax
13. Add an optimization pass
Three layers can host a rewrite. Pick the one that matches what the pass needs to see:
A pass over the generated LLVM module (lib/codegen/OptPasses.cpp, declared
in include/sere/codegen/OptPasses.h).
- Write
std::uint32_t yourPass(llvm::Module& module, …)in the anonymous namespace, iteratemodule, return how many sites changed. - Call it from
runPrePipelinePassesin the order its dependencies require, and add a counter toOptRewriteReportsoSERE_OPT_REPORT=1reports it. - Gate it on an
OptimizationOptionsfield if it is optional.
A pass over the Serem IR (lib/codegen/SeremTransform.cpp).
- Write a
TransformPasssubclass withname()andbool run(IRModule& module). - Build a replacement map of
const Value*→ValuePtrand hand it toapplyReplacements, which rewrites every operand and drops the replaced operations. Never mutate a block without doing both. - Add a
makeYourPass()factory, register it intransformPasses()(ordefaultTransformPasses()if it should always run), and declare the factory ininclude/sere/codegen/SeremTransform.h.
An LLVM pass — prefer composing existing ones.
- Add the pass name to
buildFlagPipelineinlib/codegen/OptPipeline.cppunder the switch that should enable it. - Verify the name: an unknown pass fails the compile with
cannot build pipeline '…': unknown function pass '…', which names the offender.
Finish with:
- A switch in
parseOptimizationFlagplus a field and preset entry when the pass is user-visible. - A check in
tests/opt_options.cppfor the parse and the rewrite. - A CLI test in
tests/CMakeLists.txtwhen the pass changes emitted IR. - A row in optimization.md or serem.md.
Common pitfalls
- Keyword not highlighting — enumerator placed after
KeywordWith, or missing fromkKeywords. - LSP stale —
sererebuilt but language server not restarted; or editor still pointing at a lockedbin/sere.exe. - Prelude parse errors after a real error — do not early-return from
parseIf/parseWhilesolely becausediagnostics_->hasErrors()is already true (that flagged laterelse:as errors in imported prelude). p.fieldon aUnique[T]— not valid; complete through(*p)..- New runtime symbol not found at link — declaration in
.seremust match the C symbol exactly, andsere_rtmust have been rebuilt. - Touching everything for a stdlib-only feature — if Sere can express it
with
extern "C"and existing types, skip parse/sema/codegen.