This guide provides a step-by-step walkthrough for adding a new feature to the T81Lang language. We will use the example of adding a new binary operator, the modulo operator (%), to illustrate the process.
Companion Documents:
spec/t81lang-spec.mdARCHITECTURE.mdinclude/t81/frontend/lexer.hpp, parser.hpp, ir_generator.hpplang/frontend/lexer.cpp, parser.cpp, ir_generator.cpptests/cpp/t81_frontend_*_test.cpp, tests/cpp/e2e_*_test.cppThe T81Lang compiler frontend is responsible for converting .t81 source code into TISC IR. It follows a classic pipeline:
This guide will walk you through modifying the Lexer, Parser, and IR Generator.
First, teach the lexer to recognize the new syntax.
In include/t81/frontend/lexer.hpp, add a new entry to the TokenType enum.
// in enum class TokenType
// ...
Plus, Minus, Star, Slash, Percent, // <-- Add Percent
// ...
In lang/frontend/lexer.cpp, find the next_token() method and add a case for the % character in the switch statement.
// in Lexer::next_token()
switch (c) {
// ...
case '%': return make_token(TokenType::Percent);
// ...
}
Next, update the parser to understand the operator’s precedence. The modulo operator has the same precedence as multiplication and division.
In lang/frontend/parser.cpp, find the factor() method. Update the while loop to include TokenType::Percent.
// in Parser::factor()
std::unique_ptr<Expr> Parser::factor() {
std::unique_ptr<Expr> expr = unary();
// Add TokenType::Percent to this list
while (match({TokenType::Slash, TokenType::Star, TokenType::Percent})) {
Token op = previous();
std::unique_ptr<Expr> right = unary();
expr = std::make_unique<BinaryExpr>(std::move(expr), op, std::move(right));
}
return expr;
}
The parser can now correctly place the modulo operator in the AST.
Finally, teach the IR generator how to convert the new AST node into a TISC instruction.
In lang/frontend/ir_generator.cpp, find the visit(const BinaryExpr& expr) method. Add a case for TokenType::Percent.
// in IRGenerator::visit(const BinaryExpr& expr)
std::any IRGenerator::visit(const BinaryExpr& expr) {
// ... (visit left and right operands)
switch (expr.op.type) {
// ...
case TokenType::Star:
emit({tisc::Opcode::Mul, {result, left, right}});
break;
case TokenType::Percent: // <-- Add this case
emit({tisc::Opcode::Mod, {result, left, right}});
break;
// ...
}
return result;
}
The compiler can now generate the Mod TISC instruction.
No feature is complete without a test. An end-to-end test is the best way to validate this change.
tests/cpp/, such as e2e_mod_test.cpp.% and then execute it on the VM, asserting the final result is correct. See tests/cpp/e2e_arithmetic_test.cpp for a complete example.CMakeLists.txt.This process—Lexer -> Parser -> IR Generator -> E2E Test—is the standard workflow for adding new language features.
The semantic analyzer enforces the invariants described in spec/t81lang-spec.md
(sections §2.1 on generic types and §6.2 on match semantics). When you evolve the grammar
(see RFC-0011 for the modern generic syntax) you must also extend SemanticAnalyzer so
generic inference, Option/Result exhaustiveness, and match lowering remain correct.
SemanticAnalyzer::visit(GenericTypeExpr) to treat the first
parameter as a type and later parameters as compile-time constants (integer literals or
symbolic identifiers). Store the constants as a dedicated Type::Kind::Constant so the
analyzer can distinguish Tensor[T, 2, 3] from Tensor[T, 3, 2] and enforce shape-aware
assignments.match over structural
types declares exactly one arm per variant (Some/None or Ok/Err) and that all
arms produce a consistent result type. Use the _expected_type_stack to enforce that
constructors like Some, Ok, and Err observe the contextual Option[T]/Result[T, E]
and fail early when the wrong constructors or missing arms appear.Option
or Result; otherwise the IR generator cannot emit the required OPTION_IS_SOME,
OPTION_UNWRAP, RESULT_IS_OK, or RESULT_UNWRAP_* sequences. Any change here should be
covered by the semantic analyzer regression tests (tests/cpp/semantic_analyzer_match_test.cpp
and the new tests/cpp/semantic_analyzer_generic_test.cpp).Keeping the semantic analyzer in lockstep with the spec lets the IR generator assume it can emit deterministic TISC control flow without rechecking every invariant.