29. pyxc: impl Blocks
Where We Are
Chapter 28 added traits. A class declares the traits it implements in its header, and the compiler verifies conformance when the class body closes. That works well when you write both the trait and the class together, but what if you want to implement a standard trait on a class that was already written?
After this chapter, trait conformance can be declared outside the class body entirely:
extern def printd(x: float64)
trait Adder:
def add(x: int, y: int) -> int
class Calc:
public bias: int
impl Adder for Calc:
def add(x: int, y: int) -> int:
return x + y + self.bias
def main() -> int:
var c: Calc = Calc()
c.bias = 5
printd(float64(c.add(3, 4)))
return 0
12.000000
The methods defined in the impl block become regular methods on Calc, callable with c.add(...) just like any other method.
Source Code
git clone --depth 1 https://github.com/alankarmisra/pyxc-llvm-tutorial
cd pyxc-llvm-tutorial/code/chapter-29
Grammar
This chapter adds one new production and extends top.
top = typealias | traitdef | structdef | classdef | impldef | definition | ... -- changed
impldef = "impl" identifier "for" identifier ":" eols implblock ; -- new
implblock = indent implmethod { eols implmethod } dedent ; -- new
implmethod = "def" identifier "(" [ typedparam { "," typedparam } ] ")" [ "->" type ] ":" ( simplestmt | eols block ) ; -- new
implmethod has a full body, unlike traitmethodsig. Methods in an impl block are fully defined here.
Full Grammar
code/chapter-29/pyxc.ebnf
program = [ eols ] [ top { eols top } ] [ eols ] ;
eols = eol { eol } ;
top = typealias | traitdef | structdef | classdef | impldef | definition | decorateddef | external | toplevelexpr ;
typealias = "type" identifier "=" type ;
traitdef = "trait" identifier ":" eols traitblock ;
traitblock = indent traitmethodsig { eols traitmethodsig } dedent ;
traitmethodsig = "def" identifier "(" [ typedparam { "," typedparam } ] ")" [ "->" type ] ;
structdef = "struct" identifier ":" eols structblock ;
classdef = "class" identifier [ "(" identifier { "," identifier } ")" ] ":" eols structblock ;
impldef = "impl" identifier "for" identifier ":" eols implblock ;
implblock = indent implmethod { eols implmethod } dedent ;
implmethod = "def" identifier "(" [ typedparam { "," typedparam } ] ")" [ "->" type ] ":" ( simplestmt | eols block ) ;
structblock = indent classmember { eols classmember } dedent ;
classmember = [ visibility ] ( fielddecl | methoddef ) ;
visibility = "public" | "private" ;
methoddef = "def" identifier "(" [ typedparam { "," typedparam } ] ")"
[ "->" type ] ":" ( simplestmt | eols block ) ;
fielddecl = identifier ":" type ;
definition = "def" prototype [ "->" type ] ":" ( simplestmt | eols block ) ;
decorateddef = binarydecorator eols "def" binaryopprototype [ "->" type ] ":" ( simplestmt | eols block )
| unarydecorator eols "def" unaryopprototype [ "->" type ] ":" ( simplestmt | eols block ) ;
binarydecorator = "@" "binary" "(" integer ")" ;
unarydecorator = "@" "unary" ;
binaryopprototype = customopchar "(" typedparam "," typedparam ")" ;
unaryopprototype = customopchar "(" typedparam ")" ;
external = "extern" "def" prototype [ "->" type ] ;
toplevelexpr = expression ;
prototype = identifier "(" [ typedparam { "," typedparam } ] ")" ;
typedparam = identifier ":" type ;
ifstmt = "if" expression ":" suite
[ eols "else" ":" suite ] ;
forstmt = "for"
( "var" identifier ":" type | identifier )
"=" expression "," expression "," expression ":" suite ;
varstmt = "var" varbinding { "," varbinding } ;
assignstmt = lvalue "=" expression ;
simplestmt = returnstmt | varstmt | assignstmt | expression ;
compoundstmt = ifstmt | forstmt ;
statement = simplestmt | compoundstmt ;
suite = simplestmt | compoundstmt | eols block ;
returnstmt = "return" [ expression ] ;
block = indent statement { eols statement } dedent ;
expression = unaryexpr binoprhs ;
binoprhs = { binaryop unaryexpr } ;
lvalue = identifier | fieldaccess | indexexpr ;
varbinding = identifier ":" type [ "=" expression ] ;
unaryexpr = unaryop unaryexpr | primary ;
unaryop = "-" | userdefunaryop ;
primary = castexpr | sizeofexpr | addrexpr | arrayliteral | stringliteral | identifierexpr | fieldaccess | indexexpr | numberexpr | bool_literal | parenexpr ;
castexpr = casttype "(" expression ")" ;
sizeofexpr = "sizeof" "(" type ")" ;
addrexpr = "addr" "(" lvalue ")" ;
identifierexpr = identifier | callexpr | methodcallexpr | ctorcallexpr ;
callexpr = identifier "(" [ expression { "," expression } ] ")" ;
methodcallexpr = identifier "." identifier "(" [ expression { "," expression } ] ")" ;
ctorcallexpr = identifier "(" [ expression { "," expression } ] ")" ;
fieldaccess = identifier "." identifier { "." identifier } ;
indexexpr = identifier "[" expression "]" ;
numberexpr = number ;
arrayliteral = "[" [ expression { "," expression } ] "]" ;
stringliteral = "\"" { ? any char except " and newline ? | escape } "\"" ;
escape = "\\" ( "\\" | "\"" | "n" | "t" | "0" ) ;
parenexpr = "(" expression ")" ;
binaryop = builtinbinaryop | userdefbinaryop ;
indent = INDENT ;
dedent = DEDENT ;
builtinbinaryop = "+" | "-" | "*" | "<" | "<=" | ">" | ">=" | "==" | "!=" ;
userdefbinaryop = ? any opchar defined as a custom binary operator ? ;
userdefunaryop = ? any opchar defined as a custom unary operator ? ;
customopchar = ? any opchar that is not "-" or a builtinbinaryop,
and not already defined as a custom operator ? ;
opchar = ? any single ASCII punctuation character ? ;
identifier = (letter | "_") { letter | digit | "_" } ;
builtintype = "int" | "int8" | "int16" | "int32" | "int64"
| "float" | "float32" | "float64"
| "bool" | "None" ;
aliastype = identifier ;
structtype = identifier ;
pointertype = "ptr" "[" type "]" ;
type = basetype [ arraysuffix ] ;
basetype = builtintype | aliastype | structtype | pointertype ;
arraysuffix = "[" integer "]" ;
casttype = "int" | "int8" | "int16" | "int32" | "int64"
| "float" | "float32" | "float64"
| "bool" | pointertype ;
integer = digit { digit } ;
number = digit { digit } [ "." { digit } ]
| "." digit { digit } ;
bool_literal = "True" | "False" ;
letter = "A".."Z" | "a".."z" ;
digit = "0".."9" ;
eol = "\r\n" | "\r" | "\n" ;
ws = " " | "\t" ;
INDENT = ? synthetic token emitted by lexer ? ;
DEDENT = ? synthetic token emitted by lexer ? ;
New Token and Keyword
tok_impl = -44,
Registered in the keyword table:
{"impl", tok_impl}
The for in impl TraitName for ClassName reuses the existing tok_for token — the same token produced by the for keyword in loop statements. There is no ambiguity because impl always precedes it and the parser knows it is reading an impl header, not a loop.
ParseImplDefinition — The impl Block Parser
ParseImplDefinition validates the header, parses and compiles the methods, then calls VerifyTraitConformance:
static bool ParseImplDefinition() {
getNextToken(); // eat 'impl'
// 1. Validate trait name
string TraitName = IdentifierStr;
if (!Traits.count(TraitName)) {
LogError(("Unknown trait '" + TraitName + "'").c_str());
return false;
}
getNextToken(); // eat trait name
// 2. Expect 'for' (reuses tok_for)
if (CurTok != tok_for) {
LogError("Expected 'for' in impl definition");
return false;
}
getNextToken(); // eat 'for'
// 3. Validate class name — must exist and be a class, not a struct
string ClassName = IdentifierStr;
auto CI = StructTypes.find(ClassName);
if (CI == StructTypes.end()) {
LogError(("Unknown class '" + ClassName + "'").c_str());
return false;
}
if (!CI->second.IsClass) {
LogError(("'" + ClassName +
"' is a struct, not a class; traits can only be implemented "
"on classes").c_str());
return false;
}
getNextToken(); // eat class name
// 4. Reject duplicate impl for the same trait/class pair
if (std::find(CI->second.ImplementedTraits.begin(),
CI->second.ImplementedTraits.end(), TraitName)
!= CI->second.ImplementedTraits.end()) {
LogError(("Trait '" + TraitName + "' is already implemented for class '"
+ ClassName + "'").c_str());
return false;
}
// ... eat ':', eat EOL, expect INDENT ...
// 5. Parse and compile each method body
while (CurTok != tok_dedent && ...) {
auto FnAST = ParseMethodDefinitionInClass(ClassName, /*IsPublic=*/true);
if (auto *FnIR = FnAST->codegen()) { /* optionally dump IR */ }
}
// eat DEDENT, inject tok_block_end
// 6. Record conformance and verify
CI->second.ImplementedTraits.push_back(TraitName);
if (!VerifyTraitConformance(ClassName, TraitName))
return false;
return true;
}
All methods in an impl block are forced public (IsPublic=true). Satisfying a trait contract is a public commitment — private trait methods are caught by VerifyTraitConformance's public check.
HandleImplDef calls ParseImplDefinition with the same error-recovery pattern used by HandleStructDef and HandleClassDef, and both MainLoop and FileModeLoop dispatch on tok_impl.
Methods Defined in impl Are Regular Methods
There is no runtime distinction between a method defined in the class body and one defined in an impl block. Both are stored in FunctionProtos under the mangled name ClassName.MethodName and emitted as @ClassName.MethodName in the IR. A caller cannot tell where the method was defined.
Things Worth Knowing
The trait must be defined before the impl. impl Adder for Calc: requires that Adder is already in scope.
The class must be defined before the impl. The class name is looked up in StructTypes at parse time.
Implementing a trait on a struct is rejected. The IsClass flag is checked — a struct gives: 'S' is a struct, not a class; traits can only be implemented on classes.
impl cannot be used twice for the same trait/class pair. A second impl Adder for Calc: is rejected: "Trait 'Adder' is already implemented for class 'Calc'".
What's Next
Chapter 30 adds type parameters to traits — trait Addable[T]: — so the same contract can be expressed for different element types.
Need Help?
Build issues? Questions?
- GitHub Issues: Report problems
- Discussions: Ask questions
Include:
- Your OS and version
- Full error message
- Output of
cmake --version,ninja --version, andllvm-config --version
We'll figure it out.