35. pyxc: Read-Modify-Write Operators
What I Am Building
Chapter 34 let = appear inside an expression, not just as a standalone statement. pyxc has had / and % since Chapter 4 — this chapter isn't about arithmetic operators themselves, it's about convenience over them: I haven't added compound assignment (+=, *=, etc.), and I haven't added ++ or -- either. Both are pure sugar over x = x + 1-style code; programs don't need them to express anything new. After this chapter, all of that works:
extern def printd(x: float64)
def main() -> int:
var a: int = 17
var b: int = 4
var q: int = a / b
var r: int = a % b
var x: int = 10
x += 5
x -= 3
x *= 2
x /= 4
x %= 10
var i: int = 0
i++
++i
printd(float64(q + r + x + i))
return 0
13.000000
Source Code
git clone --depth 1 https://github.com/alankarmisra/pyxc-llvm-tutorial
cd pyxc-llvm-tutorial/code/chapter-35
Grammar
I change three areas of the grammar. I replace the bare = in assignment-statement with assignment-operator, now accepting any of the six assignment operators. term and unary-expression both change to fold in % and ++/--, and a new postfix-expression production captures postfix ++/-- between unary-expression and primary:
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" external-function-signature
[ "->" type ] ;
top-level-statement = statement ;
function-signature = name "(" [ parameters ] ")" ;
external-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 } ;
simple-statement = return-statement
| break-statement
| continue-statement
| variable-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 = assignment ;
-assignment = logical-or [ "=" assignment ] ;
+assignment = logical-or [ assignment-operator assignment ] ;
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 } ;
+term = unary-expression
+ { ("*" | "/" | "%") unary-expression } ;
lvalue = name
{ "." name | "[" expression "]" } ;
variable-binding = name ":" type [ "=" expression ] ;
-factor = ("-" | "!" | "~") factor | primary ;
+unary-expression = ("-" | "!" | "~" | "++" | "--")
+ unary-expression
+ | postfix-expression ;
+postfix-expression = primary [ "++" | "--" ] ;
primary = cast-expression
| sizeof-expression
| address-expression
| array-literal
| string-literal
| character-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 } ] "]" ;
string-literal = '"' { string-character | escape } '"' ;
escape = literal-escape ;
string-character = ? any character except '"', "\\", "\r", and "\n" ? ;
character-literal = "'" ( character | character-escape ) "'" ;
character-escape = literal-escape ;
literal-escape = "\\" ( "\\" | "'" | '"' | "?"
| "a" | "b" | "f" | "n" | "r"
| "t" | "v"
| "x" hex-digit hex-digit
| octal-digit [ octal-digit
[ octal-digit ] ]
| "u" hex-digit hex-digit hex-digit hex-digit
| "U" hex-digit hex-digit hex-digit hex-digit
hex-digit hex-digit hex-digit hex-digit ) ;
character = ? any character except "'", "\\", "\r", and "\n" ? ;
hex-digit = digit | "A".."F" | "a".."f" ;
+assignment-operator = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
octal-digit = "0".."7" ;
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 | 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 Tokens and Lexer Peek-Ahead
I add seven new tokens to cover the compound assignment operators and the increment/decrement operators:
tok_plus_equal = -57,
tok_minus_equal = -58,
tok_star_equal = -59,
tok_slash_equal = -60,
tok_percent_equal = -61,
tok_plus_plus = -62,
tok_minus_minus = -63,
I produce each with a one-character peek in the lexer. The + path illustrates the pattern: when I see +, I peek at the next character to decide between +=, ++, and bare +:
if (LexerLastChar == '+') {
int Next = peek();
int Tok = tok_plus;
if (Next == '=')
Tok = (advance(), tok_plus_equal);
else if (Next == '+')
Tok = (advance(), tok_plus_plus);
LexerLastChar = advance();
return Tok;
}
- follows the same shape, plus a third alternative for -> (the return-type arrow, from Chapter 18):
if (LexerLastChar == '-') {
int Next = peek();
int Tok = tok_minus;
if (Next == '>')
Tok = (advance(), tok_arrow);
else if (Next == '=')
Tok = (advance(), tok_minus_equal);
else if (Next == '-')
Tok = (advance(), tok_minus_minus);
LexerLastChar = advance();
return Tok;
}
*, /, and % don't get ++/-- forms, so they share one combined branch instead of three separate ones — this is the branch that already returns bare tok_slash/tok_percent for plain / and %, since those tokens have existed since Chapter 4. Only the = alternative is new:
if (LexerLastChar == '*' || LexerLastChar == '/' ||
LexerLastChar == '%') {
int ThisChar = LexerLastChar;
int Tok = ThisChar == '*' ? tok_star
: ThisChar == '/' ? tok_slash : tok_percent;
if (peek() == '=') {
advance();
Tok = ThisChar == '*' ? tok_star_equal
: ThisChar == '/' ? tok_slash_equal
: tok_percent_equal;
}
LexerLastChar = advance();
return Tok;
}
One isLValue()/codegenAddress() Pair, Not Four Node Classes
p.x, arr[i], and a plain name are all different ExpressionNode subclasses, but they all need to answer the same two questions for this chapter: "can I assign to you?" and "give me your address." Rather than write separate compound-assignment machinery for each lvalue shape, I add two virtual methods to the base ExpressionNode that every lvalue-capable node already overrides:
virtual bool isLValue() const { return false; }
virtual Value *codegenAddress() { return nullptr; }
NameExpressionNode, FieldExpressionNode, IndexExpressionNode, and IndexedFieldExpressionNode each override both, returning true/a real address; everything else keeps the base class's false/nullptr. That's the whole trick: one CompoundAssignmentExpressionNode, holding a generic unique_ptr<ExpressionNode> Left, works for every lvalue shape, because it only ever calls Left->isLValue() and Left->codegenAddress() — it never needs to know which concrete node Left actually is.
class CompoundAssignmentExpressionNode : public ExpressionNode {
unique_ptr<ExpressionNode> Left;
unique_ptr<ExpressionNode> Right;
int Operator;
public:
CompoundAssignmentExpressionNode(unique_ptr<ExpressionNode> Left,
unique_ptr<ExpressionNode> Right,
int Operator, ValueType Type,
const string &StructName = "")
: Left(std::move(Left)), Right(std::move(Right)), Operator(Operator) {
setType(Type, StructName);
}
bool shouldPrintValue() const override { return false; }
Value *codegen() override;
};
shouldPrintValue() returning false is what keeps the REPL from auto-printing a 0.0 after every compound assignment — same convention every statement-shaped node has used since Chapter 11.
Parsing: One ParseAssignment, Not Four
= and the five compound-assignment operators are all handled by the same function. I parse the left side as an ordinary expression first, then check whether an assignment operator follows — if Left isn't an lvalue, that's a parse-time error regardless of which operator comes next:
static bool IsAssignmentOperator(int Token) {
return Token == tok_equal || Token == tok_plus_equal ||
Token == tok_minus_equal || Token == tok_star_equal ||
Token == tok_slash_equal || Token == tok_percent_equal;
}
static int AssignmentBinaryOperator(int Token) {
switch (Token) {
case tok_plus_equal:
return tok_plus;
case tok_minus_equal:
return tok_minus;
case tok_star_equal:
return tok_star;
case tok_slash_equal:
return tok_slash;
case tok_percent_equal:
return tok_percent;
default:
return 0;
}
}
/// assignment
/// = logical-or [ assignment-operator assignment ] ;
static unique_ptr<ExpressionNode> ParseAssignment() {
auto Left = ParseLogicalOr();
if (!Left)
return nullptr;
if (!IsAssignmentOperator(CurrentToken))
return Left;
if (!Left->isLValue())
return LogErrorExpression("Assignment target must be assignable");
ValueType LeftType = Left->getType();
string LeftTypeInfo = Left->getStructName();
int AssignmentOperator = CurrentToken;
getNextToken(); // eat the assignment operator
ExpectedLiteralTypeGuard Guard(LeftType, LeftTypeInfo);
auto Right = ParseAssignment();
if (!Right)
return nullptr;
if (AssignmentOperator != tok_equal) {
int BinaryOperator = AssignmentBinaryOperator(AssignmentOperator);
ValueType ResultType =
GetBinaryResultType(BinaryOperator, LeftType, LeftTypeInfo,
Right->getType(), Right->getStructName());
if (ResultType == ValueType::Error || !IsAssignable(LeftType, ResultType))
return LogErrorExpression("Type mismatch in assignment");
if (LeftType == ValueType::Pointer && ResultType != ValueType::Pointer)
return LogErrorExpression("Type mismatch in assignment");
return make_unique<CompoundAssignmentExpressionNode>(
std::move(Left), std::move(Right), BinaryOperator, LeftType,
LeftTypeInfo);
}
// ... plain '=' path, unchanged from Chapter 34 ...
return make_unique<AssignmentExpressionNode>(
std::move(Left), std::move(Right), LeftType, LeftTypeInfo);
}
Right recurses back into ParseAssignment itself, not one tier down — that's what makes a = b += 1 parse as a = (b += 1), right-associative, the same shape Chapter 34 already established for plain =. The type check reuses GetBinaryResultType, the same function every ordinary binary operator's type checking goes through, so x += y is rejected under exactly the same rules as x + y would be.
Compound Assignment Codegen
static Value *EmitReadModifyWriteValue(int Operator, Value *LeftValue,
ValueType LeftType,
const string &LeftTypeInfo,
Value *RightValue,
ValueType RightType) {
if (LeftType == ValueType::Pointer) {
RightValue = EmitImplicitCast(RightValue, RightType, ValueType::Int64);
if (!RightValue)
return LogErrorV("Type mismatch in assignment");
if (Operator == tok_minus)
RightValue = TheBuilder->CreateNeg(RightValue, "negindex");
ValueType ElementType = ValueType::Error;
string ElementTypeInfo;
if (!DecodePointerType(LeftTypeInfo, ElementType, ElementTypeInfo))
return LogErrorV("Invalid pointer type metadata");
return TheBuilder->CreateInBoundsGEP(
LLVMTypeFor(ElementType, ElementTypeInfo), LeftValue, RightValue,
"ptrarith");
}
RightValue = EmitImplicitCast(RightValue, RightType, LeftType);
if (!RightValue)
return LogErrorV("Type mismatch in assignment");
if (IsFloatType(LeftType)) {
if (Operator == tok_plus)
return TheBuilder->CreateFAdd(LeftValue, RightValue, "addtmp");
if (Operator == tok_minus)
return TheBuilder->CreateFSub(LeftValue, RightValue, "subtmp");
if (Operator == tok_star)
return TheBuilder->CreateFMul(LeftValue, RightValue, "multmp");
if (Operator == tok_slash)
return TheBuilder->CreateFDiv(LeftValue, RightValue, "divtmp");
return TheBuilder->CreateFRem(LeftValue, RightValue, "remtmp");
}
if (Operator == tok_plus)
return TheBuilder->CreateAdd(LeftValue, RightValue, "addtmp");
if (Operator == tok_minus)
return TheBuilder->CreateSub(LeftValue, RightValue, "subtmp");
if (Operator == tok_star)
return TheBuilder->CreateMul(LeftValue, RightValue, "multmp");
if (Operator == tok_slash)
return IsUnsignedIntType(LeftType)
? TheBuilder->CreateUDiv(LeftValue, RightValue, "divtmp")
: TheBuilder->CreateSDiv(LeftValue, RightValue, "divtmp");
return IsUnsignedIntType(LeftType)
? TheBuilder->CreateURem(LeftValue, RightValue, "remtmp")
: TheBuilder->CreateSRem(LeftValue, RightValue, "remtmp");
}
Value *CompoundAssignmentExpressionNode::codegen() {
Value *Address = Left->codegenAddress();
if (!Address)
return LogErrorV("Assignment target must be assignable");
Value *LeftValue = TheBuilder->CreateLoad(
LLVMTypeFor(getType(), getStructName()), Address, "rmw.old");
Value *RightValue = Right->codegen();
if (!RightValue)
return nullptr;
Value *Result = EmitReadModifyWriteValue(
Operator, LeftValue, getType(), getStructName(), RightValue,
Right->getType());
if (!Result)
return nullptr;
TheBuilder->CreateStore(Result, Address);
return Result;
}
EmitReadModifyWriteValue is the one place pointer arithmetic (p += 1) and ordinary arithmetic (x += 1) both go through — the pointer branch is the same CreateInBoundsGEP shape Chapter 26 already used for p + n, just reached from a different call site. p.x never has its own copy of this logic: Left->codegenAddress() on a FieldExpressionNode already knows how to compute the field's address, so CompoundAssignmentExpressionNode::codegen() doesn't need to know or care that Left happens to be a field access this time.
Prefix and Postfix ++/--
One node covers all four combinations of prefix/postfix × increment/decrement:
class IncrementDecrementExpressionNode : public ExpressionNode {
unique_ptr<ExpressionNode> Operand;
bool IsIncrement;
bool IsPrefix;
public:
IncrementDecrementExpressionNode(unique_ptr<ExpressionNode> Operand,
bool IsIncrement, bool IsPrefix,
ValueType Type,
const string &StructName = "")
: Operand(std::move(Operand)), IsIncrement(IsIncrement),
IsPrefix(IsPrefix) {
setType(Type, StructName);
}
Value *codegen() override;
};
Codegen loads the old value through Operand->codegenAddress(), computes old ± 1 through the same EmitReadModifyWriteValue compound assignment already uses, stores the result, and returns IsPrefix ? NewValue : OldValue:
Value *IncrementDecrementExpressionNode::codegen() {
Value *Address = Operand->codegenAddress();
if (!Address)
return LogErrorV("Increment/decrement target must be assignable");
Value *OldValue = TheBuilder->CreateLoad(
LLVMTypeFor(getType(), getStructName()), Address, "incdec.old");
Value *One = nullptr;
ValueType OneType = getType();
if (getType() == ValueType::Pointer) {
One = ConstantInt::get(Type::getInt64Ty(*TheContext), 1);
OneType = ValueType::Int64;
} else if (IsIntType(getType())) {
One = ConstantInt::get(LLVMTypeFor(getType()), 1);
} else {
One = ConstantFP::get(LLVMTypeFor(getType()), 1.0);
}
Value *NewValue = EmitReadModifyWriteValue(
IsIncrement ? tok_plus : tok_minus, OldValue, getType(),
getStructName(), One, OneType);
if (!NewValue)
return nullptr;
TheBuilder->CreateStore(NewValue, Address);
return IsPrefix ? NewValue : OldValue;
}
Returning OldValue for postfix and NewValue for prefix is what makes i++ evaluate to the value before the increment and ++i evaluate to the value after it, matching C semantics. p++ on a pointer automatically advances by one element, not one byte, because EmitReadModifyWriteValue's pointer branch is the same code p += 1 goes through.
Parsing ++/--
Postfix lives in ParsePostfixExpression, right after parsing the primary. unary-expression's grammar rule is ("-" | "!" | "~" | "++" | "--") unary-expression | postfix-expression — postfix is what ParseUnaryExpression falls through to once none of the prefix operators match:
static unique_ptr<ExpressionNode> ParsePostfixExpression() {
auto Operand = ParsePrimary();
if (!Operand)
return nullptr;
if (CurrentToken != tok_plus_plus && CurrentToken != tok_minus_minus)
return Operand;
if (!Operand->isLValue())
return LogErrorExpression(
"Increment/decrement target must be assignable");
if (!IsNumericType(Operand->getType()) &&
Operand->getType() != ValueType::Pointer)
return LogErrorExpression(
"Increment/decrement requires numeric or pointer type");
bool IsIncrement = CurrentToken == tok_plus_plus;
ValueType Type = Operand->getType();
string TypeInfo = Operand->getStructName();
getNextToken(); // eat '++' or '--'
return make_unique<IncrementDecrementExpressionNode>(
std::move(Operand), IsIncrement, false, Type, TypeInfo);
}
Prefix is checked first, at the top of ParseUnaryExpression, before -, !, and ~:
static unique_ptr<ExpressionNode> ParseUnaryExpression() {
if (CurrentToken == tok_plus_plus || CurrentToken == tok_minus_minus) {
bool IsIncrement = CurrentToken == tok_plus_plus;
getNextToken(); // eat '++' or '--'
auto Operand = ParseUnaryExpression();
if (!Operand)
return nullptr;
if (!Operand->isLValue())
return LogErrorExpression(
"Increment/decrement target must be assignable");
if (!IsNumericType(Operand->getType()) &&
Operand->getType() != ValueType::Pointer)
return LogErrorExpression(
"Increment/decrement requires numeric or pointer type");
ValueType Type = Operand->getType();
string TypeInfo = Operand->getStructName();
return make_unique<IncrementDecrementExpressionNode>(
std::move(Operand), IsIncrement, true, Type, TypeInfo);
}
// ... '-', '!', '~' unchanged, falling through to ParsePostfixExpression ...
}
Because ParseUnaryExpression recurses on itself for the prefix case, ++--x is syntactically valid (prefix increment applied to a prefix decrement), though it's only meaningful if x is assignable at each level — isLValue() is checked fresh at each recursion.
Build and Run
cd code/chapter-35
cmake -S . -B build && cmake --build build
./build/pyxc
llvm-lit -v test/
Try It
Compound assignment on a field
extern def printd(x: float64)
struct Point:
x: int
def main() -> int:
var p: Point
p.x = 10
p.x += 5
printd(float64(p.x))
return 0
15.000000
Compound assignment on an array element
extern def printd(x: float64)
def main() -> int:
var arr: int[3] = [1, 2, 3]
arr[1] *= 10
printd(float64(arr[1]))
return 0
20.000000
Prefix vs. postfix, as values
extern def printd(x: float64)
def main() -> int:
var i: int = 5
var a: int = i++ # a gets the old value, 5; i becomes 6
var b: int = ++i # i becomes 7 first, b gets the new value, 7
printd(float64(a))
printd(float64(b))
printd(float64(i))
return 0
5.000000
7.000000
7.000000
What's Next
Chapter 36 adds the class keyword.
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.