29. pyxc: Type Aliases
What I Am Building
Chapter 28 gave me the heap: malloc, free, sizeof, and pointer casts. Between structs, pointers, and now heap-allocated memory, my type annotations are getting long and repetitive — ptr[Node] shows up everywhere. I want a name for it.
After this chapter:
extern def printd(x: float64) struct Point: x: int y: int type Vec2 = Point def magnitude_sq(v: Vec2) -> int: return v.x * v.x + v.y * v.y def main() -> int: var p: Vec2 p.x = 3 p.y = 4 printd(float64(magnitude_sq(p))) return 025.000000
Vec2 is a Point in every way that matters — same fields, same layout, same struct underneath — it just has a name I find more meaningful in this context. This is also what sets up Chapter 30: once string literals exist, type string = ptr[int8] gives the byte-pointer C functions expect a name I don't have to keep spelling out.
Source Code
git clone --depth 1 https://github.com/alankarmisra/pyxc-llvm-tutorial
cd pyxc-llvm-tutorial/code/chapter-29
Grammar
top-level-item gains type-alias, and type gains alias-type. alias-type and struct-type are both written identically, as a plain name; ParseTypeToken is what actually tells them apart, by trying TypeAliases before StructTypes and rejecting the name if neither lookup succeeds. Everything else is unchanged from Chapter 30:
program = [ end-of-lines ]
[ top-level-item
{ end-of-lines top-level-item } ]
[ end-of-lines ] ;
end-of-lines = end-of-line { end-of-line } ;
top-level-item = function-definition
+ | type-alias
| struct-definition
| external
| top-level-statement ;
struct-definition = "struct" name ":" end-of-lines
struct-block ;
+type-alias = "type" name "=" type ;
struct-block = indent field-declaration
{ end-of-lines field-declaration } dedent ;
field-declaration = name ":" type ;
function-definition = "def" function-signature [ "->" type ] ":"
( simple-statement
| end-of-lines block ) ;
external = "extern" "def" function-signature [ "->" type ] ;
top-level-statement = statement ;
function-signature = name "(" [ parameters ] ")" ;
parameters = typed-parameter { "," typed-parameter } ;
typed-parameter = name ":" type ;
if-statement = "if" expression ":" suite
{ [ end-of-lines ] "elif" expression ":" suite }
[ [ end-of-lines ] "else" ":" suite ] ;
for-statement = "for" ( "var" name ":" type | name )
"=" expression ","
expression "," expression ":" suite ;
while-statement = "while" expression ":" suite ;
do-while-statement = "do" ":" suite [ end-of-lines ]
"while" expression ;
switch-statement = "switch" expression ":" end-of-lines
indent switch-body dedent ;
switch-body = switch-case
{ end-of-lines switch-case }
[ end-of-lines default-case ] ;
switch-case = "case" switch-integer
{ "," switch-integer } ":" suite ;
default-case = "default" ":" suite ;
variable-statement = "var" variable-binding
{ "," variable-binding } ;
assignment-statement = lvalue "=" expression ;
simple-statement = return-statement
| break-statement
| continue-statement
| variable-statement
| assignment-statement
| expression ;
compound-statement = if-statement
| for-statement
| while-statement
| do-while-statement
| switch-statement ;
statement = simple-statement | compound-statement ;
suite = simple-statement
| compound-statement
| end-of-lines block ;
return-statement = "return" [ expression ] ;
break-statement = "break" ;
continue-statement = "continue" ;
statement-separator = end-of-lines | BLOCK_END ;
block = indent statement
{ statement-separator statement } dedent ;
expression = logical-or ;
logical-or = logical-and { "||" logical-and } ;
logical-and = bitwise-or { "&&" bitwise-or } ;
bitwise-or = bitwise-xor { "|" bitwise-xor } ;
bitwise-xor = bitwise-and { "^" bitwise-and } ;
bitwise-and = equality { "&" equality } ;
equality = relational { ("==" | "!=") relational } ;
relational = shift { ("<" | "<=" | ">" | ">=") shift } ;
shift = sum { ("<<" | ">>") sum } ;
sum = term { ("+" | "-") term } ;
term = factor { ("*" | "/" | "%") factor } ;
lvalue = name
{ "." name | "[" expression "]" } ;
variable-binding = name ":" type [ "=" expression ] ;
factor = ("-" | "!" | "~") factor | primary ;
primary = cast-expression
| sizeof-expression
| address-expression
| array-literal
| name-expression
| number-expression
| boolean-literal
| parenthesized-expression ;
cast-expression = cast-type "(" expression ")" ;
sizeof-expression = "sizeof" "(" type ")" ;
address-expression = "addr" "(" lvalue ")" ;
array-literal = "[" [ expression
{ "," expression } ] "]" ;
name-expression = lvalue | call-expression ;
call-expression = name "(" [ arguments ] ")" ;
arguments = expression { "," expression } ;
number-expression = number ;
parenthesized-expression = "(" expression ")" ;
indent = INDENT ;
dedent = DEDENT ;
name = (letter | "_")
{ letter | digit | "_" } ;
type = base-type [ array-suffix ] ;
-base-type = builtin-type | struct-type | pointer-type ;
+base-type = builtin-type | alias-type | struct-type
+ | pointer-type ;
pointer-type = "ptr" "[" type "]" ;
array-suffix = "[" integer "]" ;
builtin-type = "int" | "int8" | "int16" | "int32"
| "int64" | "uint8" | "uint16"
| "uint32" | "uint64"
| "float" | "float32"
| "float64" | "bool" | "None" ;
struct-type = name ;
+alias-type = name ;
cast-type = builtin-cast-type | pointer-type ;
builtin-cast-type = "int" | "int8" | "int16" | "int32"
| "int64" | "uint8" | "uint16"
| "uint32" | "uint64"
| "float" | "float32"
| "float64" | "bool" ;
number = ( digit { digit } [ "." { digit } ]
| "." digit { digit } ) [ exponent ] ;
switch-integer = [ "-" ] digit { digit } ;
exponent = ( "e" | "E" ) [ "+" | "-" ]
digit { digit } ;
boolean-literal = "True" | "False" ;
integer = digit { digit } ;
letter = "A".."Z" | "a".."z" ;
digit = "0".."9" ;
end-of-line = "\r\n" | "\r" | "\n" ;
(*
A `comment` begins with "#" and continues to the end of the line. The lexer
ignores its text and returns an end-of-line token when one follows it.
*)
comment = "#" { comment-character } ;
comment-character = ? any character except "\r" and "\n" ? ;
(*
`whitespace` may appear before or between tokens
and is ignored by the lexer.
*)
whitespace = " " | "\t" | "\v" | "\f" ;
INDENT = ? synthetic token emitted by lexer when indentation increases ? ;
DEDENT = ? synthetic token emitted by lexer when indentation decreases ? ;
BLOCK_END = ? synthetic token injected into the stream by ParseBlock
immediately after it consumes DEDENT ? ;
New Keyword: type
tok_type = -39,
{"type", tok_type}
The Type-Alias Table
static std::map<string, std::pair<ValueType, string>> TypeAliases;
An alias maps a name to a fully-resolved type: the same (ValueType, StructName) pair I already use everywhere a type needs a pointee or struct name attached, going back to Chapter 24's structs. TypeAliases gets cleared alongside every other per-file symbol table in ResetParserStateForFile, so aliases don't leak from one compiled file into the next:
static void ResetParserStateForFile() {
FunctionSignatures.clear();
StructTypes.clear();
TypeAliases.clear();
GlobalVarTypes.clear();
GlobalVarStructTypes.clear();
GlobalVarDecls.clear();
VarScopes.clear();
VarStructScopes.clear();
Extending Type Parsing for Aliases
Every type annotation in pyxc, parameter types, return types, var declarations, sizeof operands, cast targets, goes through ParseTypeToken. That means I only have to teach alias resolution to one function for it to work everywhere else automatically.
Before this chapter, an unrecognized name here was just an error. Now I check TypeAliases first:
case tok_name: {
string TyName = Name;
auto AliasIt = TypeAliases.find(TyName);
if (AliasIt != TypeAliases.end()) {
getNextToken();
if (StructName)
*StructName = AliasIt->second.second;
return AliasIt->second.first;
}
if (!StructTypes.count(TyName)) {
LogErrorExpression(("Unknown type '" + TyName + "'").c_str());
return ValueType::Error;
}
getNextToken();
if (StructName)
*StructName = TyName;
return ValueType::Struct;
}
If the name is a known alias, I return whatever it resolves to and I'm done; nothing downstream can even tell an alias was involved. If it isn't an alias, the old struct-name check runs exactly as before.
Parsing the Definition Itself
static bool ParseTypeAliasDefinition() {
// CurrentToken is 'type'
getNextToken(); // eat 'type'
if (CurrentToken != tok_name) {
LogErrorExpression("Expected alias name after 'type'");
return false;
}
string AliasName = Name;
if (TypeAliases.count(AliasName)) {
LogErrorExpression(("Type alias '" + AliasName + "' is already defined").c_str());
return false;
}
if (StructTypes.count(AliasName)) {
LogErrorExpression(
("Name '" + AliasName + "' is already defined as a struct").c_str());
return false;
}
getNextToken(); // eat alias name
if (CurrentToken != tok_equal) {
LogErrorExpression("Expected '=' in type alias");
return false;
}
getNextToken(); // eat '='
string AliasStructName;
ValueType AliasType = ParseTypeToken(&AliasStructName);
if (AliasType == ValueType::Error)
return false;
TypeAliases[AliasName] = {AliasType, AliasStructName};
return true;
}
The right-hand side is parsed with the exact same ParseTypeToken I just extended, which means aliasing an alias works for free: type Score = MyInt resolves MyInt through the same lookup, and whatever MyInt already resolves to is what gets stored under Score. There's no chain kept around to walk later, by the time this function returns, Score and MyInt's underlying type are indistinguishable.
HandleTypeAliasDef wraps this the same way every other top-level form is wrapped, and tok_type is wired into both the REPL and file-mode dispatch switches.
Conflict Rules
Aliases and structs share one namespace, so I check both directions.
Defining the same alias twice:
type Foo = int
type Foo = int64
Error (Line 2, Column 6): Type alias 'Foo' is already defined
type Foo
^~~~
An alias colliding with an existing struct:
struct Foo:
x: int
type Foo = int
Error (Line 3, Column 6): Name 'Foo' is already defined as a struct
type Foo
^~~~
And the reverse, a struct colliding with an existing alias, checked inside ParseStructDefinition itself:
if (TypeAliases.count(StructName)) {
LogErrorExpression(("Name '" + StructName + "' is already defined as a type alias")
.c_str());
return false;
}
type Foo = int
struct Foo:
x: int
Error (Line 2, Column 8): Name 'Foo' is already defined as a type alias
struct Foo:
^~~~
There's no forward reference either way. An alias has to exist in TypeAliases at the moment its name is looked up, and that lookup only ever happens while parsing something that comes after the type line:
def use_it(x: Meters) -> Meters:
return x
type Meters = int64
Error (Line 1, Column 15): Unknown type 'Meters'
def use_it(x: Meters)
^~~~
IR Transparency
An alias produces no IR of its own. It's resolved entirely while parsing, so by the time code generation runs, there's nothing left that knows the alias ever existed.
type Score = int64
def id(x: Score) -> Score:
return x
define i64 @id(i64 %x) {
Score doesn't appear anywhere; LLVM sees i64, exactly as if I'd written int64 directly. Chaining aliases doesn't change this:
type MyInt = int
type Score = MyInt
def id(x: Score) -> Score:
return x
define i64 @id(i64 %x) {
Same IR. Score resolved through MyInt down to int at the moment type Score = MyInt was parsed, so there's no chain left to collapse later, there was never a chain to begin with once parsing moved on.
Build and Run
cd code/chapter-29
cmake -S . -B build && cmake --build build
llvm-lit -v test/
Try It
string as a Parameter Type
extern def puts(s: ptr[int8]) -> int
type string = ptr[int8]
def greet(name: string) -> int:
return puts(name)
def main() -> int:
greet("world")
return 0
world
Aliasing a Struct
extern def printd(x: float64)
struct Point:
x: int
y: int
type Vec2 = Point
def main() -> int:
var v: Vec2
v.x = 3
v.y = 4
printd(float64(v.x))
return 0
3.000000
Vec2 behaves exactly like Point everywhere: as a var type, and for field access.
Inspecting the IR
pyxc --emit llvm-ir -o out.ll program.pyxc
grep 'define' out.ll
For the Score example above:
define i64 @id(i64 %x)
Known Limitations
No forward references. An alias has to be defined before anything uses it. type List = ptr[List] fails because List doesn't exist yet in TypeAliases when the right-hand side is parsed.
No recursive aliases, for the same reason: a self-referential definition would need the forward reference this chapter doesn't support.
Aliases are purely syntactic. There's no nominal typing anywhere in this. Score and int64 are one type as far as the compiler is concerned, so a function expecting Score accepts a plain int64 without complaint.
No parameterized aliases. type Pair[T] = ... isn't supported; type parameters are out of scope for this chapter.
No scoping. Every alias is global to the module. There's no way to limit one to a single function or file.
What's Next
Chapter 30 adds string literals and real C library calls.
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
I'll help you figure it out.